OpenTriage_AI / services /github_service.py
KrishnaCosmic's picture
Fix missing await for async AI service calls
a8b3a89
Raw History Blame Contribute Delete
19.6 kB
import httpx
import logging
from typing import List, Dict, Optional
from fastapi import HTTPException
logger = logging.getLogger(__name__)
class GitHubService:
"""Service for interacting with GitHub API."""
def __init__(self):
self.base_url = "https://api.github.com"
async def fetch_maintainer_repos(
self,
github_access_token: str,
existing_repos: List[str]
) -> List[Dict[str, str]]:
"""
Fetch all repositories where the user has admin, maintain, or push permissions,
excluding repos already added to the website.
Args:
github_access_token: GitHub OAuth token
existing_repos: List of repo full names already added
Returns:
List of dicts with 'owner' and 'repo' keys
"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/user/repos",
params={"per_page": 100, "type": "owner,collaborator"},
headers={
"Authorization": f"Bearer {github_access_token}",
"Accept": "application/vnd.github+json"
},
timeout=30.0
)
if response.status_code != 200:
raise HTTPException(
status_code=response.status_code,
detail=f"GitHub API error: {response.status_code} {response.text}"
)
repos = response.json()
# Filter repos with required permissions and not in existing_repos
filtered_repos = []
for repo in repos:
permissions = repo.get('permissions', {})
has_permission = (
permissions.get('admin') or
permissions.get('maintain') or
permissions.get('push')
)
if has_permission and repo['full_name'] not in existing_repos:
filtered_repos.append({
'owner': repo['owner']['login'],
'repo': repo['name']
})
return filtered_repos
except httpx.TimeoutException:
raise HTTPException(status_code=504, detail="GitHub API request timed out")
except Exception as e:
logger.error(f"Failed to fetch maintainer repos: {e}")
raise HTTPException(status_code=500, detail=f"Failed to fetch maintainer repos: {str(e)}")
async def fetch_unmerged_pull_requests(
self,
github_access_token: str,
owner: str,
repo: str
) -> List[Dict[str, any]]:
"""
Fetch only pull requests that are open and not merged.
Args:
github_access_token: GitHub OAuth token
owner: Repository owner
repo: Repository name
Returns:
List of dicts with 'number' and 'title' keys
"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/repos/{owner}/{repo}/pulls",
params={"state": "open"},
headers={
"Authorization": f"Bearer {github_access_token}",
"Accept": "application/vnd.github+json"
},
timeout=30.0
)
if response.status_code != 200:
raise HTTPException(
status_code=response.status_code,
detail=f"GitHub API error: {response.status_code} {response.text}"
)
pulls = response.json()
# Filter only unmerged PRs (merged_at is null)
unmerged_prs = [
{
'number': pr['number'],
'title': pr['title']
}
for pr in pulls
if pr.get('merged_at') is None
]
return unmerged_prs
except httpx.TimeoutException:
raise HTTPException(status_code=504, detail="GitHub API request timed out")
except Exception as e:
logger.error(f"Failed to fetch unmerged PRs for {owner}/{repo}: {e}")
raise HTTPException(
status_code=500,
detail=f"Failed to fetch unmerged PRs for {owner}/{repo}: {str(e)}"
)
async def comment_on_pull_request(
self,
github_access_token: str,
owner: str,
repo: str,
pr_number: int,
comment_text: str
) -> Dict:
"""
Post a comment on a pull request after verifying repo access.
Args:
github_access_token: GitHub OAuth token
owner: Repository owner
repo: Repository name
pr_number: Pull request number
comment_text: Comment body text
Returns:
GitHub API response for the created comment
"""
try:
async with httpx.AsyncClient() as client:
# First, verify repo access
repo_response = await client.get(
f"{self.base_url}/repos/{owner}/{repo}",
headers={
"Authorization": f"Bearer {github_access_token}",
"Accept": "application/vnd.github+json"
},
timeout=30.0
)
if repo_response.status_code != 200:
raise HTTPException(
status_code=repo_response.status_code,
detail=f"Cannot access repository: {repo_response.status_code} {repo_response.text}"
)
repo_data = repo_response.json()
permissions = repo_data.get('permissions', {})
# Check if user has required permissions
if not (permissions.get('admin') or permissions.get('maintain') or permissions.get('push')):
raise HTTPException(
status_code=403,
detail="Insufficient permissions to comment on this repository"
)
# Post the comment
comment_response = await client.post(
f"{self.base_url}/repos/{owner}/{repo}/issues/{pr_number}/comments",
json={"body": comment_text},
headers={
"Authorization": f"Bearer {github_access_token}",
"Accept": "application/vnd.github+json",
"Content-Type": "application/json"
},
timeout=30.0
)
if comment_response.status_code not in [200, 201]:
raise HTTPException(
status_code=comment_response.status_code,
detail=f"Failed to post comment: {comment_response.status_code} {comment_response.text}"
)
return comment_response.json()
except HTTPException:
raise
except httpx.TimeoutException:
raise HTTPException(status_code=504, detail="GitHub API request timed out")
except Exception as e:
logger.error(f"Failed to comment on PR #{pr_number} in {owner}/{repo}: {e}")
raise HTTPException(
status_code=500,
detail=f"Failed to comment on PR #{pr_number} in {owner}/{repo}: {str(e)}"
)
async def comment_on_issue(
self,
github_access_token: str,
owner: str,
repo: str,
issue_number: int,
comment_text: str
) -> Dict:
"""
Post a comment on a GitHub issue after verifying repo access.
Args:
github_access_token: GitHub OAuth token
owner: Repository owner
repo: Repository name
issue_number: Issue number
comment_text: Comment body text
Returns:
GitHub API response for the created comment
"""
try:
async with httpx.AsyncClient() as client:
# First, verify repo access
repo_response = await client.get(
f"{self.base_url}/repos/{owner}/{repo}",
headers={
"Authorization": f"Bearer {github_access_token}",
"Accept": "application/vnd.github+json"
},
timeout=30.0
)
if repo_response.status_code != 200:
raise HTTPException(
status_code=repo_response.status_code,
detail=f"Cannot access repository: {repo_response.status_code} {repo_response.text}"
)
repo_data = repo_response.json()
permissions = repo_data.get('permissions', {})
# Check if user has required permissions
if not (permissions.get('admin') or permissions.get('maintain') or permissions.get('push')):
raise HTTPException(
status_code=403,
detail="Insufficient permissions to comment on this repository"
)
# Post the comment
comment_response = await client.post(
f"{self.base_url}/repos/{owner}/{repo}/issues/{issue_number}/comments",
json={"body": comment_text},
headers={
"Authorization": f"Bearer {github_access_token}",
"Accept": "application/vnd.github+json",
"Content-Type": "application/json"
},
timeout=30.0
)
if comment_response.status_code not in [200, 201]:
raise HTTPException(
status_code=comment_response.status_code,
detail=f"Failed to post comment: {comment_response.status_code} {comment_response.text}"
)
return comment_response.json()
except HTTPException:
raise
except httpx.TimeoutException:
raise HTTPException(status_code=504, detail="GitHub API request timed out")
except Exception as e:
logger.error(f"Failed to comment on issue #{issue_number} in {owner}/{repo}: {e}")
raise HTTPException(
status_code=500,
detail=f"Failed to comment on issue #{issue_number} in {owner}/{repo}: {str(e)}"
)
async def fetch_issue_comments(
self,
github_access_token: str,
owner: str,
repo: str,
issue_number: int
) -> List[Dict]:
"""
Fetch all comments for a GitHub issue.
Args:
github_access_token: GitHub OAuth token
owner: Repository owner
repo: Repository name
issue_number: Issue number
Returns:
List of comment objects from GitHub API
"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/repos/{owner}/{repo}/issues/{issue_number}/comments",
headers={
"Authorization": f"Bearer {github_access_token}",
"Accept": "application/vnd.github+json"
},
timeout=30.0
)
if response.status_code != 200:
raise HTTPException(
status_code=response.status_code,
detail=f"Failed to fetch comments: {response.status_code} {response.text}"
)
return response.json()
except HTTPException:
raise
except httpx.TimeoutException:
raise HTTPException(status_code=504, detail="GitHub API request timed out")
except Exception as e:
logger.error(f"Failed to fetch comments for issue #{issue_number} in {owner}/{repo}: {e}")
raise HTTPException(
status_code=500,
detail=f"Failed to fetch comments for issue #{issue_number} in {owner}/{repo}: {str(e)}"
)
async def fetch_repo_issues(self, repo_full_name: str, github_access_token: Optional[str] = None, include_prs: bool = True) -> Dict:
"""Fetch issues and PRs from a GitHub repository."""
try:
async with httpx.AsyncClient() as client:
issues_url = f"{self.base_url}/repos/{repo_full_name}/issues"
params = {"state": "all", "per_page": 100}
headers = {"Accept": "application/vnd.github+json"}
if github_access_token:
headers["Authorization"] = f"Bearer {github_access_token}"
response = await client.get(issues_url, params=params, headers=headers, timeout=30.0)
if response.status_code != 200:
raise HTTPException(
status_code=400,
detail=f"Failed to fetch from GitHub: {response.text}"
)
items = response.json()
issues = []
prs = []
for item in items:
is_pr = 'pull_request' in item
if is_pr:
prs.append(item)
else:
issues.append(item)
return {"issues": issues, "prs": prs if include_prs else []}
except Exception as e:
logger.error(f"GitHub fetch error: {e}")
raise HTTPException(status_code=500, detail=str(e))
async def fetch_user_activity(self, username: str, github_access_token: Optional[str] = None) -> Dict:
"""Fetch a user's GitHub activity (issues and PRs)."""
try:
async with httpx.AsyncClient() as client:
search_url = f"{self.base_url}/search/issues"
params = {
"q": f"author:{username}",
"per_page": 100,
"sort": "created",
"order": "desc"
}
headers = {"Accept": "application/vnd.github+json"}
if github_access_token:
headers["Authorization"] = f"Bearer {github_access_token}"
response = await client.get(search_url, params=params, headers=headers, timeout=30.0)
if response.status_code != 200:
logger.error(f"GitHub search error: {response.text}")
return {"issues": [], "prs": []}
items = response.json().get('items', [])
issues = []
prs = []
for item in items:
is_pr = 'pull_request' in item
if is_pr:
prs.append(item)
else:
issues.append(item)
return {"issues": issues, "prs": prs}
except Exception as e:
logger.error(f"GitHub user activity fetch error: {e}")
return {"issues": [], "prs": []}
async def fetch_repository_readme(self, repo_full_name: str, github_access_token: Optional[str] = None) -> str:
"""
Fetch the README content for a repository.
"""
try:
async with httpx.AsyncClient() as client:
url = f"{self.base_url}/repos/{repo_full_name}/readme"
headers = {"Accept": "application/vnd.github.raw+json"}
if github_access_token:
headers["Authorization"] = f"Bearer {github_access_token}"
response = await client.get(url, headers=headers, timeout=30.0)
if response.status_code == 404:
return ""
if response.status_code != 200:
logger.error(f"Failed to fetch README for {repo_full_name}: {response.text}")
return ""
return response.text
except Exception as e:
logger.error(f"README fetch error for {repo_full_name}: {e}")
return ""
async def fetch_contributing_file(self, repo_full_name: str, github_access_token: Optional[str] = None) -> str:
"""
Fetch the CONTRIBUTING.md content for a repository.
Tries multiple common paths: CONTRIBUTING.md, .github/CONTRIBUTING.md, docs/CONTRIBUTING.md
"""
paths_to_try = [
"CONTRIBUTING.md",
".github/CONTRIBUTING.md",
"docs/CONTRIBUTING.md",
"contributing.md",
]
try:
async with httpx.AsyncClient() as client:
headers = {"Accept": "application/vnd.github.raw+json"}
if github_access_token:
headers["Authorization"] = f"Bearer {github_access_token}"
for path in paths_to_try:
url = f"{self.base_url}/repos/{repo_full_name}/contents/{path}"
response = await client.get(url, headers=headers, timeout=30.0)
if response.status_code == 200:
return response.text
return ""
except Exception as e:
logger.error(f"CONTRIBUTING fetch error for {repo_full_name}: {e}")
return ""
async def fetch_repository_docs(
self,
repo_full_name: str,
github_access_token: Optional[str] = None
) -> Dict[str, str]:
"""
Fetch README and CONTRIBUTING files for RAG indexing.
Returns dict with 'readme' and 'contributing' keys.
"""
readme = await self.fetch_repository_readme(repo_full_name, github_access_token)
contributing = await self.fetch_contributing_file(repo_full_name, github_access_token)
return {
"readme": readme,
"contributing": contributing
}
# Singleton instance
github_service = GitHubService()