Skip to content

Commit f9fbec5

Browse files
committed
Update (base update)
[ghstack-poisoned]
1 parent 982575b commit f9fbec5

9 files changed

Lines changed: 529 additions & 132 deletions

File tree

greenlight/src/greenlight/github_client.py

Lines changed: 51 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import re
1010
from dataclasses import dataclass
11-
from typing import TYPE_CHECKING, Protocol
11+
from typing import TYPE_CHECKING
1212

1313
from greenlight import constants
1414
from greenlight.pr_hash import HumanEvent, PRFingerprint, compute_pr_hash, is_bot
@@ -20,125 +20,18 @@
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

16175
def 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

16797
def list_open_prs_by_authors(client: _RepoClient, repo: str, authors: Iterable[str]) -> list[OpenPR]:
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""Structural (Protocol) contracts the GitHub client speaks."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING, Protocol
6+
7+
if TYPE_CHECKING:
8+
from collections.abc import Iterable
9+
from datetime import datetime
10+
11+
class _PRUser(Protocol):
12+
@property
13+
def login(self) -> str | None: ...
14+
15+
class _Label(Protocol):
16+
@property
17+
def name(self) -> str: ...
18+
19+
class _PullRequest(Protocol):
20+
@property
21+
def number(self) -> int: ...
22+
@property
23+
def user(self) -> _PRUser | None: ...
24+
@property
25+
def title(self) -> str: ...
26+
@property
27+
def html_url(self) -> str: ...
28+
@property
29+
def head(self) -> _PRBase: ...
30+
@property
31+
def updated_at(self) -> datetime | None: ...
32+
@property
33+
def labels(self) -> Iterable[_Label]: ...
34+
@property
35+
def draft(self) -> bool: ...
36+
37+
class _Repo(Protocol):
38+
def get_pulls(self, state: str) -> Iterable[_PullRequest]: ...
39+
40+
class _RepoClient(Protocol):
41+
def get_repo(self, full_name_or_id: str) -> _Repo: ...
42+
43+
class _PRActor(Protocol):
44+
@property
45+
def login(self) -> str | None: ...
46+
@property
47+
def type(self) -> str: ...
48+
49+
class _PRComment(Protocol):
50+
@property
51+
def id(self) -> int: ...
52+
@property
53+
def user(self) -> _PRActor | None: ...
54+
@property
55+
def body(self) -> str: ...
56+
57+
class _PRReview(Protocol):
58+
@property
59+
def id(self) -> int: ...
60+
@property
61+
def user(self) -> _PRActor | None: ...
62+
@property
63+
def body(self) -> str: ...
64+
@property
65+
def state(self) -> str: ...
66+
67+
class _PRBase(Protocol):
68+
@property
69+
def sha(self) -> str: ...
70+
71+
class _FingerprintPR(Protocol):
72+
@property
73+
def head(self) -> _PRBase: ...
74+
def get_issue_comments(self) -> Iterable[_PRComment]: ...
75+
def get_review_comments(self) -> Iterable[_PRComment]: ...
76+
def get_reviews(self) -> Iterable[_PRReview]: ...
77+
78+
class _ScanRepo(Protocol):
79+
def get_pull(self, number: int) -> _FingerprintPR: ...
80+
81+
class _AuthorPR(Protocol):
82+
@property
83+
def user(self) -> _PRUser | None: ...
84+
85+
class _AuthorRepo(Protocol):
86+
def get_pull(self, number: int) -> _AuthorPR: ...
87+
88+
class _AuthorClient(Protocol):
89+
def get_repo(self, full_name_or_id: str) -> _AuthorRepo: ...
90+
91+
class _VerdictReview(Protocol):
92+
@property
93+
def id(self) -> int: ...
94+
@property
95+
def user(self) -> _PRUser | None: ...
96+
@property
97+
def state(self) -> str: ...
98+
def dismiss(self, message: str) -> None: ...
99+
100+
class _VerdictComment(Protocol):
101+
@property
102+
def body(self) -> str: ...
103+
@property
104+
def user(self) -> _PRUser | None: ...
105+
def edit(self, body: str) -> None: ...
106+
107+
class VerdictPR(Protocol):
108+
@property
109+
def head(self) -> _PRBase: ...
110+
def create_review(self, *, body: str, event: str) -> object: ...
111+
def create_issue_comment(self, body: str) -> object: ...
112+
def get_issue_comments(self) -> Iterable[_VerdictComment]: ...
113+
def get_reviews(self) -> Iterable[_VerdictReview]: ...
114+
115+
class _VerdictRepo(Protocol):
116+
def get_pull(self, number: int) -> VerdictPR: ...
117+
118+
119+
class VerdictClient(Protocol):
120+
"""Structural GitHub client for the verdict path; the real ``github.Github`` satisfies it."""
121+
122+
def get_repo(self, full_name_or_id: str) -> _VerdictRepo: ...
123+
124+
125+
class ScanClient(Protocol):
126+
"""Structural GitHub client for the scan/fingerprint path; the real ``github.Github`` satisfies it."""
127+
128+
def get_repo(self, full_name_or_id: str) -> _ScanRepo: ...

greenlight/src/greenlight/review.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
from github import Github
3939

4040
from greenlight.config import Config
41-
from greenlight.github_client import OpenPR, VerdictPR
41+
from greenlight.github_client import OpenPR
42+
from greenlight.github_types import VerdictPR
4243
from greenlight.review_gate import ReviewSkip
4344
from greenlight.scan_runner import FingerprintFn
4445
from greenlight.state import PRState

0 commit comments

Comments
 (0)