from datetime import datetime, timedelta from enum import Enum import json import logging import os from typing import Any, Dict, List, Optional import requests logger = logging.getLogger(__name__) class GitHubServiceType(Enum): """GitHub service types""" REPOSITORY = "repository" ISSUE = "issue" PULL_REQUEST = "pull_request" CODE_REVIEW = "code_review" WORKFLOW = "workflow" TEAM = "team" PROJECT = "project" WEBHOOK = "webhook" class GitHubService: """Enhanced GitHub API integration service with comprehensive features""" def __init__(self): self.api_base_url = "https://api.github.com" self.timeout = 30 self.max_retries = 3 # Load configuration from environment self.client_id = os.getenv("GITHUB_CLIENT_ID") self.client_secret = os.getenv("GITHUB_CLIENT_SECRET") self.redirect_uri = os.getenv( "GITHUB_REDIRECT_URI", "http://localhost:3000/api/integrations/github/callback", ) # Rate limiting tracking self.rate_limit_remaining = None self.rate_limit_reset = None def _make_request( self, method: str, endpoint: str, data: Optional[Dict] = None, headers: Optional[Dict] = None, retry_count: int = 0, ) -> Optional[Dict]: """Make HTTP request to GitHub API with error handling and retry logic""" try: url = f"{self.api_base_url}{endpoint}" request_headers = self._get_headers() if headers: request_headers.update(headers) logger.info(f"Making GitHub API request: {method} {endpoint}") response = requests.request( method=method, url=url, json=data, headers=request_headers, timeout=self.timeout, ) # Update rate limit info if "X-RateLimit-Remaining" in response.headers: self.rate_limit_remaining = int( response.headers["X-RateLimit-Remaining"] ) if "X-RateLimit-Reset" in response.headers: self.rate_limit_reset = int(response.headers["X-RateLimit-Reset"]) if response.status_code == 200: return response.json() elif response.status_code == 201: return response.json() elif response.status_code == 204: return { "status": "success", "message": "Operation completed successfully", } elif response.status_code == 429 and retry_count < self.max_retries: # Rate limited, wait and retry reset_time = self.rate_limit_reset wait_time = max(reset_time - datetime.now().timestamp(), 1) logger.warning( f"Rate limited, waiting {wait_time} seconds before retry" ) import time time.sleep(wait_time) return self._make_request( method, endpoint, data, headers, retry_count + 1 ) else: logger.error( f"GitHub API error {response.status_code}: {response.text}" ) return None except requests.exceptions.RequestException as e: logger.error(f"Request error: {e}") if retry_count < self.max_retries: logger.info(f"Retrying request (attempt {retry_count + 1})") return self._make_request( method, endpoint, data, headers, retry_count + 1 ) return None except Exception as e: logger.error(f"Unexpected error: {e}") return None def _get_headers(self) -> Dict[str, str]: """Get default headers for GitHub API requests""" headers = { "Accept": "application/vnd.github.v3+json", "Content-Type": "application/json", } if hasattr(self, "access_token") and self.access_token: headers["Authorization"] = f"token {self.access_token}" return headers def set_access_token(self, access_token: str): """Set GitHub access token for authenticated requests""" self.access_token = access_token async def get_user_profile(self) -> Optional[Dict]: """Get authenticated user profile""" return self._make_request("GET", "/user") async def get_organizations(self) -> List[Dict]: """Get user's organizations""" result = self._make_request("GET", "/user/orgs") return result if result else [] async def get_repositories( self, org: Optional[str] = None, visibility: str = "all" ) -> List[Dict]: """Get repositories for user or organization""" if org: endpoint = f"/orgs/{org}/repos" else: endpoint = "/user/repos" params = {"visibility": visibility, "per_page": 100} result = self._make_request("GET", endpoint) return result if result else [] async def get_repository(self, owner: str, repo: str) -> Optional[Dict]: """Get specific repository details""" return self._make_request("GET", f"/repos/{owner}/{repo}") async def create_repository( self, name: str, description: str = "", private: bool = False, auto_init: bool = False, ) -> Optional[Dict]: """Create a new repository""" data = { "name": name, "description": description, "private": private, "auto_init": auto_init, } return self._make_request("POST", "/user/repos", data) async def get_issues( self, owner: str, repo: str, state: str = "open", labels: Optional[List[str]] = None, ) -> List[Dict]: """Get issues for a repository""" endpoint = f"/repos/{owner}/{repo}/issues" params = {"state": state} if labels: params["labels"] = ",".join(labels) result = self._make_request("GET", endpoint) return result if result else [] async def create_issue( self, owner: str, repo: str, title: str, body: str = "", labels: Optional[List[str]] = None, assignees: Optional[List[str]] = None, ) -> Optional[Dict]: """Create a new issue""" data = { "title": title, "body": body, "labels": labels or [], "assignees": assignees or [], } return self._make_request("POST", f"/repos/{owner}/{repo}/issues", data) async def update_issue( self, owner: str, repo: str, issue_number: int, title: Optional[str] = None, body: Optional[str] = None, state: Optional[str] = None, labels: Optional[List[str]] = None, ) -> Optional[Dict]: """Update an existing issue""" data = {} if title is not None: data["title"] = title if body is not None: data["body"] = body if state is not None: data["state"] = state if labels is not None: data["labels"] = labels return self._make_request( "PATCH", f"/repos/{owner}/{repo}/issues/{issue_number}", data ) async def get_pull_requests( self, owner: str, repo: str, state: str = "open" ) -> List[Dict]: """Get pull requests for a repository""" endpoint = f"/repos/{owner}/{repo}/pulls" params = {"state": state} result = self._make_request("GET", endpoint) return result if result else [] async def create_pull_request( self, owner: str, repo: str, title: str, head: str, base: str, body: str = "" ) -> Optional[Dict]: """Create a new pull request""" data = {"title": title, "head": head, "base": base, "body": body} return self._make_request("POST", f"/repos/{owner}/{repo}/pulls", data) async def get_pull_request_reviews( self, owner: str, repo: str, pull_number: int ) -> List[Dict]: """Get reviews for a pull request""" result = self._make_request( "GET", f"/repos/{owner}/{repo}/pulls/{pull_number}/reviews" ) return result if result else [] async def create_pull_request_review( self, owner: str, repo: str, pull_number: int, body: str, event: str = "COMMENT" ) -> Optional[Dict]: """Create a review for a pull request""" data = { "body": body, "event": event, # APPROVE, REQUEST_CHANGES, COMMENT } return self._make_request( "POST", f"/repos/{owner}/{repo}/pulls/{pull_number}/reviews", data ) async def get_workflow_runs( self, owner: str, repo: str, branch: Optional[str] = None ) -> List[Dict]: """Get workflow runs for a repository""" endpoint = f"/repos/{owner}/{repo}/actions/runs" params = {} if branch: params["branch"] = branch result = self._make_request("GET", endpoint) if result and "workflow_runs" in result: return result["workflow_runs"] return [] async def trigger_workflow( self, owner: str, repo: str, workflow_id: str, ref: str = "main" ) -> Optional[Dict]: """Trigger a workflow dispatch""" data = {"ref": ref} return self._make_request( "POST", f"/repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches", data, ) async def get_teams(self, org: str) -> List[Dict]: """Get teams for an organization""" result = self._make_request("GET", f"/orgs/{org}/teams") return result if result else [] async def get_team_members(self, org: str, team_slug: str) -> List[Dict]: """Get members of a team""" result = self._make_request("GET", f"/orgs/{org}/teams/{team_slug}/members") return result if result else [] async def get_projects(self, owner: str, repo: str) -> List[Dict]: """Get projects for a repository""" result = self._make_request("GET", f"/repos/{owner}/{repo}/projects") return result if result else [] async def create_webhook( self, owner: str, repo: str, url: str, events: List[str] = ["push", "pull_request"], ) -> Optional[Dict]: """Create a webhook for a repository""" data = { "name": "web", "active": True, "events": events, "config": {"url": url, "content_type": "json"}, } return self._make_request("POST", f"/repos/{owner}/{repo}/hooks", data) async def search_code(self, query: str, org: Optional[str] = None) -> List[Dict]: """Search code across repositories""" if org: query = f"org:{org} {query}" result = self._make_request("GET", f"/search/code?q={query}") if result and "items" in result: return result["items"] return [] async def search_issues(self, query: str, org: Optional[str] = None) -> List[Dict]: """Search issues and pull requests""" if org: query = f"org:{org} {query}" result = self._make_request("GET", f"/search/issues?q={query}") if result and "items" in result: return result["items"] return [] async def get_branches(self, owner: str, repo: str) -> List[Dict]: """Get branches for a repository""" result = self._make_request("GET", f"/repos/{owner}/{repo}/branches") return result if result else [] async def create_branch( self, owner: str, repo: str, branch_name: str, from_branch: str = "main" ) -> Optional[Dict]: """Create a new branch""" # First get the SHA of the base branch ref_result = self._make_request( "GET", f"/repos/{owner}/{repo}/git/refs/heads/{from_branch}" ) if not ref_result: return None sha = ref_result["object"]["sha"] data = {"ref": f"refs/heads/{branch_name}", "sha": sha} return self._make_request("POST", f"/repos/{owner}/{repo}/git/refs", data) async def get_commits( self, owner: str, repo: str, branch: str = "main", since: Optional[str] = None ) -> List[Dict]: """Get commits for a repository""" endpoint = f"/repos/{owner}/{repo}/commits" params = {"sha": branch} if since: params["since"] = since result = self._make_request("GET", endpoint) return result if result else [] async def get_rate_limit(self) -> Optional[Dict]: """Get current rate limit status""" return self._make_request("GET", "/rate_limit") async def health_check(self) -> Dict[str, Any]: """Perform health check of GitHub integration""" try: # Test basic API connectivity user_profile = await self.get_user_profile() rate_limit = await self.get_rate_limit() if user_profile and rate_limit: return { "status": "healthy", "service": "github", "user": user_profile.get("login"), "rate_limit_remaining": self.rate_limit_remaining, "rate_limit_reset": self.rate_limit_reset, "timestamp": datetime.now().isoformat(), } else: return { "status": "unhealthy", "service": "github", "error": "Unable to fetch user profile or rate limit", "timestamp": datetime.now().isoformat(), } except Exception as e: logger.error(f"GitHub health check failed: {e}") return { "status": "unhealthy", "service": "github", "error": str(e), "timestamp": datetime.now().isoformat(), } # Global service instance github_service = GitHubService()