33import fnmatch
44import os
55import time
6+ from contextlib import contextmanager
67from dataclasses import dataclass
78from datetime import datetime , timedelta , timezone
89from math import ceil
9- from typing import TYPE_CHECKING , Any , Dict , List , Optional
10+ from typing import TYPE_CHECKING , Any , Dict , Iterator , List , Optional
1011
1112from gittensor .utils .utils import parse_repo_name
1213
@@ -174,7 +175,51 @@ def make_headers(token: str) -> Dict[str, str]:
174175
175176def make_graphql_headers (token : str ) -> Dict [str , str ]:
176177 """Build GitHub GraphQL headers for a PAT."""
177- return {'Authorization' : f'Bearer { token } ' , 'Content-Type' : 'application/json' }
178+ return {
179+ 'Authorization' : f'Bearer { token } ' ,
180+ 'Content-Type' : 'application/json' ,
181+ 'Accept' : 'application/json' ,
182+ }
183+
184+
185+ def make_anonymous_headers () -> Dict [str , str ]:
186+ """Build GitHub HTTP headers for unauthenticated calls."""
187+ return {'Accept' : 'application/vnd.github.v3+json' , 'User-Agent' : 'gittensor-cli' }
188+
189+
190+ _session_cache : Optional [Dict [str , requests .Session ]] = None
191+
192+
193+ @contextmanager
194+ def session_scope () -> Iterator [None ]:
195+ """Share requests.Session per PAT within the scope; close all on exit. Not re-entrant."""
196+ global _session_cache
197+ if _session_cache is not None :
198+ raise RuntimeError ('session_scope is not re-entrant' )
199+ _session_cache = {}
200+ try :
201+ yield
202+ finally :
203+ cache = _session_cache
204+ _session_cache = None
205+ for s in cache .values ():
206+ s .close ()
207+
208+
209+ def _build_session (token : str ) -> requests .Session :
210+ session = requests .Session ()
211+ session .headers .update (make_headers (token ) if token else make_anonymous_headers ())
212+ return session
213+
214+
215+ def get_session (token : str ) -> requests .Session :
216+ """Return a requests.Session for the given PAT, reusing one within a session_scope() and allocating fresh otherwise."""
217+ if _session_cache is None :
218+ return _build_session (token )
219+ key = token or ''
220+ if key not in _session_cache :
221+ _session_cache [key ] = _build_session (token )
222+ return _session_cache [key ]
178223
179224
180225def get_github_id (token : str ) -> Optional [str ]:
@@ -189,12 +234,12 @@ def get_github_id(token: str) -> Optional[str]:
189234 if not token :
190235 return None
191236
192- headers = make_headers (token )
237+ session = get_session (token )
193238
194239 # Retry logic for timeout issues
195240 for attempt in range (6 ):
196241 try :
197- response = requests .get (f'{ BASE_GITHUB_API_URL } /user' , headers = headers , timeout = GITHUB_HTTP_TIMEOUT_SECONDS )
242+ response = session .get (f'{ BASE_GITHUB_API_URL } /user' , timeout = GITHUB_HTTP_TIMEOUT_SECONDS )
198243 if response .status_code == 200 :
199244 try :
200245 user_data : Dict [str , Any ] = response .json ()
@@ -235,14 +280,13 @@ def get_merge_base_sha(repository: str, base_sha: str, head_sha: str, token: str
235280 Returns:
236281 Merge-base commit SHA, or None if the request fails
237282 """
238- headers = make_headers (token )
283+ session = get_session (token )
239284 max_attempts = 3
240285
241286 for attempt in range (max_attempts ):
242287 try :
243- response = requests .get (
288+ response = session .get (
244289 f'{ BASE_GITHUB_API_URL } /repos/{ repository } /compare/{ base_sha } ...{ head_sha } ' ,
245- headers = headers ,
246290 timeout = 15 ,
247291 )
248292
@@ -293,7 +337,7 @@ def get_pull_request_file_changes(repository: str, pr_number: int, token: str) -
293337 """
294338 max_attempts = 3
295339 per_page = 100
296- headers = make_headers (token )
340+ session = get_session (token )
297341
298342 all_file_diffs : list = []
299343 page = 1
@@ -302,9 +346,8 @@ def get_pull_request_file_changes(repository: str, pr_number: int, token: str) -
302346
303347 while attempt < max_attempts :
304348 try :
305- response = requests .get (
349+ response = session .get (
306350 f'{ BASE_GITHUB_API_URL } /repos/{ repository } /pulls/{ pr_number } /files' ,
307- headers = headers ,
308351 params = {'per_page' : per_page , 'page' : page },
309352 timeout = 15 ,
310353 )
@@ -485,20 +528,15 @@ def _search_issue_referencing_prs_rest(
485528 if issue_number < 1 :
486529 return []
487530
488- if token :
489- headers = make_headers (token )
490- else :
491- headers = {'Accept' : 'application/vnd.github.v3+json' }
492- headers .setdefault ('User-Agent' , 'gittensor-cli' )
531+ session = get_session (token or '' )
493532
494533 state_clause = f' state:{ state } ' if state != 'all' else ''
495534 max_attempts = 3
496535 for attempt in range (max_attempts ):
497536 try :
498- resp = requests .get (
537+ resp = session .get (
499538 f'{ BASE_GITHUB_API_URL } /search/issues' ,
500539 params = {'q' : f'repo:{ repo } type:pr{ state_clause } { issue_number } in:title,body' , 'per_page' : '50' },
501- headers = headers ,
502540 timeout = 10 ,
503541 )
504542 resp .raise_for_status ()
@@ -593,11 +631,12 @@ def execute_graphql_query(
593631 Returns:
594632 Parsed JSON response data, or None if all attempts failed
595633 """
634+ session = get_session (token )
596635 headers = make_graphql_headers (token )
597636
598637 for attempt in range (max_attempts ):
599638 try :
600- response = requests .post (
639+ response = session .post (
601640 f'{ BASE_GITHUB_API_URL } /graphql' ,
602641 headers = headers ,
603642 json = {'query' : query , 'variables' : variables },
@@ -670,6 +709,7 @@ def get_github_graphql_query(
670709 """
671710
672711 max_attempts = 8
712+ session = get_session (token )
673713 headers = make_graphql_headers (token )
674714 limit = page_size if page_size is not None else min (100 , max_prs - merged_pr_count )
675715
@@ -681,7 +721,7 @@ def get_github_graphql_query(
681721 'maxChangesRequestedReviews' : _MAX_CHANGES_REQUESTED_REVIEWS ,
682722 }
683723 try :
684- response = requests .post (
724+ response = session .post (
685725 f'{ BASE_GITHUB_API_URL } /graphql' ,
686726 headers = headers ,
687727 json = {'query' : QUERY , 'variables' : variables },
@@ -1068,12 +1108,11 @@ def check_github_issue_closed(repo: str, issue_number: int, token: str) -> Optio
10681108 Returns:
10691109 Dict with 'is_closed', 'solver_github_id', 'pr_number', 'solver_lookup_failed' or None on error
10701110 """
1071- headers = make_headers (token )
1111+ session = get_session (token )
10721112
10731113 try :
1074- response = requests .get (
1114+ response = session .get (
10751115 f'{ BASE_GITHUB_API_URL } /repos/{ repo } /issues/{ issue_number } ' ,
1076- headers = headers ,
10771116 timeout = 15 ,
10781117 )
10791118
0 commit comments