Skip to content

Commit adfda5f

Browse files
refactor: move github fetching to seperate file
1 parent 38ad25b commit adfda5f

3 files changed

Lines changed: 243 additions & 186 deletions

File tree

monty/exts/info/github/_handlers.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -259,16 +259,18 @@ def render_full(
259259
return obj.name or obj.login, [text_display]
260260

261261

262-
class RepoRenderer(GitHubRenderer[githubkit.rest.Repository, ghretos.Repo]):
262+
class RepoRenderer(GitHubRenderer[githubkit.rest.Repository | githubkit.rest.FullRepository, ghretos.Repo]):
263263
def render_tiny(
264264
self,
265-
obj: githubkit.rest.Repository,
265+
obj: githubkit.rest.Repository | githubkit.rest.FullRepository,
266266
*,
267267
context: ghretos.Repo,
268268
) -> str:
269269
return f"📦 [{obj.name}](<{obj.html_url}>)"
270270

271-
def render_ogp_cv2(self, obj: githubkit.rest.Repository, *, context: ghretos.Repo) -> disnake.ui.Container:
271+
def render_ogp_cv2(
272+
self, obj: githubkit.rest.Repository | githubkit.rest.FullRepository, *, context: ghretos.Repo
273+
) -> disnake.ui.Container:
272274
container = disnake.ui.Container()
273275
text_display = disnake.ui.TextDisplay("")
274276
text_display.content = f"## [{obj.name}](<{obj.html_url}>)\n\n"
@@ -288,7 +290,7 @@ def render_ogp_cv2(self, obj: githubkit.rest.Repository, *, context: ghretos.Rep
288290

289291
def render_full(
290292
self,
291-
obj: githubkit.rest.Repository,
293+
obj: githubkit.rest.Repository | githubkit.rest.FullRepository,
292294
*,
293295
context: ghretos.Repo,
294296
) -> tuple[str, list[disnake.ui.TextDisplay]]:

monty/exts/info/github/client.py

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import base64
2+
3+
import ghretos
4+
import githubkit
5+
import githubkit.exception
6+
import githubkit.rest
7+
import msgpack
8+
9+
from . import graphql_models
10+
11+
12+
DISCUSSION_COMMENT_GRAPHQL_QUERY = """
13+
query getDiscussionComment($id: ID!) {
14+
node(id: $id) {
15+
... on DiscussionComment {
16+
id
17+
html_url: url
18+
body
19+
created_at: createdAt
20+
user: author {
21+
__typename
22+
login
23+
html_url: url
24+
avatar_url: avatarUrl
25+
}
26+
}
27+
}
28+
}
29+
"""
30+
31+
32+
class GitHubFetcher:
33+
"""Wrapper methods around githubkit to fetch GitHub resources.
34+
35+
This allows for reimplementing fetching logic in one place, such as using GraphQL.
36+
"""
37+
38+
def __init__(self, client: githubkit.GitHub) -> None:
39+
self.client = client
40+
self.headers = {
41+
"Accept": "application/vnd.github.full+json",
42+
}
43+
44+
def _format_github_global_id(self, prefix: str, *ids: int, template: int = 0) -> str:
45+
# This is not documented, but is at least the current format as of writing this comment.
46+
# These IDs are supposed to be treated as opaque strings, but fetching specific resources like
47+
# issue/discussion comments via graphql is a huge pain otherwise when only knowing the integer ID
48+
packed = msgpack.packb(
49+
[
50+
# template index; global IDs of a specific type *can* have multiple different templates
51+
# (i.e. sets of variables that follow); in almost all cases, this is 0
52+
template,
53+
# resource IDs, variable amount depending on global ID type
54+
*ids,
55+
]
56+
)
57+
encoded = base64.urlsafe_b64encode(packed).decode()
58+
encoded = encoded.rstrip("=") # this isn't necessary, but github generates these IDs without padding
59+
return f"{prefix}_{encoded}"
60+
61+
async def fetch_user(self, *, username: str) -> githubkit.rest.PublicUser:
62+
"""Fetch a GitHub user by username."""
63+
r = await self.client.rest.users.async_get_by_username(username=username)
64+
data = r.parsed_data
65+
# Even though we use a token with no additional scopes, validate that we CERTAINLY only have public data.
66+
if data.user_view_type != "public" or not isinstance(data, githubkit.rest.PublicUser):
67+
msg = "User is not public"
68+
raise ValueError(msg)
69+
return data
70+
71+
async def fetch_repo(self, *, owner: str, repo: str) -> githubkit.rest.FullRepository:
72+
"""Fetch a GitHub repository by owner and name."""
73+
r = await self.client.rest.repos.async_get(owner=owner, repo=repo)
74+
return r.parsed_data
75+
76+
async def fetch_issue(self, *, owner: str, repo: str, issue_number: int) -> githubkit.rest.Issue:
77+
"""Fetch a GitHub issue by owner, name, and issue number."""
78+
r = await self.client.rest.issues.async_get(
79+
owner=owner,
80+
repo=repo,
81+
issue_number=issue_number,
82+
)
83+
return r.parsed_data
84+
85+
async def fetch_pull_request(self, *, owner: str, repo: str, issue_number: int) -> githubkit.rest.Issue:
86+
"""Fetch a GitHub issue by owner, name, and issue number."""
87+
r = await self.client.rest.issues.async_get(
88+
owner=owner,
89+
repo=repo,
90+
issue_number=issue_number,
91+
)
92+
return r.parsed_data
93+
94+
async def fetch_discussion(self, *, owner: str, repo: str, discussion_number: int) -> githubkit.rest.Discussion:
95+
"""Fetch a GitHub discussion by owner, name, and discussion number."""
96+
url = f"/repos/{owner}/{repo}/discussions/{discussion_number}"
97+
r = await self.client.arequest(
98+
"GET",
99+
url,
100+
headers={"X-GitHub-Api-Version": self.client.rest.meta._REST_API_VERSION},
101+
response_model=githubkit.rest.Discussion,
102+
)
103+
return r.parsed_data
104+
105+
async def fetch_repo_numberable(
106+
self, *, owner: str, repo: str, number: int
107+
) -> githubkit.rest.Issue | githubkit.rest.Discussion:
108+
"""Fetch a GitHub issue or discussion by owner, name, and number."""
109+
try:
110+
return await self.fetch_issue(owner=owner, repo=repo, issue_number=number)
111+
except githubkit.exception.RequestFailed as e:
112+
if e.response.status_code != 404:
113+
raise
114+
return await self.fetch_discussion(owner=owner, repo=repo, discussion_number=number)
115+
116+
async def fetch_issue_comment(self, *, owner: str, repo: str, comment_id: int) -> githubkit.rest.IssueComment:
117+
"""Fetch a GitHub issue comment by owner, name, and comment ID."""
118+
r = await self.client.rest.issues.async_get_comment(
119+
owner=owner,
120+
repo=repo,
121+
comment_id=comment_id,
122+
)
123+
return r.parsed_data
124+
125+
async def fetch_pull_request_comment(
126+
self, *, owner: str, repo: str, comment_id: int
127+
) -> githubkit.rest.IssueComment:
128+
"""Fetch a GitHub pull request comment by owner, name, and comment ID."""
129+
r = await self.client.rest.issues.async_get_comment(
130+
owner=owner,
131+
repo=repo,
132+
comment_id=comment_id,
133+
)
134+
return r.parsed_data
135+
136+
async def fetch_pull_request_review_comment(
137+
self, *, owner: str, repo: str, comment_id: int
138+
) -> githubkit.rest.PullRequestReviewComment:
139+
"""Fetch a GitHub pull request review comment by owner, name, and comment ID."""
140+
r = await self.client.rest.pulls.async_get_review_comment(
141+
owner=owner,
142+
repo=repo,
143+
comment_id=comment_id,
144+
)
145+
return r.parsed_data
146+
147+
async def fetch_discussion_comment(self, *, comment_id: int) -> graphql_models.DiscussionComment:
148+
"""Fetch a GitHub discussion comment by comment ID."""
149+
r = await self.client.graphql.arequest(
150+
DISCUSSION_COMMENT_GRAPHQL_QUERY,
151+
variables={"id": self._format_github_global_id("DC", 0, comment_id)},
152+
)
153+
# Move `__typename` to `type` to fit the models
154+
r["node"]["user"]["type"] = r["node"]["user"].pop("__typename")
155+
return graphql_models.DiscussionComment(**r["node"])
156+
157+
async def fetch_issue_event(self, *, owner: str, repo: str, event_id: int) -> githubkit.rest.IssueEvent:
158+
"""Fetch a GitHub issue event by owner, name, and event ID."""
159+
r = await self.client.rest.issues.async_get_event(
160+
owner=owner,
161+
repo=repo,
162+
event_id=event_id,
163+
)
164+
return r.parsed_data
165+
166+
async def fetch_pull_request_event(self, *, owner: str, repo: str, event_id: int) -> githubkit.rest.IssueEvent:
167+
"""Fetch a GitHub pull request event by owner, name, and event ID."""
168+
r = await self.client.rest.issues.async_get_event(
169+
owner=owner,
170+
repo=repo,
171+
event_id=event_id,
172+
)
173+
return r.parsed_data
174+
175+
async def fetch_commit(self, *, owner: str, repo: str, sha: str) -> githubkit.rest.Commit:
176+
"""Fetch a GitHub commit by owner, name, and SHA."""
177+
r = await self.client.rest.repos.async_get_commit(
178+
owner=owner,
179+
repo=repo,
180+
ref=sha,
181+
)
182+
return r.parsed_data
183+
184+
# TODO: remove this method
185+
async def fetch_resource(self, obj: ghretos.GitHubResource) -> githubkit.GitHubModel:
186+
"""Fetch a GitHub object by its type and identifiers.
187+
188+
This method is a convenience wrapper around the other fetch methods in this class.
189+
"""
190+
match obj:
191+
case ghretos.User():
192+
return await self.fetch_user(username=obj.login)
193+
case ghretos.Repo():
194+
return await self.fetch_repo(owner=obj.owner, repo=obj.name)
195+
case ghretos.NumberedResource():
196+
return await self.fetch_repo_numberable(owner=obj.repo.owner, repo=obj.repo.name, number=obj.number)
197+
case ghretos.Issue():
198+
return await self.fetch_issue(owner=obj.repo.owner, repo=obj.repo.name, issue_number=obj.number)
199+
case ghretos.PullRequest():
200+
return await self.fetch_pull_request(owner=obj.repo.owner, repo=obj.repo.name, issue_number=obj.number)
201+
case ghretos.Discussion():
202+
return await self.fetch_discussion(
203+
owner=obj.repo.owner, repo=obj.repo.owner, discussion_number=obj.number
204+
)
205+
case ghretos.IssueComment():
206+
return await self.fetch_issue_comment(
207+
owner=obj.repo.owner, repo=obj.repo.name, comment_id=obj.comment_id
208+
)
209+
case ghretos.PullRequestComment():
210+
return await self.fetch_pull_request_comment(
211+
owner=obj.repo.owner, repo=obj.repo.name, comment_id=obj.comment_id
212+
)
213+
case ghretos.PullRequestReviewComment():
214+
return await self.fetch_pull_request_review_comment(
215+
owner=obj.repo.owner, repo=obj.repo.name, comment_id=obj.comment_id
216+
)
217+
case ghretos.DiscussionComment():
218+
return await self.fetch_discussion_comment(comment_id=obj.comment_id)
219+
case ghretos.IssueEvent():
220+
return await self.fetch_issue_event(owner=obj.repo.owner, repo=obj.repo.name, event_id=obj.event_id)
221+
case ghretos.PullRequestEvent():
222+
return await self.fetch_pull_request_event(
223+
owner=obj.repo.owner, repo=obj.repo.name, event_id=obj.event_id
224+
)
225+
case ghretos.Commit():
226+
return await self.fetch_commit(owner=obj.repo.owner, repo=obj.repo.name, sha=obj.sha)
227+
msg = f"Fetching for resource type {type(obj)} is not implemented"
228+
raise NotImplementedError(msg)

0 commit comments

Comments
 (0)