OpenTriage_AI / services /github_service.py
KrishnaCosmic's picture
Fix missing await for async AI service calls
a8b3a89
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()