ghapi 2.1.3__tar.gz → 2.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ghapi-2.1.3/ghapi.egg-info → ghapi-2.1.4}/PKG-INFO +5 -5
- {ghapi-2.1.3 → ghapi-2.1.4}/README.md +1 -1
- ghapi-2.1.4/ghapi/__init__.py +8 -0
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi/_modidx.py +2 -1
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi/core.py +90 -61
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi/gh_spec.py +770 -66
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi/gql_spec.py +29 -21
- ghapi-2.1.4/ghapi/graphql.py +47 -0
- ghapi-2.1.4/ghapi/skill.py +80 -0
- {ghapi-2.1.3 → ghapi-2.1.4/ghapi.egg-info}/PKG-INFO +5 -5
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi.egg-info/requires.txt +3 -3
- {ghapi-2.1.3 → ghapi-2.1.4}/pyproject.toml +3 -3
- ghapi-2.1.3/ghapi/__init__.py +0 -8
- ghapi-2.1.3/ghapi/graphql.py +0 -39
- ghapi-2.1.3/ghapi/skill.py +0 -136
- {ghapi-2.1.3 → ghapi-2.1.4}/CONTRIBUTING.md +0 -0
- {ghapi-2.1.3 → ghapi-2.1.4}/LICENSE +0 -0
- {ghapi-2.1.3 → ghapi-2.1.4}/MANIFEST.in +0 -0
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi/actions.py +0 -0
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi/all.py +0 -0
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi/auth.py +0 -0
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi/build_lib.py +0 -0
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi/cli.py +0 -0
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi/event.py +0 -0
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi/page.py +0 -0
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi/py.typed +0 -0
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi/templates.py +0 -0
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi.egg-info/SOURCES.txt +0 -0
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi.egg-info/dependency_links.txt +0 -0
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi.egg-info/entry_points.txt +0 -0
- {ghapi-2.1.3 → ghapi-2.1.4}/ghapi.egg-info/top_level.txt +0 -0
- {ghapi-2.1.3 → ghapi-2.1.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ghapi
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.4
|
|
4
4
|
Summary: A python client for the GitHub API
|
|
5
5
|
Author-email: Jeremy Howard <info@fast.ai>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -15,8 +15,8 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
15
15
|
Requires-Python: >=3.10
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
|
-
Requires-Dist: fastcore>=2.2.
|
|
19
|
-
Requires-Dist: fastspec>=0.2.
|
|
18
|
+
Requires-Dist: fastcore>=2.2.23
|
|
19
|
+
Requires-Dist: fastspec>=0.2.5
|
|
20
20
|
Requires-Dist: fasttransport>=0.0.2
|
|
21
21
|
Requires-Dist: tomli; python_version < "3.11"
|
|
22
22
|
Provides-Extra: dev
|
|
@@ -27,7 +27,7 @@ Requires-Dist: nbdev; extra == "dev"
|
|
|
27
27
|
Requires-Dist: pyyaml; extra == "dev"
|
|
28
28
|
Requires-Dist: pysym2md>=0.0.6; extra == "dev"
|
|
29
29
|
Requires-Dist: llms-txt>=0.0.6; extra == "dev"
|
|
30
|
-
Requires-Dist: pyskills; extra == "dev"
|
|
30
|
+
Requires-Dist: pyskills>=0.0.30; extra == "dev"
|
|
31
31
|
Dynamic: license-file
|
|
32
32
|
|
|
33
33
|
# ghapi
|
|
@@ -154,7 +154,7 @@ api.git
|
|
|
154
154
|
- [git.create_tree](https://docs.github.com/rest/git/trees#create-a-tree)(owner, repo, tree, base_tree): *Create a tree*
|
|
155
155
|
- [git.get_tree](https://docs.github.com/rest/git/trees#get-a-tree)(owner, repo, tree_sha, recursive): *Get a tree*
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
Read an endpoint’s full documentation on the generated object. Its display includes parameter descriptions, bound defaults, request controls, and async usage. With pyskills, use `doc(api.git.get_ref)` for the same documentation, or `xdir(api.git, 'ref')` to search names. Construction and discovery send no requests:
|
|
158
158
|
|
|
159
159
|
``` python
|
|
160
160
|
api.git.get_ref
|
|
@@ -122,7 +122,7 @@ api.git
|
|
|
122
122
|
- [git.create_tree](https://docs.github.com/rest/git/trees#create-a-tree)(owner, repo, tree, base_tree): *Create a tree*
|
|
123
123
|
- [git.get_tree](https://docs.github.com/rest/git/trees#get-a-tree)(owner, repo, tree_sha, recursive): *Get a tree*
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
Read an endpoint’s full documentation on the generated object. Its display includes parameter descriptions, bound defaults, request controls, and async usage. With pyskills, use `doc(api.git.get_ref)` for the same documentation, or `xdir(api.git, 'ref')` to search names. Construction and discovery send no requests:
|
|
126
126
|
|
|
127
127
|
``` python
|
|
128
128
|
api.git.get_ref
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""A python client for the GitHub API
|
|
2
|
+
|
|
3
|
+
Modules:
|
|
4
|
+
|
|
5
|
+
- `ghapi.graphql`: Query GitHub's GraphQL API with schema-aware chaining, batched requests and raw GraphQL
|
|
6
|
+
- `ghapi.skill`: GitHub REST and GraphQL access, plus local git operations. Read before working with GitHub issues, PRs, CI, releases, branches, or gists, and before repo-local git operations."""
|
|
7
|
+
|
|
8
|
+
__version__ = "2.1.4"
|
|
@@ -90,10 +90,11 @@ d = { 'settings': { 'branch': 'main',
|
|
|
90
90
|
'ghapi.core._age': ('core.html#_age', 'ghapi/core.py'),
|
|
91
91
|
'ghapi.core._dur': ('core.html#_dur', 'ghapi/core.py'),
|
|
92
92
|
'ghapi.core._filter_diff': ('core.html#_filter_diff', 'ghapi/core.py'),
|
|
93
|
-
'ghapi.core._find_matches': ('core.html#_find_matches', 'ghapi/core.py'),
|
|
94
93
|
'ghapi.core._fmt_replies': ('core.html#_fmt_replies', 'ghapi/core.py'),
|
|
95
94
|
'ghapi.core._include': ('core.html#_include', 'ghapi/core.py'),
|
|
96
95
|
'ghapi.core._listed': ('core.html#_listed', 'ghapi/core.py'),
|
|
96
|
+
'ghapi.core._load_tmpl': ('core.html#_load_tmpl', 'ghapi/core.py'),
|
|
97
|
+
'ghapi.core._parse_section': ('core.html#_parse_section', 'ghapi/core.py'),
|
|
97
98
|
'ghapi.core._parse_tmpl': ('core.html#_parse_tmpl', 'ghapi/core.py'),
|
|
98
99
|
'ghapi.core._pr_mark': ('core.html#_pr_mark', 'ghapi/core.py'),
|
|
99
100
|
'ghapi.core._reduce_ctx': ('core.html#_reduce_ctx', 'ghapi/core.py'),
|
|
@@ -5,8 +5,8 @@ Docs: https://ghapi.fast.ai/core.html.md"""
|
|
|
5
5
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.
|
|
6
6
|
|
|
7
7
|
# %% auto #0
|
|
8
|
-
__all__ = ['GH_HOST', 'pspec', 'img_md_pat', 'EMPTY_TREE_SHA', 'MAXLEN', '
|
|
9
|
-
'gh_patch', 'GhApi', 'call_gh', 'date2gh', 'gh2date', 'GhRows', '
|
|
8
|
+
__all__ = ['GH_HOST', 'pspec', 'img_md_pat', 'EMPTY_TREE_SHA', 'MAXLEN', 'print_summary', 'GhTransport', 'GhSyncTransport',
|
|
9
|
+
'gh_patch', 'GhApi', 'call_gh', 'date2gh', 'gh2date', 'GhRows', 'create_release', 'delete_release',
|
|
10
10
|
'list_tags', 'list_branches', 'create_branch_empty', 'delete_tag', 'delete_branch', 'get_branch',
|
|
11
11
|
'commit_tree', 'list_files', 'get_content', 'create_or_update_file', 'create_file', 'delete_file',
|
|
12
12
|
'update_contents', 'enable_pages', 'read_issue', 'read_pr', 'pr_file_diff', 'PullRow', 'PullRows',
|
|
@@ -42,9 +42,17 @@ _all_ = ['APIError']
|
|
|
42
42
|
GH_HOST = os.getenv('GH_HOST', "https://api.github.com")
|
|
43
43
|
_DOC_URL = 'https://docs.github.com/'
|
|
44
44
|
|
|
45
|
+
_docroot = 'https://docs.github.com/rest/reference/'
|
|
46
|
+
|
|
45
47
|
# %% ../nbs/00_core.ipynb #ba730c65
|
|
46
48
|
pspec = SpecParser.from_dict(spec)
|
|
47
49
|
|
|
50
|
+
# %% ../nbs/00_core.ipynb #f361159b
|
|
51
|
+
def print_summary(method, url, kwargs):
|
|
52
|
+
"Debug callback for `GhApi(debug=...)`: print each request with the token (if any) removed"
|
|
53
|
+
hdrs = {k:v for k,v in (kwargs.get('headers') or {}).items() if k.lower()!='authorization'}
|
|
54
|
+
print(method, url, {**kwargs, 'headers':hdrs})
|
|
55
|
+
|
|
48
56
|
# %% ../nbs/00_core.ipynb #d4a51c1a
|
|
49
57
|
_binary_cts = ('octet-stream', 'zip', 'gzip', 'tar', 'image/', 'audio/', 'video/')
|
|
50
58
|
|
|
@@ -89,15 +97,6 @@ class GhSyncTransport(SyncTransport, GhTransport):
|
|
|
89
97
|
self._pre(method, url, kwargs)
|
|
90
98
|
return self._post(SyncTransport.request(self, method, url, raw=True, **kwargs), raw)
|
|
91
99
|
|
|
92
|
-
# %% ../nbs/00_core.ipynb #531aee7d
|
|
93
|
-
_docroot = 'https://docs.github.com/rest/reference/'
|
|
94
|
-
|
|
95
|
-
# %% ../nbs/00_core.ipynb #f361159b
|
|
96
|
-
def print_summary(method, url, kwargs):
|
|
97
|
-
"Debug callback for `GhApi(debug=...)`: print each request with the token (if any) removed"
|
|
98
|
-
hdrs = {k:v for k,v in (kwargs.get('headers') or {}).items() if k.lower()!='authorization'}
|
|
99
|
-
print(method, url, {**kwargs, 'headers':hdrs})
|
|
100
|
-
|
|
101
100
|
# %% ../nbs/00_core.ipynb #cecd0177
|
|
102
101
|
_gh_override = ContextVar('_gh_override', default={})
|
|
103
102
|
|
|
@@ -120,8 +119,20 @@ def gh_patch(fn):
|
|
|
120
119
|
# %% ../nbs/00_core.ipynb #83e8a9ce
|
|
121
120
|
class GhApi(OpenAPIClient):
|
|
122
121
|
"GitHub API client. Endpoint groups (`issues`, `pulls`, ...) are generated per-instance from GitHub's OpenAPI metadata, so the class shows only convenience methods -- inspect a live instance, e.g. `doc(GhApi())`, to see the full API."
|
|
123
|
-
def __init__(self,
|
|
124
|
-
|
|
122
|
+
def __init__(self,
|
|
123
|
+
owner=None, # Default repository owner; endpoint calls can override it
|
|
124
|
+
repo=None, # Default repository name; endpoint calls can override it
|
|
125
|
+
token=None, # Personal access token; otherwise GITHUB_TOKEN
|
|
126
|
+
jwt_token=None, # JWT; otherwise GITHUB_JWT_TOKEN; takes precedence over token
|
|
127
|
+
debug=None, # Request callback (method, url, kwargs); print_summary omits authorization
|
|
128
|
+
limit_cb=None, # Callback (remaining, quota) when rate-limit headers change
|
|
129
|
+
gh_host=None, # API base URL; otherwise GH_HOST
|
|
130
|
+
authenticate=True, # False skips credentials; missing credentials otherwise warn and remain unauthenticated
|
|
131
|
+
timeout=60.0, # Request timeout in seconds
|
|
132
|
+
sync=False, # Generated endpoints block instead of returning awaitables; async convenience methods still need an async client
|
|
133
|
+
**kwargs # Additional default route/query parameters
|
|
134
|
+
):
|
|
135
|
+
"Construct the generated API locally; no request is made until an operation is called"
|
|
125
136
|
self.headers = { 'Accept': 'application/vnd.github.v3+json' }
|
|
126
137
|
if authenticate:
|
|
127
138
|
token = token or os.getenv('GITHUB_TOKEN', None)
|
|
@@ -139,14 +150,6 @@ class GhApi(OpenAPIClient):
|
|
|
139
150
|
self.groups = mk_groups(self.ops)
|
|
140
151
|
for k,v in self.groups.items(): setattr(self, k, v)
|
|
141
152
|
|
|
142
|
-
def __call__(self, path:str, verb:str=None, headers:dict=None, route:dict=None, query:dict=None, data=None):
|
|
143
|
-
"Call a fully specified `path` (or full URL) using HTTP `verb` directly (returns an awaitable on an async client)"
|
|
144
|
-
if verb is None: verb = 'POST' if data else 'GET'
|
|
145
|
-
if route: path = path.format(**{k:quote(str(v), safe='') for k,v in route.items()})
|
|
146
|
-
if not path.startswith(('http://', 'https://')): path = self.gh_host + path
|
|
147
|
-
kw = dict(content=data) if isinstance(data, (bytes,str)) else dict(json=data)
|
|
148
|
-
return self.transport.request(verb, path, headers=headers, params=query, **kw)
|
|
149
|
-
|
|
150
153
|
@property
|
|
151
154
|
def debug(self): return self.transport.debug
|
|
152
155
|
@debug.setter
|
|
@@ -158,10 +161,24 @@ class GhApi(OpenAPIClient):
|
|
|
158
161
|
|
|
159
162
|
def _repr_markdown_(self): return "\n".join(f"- [{o}]({_docroot + o.replace('_', '-')})" for o in sorted(self.groups))
|
|
160
163
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
164
|
+
|
|
165
|
+
# %% ../nbs/00_core.ipynb #5ba617de
|
|
166
|
+
@patch
|
|
167
|
+
def __call__(self:GhApi, path:str, verb:str=None, headers:dict=None, route:dict=None, query:dict=None, data=None):
|
|
168
|
+
"Call a fully specified `path` (or full URL) using HTTP `verb` directly (returns an awaitable on an async client)"
|
|
169
|
+
if verb is None: verb = 'POST' if data else 'GET'
|
|
170
|
+
if route: path = path.format(**{k:quote(str(v), safe='') for k,v in route.items()})
|
|
171
|
+
if not path.startswith(('http://', 'https://')): path = self.gh_host + path
|
|
172
|
+
kw = dict(content=data) if isinstance(data, (bytes,str)) else dict(json=data)
|
|
173
|
+
return self.transport.request(verb, path, headers=headers, params=query, **kw)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# %% ../nbs/00_core.ipynb #2f980e70
|
|
177
|
+
@patch
|
|
178
|
+
def __getitem__(self:GhApi, k):
|
|
179
|
+
"Lookup an endpoint by path and verb (which defaults to 'GET')"
|
|
180
|
+
a,b = k if isinstance(k,tuple) else (k,'GET')
|
|
181
|
+
return self.func_dict[f'{a}:{b.upper()}']
|
|
165
182
|
|
|
166
183
|
# %% ../nbs/00_core.ipynb #562eee39
|
|
167
184
|
def call_gh(op:str, *args, token=None, **kwargs):
|
|
@@ -172,12 +189,13 @@ def call_gh(op:str, *args, token=None, **kwargs):
|
|
|
172
189
|
|
|
173
190
|
# %% ../nbs/00_core.ipynb #05cbdf91
|
|
174
191
|
def date2gh(dt:datetime)->str:
|
|
175
|
-
"Convert `dt`
|
|
176
|
-
|
|
192
|
+
"Convert `dt` to GitHub's UTC timestamp format; naive datetimes are assumed UTC"
|
|
193
|
+
if dt.tzinfo is not None: dt = dt.astimezone(timezone.utc)
|
|
194
|
+
return f'{dt.replace(tzinfo=None, microsecond=0).isoformat()}Z'
|
|
177
195
|
|
|
178
196
|
# %% ../nbs/00_core.ipynb #3f4c8b27
|
|
179
197
|
def gh2date(dtstr:str)->datetime:
|
|
180
|
-
"Convert
|
|
198
|
+
"Convert a GitHub UTC date string to a naive UTC `datetime`"
|
|
181
199
|
return datetime.fromisoformat(dtstr.replace('Z', ''))
|
|
182
200
|
|
|
183
201
|
# %% ../nbs/00_core.ipynb #7876622c
|
|
@@ -231,13 +249,6 @@ async def update_gist(self:GhApi, gist_id:str, content:str):
|
|
|
231
249
|
fname = first(gist.files.keys())
|
|
232
250
|
return (await self.gists.update(gist_id, files={fname: {'content': content}})).html_url
|
|
233
251
|
|
|
234
|
-
# %% ../nbs/00_core.ipynb #4b7a278c
|
|
235
|
-
@gh_patch
|
|
236
|
-
async def delete_release(self:GhApi, release):
|
|
237
|
-
"Delete a release and its associated tag"
|
|
238
|
-
await self.repos.delete_release(release.id)
|
|
239
|
-
await self.git.delete_ref(f'tags/{release.tag_name}')
|
|
240
|
-
|
|
241
252
|
# %% ../nbs/00_core.ipynb #b2bf7e22
|
|
242
253
|
@patch
|
|
243
254
|
async def upload_file(self:GhApi, rel, fn):
|
|
@@ -247,17 +258,23 @@ async def upload_file(self:GhApi, rel, fn):
|
|
|
247
258
|
mime = mimetypes.guess_type(fn, False)[0] or 'application/octet-stream'
|
|
248
259
|
return await self(url, 'POST', headers={'Content-Type':mime}, query = {'name':fn.name}, data=fn.read_bytes())
|
|
249
260
|
|
|
250
|
-
# %% ../nbs/00_core.ipynb #3cad71a4
|
|
251
261
|
@gh_patch
|
|
252
262
|
async def create_release(self:GhApi, tag_name, branch=None, name=None, body='',
|
|
253
|
-
draft=False, prerelease=False, files=None, make_latest=UNSET):
|
|
263
|
+
draft=False, prerelease=False, files=None, make_latest=UNSET, generate_release_notes=UNSET):
|
|
254
264
|
"Wrapper for `GhApi.repos.create_release` which also uploads `files`; `branch` defaults to the repo's default branch"
|
|
255
265
|
if name is None: name = 'v'+tag_name
|
|
256
266
|
rel = await self.repos.create_release(tag_name, target_commitish=branch or UNSET, name=name, body=body,
|
|
257
|
-
draft=draft, prerelease=prerelease, make_latest=make_latest)
|
|
267
|
+
draft=draft, prerelease=prerelease, make_latest=make_latest, generate_release_notes=generate_release_notes)
|
|
258
268
|
for file in listify(files): await self.upload_file(rel, file)
|
|
259
269
|
return rel
|
|
260
270
|
|
|
271
|
+
# %% ../nbs/00_core.ipynb #4b7a278c
|
|
272
|
+
@gh_patch
|
|
273
|
+
async def delete_release(self:GhApi, release):
|
|
274
|
+
"Delete a release and its associated tag"
|
|
275
|
+
await self.repos.delete_release(release.id)
|
|
276
|
+
await self.git.delete_ref(f'tags/{release.tag_name}')
|
|
277
|
+
|
|
261
278
|
# %% ../nbs/00_core.ipynb #2be73ae0
|
|
262
279
|
@gh_patch
|
|
263
280
|
async def list_tags(self:GhApi, prefix:str=''):
|
|
@@ -341,7 +358,6 @@ async def create_or_update_file(self:GhApi, path, message, committer, author, co
|
|
|
341
358
|
return await self.repos.create_or_update_file_contents(path, message, content=content,
|
|
342
359
|
branch=branch, committer=committer or {}, author=author or {}, **kwargs)
|
|
343
360
|
|
|
344
|
-
# %% ../nbs/00_core.ipynb #5044445d
|
|
345
361
|
@gh_patch
|
|
346
362
|
async def create_file(self:GhApi, path, message, committer, author, content=None, branch=None):
|
|
347
363
|
if not branch: branch = (await self.repos.get())['default_branch']
|
|
@@ -351,21 +367,16 @@ async def create_file(self:GhApi, path, message, committer, author, content=None
|
|
|
351
367
|
@gh_patch
|
|
352
368
|
async def delete_file(self:GhApi, path, message, committer, author, sha=None, branch=None):
|
|
353
369
|
if not branch: branch = (await self.repos.get())['default_branch']
|
|
354
|
-
if sha is None: sha = (await self.list_files())[path].sha
|
|
370
|
+
if sha is None: sha = (await self.list_files(branch))[path].sha
|
|
355
371
|
return await self.repos.delete_file(path, message=message, sha=sha, branch=branch, committer=committer, author=author)
|
|
356
372
|
|
|
357
373
|
# %% ../nbs/00_core.ipynb #93f6b559
|
|
358
374
|
@gh_patch
|
|
359
375
|
async def update_contents(self:GhApi, path, message, committer, author, content, sha=None, branch=None):
|
|
360
376
|
if not branch: branch = (await self.repos.get())['default_branch']
|
|
361
|
-
if sha is None: sha = (await self.list_files())[path].sha
|
|
377
|
+
if sha is None: sha = (await self.list_files(branch))[path].sha
|
|
362
378
|
return await self.create_or_update_file(path, message, committer=committer, author=author, content=content, sha=sha, branch=branch)
|
|
363
379
|
|
|
364
|
-
# %% ../nbs/00_core.ipynb #444784ce
|
|
365
|
-
def _find_matches(path, pats):
|
|
366
|
-
"Returns matched patterns"
|
|
367
|
-
return L(pats).filter(lambda p: fnmatch.fnmatch(path, p))
|
|
368
|
-
|
|
369
380
|
# %% ../nbs/00_core.ipynb #9ffb3b8f
|
|
370
381
|
def _include(path, include, exclude):
|
|
371
382
|
"Returns True if path matches include patterns (if any) and doesn't match any exclude pattern."
|
|
@@ -391,7 +402,6 @@ async def get_file_content(self:GhApi, path, owner, repo, branch="main"):
|
|
|
391
402
|
o['content_decoded'] = base64.b64decode(o.content).decode('utf-8')
|
|
392
403
|
return o
|
|
393
404
|
|
|
394
|
-
# %% ../nbs/00_core.ipynb #d02d1bab
|
|
395
405
|
@patch
|
|
396
406
|
@delegates(GhApi.get_repo_files)
|
|
397
407
|
async def get_repo_contents(self:GhApi, owner, repo, branch='main',
|
|
@@ -427,12 +437,15 @@ class _IssueInfo(AttrDict):
|
|
|
427
437
|
|
|
428
438
|
@gh_patch
|
|
429
439
|
async def read_issue(self:GhApi, issue_number:int):
|
|
430
|
-
"Fetch an issue or PR
|
|
440
|
+
"""Fetch an issue or PR as structured data with a compact display.
|
|
441
|
+
|
|
442
|
+
Fields include `title`, `body`, `is_pr`, and general `comments`. PRs also have the full `diff`, inline `review_comments`, and `reviews`. Display the result bare for the summary; inspect those fields for detail. General PR comments use the Issues API, not the Pulls review API.
|
|
443
|
+
"""
|
|
431
444
|
try: pr = await self.pulls.get(issue_number)
|
|
432
445
|
except APIError: pr = None
|
|
433
446
|
iss = await self.issues.get(issue_number)
|
|
434
|
-
res = dict(title=iss.title, body=iss.body or '', is_pr=pr is not None,
|
|
435
|
-
|
|
447
|
+
res = dict(title=iss.title, body=iss.body or '', is_pr=pr is not None,
|
|
448
|
+
comments=await self.issues.list_comments(issue_number))
|
|
436
449
|
if pr is not None:
|
|
437
450
|
res['diff'] = await self.pulls.get(issue_number, headers_={'Accept': 'application/vnd.github.v3.diff'})
|
|
438
451
|
res['review_comments'] = await self.pulls.list_review_comments(issue_number)
|
|
@@ -447,9 +460,9 @@ async def read_issue(self:GhApi, issue_number:int):
|
|
|
447
460
|
def _filter_diff(diff, folder='', skip_files=('_modidx.py',)):
|
|
448
461
|
"Filter unified diff to only include files under `folder`, skipping `skip_files`"
|
|
449
462
|
sections = re.split(r'(?=^diff --git )', diff, flags=re.MULTILINE)
|
|
450
|
-
return ''.join(s for s in sections
|
|
451
|
-
|
|
452
|
-
|
|
463
|
+
return ''.join(s for s in sections
|
|
464
|
+
if s.startswith(f'diff --git a/{folder}')
|
|
465
|
+
and not any(f'/{f} ' in s.split('\n')[0] for f in skip_files))
|
|
453
466
|
|
|
454
467
|
def _reduce_ctx(diff):
|
|
455
468
|
"Keep only diff headers and changed lines"
|
|
@@ -527,6 +540,7 @@ class PullRow(AttrDict):
|
|
|
527
540
|
def line(self, maxlen=MAXLEN): return f'#{self.number} {_age(self.updated_at)} {_pr_mark(self)}{truncstr(self.title, maxlen)}'
|
|
528
541
|
__repr__ = line
|
|
529
542
|
|
|
543
|
+
# %% ../nbs/00_core.ipynb #21b48014
|
|
530
544
|
class PullRows(GhRows):
|
|
531
545
|
"Pull request rows, displayed with titles truncated to `maxlen`"
|
|
532
546
|
def __init__(self, items, maxlen=MAXLEN):
|
|
@@ -546,18 +560,26 @@ async def list_prs(
|
|
|
546
560
|
prs = await self.pulls.list(state=state, sort='updated', direction='desc', per_page=per_page, page=page)
|
|
547
561
|
return PullRows([PullRow(o) for o in prs], maxlen=maxlen)
|
|
548
562
|
|
|
563
|
+
# %% ../nbs/00_core.ipynb #c74057c8
|
|
564
|
+
def _parse_section(b):
|
|
565
|
+
attrs = b['attributes']
|
|
566
|
+
options = [o['label'] if isinstance(o, dict) else o for o in attrs.get('options', [])]
|
|
567
|
+
return dict(label=attrs['label'], type=b['type'], required=bool(nested_idx(b, 'validations', 'required')), options=options)
|
|
568
|
+
|
|
569
|
+
|
|
549
570
|
# %% ../nbs/00_core.ipynb #55a976ea
|
|
550
571
|
def _parse_tmpl(name, txt):
|
|
551
572
|
"Parse issue template `txt`: yml forms into section labels for `issue_body`; md templates kept as `raw`"
|
|
552
573
|
if not name.endswith(('.yml','.yaml')): return dict2obj(dict(name=name, raw=txt))
|
|
553
574
|
import yaml
|
|
554
575
|
d = yaml.safe_load(txt)
|
|
555
|
-
secs = [
|
|
556
|
-
required=bool(nested_idx(b, 'validations', 'required')),
|
|
557
|
-
options=[o['label'] if isinstance(o, dict) else o for o in b['attributes'].get('options', [])])
|
|
558
|
-
for b in d.get('body', []) if b['type']!='markdown']
|
|
576
|
+
secs = [_parse_section(b) for b in d.get('body', []) if b['type']!='markdown']
|
|
559
577
|
return dict2obj(dict(name=name, title=d.get('name'), description=d.get('description'), sections=secs))
|
|
560
578
|
|
|
579
|
+
async def _load_tmpl(api, f, **kw):
|
|
580
|
+
txt = (await api.get_content(f.path, **kw)).decode()
|
|
581
|
+
return _parse_tmpl(f.name, txt)
|
|
582
|
+
|
|
561
583
|
@gh_patch
|
|
562
584
|
async def issue_template(self:GhApi):
|
|
563
585
|
"Parsed issue templates for the repo (or the owner's `.github` repo as fallback); pass one to `issue_body`"
|
|
@@ -567,8 +589,8 @@ async def issue_template(self:GhApi):
|
|
|
567
589
|
except APIError as e:
|
|
568
590
|
if e.status_code != 404: raise
|
|
569
591
|
continue
|
|
570
|
-
return L([
|
|
571
|
-
|
|
592
|
+
return L([await _load_tmpl(self, f, **kw) for f in fs
|
|
593
|
+
if f.name.endswith(('.md','.yml','.yaml')) and f.name != 'config.yml'])
|
|
572
594
|
return L()
|
|
573
595
|
|
|
574
596
|
# %% ../nbs/00_core.ipynb #173fc534
|
|
@@ -598,6 +620,7 @@ class CheckRun(AttrDict):
|
|
|
598
620
|
class CommitStatus(AttrDict):
|
|
599
621
|
def __repr__(self): return f'{self.context}: {self.state}'
|
|
600
622
|
|
|
623
|
+
# %% ../nbs/00_core.ipynb #6a9289a9
|
|
601
624
|
class _CheckStatus(AttrDict):
|
|
602
625
|
def _repr_markdown_(self):
|
|
603
626
|
runs = self.check_runs
|
|
@@ -610,7 +633,10 @@ class _CheckStatus(AttrDict):
|
|
|
610
633
|
|
|
611
634
|
@gh_patch
|
|
612
635
|
async def check_status(self:GhApi, ref:str):
|
|
613
|
-
"Combined commit status and check-run results for `ref` (a SHA, branch, or tag)
|
|
636
|
+
"""Combined commit status and check-run results for `ref` (a SHA, branch, or tag).
|
|
637
|
+
|
|
638
|
+
Display the result bare for the check-run verdict and all run/status rows. `.state` comes only from legacy commit statuses and can be pending even when every Actions check passed. `.check_runs` and `.statuses` retain the structured rows.
|
|
639
|
+
"""
|
|
614
640
|
combined = await self.repos.get_combined_status_for_ref(ref)
|
|
615
641
|
checks = await self.checks.list_for_ref(ref)
|
|
616
642
|
return _CheckStatus(dict(state=combined.state, statuses=GhRows(CommitStatus(o) for o in combined.statuses),
|
|
@@ -624,7 +650,10 @@ async def pr_status(self:GhApi, pull_number:int):
|
|
|
624
650
|
# %% ../nbs/00_core.ipynb #569864f5
|
|
625
651
|
@gh_patch
|
|
626
652
|
async def failed_step_log(self:GhApi, job_id:int):
|
|
627
|
-
"
|
|
653
|
+
"""Return the failed steps' log sections, each headed by its step name.
|
|
654
|
+
|
|
655
|
+
An Actions check run's id is its job id. Pass that id from `check_status` or `pr_status`. The returned text's display is truncated at 8,000 characters. GitHub can expire or delete stored logs.
|
|
656
|
+
"""
|
|
628
657
|
job = await self.actions.get_job_for_workflow_run(job_id=job_id)
|
|
629
658
|
zf = zipfile.ZipFile(io.BytesIO(await self.actions.download_workflow_run_logs(run_id=job.run_id)))
|
|
630
659
|
res = []
|