11"""GitHub API interactions for context collection."""
22
3+ from __future__ import annotations
4+
35from dataclasses import dataclass
46from typing import Optional
57
6- import requests
8+ from lab_connectors . http import HttpClient
79
810
911@dataclass
@@ -51,11 +53,12 @@ def __init__(self, org: str, token: Optional[str] = None):
5153 self .org = org
5254 self .token = token
5355 self .base_url = "https://api.github.com"
56+ self ._http = HttpClient (timeout = 10 )
5457 # Populated by get_prs/get_issues — maps "<repo>:prs" or "<repo>:issues" to error message
5558 self .fetch_errors : dict [str , str ] = {}
5659
5760 def collector_warning (self ) -> str | None :
58- """Return a human-readable warning if fetch errors suggest rate-limit or auth degradation."""
61+ """Return a warning if fetch errors suggest rate-limit or auth degradation."""
5962 if not self .fetch_errors :
6063 return None
6164 msgs = " " .join (self .fetch_errors .values ()).lower ()
@@ -105,19 +108,29 @@ def get_issues(self, repos: list[str], state: str = "open") -> list[Issue]:
105108 self .fetch_errors [f"{ repo } :issues" ] = str (e )
106109 return issues
107110
111+ def _raise_on_bad_status (self , result , url_desc : str ) -> None :
112+ """Raise RuntimeError if result is error or response status >= 400."""
113+ if not result .is_ok :
114+ raise RuntimeError (f"{ url_desc } : { result .err } " )
115+ status = result .response .status_code # type: ignore[union-attr]
116+ if status >= 400 :
117+ raise RuntimeError (f"{ url_desc } : HTTP { status } " )
118+
119+ def _headers (self ) -> dict [str , str ]:
120+ """Build Authorization headers if token is set."""
121+ if self .token :
122+ return {"Authorization" : f"token { self .token } " }
123+ return {}
124+
108125 def _get_repo_prs (self , repo : str , state : str = "open" ) -> list [PR ]:
109126 """Get PRs for a specific repo."""
110127 url = f"{ self .base_url } /repos/{ self .org } /{ repo } /pulls"
111128 params = {"state" : state , "per_page" : 50 }
112- headers = {}
113- if self .token :
114- headers ["Authorization" ] = f"token { self .token } "
115-
116- response = requests .get (url , params = params , headers = headers , timeout = 10 )
117- response .raise_for_status ()
129+ result = self ._http .get (url , params = params , headers = self ._headers ())
130+ self ._raise_on_bad_status (result , url )
118131
119132 prs = []
120- for item in response .json ():
133+ for item in result . response .json (): # type: ignore[union-attr]
121134 prs .append (
122135 PR (
123136 number = item ["number" ],
@@ -145,13 +158,10 @@ def get_raw_file(self, repo: str, path: str, ref: str = "main") -> str | None:
145158 Raw file content as string, or None on failure.
146159 """
147160 url = f"https://raw.githubusercontent.com/{ self .org } /{ repo } /{ ref } /{ path } "
148- headers = {}
149- if self .token :
150- headers ["Authorization" ] = f"token { self .token } "
161+ result = self ._http .get (url , headers = self ._headers ())
151162 try :
152- response = requests .get (url , headers = headers , timeout = 10 )
153- response .raise_for_status ()
154- return response .text
163+ self ._raise_on_bad_status (result , url )
164+ return result .response .text # type: ignore[union-attr]
155165 except Exception as exc :
156166 self .fetch_errors [f"{ repo } :{ path } " ] = str (exc )
157167 return None
@@ -161,24 +171,21 @@ def get_repos_info(self, repos: list[str]) -> dict[str, RepoInfo]:
161171
162172 Returns a dict keyed by repo name. Missing/failed repos are skipped silently.
163173 """
164- result : dict [str , RepoInfo ] = {}
165- headers = {}
166- if self .token :
167- headers ["Authorization" ] = f"token { self .token } "
174+ result_map : dict [str , RepoInfo ] = {}
168175 for repo in repos :
169176 try :
170177 url = f"{ self .base_url } /repos/{ self .org } /{ repo } "
171- response = requests . get (url , headers = headers , timeout = 10 )
172- response . raise_for_status ( )
173- data = response .json ()
174- result [repo ] = RepoInfo (
178+ result = self . _http . get (url , headers = self . _headers () )
179+ self . _raise_on_bad_status ( result , url )
180+ data = result . response .json () # type: ignore[union-attr]
181+ result_map [repo ] = RepoInfo (
175182 name = repo ,
176183 description = data .get ("description" ) or "" ,
177184 url = data .get ("html_url" , "" ),
178185 )
179186 except Exception as exc :
180187 self .fetch_errors [f"{ repo } :info" ] = str (exc )
181- return result
188+ return result_map
182189
183190 def _get_repo_issues (self , repo : str , state : str = "open" ) -> list [Issue ]:
184191 """Get issues for a specific repo (excluding pull requests).
@@ -188,15 +195,11 @@ def _get_repo_issues(self, repo: str, state: str = "open") -> list[Issue]:
188195 """
189196 url = f"{ self .base_url } /repos/{ self .org } /{ repo } /issues"
190197 params = {"state" : state , "per_page" : 50 }
191- headers = {}
192- if self .token :
193- headers ["Authorization" ] = f"token { self .token } "
194-
195- response = requests .get (url , params = params , headers = headers , timeout = 10 )
196- response .raise_for_status ()
198+ result = self ._http .get (url , params = params , headers = self ._headers ())
199+ self ._raise_on_bad_status (result , url )
197200
198201 issues = []
199- for item in response .json ():
202+ for item in result . response .json (): # type: ignore[union-attr]
200203 # Skip pull requests: they have a pull_request field
201204 if "pull_request" in item :
202205 continue
0 commit comments