from typing import Optional import backoff import requests from ddtrace import tracer _MAX_TRIES = 5 def _on_sonar_backoff(details): project_key = details["args"][1] print( f"Sonar request failed for project '{project_key}'" f" (attempt {details['tries']}/{_MAX_TRIES}), retrying in {details['wait']:.1f}s: {details['exception']}" ) def _on_sonar_giveup(details): project_key = details["args"][1] print( f"Warning: Sonar unavailable for project '{project_key}' after {details['tries']} attempts: proceeding " "without Sonar data. Note that Software Catalog will temporarily be missing the Sonar project URL." ) class Sonar: def __init__(self, base_url: str, api_token: str): self.base_url = base_url self.api_token = api_token self.headers = { "Authorization": f"Bearer {self.api_token}", "Content-Type": "application/json", } @tracer.wrap(service="datadog-tools", resource="Sonar.get_project") @backoff.on_exception( backoff.expo, requests.exceptions.RequestException, max_tries=_MAX_TRIES, on_backoff=_on_sonar_backoff, on_giveup=_on_sonar_giveup, raise_on_giveup=False, ) def get_project(self, project_key: str) -> Optional[dict]: """ Retrieve SonarQube project details by project key. """ url = f"{self.base_url}/api/projects/search?projects={project_key}" response = requests.get(url, headers=self.headers, timeout=10) response.raise_for_status() data = response.json() return data["components"][0] if data.get("components") else None @tracer.wrap(service="datadog-tools", resource="Sonar.get_project_url") def get_project_url(self, project_key: str) -> str: """ Construct the SonarQube project dashboard URL. """ return f"{self.base_url}/dashboard?id={project_key}"