Skip to content

Commit dbd1b81

Browse files
use github app for service instead of personal pat (with the option to for local dev)
1 parent 8c540d4 commit dbd1b81

File tree

3 files changed

+73
-17
lines changed

3 files changed

+73
-17
lines changed

backend/app/routers/generate.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515
router = APIRouter(prefix="/generate", tags=["Claude"])
1616

1717
# Initialize services
18-
github_token = os.getenv("GITHUB_PAT") # might hit rate limit on just my token
19-
github_service = GitHubService(github_token)
18+
github_service = GitHubService()
2019
claude_service = ClaudeService()
2120

2221

backend/app/services/github_service.py

+68-15
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,80 @@
11
import requests
2+
import jwt
3+
import time
4+
from datetime import datetime, timedelta
25
from dotenv import load_dotenv
6+
import os
37

48
load_dotenv()
59

610

711
class GitHubService:
8-
def __init__(self, github_token):
9-
self.github_token = github_token
10-
self.headers = {
11-
"Authorization": f"token {self.github_token}",
12-
"Accept": "application/vnd.github+json"
12+
def __init__(self):
13+
# Try app authentication first
14+
self.client_id = os.getenv("GITHUB_CLIENT_ID")
15+
self.private_key = os.getenv("GITHUB_PRIVATE_KEY")
16+
self.installation_id = os.getenv("GITHUB_INSTALLATION_ID")
17+
18+
# Fallback to PAT if app credentials not found
19+
self.github_token = os.getenv("GITHUB_PAT")
20+
21+
if not all([self.client_id, self.private_key, self.installation_id]) and not self.github_token:
22+
raise ValueError(
23+
"Either GitHub App credentials or PAT must be provided")
24+
25+
self.access_token = None
26+
self.token_expires_at = None
27+
28+
# autopep8: off
29+
def _generate_jwt(self):
30+
now = int(time.time())
31+
payload = {
32+
"iat": now,
33+
"exp": now + (10 * 60), # 10 minutes
34+
"iss": self.client_id
35+
}
36+
# Convert PEM string format to proper newlines
37+
return jwt.encode(payload, self.private_key, algorithm="RS256") # type: ignore
38+
# autopep8: on
39+
40+
def _get_installation_token(self):
41+
if self.access_token and self.token_expires_at > datetime.now(): # type: ignore
42+
return self.access_token
43+
44+
jwt_token = self._generate_jwt()
45+
response = requests.post(
46+
f"https://api.github.com/app/installations/{
47+
self.installation_id}/access_tokens",
48+
headers={
49+
"Authorization": f"Bearer {jwt_token}",
50+
"Accept": "application/vnd.github+json"
51+
}
52+
)
53+
data = response.json()
54+
self.access_token = data["token"]
55+
self.token_expires_at = datetime.now() + timedelta(hours=1)
56+
return self.access_token
57+
58+
def _get_headers(self):
59+
# Use PAT if app credentials not available
60+
if not all([self.client_id, self.private_key, self.installation_id]):
61+
return {
62+
"Authorization": f"token {self.github_token}",
63+
"Accept": "application/vnd.github+json"
64+
}
65+
66+
# Otherwise use app authentication
67+
token = self._get_installation_token()
68+
return {
69+
"Authorization": f"Bearer {token}",
70+
"Accept": "application/vnd.github+json",
71+
"X-GitHub-Api-Version": "2022-11-28"
1372
}
1473

1574
def get_default_branch(self, username, repo):
1675
"""Get the default branch of the repository."""
1776
api_url = f"https://api.github.com/repos/{username}/{repo}"
18-
response = requests.get(api_url, headers=self.headers)
77+
response = requests.get(api_url, headers=self._get_headers())
1978

2079
if response.status_code == 200:
2180
return response.json().get('default_branch')
@@ -57,7 +116,7 @@ def should_include_file(path):
57116
if branch:
58117
api_url = f"https://api.github.com/repos/{
59118
username}/{repo}/git/trees/{branch}?recursive=1"
60-
response = requests.get(api_url, headers=self.headers)
119+
response = requests.get(api_url, headers=self._get_headers())
61120

62121
if response.status_code == 200:
63122
data = response.json()
@@ -71,7 +130,7 @@ def should_include_file(path):
71130
for branch in ['main', 'master']:
72131
api_url = f"https://api.github.com/repos/{
73132
username}/{repo}/git/trees/{branch}?recursive=1"
74-
response = requests.get(api_url, headers=self.headers)
133+
response = requests.get(api_url, headers=self._get_headers())
75134

76135
if response.status_code == 200:
77136
data = response.json()
@@ -96,13 +155,7 @@ def get_github_readme(self, username, repo):
96155
str: The contents of the README file.
97156
"""
98157
api_url = f"https://api.github.com/repos/{username}/{repo}/readme"
99-
100-
headers = {
101-
"Authorization": f"token {self.github_token}",
102-
"Accept": "application/vnd.github+json"
103-
}
104-
105-
response = requests.get(api_url, headers=headers)
158+
response = requests.get(api_url, headers=self._get_headers())
106159

107160
if response.status_code == 404:
108161
raise ValueError("Repository not found.")

backend/requirements.txt

+4
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ anthropic==0.42.0
33
anyio==4.7.0
44
api-analytics==1.2.5
55
certifi==2024.12.14
6+
cffi==1.17.1
67
charset-normalizer==3.4.0
78
click==8.1.7
9+
cryptography==44.0.0
810
Deprecated==1.2.15
911
distro==1.9.0
1012
dnspython==2.7.0
@@ -23,9 +25,11 @@ markdown-it-py==3.0.0
2325
MarkupSafe==3.0.2
2426
mdurl==0.1.2
2527
packaging==24.2
28+
pycparser==2.22
2629
pydantic==2.10.3
2730
pydantic_core==2.27.1
2831
Pygments==2.18.0
32+
PyJWT==2.10.1
2933
python-dotenv==1.0.1
3034
python-multipart==0.0.19
3135
PyYAML==6.0.2

0 commit comments

Comments
 (0)