"""Bitbucket API service""" import httpx from typing import List, Dict, Any from app.services.base import BaseGitService, FileChange, PRInfo class BitbucketService(BaseGitService): """Service for interacting with Bitbucket API""" def __init__(self, base_url: str, token: str, repo_owner: str, repo_name: str): # Bitbucket Cloud uses api.bitbucket.org super().__init__("https://api.bitbucket.org/2.0", token, repo_owner, repo_name) def _get_headers(self) -> Dict[str, str]: """Get headers for API requests""" return { "Authorization": f"Bearer {self.token}", "Content-Type": "application/json" } def _get_repo_path(self) -> str: """Get repository API path""" return f"{self.base_url}/repositories/{self.repo_owner}/{self.repo_name}" async def get_pull_request(self, pr_number: int) -> PRInfo: """Get pull request information from Bitbucket""" url = f"{self._get_repo_path()}/pullrequests/{pr_number}" async with httpx.AsyncClient() as client: response = await client.get(url, headers=self._get_headers()) response.raise_for_status() data = response.json() return PRInfo( number=data["id"], title=data["title"], description=data.get("description", ""), author=data["author"]["display_name"], source_branch=data["source"]["branch"]["name"], target_branch=data["destination"]["branch"]["name"], url=data["links"]["html"]["href"], state=data["state"] ) async def get_pr_files(self, pr_number: int) -> List[FileChange]: """Get list of changed files in PR""" url = f"{self._get_repo_path()}/pullrequests/{pr_number}/diffstat" async with httpx.AsyncClient() as client: response = await client.get(url, headers=self._get_headers()) response.raise_for_status() data = response.json() changes = [] for file in data.get("values", []): status = file.get("status", "modified") changes.append(FileChange( filename=file["new"]["path"] if file.get("new") else file["old"]["path"], status=status, additions=file.get("lines_added", 0), deletions=file.get("lines_removed", 0) )) return changes async def get_file_content(self, file_path: str, ref: str) -> str: """Get file content at specific ref""" url = f"{self._get_repo_path()}/src/{ref}/{file_path}" async with httpx.AsyncClient() as client: response = await client.get(url, headers=self._get_headers()) response.raise_for_status() return response.text async def create_review_comment( self, pr_number: int, file_path: str, line_number: int, comment: str, commit_id: str ) -> Dict[str, Any]: """Create a review comment on PR""" url = f"{self._get_repo_path()}/pullrequests/{pr_number}/comments" payload = { "content": { "raw": comment }, "inline": { "path": file_path, "to": line_number } } async with httpx.AsyncClient() as client: response = await client.post( url, headers=self._get_headers(), json=payload ) response.raise_for_status() return response.json() async def create_review( self, pr_number: int, comments: List[Dict[str, Any]], body: str = "", event: str = "COMMENT" ) -> Dict[str, Any]: """Create a review with separate comment for each issue Args: pr_number: PR number comments: List of comments body: Overall review summary (markdown supported) event: Review event (не используется, для совместимости) """ print(f"\n📤 Публикация ревью в Bitbucket PR #{pr_number}") print(f" Комментариев для публикации: {len(comments)}") url = f"{self._get_repo_path()}/pullrequests/{pr_number}/comments" # 1. Сначала публикуем общий summary if body: print(f"\n 📝 Публикация общего summary ({len(body)} символов)...") payload = { "content": { "raw": body } } async with httpx.AsyncClient(timeout=30.0) as client: response = await client.post( url, headers=self._get_headers(), json=payload ) response.raise_for_status() print(f" ✅ Summary опубликован!") # 2. Затем публикуем каждую проблему отдельным комментарием if comments: print(f"\n 💬 Публикация {len(comments)} отдельных комментариев...") for i, comment in enumerate(comments, 1): severity_emoji = { "ERROR": "❌", "WARNING": "⚠️", "INFO": "ℹ️" }.get(comment.get("severity", "INFO").upper(), "💬") # Bitbucket ссылка на строку file_url = f"https://bitbucket.org/{self.repo_owner}/{self.repo_name}/pull-requests/{pr_number}/diff#{comment['file_path']}T{comment['line_number']}" # Форматируем комментарий comment_body = f"{severity_emoji} **[`{comment['file_path']}:{comment['line_number']}`]({file_url})**\n\n" comment_body += f"**{comment.get('severity', 'INFO').upper()}**: {comment['content']}" payload = { "content": { "raw": comment_body } } try: async with httpx.AsyncClient(timeout=30.0) as client: response = await client.post( url, headers=self._get_headers(), json=payload ) response.raise_for_status() print(f" ✅ {i}/{len(comments)}: {comment['file_path']}:{comment['line_number']}") except Exception as e: print(f" ❌ {i}/{len(comments)}: Ошибка - {e}") print(f"\n 🎉 Все комментарии опубликованы!") return {"summary": "posted", "comments_count": len(comments)}