88
99import re
1010from dataclasses import dataclass
11- from typing import TYPE_CHECKING , Protocol
11+ from typing import TYPE_CHECKING
1212
1313from greenlight import constants
1414from greenlight .pr_hash import HumanEvent , PRFingerprint , compute_pr_hash , is_bot
2020 from datetime import datetime
2121
2222 from github import Github
23-
24- class _PRUser (Protocol ):
25- @property
26- def login (self ) -> str | None : ...
27-
28- class _Label (Protocol ):
29- @property
30- def name (self ) -> str : ...
31-
32- class _PullRequest (Protocol ):
33- @property
34- def number (self ) -> int : ...
35- @property
36- def user (self ) -> _PRUser | None : ...
37- @property
38- def title (self ) -> str : ...
39- @property
40- def html_url (self ) -> str : ...
41- @property
42- def head (self ) -> _PRBase : ...
43- @property
44- def updated_at (self ) -> datetime | None : ...
45- @property
46- def labels (self ) -> Iterable [_Label ]: ...
47- @property
48- def draft (self ) -> bool : ...
49-
50- class _Repo (Protocol ):
51- def get_pulls (self , state : str ) -> Iterable [_PullRequest ]: ...
52-
53- class _RepoClient (Protocol ):
54- def get_repo (self , full_name_or_id : str ) -> _Repo : ...
55-
56- class _PRActor (Protocol ):
57- @property
58- def login (self ) -> str | None : ...
59- @property
60- def type (self ) -> str : ...
61-
62- class _PRComment (Protocol ):
63- @property
64- def id (self ) -> int : ...
65- @property
66- def user (self ) -> _PRActor | None : ...
67- @property
68- def body (self ) -> str : ...
69-
70- class _PRReview (Protocol ):
71- @property
72- def id (self ) -> int : ...
73- @property
74- def user (self ) -> _PRActor | None : ...
75- @property
76- def body (self ) -> str : ...
77- @property
78- def state (self ) -> str : ...
79-
80- class _PRBase (Protocol ):
81- @property
82- def sha (self ) -> str : ...
83-
84- class _FingerprintPR (Protocol ):
85- @property
86- def head (self ) -> _PRBase : ...
87- def get_issue_comments (self ) -> Iterable [_PRComment ]: ...
88- def get_review_comments (self ) -> Iterable [_PRComment ]: ...
89- def get_reviews (self ) -> Iterable [_PRReview ]: ...
90-
91- class _ScanRepo (Protocol ):
92- def get_pull (self , number : int ) -> _FingerprintPR : ...
93-
94- class _AuthorPR (Protocol ):
95- @property
96- def user (self ) -> _PRUser | None : ...
97-
98- class _AuthorRepo (Protocol ):
99- def get_pull (self , number : int ) -> _AuthorPR : ...
100-
101- class _AuthorClient (Protocol ):
102- def get_repo (self , full_name_or_id : str ) -> _AuthorRepo : ...
103-
104- class _VerdictReview (Protocol ):
105- @property
106- def id (self ) -> int : ...
107- @property
108- def user (self ) -> _PRUser | None : ...
109- @property
110- def state (self ) -> str : ...
111- def dismiss (self , message : str ) -> None : ...
112-
113- class _VerdictComment (Protocol ):
114- @property
115- def body (self ) -> str : ...
116- @property
117- def user (self ) -> _PRUser | None : ...
118- def edit (self , body : str ) -> None : ...
119-
120- class VerdictPR (Protocol ):
121- @property
122- def head (self ) -> _PRBase : ...
123- def create_review (self , * , body : str , event : str ) -> object : ...
124- def create_issue_comment (self , body : str ) -> object : ...
125- def get_issue_comments (self ) -> Iterable [_VerdictComment ]: ...
126- def get_reviews (self ) -> Iterable [_VerdictReview ]: ...
127-
128- class _VerdictRepo (Protocol ):
129- def get_pull (self , number : int ) -> VerdictPR : ...
130-
131-
132- class VerdictClient (Protocol ):
133- """Structural GitHub client for the verdict path; the real ``github.Github`` satisfies it."""
134-
135- def get_repo (self , full_name_or_id : str ) -> _VerdictRepo : ...
136-
137-
138- class ScanClient (Protocol ):
139- """Structural GitHub client for the scan/fingerprint path; the real ``github.Github`` satisfies it."""
140-
141- def get_repo (self , full_name_or_id : str ) -> _ScanRepo : ...
23+ from urllib3 .util .retry import Retry
24+
25+ from greenlight .github_types import (
26+ ScanClient ,
27+ VerdictClient ,
28+ VerdictPR ,
29+ _AuthorClient ,
30+ _FingerprintPR ,
31+ _PRActor ,
32+ _PRReview ,
33+ _RepoClient ,
34+ )
14235
14336
14437@dataclass (frozen = True , slots = True )
@@ -157,11 +50,48 @@ class OpenPR:
15750# worst-case pagination outlast the per-iteration runtime watchdog.
15851_GITHUB_TIMEOUT_SECONDS : int = 15
15952
53+ _GITHUB_RETRY_TOTAL : int = 2
54+ _GITHUB_RETRY_BACKOFF_FACTOR : float = 0.5
55+ _GITHUB_RETRY_BACKOFF_MAX_SECONDS : float = 5.0
56+
57+
58+ def _build_retry () -> Retry :
59+ # Not PyGithub's default GithubRetry: it force-lists 403 and sleeps in-call until the
60+ # rate-limit reset, which would stall a fingerprint worker past the per-iteration runtime
61+ # budget. A plain urllib3 Retry with a 5xx-only forcelist lets a rate limit raise at once.
62+ from urllib3 .util .retry import Retry
63+
64+ return Retry (
65+ total = _GITHUB_RETRY_TOTAL ,
66+ backoff_factor = _GITHUB_RETRY_BACKOFF_FACTOR ,
67+ backoff_max = _GITHUB_RETRY_BACKOFF_MAX_SECONDS ,
68+ status_forcelist = frozenset (range (500 , 600 )),
69+ allowed_methods = frozenset ({"GET" , "HEAD" , "PUT" , "DELETE" }),
70+ respect_retry_after_header = False ,
71+ raise_on_status = True ,
72+ )
73+
16074
16175def build_client (token : str ) -> Github :
16276 from github import Auth , Github # lazy: keeps this module importable without the dep
16377
164- return Github (auth = Auth .Token (token ), per_page = 100 , timeout = _GITHUB_TIMEOUT_SECONDS , lazy = True )
78+ return Github (
79+ auth = Auth .Token (token ),
80+ per_page = 100 ,
81+ timeout = _GITHUB_TIMEOUT_SECONDS ,
82+ retry = _build_retry (),
83+ lazy = True ,
84+ )
85+
86+
87+ def is_rate_limit_error (exc : BaseException ) -> bool :
88+ # GitHub delivers a rate limit as 403 (-> RateLimitExceededException) or 429 (-> base
89+ # GithubException); 429 must stay off _build_retry's forcelist or it surfaces as a RetryError.
90+ from github import GithubException , RateLimitExceededException
91+
92+ if isinstance (exc , RateLimitExceededException ):
93+ return True
94+ return isinstance (exc , GithubException ) and getattr (exc , "status" , None ) == 429
16595
16696
16797def list_open_prs_by_authors (client : _RepoClient , repo : str , authors : Iterable [str ]) -> list [OpenPR ]:
0 commit comments