import os import requests from requests.auth import HTTPBasicAuth import logging logger = logging.getLogger(__name__) class BitbucketClient: def __init__(self): self.username = os.getenv("BITBUCKET_USERNAME") self.email = os.getenv("BITBUCKET_EMAIL") self.app_password = os.getenv("BITBUCKET_APP_PASSWORD") self.token = os.getenv("BITBUCKET_TOKEN") self.base_url = "https://api.bitbucket.org/2.0" # Determine the identifier for Basic Auth (Email or Username) self.auth_user = self.email or self.username if not self.token and (not self.auth_user or not self.app_password): logger.warning("Bitbucket credentials (token or email/password) not fully configured.") def _get_auth_params(self, is_diff: bool = False): """ Helper to return auth configuration based on available environment variables. """ headers = {} if is_diff: headers["Accept"] = "text/plain" else: headers["Accept"] = "application/json" # If we have both email and token, and token starts with 'ATA' (Bitbucket App Password/Token), # we MUST use Basic Auth (email:token). if self.email and self.token and self.token.startswith("ATA"): logger.info("Detected Bitbucket App Password/Token. Using Basic Auth with email.") return { "auth": HTTPBasicAuth(self.email, self.token), "headers": headers } if self.token: # Check if token is already a full header string if self.token.startswith("Basic ") or self.token.startswith("Bearer "): headers["Authorization"] = self.token else: # Default to Bearer for other tokens (like OAuth) headers["Authorization"] = f"Bearer {self.token}" return {"headers": headers} else: return { "auth": HTTPBasicAuth(self.auth_user, self.app_password), "headers": headers } def get_diff(self, workspace: str, repo_slug: str, pr_id: int) -> str: """ Fetch the diff for a specific pull request. """ url = f"{self.base_url}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/diff" try: auth_params = self._get_auth_params(is_diff=True) response = requests.get(url, **auth_params) response.raise_for_status() return response.text except Exception as e: logger.error(f"Failed to fetch diff from Bitbucket: {str(e)}") raise def get_pr_details(self, workspace: str, repo_slug: str, pr_id: int) -> dict: """ Fetch pull request details (like author email). """ url = f"{self.base_url}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}" try: auth_params = self._get_auth_params(is_diff=False) response = requests.get(url, **auth_params) response.raise_for_status() return response.json() except Exception as e: logger.error(f"Failed to fetch PR details from Bitbucket: {str(e)}") raise @staticmethod def parse_url(url: str) -> dict: """ Parse a Bitbucket API URL to extract workspace, repo_slug, and pr_id. Example: https://api.bitbucket.org/2.0/repositories/workspace/repo/pullrequests/123/diff """ import re pattern = r"repositories/([^/]+)/([^/]+)/pullrequests/(\d+)" match = re.search(pattern, url) if match: return { "workspace": match.group(1), "repo_slug": match.group(2), "pr_id": int(match.group(3)) } raise ValueError("Invalid Bitbucket PR URL format")