1- import logging
2- from typing import Any
1+ from typing import NoReturn
32
4- import httpx
5- from fastapi import APIRouter , Depends , HTTPException , Query , status
3+ from fastapi import APIRouter , Depends , HTTPException , Query
64
7- from app .core .deps import require_github_token
5+ from app .core .deps import get_github_service
86from app .core .security import get_current_user
97from app .models .db_models .user import User
108from app .models .schemas .github import (
1311 GitHubCollaborator ,
1412 GitHubPRCommentsResponse ,
1513 GitHubPRListResponse ,
16- GitHubPullRequest ,
17- GitHubRepo ,
1814 GitHubReposResponse ,
19- GitHubReviewComment ,
2015)
16+ from app .services .exceptions import GitHubException
17+ from app .services .github import GitHubService
2118
2219router = APIRouter ()
23- logger = logging .getLogger (__name__ )
2420
25- GITHUB_API_BASE = "https://api.github.com"
2621
27-
28- def _github_headers (token : str ) -> dict [str , str ]:
29- return {
30- "Authorization" : f"Bearer { token } " ,
31- "Accept" : "application/vnd.github+json" ,
32- "X-GitHub-Api-Version" : "2022-11-28" ,
33- }
34-
35-
36- def _check_github_response (response : httpx .Response ) -> None :
37- if response .status_code == 401 :
38- raise HTTPException (
39- status_code = status .HTTP_401_UNAUTHORIZED ,
40- detail = "GitHub token is invalid or expired" ,
41- )
42- if response .status_code != 200 :
43- logger .warning (
44- "GitHub API returned %d: %s" , response .status_code , response .text [:200 ]
45- )
46- raise HTTPException (
47- status_code = status .HTTP_502_BAD_GATEWAY ,
48- detail = "GitHub API request failed" ,
49- )
22+ def _raise_from_github (exc : GitHubException ) -> NoReturn :
23+ raise HTTPException (status_code = exc .status_code , detail = exc .message ) from exc
5024
5125
5226@router .get ("/repositories" , response_model = GitHubReposResponse )
@@ -55,110 +29,25 @@ async def list_repositories(
5529 page : int = Query (default = 1 , ge = 1 ),
5630 per_page : int = Query (default = 20 , ge = 1 , le = 100 ),
5731 _current_user : User = Depends (get_current_user ),
58- github_token : str = Depends (require_github_token ),
32+ github : GitHubService = Depends (get_github_service ),
5933) -> GitHubReposResponse :
60- headers = _github_headers (github_token )
61-
62- async with httpx .AsyncClient (timeout = 10.0 ) as client :
63- if q .strip ():
64- response = await client .get (
65- f"{ GITHUB_API_BASE } /search/repositories" ,
66- params = {
67- "q" : q .strip (),
68- "sort" : "updated" ,
69- "order" : "desc" ,
70- "per_page" : per_page ,
71- "page" : page ,
72- },
73- headers = headers ,
74- )
75- else :
76- response = await client .get (
77- f"{ GITHUB_API_BASE } /user/repos" ,
78- params = {
79- "sort" : "pushed" ,
80- "direction" : "desc" ,
81- "per_page" : per_page ,
82- "page" : page ,
83- "affiliation" : "owner,collaborator,organization_member" ,
84- },
85- headers = headers ,
86- )
87-
88- _check_github_response (response )
89-
90- data = response .json ()
91- raw_repos = data .get ("items" , data ) if isinstance (data , dict ) else data
92-
93- repos = [
94- GitHubRepo (
95- name = r ["name" ],
96- full_name = r ["full_name" ],
97- description = r .get ("description" ),
98- language = r .get ("language" ),
99- html_url = r ["html_url" ],
100- clone_url = r ["clone_url" ],
101- private = r .get ("private" , False ),
102- pushed_at = r .get ("pushed_at" ),
103- stargazers_count = r .get ("stargazers_count" , 0 ),
104- )
105- for r in raw_repos
106- ]
107-
108- return GitHubReposResponse (items = repos , has_more = len (raw_repos ) == per_page )
34+ try :
35+ return await github .list_repositories (q , page , per_page )
36+ except GitHubException as exc :
37+ _raise_from_github (exc )
10938
11039
11140@router .get ("/pulls" , response_model = GitHubPRListResponse )
11241async def list_pull_requests (
11342 owner : str = Query (..., min_length = 1 ),
11443 repo : str = Query (..., min_length = 1 ),
11544 _current_user : User = Depends (get_current_user ),
116- github_token : str = Depends (require_github_token ),
45+ github : GitHubService = Depends (get_github_service ),
11746) -> GitHubPRListResponse :
118- headers = _github_headers (github_token )
119-
120- async with httpx .AsyncClient (timeout = 10.0 ) as client :
121- response = await client .get (
122- f"{ GITHUB_API_BASE } /repos/{ owner } /{ repo } /pulls" ,
123- params = {
124- "state" : "open" ,
125- "per_page" : 30 ,
126- "sort" : "updated" ,
127- "direction" : "desc" ,
128- },
129- headers = headers ,
130- )
131-
132- _check_github_response (response )
133-
134- raw_prs = response .json ()
135- prs = [
136- GitHubPullRequest (
137- number = pr ["number" ],
138- title = pr ["title" ],
139- body = pr .get ("body" ),
140- state = pr ["state" ],
141- html_url = pr ["html_url" ],
142- head = {
143- "ref" : pr ["head" ]["ref" ],
144- "repo" : {
145- "full_name" : pr ["head" ]["repo" ]["full_name" ]
146- if pr ["head" ].get ("repo" )
147- else ""
148- },
149- },
150- base = {"ref" : pr ["base" ]["ref" ]},
151- user = {
152- "login" : pr ["user" ]["login" ],
153- "avatar_url" : pr ["user" ].get ("avatar_url" , "" ),
154- },
155- draft = pr .get ("draft" , False ),
156- review_comments = pr .get ("review_comments" , 0 ),
157- )
158- for pr in raw_prs
159- ]
160-
161- return GitHubPRListResponse (items = prs )
47+ try :
48+ return await github .list_pull_requests (owner , repo )
49+ except GitHubException as exc :
50+ _raise_from_github (exc )
16251
16352
16453@router .get (
@@ -170,152 +59,34 @@ async def get_pr_comments(
17059 repo : str ,
17160 number : int ,
17261 _current_user : User = Depends (get_current_user ),
173- github_token : str = Depends (require_github_token ),
62+ github : GitHubService = Depends (get_github_service ),
17463) -> GitHubPRCommentsResponse :
175- headers = _github_headers (github_token )
176- url = f"{ GITHUB_API_BASE } /repos/{ owner } /{ repo } /pulls/{ number } /comments"
177- raw_comments : list [dict [str , Any ]] = []
178-
179- async with httpx .AsyncClient (timeout = 10.0 ) as client :
180- page = 1
181- while page <= 10 :
182- response = await client .get (
183- url ,
184- params = {"per_page" : 100 , "page" : page },
185- headers = headers ,
186- )
187- _check_github_response (response )
188- batch = response .json ()
189- raw_comments .extend (batch )
190- if len (batch ) < 100 :
191- break
192- page += 1
193-
194- comments = [
195- GitHubReviewComment (
196- id = c ["id" ],
197- body = c ["body" ],
198- path = c .get ("path" ),
199- line = c .get ("line" ) or c .get ("original_line" ),
200- user = {
201- "login" : c ["user" ]["login" ],
202- "avatar_url" : c ["user" ].get ("avatar_url" , "" ),
203- },
204- created_at = c ["created_at" ],
205- )
206- for c in raw_comments
207- ]
208-
209- return GitHubPRCommentsResponse (comments = comments )
64+ try :
65+ return await github .get_pr_comments (owner , repo , number )
66+ except GitHubException as exc :
67+ _raise_from_github (exc )
21068
21169
21270@router .post ("/pulls" , response_model = CreatePullRequestResponse )
21371async def create_pull_request (
21472 request : CreatePullRequestRequest ,
21573 _current_user : User = Depends (get_current_user ),
216- github_token : str = Depends (require_github_token ),
74+ github : GitHubService = Depends (get_github_service ),
21775) -> CreatePullRequestResponse :
218- headers = _github_headers (github_token )
219-
220- async with httpx .AsyncClient (timeout = 15.0 ) as client :
221- response = await client .post (
222- f"{ GITHUB_API_BASE } /repos/{ request .owner } /{ request .repo } /pulls" ,
223- json = {
224- "title" : request .title ,
225- "body" : request .body ,
226- "head" : request .head ,
227- "base" : request .base ,
228- },
229- headers = headers ,
230- )
231-
232- if response .status_code == 401 :
233- raise HTTPException (
234- status_code = status .HTTP_401_UNAUTHORIZED ,
235- detail = "GitHub token is invalid or expired" ,
236- )
237- if response .status_code not in (200 , 201 ):
238- logger .warning (
239- "GitHub API returned %d: %s" ,
240- response .status_code ,
241- response .text [:200 ],
242- )
243- try :
244- detail = response .json ().get ("message" , "Failed to create pull request" )
245- except (ValueError , KeyError ):
246- detail = "Failed to create pull request"
247- raise HTTPException (
248- status_code = status .HTTP_502_BAD_GATEWAY ,
249- detail = detail ,
250- )
251-
252- pr_data = response .json ()
253-
254- reviewer_warning = None
255- if request .reviewers :
256- try :
257- reviewer_resp = await client .post (
258- f"{ GITHUB_API_BASE } /repos/{ request .owner } /{ request .repo } /pulls/{ pr_data ['number' ]} /requested_reviewers" ,
259- json = {"reviewers" : request .reviewers },
260- headers = headers ,
261- )
262- if reviewer_resp .status_code not in (200 , 201 ):
263- reviewer_warning = "Failed to assign reviewers"
264- logger .warning (
265- "Failed to assign reviewers to PR #%d: %s" ,
266- pr_data ["number" ],
267- reviewer_resp .text [:200 ],
268- )
269- except httpx .HTTPError :
270- reviewer_warning = "Failed to assign reviewers"
271- logger .warning (
272- "Failed to assign reviewers to PR #%d" , pr_data ["number" ]
273- )
274-
275- return CreatePullRequestResponse (
276- number = pr_data ["number" ],
277- html_url = pr_data ["html_url" ],
278- title = pr_data ["title" ],
279- reviewer_warning = reviewer_warning ,
280- )
76+ try :
77+ return await github .create_pull_request (request )
78+ except GitHubException as exc :
79+ _raise_from_github (exc )
28180
28281
28382@router .get ("/collaborators" )
28483async def list_collaborators (
28584 owner : str = Query (..., min_length = 1 ),
28685 repo : str = Query (..., min_length = 1 ),
28786 _current_user : User = Depends (get_current_user ),
288- github_token : str = Depends (require_github_token ),
87+ github : GitHubService = Depends (get_github_service ),
28988) -> list [GitHubCollaborator ]:
290- headers = _github_headers (github_token )
291-
292- async with httpx .AsyncClient (timeout = 10.0 ) as client :
293- response = await client .get (
294- f"{ GITHUB_API_BASE } /repos/{ owner } /{ repo } /collaborators" ,
295- params = {"per_page" : 50 },
296- headers = headers ,
297- )
298-
299- if response .status_code == 401 :
300- raise HTTPException (
301- status_code = status .HTTP_401_UNAUTHORIZED ,
302- detail = "GitHub token is invalid or expired" ,
303- )
304- # 403/404 are expected when user lacks push access to the repo
305- if response .status_code in (403 , 404 ):
306- return []
307- if response .status_code != 200 :
308- logger .warning (
309- "GitHub collaborators API returned %d: %s" ,
310- response .status_code ,
311- response .text [:200 ],
312- )
313- raise HTTPException (
314- status_code = status .HTTP_502_BAD_GATEWAY ,
315- detail = "Failed to load collaborators" ,
316- )
317-
318- return [
319- GitHubCollaborator (login = c ["login" ], avatar_url = c .get ("avatar_url" , "" ))
320- for c in response .json ()
321- ]
89+ try :
90+ return await github .list_collaborators (owner , repo )
91+ except GitHubException as exc :
92+ _raise_from_github (exc )
0 commit comments