"""GitHub API service""" import httpx from typing import List, Dict, Any from app.services.base import BaseGitService, FileChange, PRInfo class GitHubService(BaseGitService): """Service for interacting with GitHub API""" def __init__(self, base_url: str, token: str, repo_owner: str, repo_name: str): # GitHub always uses api.github.com super().__init__("https://api.github.com", token, repo_owner, repo_name) def _get_headers(self) -> Dict[str, str]: """Get headers for API requests""" return { "Authorization": f"token {self.token}", "Accept": "application/vnd.github.v3+json", "Content-Type": "application/json" } def _get_repo_path(self) -> str: """Get repository API path""" return f"{self.base_url}/repos/{self.repo_owner}/{self.repo_name}" async def get_pull_request(self, pr_number: int) -> PRInfo: """Get pull request information from GitHub""" url = f"{self._get_repo_path()}/pulls/{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["number"], title=data["title"], description=data.get("body", ""), author=data["user"]["login"], source_branch=data["head"]["ref"], target_branch=data["base"]["ref"], url=data["html_url"], 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()}/pulls/{pr_number}/files" async with httpx.AsyncClient() as client: response = await client.get(url, headers=self._get_headers()) response.raise_for_status() files_data = response.json() changes = [] for file in files_data: changes.append(FileChange( filename=file["filename"], status=file["status"], additions=file.get("additions", 0), deletions=file.get("deletions", 0), patch=file.get("patch") )) 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()}/contents/{file_path}" async with httpx.AsyncClient() as client: response = await client.get( url, headers=self._get_headers(), params={"ref": ref} ) response.raise_for_status() data = response.json() # GitHub returns base64 encoded content import base64 content = base64.b64decode(data["content"]).decode("utf-8") return content 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()}/pulls/{pr_number}/comments" payload = { "body": comment, "commit_id": commit_id, "path": file_path, "line": line_number, "side": "RIGHT" } 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📤 Публикация ревью в GitHub PR #{pr_number}") print(f" Комментариев для публикации: {len(comments)}") url = f"{self._get_repo_path()}/issues/{pr_number}/comments" # 1. Сначала публикуем общий summary if body: print(f"\n 📝 Публикация общего summary ({len(body)} символов)...") payload = {"body": 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(), "💬") # GitHub ссылка на строку file_url = f"https://github.com/{self.repo_owner}/{self.repo_name}/pull/{pr_number}/files#L{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 = {"body": 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)}