ghapi 2.0.2__tar.gz → 2.0.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.
Files changed (29) hide show
  1. {ghapi-2.0.2/ghapi.egg-info → ghapi-2.0.4}/PKG-INFO +2 -2
  2. ghapi-2.0.4/ghapi/__init__.py +1 -0
  3. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi/_modidx.py +24 -20
  4. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi/core.py +63 -22
  5. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi/skill.py +20 -3
  6. {ghapi-2.0.2 → ghapi-2.0.4/ghapi.egg-info}/PKG-INFO +2 -2
  7. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi.egg-info/requires.txt +1 -1
  8. {ghapi-2.0.2 → ghapi-2.0.4}/pyproject.toml +1 -1
  9. ghapi-2.0.2/ghapi/__init__.py +0 -1
  10. {ghapi-2.0.2 → ghapi-2.0.4}/CONTRIBUTING.md +0 -0
  11. {ghapi-2.0.2 → ghapi-2.0.4}/LICENSE +0 -0
  12. {ghapi-2.0.2 → ghapi-2.0.4}/MANIFEST.in +0 -0
  13. {ghapi-2.0.2 → ghapi-2.0.4}/README.md +0 -0
  14. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi/actions.py +0 -0
  15. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi/all.py +0 -0
  16. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi/auth.py +0 -0
  17. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi/build_lib.py +0 -0
  18. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi/cli.py +0 -0
  19. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi/event.py +0 -0
  20. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi/gh_spec.py +0 -0
  21. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi/graphql.py +0 -0
  22. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi/page.py +0 -0
  23. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi/py.typed +0 -0
  24. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi/templates.py +0 -0
  25. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi.egg-info/SOURCES.txt +0 -0
  26. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi.egg-info/dependency_links.txt +0 -0
  27. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi.egg-info/entry_points.txt +0 -0
  28. {ghapi-2.0.2 → ghapi-2.0.4}/ghapi.egg-info/top_level.txt +0 -0
  29. {ghapi-2.0.2 → ghapi-2.0.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghapi
3
- Version: 2.0.2
3
+ Version: 2.0.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
@@ -16,7 +16,7 @@ Requires-Python: >=3.10
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: fastcore>=1.14.5
19
- Requires-Dist: fastspec>=0.1.0
19
+ Requires-Dist: fastspec>=0.1.2
20
20
  Provides-Extra: dev
21
21
  Requires-Dist: fastgit; extra == "dev"
22
22
  Requires-Dist: matplotlib; extra == "dev"
@@ -0,0 +1 @@
1
+ __version__ = "2.0.4"
@@ -48,34 +48,15 @@ d = { 'settings': { 'branch': 'main',
48
48
  'ghapi.core.GhApi.__init__': ('core.html#ghapi.__init__', 'ghapi/core.py'),
49
49
  'ghapi.core.GhApi._get_repo_files': ('core.html#ghapi._get_repo_files', 'ghapi/core.py'),
50
50
  'ghapi.core.GhApi._repr_markdown_': ('core.html#ghapi._repr_markdown_', 'ghapi/core.py'),
51
- 'ghapi.core.GhApi.check_status': ('core.html#ghapi.check_status', 'ghapi/core.py'),
52
- 'ghapi.core.GhApi.create_branch_empty': ('core.html#ghapi.create_branch_empty', 'ghapi/core.py'),
53
- 'ghapi.core.GhApi.create_file': ('core.html#ghapi.create_file', 'ghapi/core.py'),
54
51
  'ghapi.core.GhApi.create_gist': ('core.html#ghapi.create_gist', 'ghapi/core.py'),
55
- 'ghapi.core.GhApi.create_or_update_file': ('core.html#ghapi.create_or_update_file', 'ghapi/core.py'),
56
- 'ghapi.core.GhApi.create_release': ('core.html#ghapi.create_release', 'ghapi/core.py'),
57
52
  'ghapi.core.GhApi.debug': ('core.html#ghapi.debug', 'ghapi/core.py'),
58
- 'ghapi.core.GhApi.delete_branch': ('core.html#ghapi.delete_branch', 'ghapi/core.py'),
59
- 'ghapi.core.GhApi.delete_file': ('core.html#ghapi.delete_file', 'ghapi/core.py'),
60
- 'ghapi.core.GhApi.delete_release': ('core.html#ghapi.delete_release', 'ghapi/core.py'),
61
- 'ghapi.core.GhApi.delete_tag': ('core.html#ghapi.delete_tag', 'ghapi/core.py'),
62
- 'ghapi.core.GhApi.enable_pages': ('core.html#ghapi.enable_pages', 'ghapi/core.py'),
63
- 'ghapi.core.GhApi.get_branch': ('core.html#ghapi.get_branch', 'ghapi/core.py'),
64
- 'ghapi.core.GhApi.get_content': ('core.html#ghapi.get_content', 'ghapi/core.py'),
65
53
  'ghapi.core.GhApi.get_file_content': ('core.html#ghapi.get_file_content', 'ghapi/core.py'),
66
54
  'ghapi.core.GhApi.get_repo_contents': ('core.html#ghapi.get_repo_contents', 'ghapi/core.py'),
67
55
  'ghapi.core.GhApi.get_repo_files': ('core.html#ghapi.get_repo_files', 'ghapi/core.py'),
68
56
  'ghapi.core.GhApi.gist_file': ('core.html#ghapi.gist_file', 'ghapi/core.py'),
69
- 'ghapi.core.GhApi.issue_template': ('core.html#ghapi.issue_template', 'ghapi/core.py'),
70
57
  'ghapi.core.GhApi.limit_rem': ('core.html#ghapi.limit_rem', 'ghapi/core.py'),
71
- 'ghapi.core.GhApi.list_branches': ('core.html#ghapi.list_branches', 'ghapi/core.py'),
72
- 'ghapi.core.GhApi.list_files': ('core.html#ghapi.list_files', 'ghapi/core.py'),
73
- 'ghapi.core.GhApi.list_tags': ('core.html#ghapi.list_tags', 'ghapi/core.py'),
74
58
  'ghapi.core.GhApi.load_gist': ('core.html#ghapi.load_gist', 'ghapi/core.py'),
75
- 'ghapi.core.GhApi.pr_status': ('core.html#ghapi.pr_status', 'ghapi/core.py'),
76
- 'ghapi.core.GhApi.read_issue': ('core.html#ghapi.read_issue', 'ghapi/core.py'),
77
59
  'ghapi.core.GhApi.recv_hdrs': ('core.html#ghapi.recv_hdrs', 'ghapi/core.py'),
78
- 'ghapi.core.GhApi.update_contents': ('core.html#ghapi.update_contents', 'ghapi/core.py'),
79
60
  'ghapi.core.GhApi.update_gist': ('core.html#ghapi.update_gist', 'ghapi/core.py'),
80
61
  'ghapi.core.GhApi.upload_file': ('core.html#ghapi.upload_file', 'ghapi/core.py'),
81
62
  'ghapi.core.GhSyncTransport': ('core.html#ghsynctransport', 'ghapi/core.py'),
@@ -90,6 +71,8 @@ d = { 'settings': { 'branch': 'main',
90
71
  'ghapi.core._CheckStatus._repr_markdown_': ('core.html#_checkstatus._repr_markdown_', 'ghapi/core.py'),
91
72
  'ghapi.core._IssueInfo': ('core.html#_issueinfo', 'ghapi/core.py'),
92
73
  'ghapi.core._IssueInfo._repr_markdown_': ('core.html#_issueinfo._repr_markdown_', 'ghapi/core.py'),
74
+ 'ghapi.core._LiveDefaults': ('core.html#_livedefaults', 'ghapi/core.py'),
75
+ 'ghapi.core._LiveDefaults.items': ('core.html#_livedefaults.items', 'ghapi/core.py'),
93
76
  'ghapi.core._filter_diff': ('core.html#_filter_diff', 'ghapi/core.py'),
94
77
  'ghapi.core._find_matches': ('core.html#_find_matches', 'ghapi/core.py'),
95
78
  'ghapi.core._fmt_replies': ('core.html#_fmt_replies', 'ghapi/core.py'),
@@ -98,12 +81,33 @@ d = { 'settings': { 'branch': 'main',
98
81
  'ghapi.core._reduce_ctx': ('core.html#_reduce_ctx', 'ghapi/core.py'),
99
82
  'ghapi.core._run_subp': ('core.html#_run_subp', 'ghapi/core.py'),
100
83
  'ghapi.core.call_gh': ('core.html#call_gh', 'ghapi/core.py'),
84
+ 'ghapi.core.check_status': ('core.html#check_status', 'ghapi/core.py'),
85
+ 'ghapi.core.commit_tree': ('core.html#commit_tree', 'ghapi/core.py'),
86
+ 'ghapi.core.create_branch_empty': ('core.html#create_branch_empty', 'ghapi/core.py'),
87
+ 'ghapi.core.create_file': ('core.html#create_file', 'ghapi/core.py'),
88
+ 'ghapi.core.create_or_update_file': ('core.html#create_or_update_file', 'ghapi/core.py'),
89
+ 'ghapi.core.create_release': ('core.html#create_release', 'ghapi/core.py'),
101
90
  'ghapi.core.date2gh': ('core.html#date2gh', 'ghapi/core.py'),
91
+ 'ghapi.core.delete_branch': ('core.html#delete_branch', 'ghapi/core.py'),
92
+ 'ghapi.core.delete_file': ('core.html#delete_file', 'ghapi/core.py'),
93
+ 'ghapi.core.delete_release': ('core.html#delete_release', 'ghapi/core.py'),
94
+ 'ghapi.core.delete_tag': ('core.html#delete_tag', 'ghapi/core.py'),
95
+ 'ghapi.core.enable_pages': ('core.html#enable_pages', 'ghapi/core.py'),
96
+ 'ghapi.core.get_branch': ('core.html#get_branch', 'ghapi/core.py'),
97
+ 'ghapi.core.get_content': ('core.html#get_content', 'ghapi/core.py'),
102
98
  'ghapi.core.gh2date': ('core.html#gh2date', 'ghapi/core.py'),
99
+ 'ghapi.core.gh_patch': ('core.html#gh_patch', 'ghapi/core.py'),
103
100
  'ghapi.core.issue_body': ('core.html#issue_body', 'ghapi/core.py'),
101
+ 'ghapi.core.issue_template': ('core.html#issue_template', 'ghapi/core.py'),
102
+ 'ghapi.core.list_branches': ('core.html#list_branches', 'ghapi/core.py'),
103
+ 'ghapi.core.list_files': ('core.html#list_files', 'ghapi/core.py'),
104
+ 'ghapi.core.list_tags': ('core.html#list_tags', 'ghapi/core.py'),
104
105
  'ghapi.core.pr_file_diff': ('core.html#pr_file_diff', 'ghapi/core.py'),
106
+ 'ghapi.core.pr_status': ('core.html#pr_status', 'ghapi/core.py'),
105
107
  'ghapi.core.print_summary': ('core.html#print_summary', 'ghapi/core.py'),
106
- 'ghapi.core.read_pr': ('core.html#read_pr', 'ghapi/core.py')},
108
+ 'ghapi.core.read_issue': ('core.html#read_issue', 'ghapi/core.py'),
109
+ 'ghapi.core.read_pr': ('core.html#read_pr', 'ghapi/core.py'),
110
+ 'ghapi.core.update_contents': ('core.html#update_contents', 'ghapi/core.py')},
107
111
  'ghapi.event': { 'ghapi.event.GhApi.fetch_events': ('event.html#ghapi.fetch_events', 'ghapi/event.py'),
108
112
  'ghapi.event.GhApi.list_events': ('event.html#ghapi.list_events', 'ghapi/event.py'),
109
113
  'ghapi.event.GhApi.list_events_parallel': ('event.html#ghapi.list_events_parallel', 'ghapi/event.py'),
@@ -5,10 +5,16 @@ 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', 'GhTransport', 'GhSyncTransport', 'print_summary', 'GhApi',
9
- 'call_gh', 'date2gh', 'gh2date', 'read_pr', 'pr_file_diff', 'issue_body', 'APIError']
8
+ __all__ = ['GH_HOST', 'pspec', 'img_md_pat', 'EMPTY_TREE_SHA', 'GhTransport', 'GhSyncTransport', 'print_summary', 'gh_patch',
9
+ 'GhApi', 'call_gh', 'date2gh', 'gh2date', 'delete_release', 'create_release', 'list_tags', 'list_branches',
10
+ 'create_branch_empty', 'delete_tag', 'delete_branch', 'get_branch', 'commit_tree', 'list_files',
11
+ 'get_content', 'create_or_update_file', 'create_file', 'delete_file', 'update_contents', 'enable_pages',
12
+ 'read_issue', 'read_pr', 'pr_file_diff', 'issue_template', 'issue_body', 'check_status', 'pr_status',
13
+ 'APIError']
10
14
 
11
15
  # %% ../nbs/00_core.ipynb #5b5cba7b
16
+ from contextvars import ContextVar
17
+
12
18
  from fastcore.all import *
13
19
  from fastspec.spec import SpecParser
14
20
  from fastspec.oapi import OpenAPIClient, OpFunc, SyncOpFunc, _build_groups, UNSET
@@ -85,6 +91,23 @@ def print_summary(method, url, kwargs):
85
91
  hdrs = {k:v for k,v in (kwargs.get('headers') or {}).items() if k.lower()!='authorization'}
86
92
  print(method, url, {**kwargs, 'headers':hdrs})
87
93
 
94
+ # %% ../nbs/00_core.ipynb #cecd0177
95
+ _gh_override = ContextVar('_gh_override', default={})
96
+
97
+ class _LiveDefaults(dict):
98
+ "Endpoint defaults, superseded at call time by any active `gh_patch` override"
99
+ def items(self): return {**self, **_gh_override.get()}.items()
100
+
101
+ def gh_patch(fn):
102
+ "`patch` `fn` into `GhApi`, adding `owner`/`repo` params that override the client defaults for this call and its internal calls"
103
+ async def _f(self, *args, owner=UNSET, repo=UNSET, **kwargs):
104
+ over = {k:v for k,v in zip(('owner','repo'),(owner,repo)) if v is not UNSET}
105
+ if not over: return await fn(self, *args, **kwargs)
106
+ token = _gh_override.set({**_gh_override.get(), **over})
107
+ try: return await fn(self, *args, **kwargs)
108
+ finally: _gh_override.reset(token)
109
+ return patch(splice_sig(_f, fn, 'self'))
110
+
88
111
  # %% ../nbs/00_core.ipynb #83e8a9ce
89
112
  class GhApi(OpenAPIClient):
90
113
  "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."
@@ -102,7 +125,7 @@ class GhApi(OpenAPIClient):
102
125
  self.token,self.gh_host = token,gh_host or GH_HOST
103
126
  tcls,fcls = (GhSyncTransport,SyncOpFunc) if sync else (GhTransport,OpFunc)
104
127
  self.transport = tcls(debug=debug, limit_cb=limit_cb, timeout=timeout, base_headers=self.headers)
105
- self.ops = [fcls(o, self.transport, self.gh_host, defaults=kwargs) for o in pspec.ops]
128
+ self.ops = [fcls(o, self.transport, self.gh_host, defaults=_LiveDefaults(kwargs)) for o in pspec.ops]
106
129
  self.func_dict = {f'{o.path}:{o.verb.upper()}':o for o in self.ops}
107
130
  self.groups = _build_groups(self.ops)
108
131
  for k,v in self.groups.items(): setattr(self, k, v)
@@ -195,7 +218,7 @@ async def update_gist(self:GhApi, gist_id:str, content:str):
195
218
  return (await self.gists.update(gist_id, files={fname: {'content': content}})).html_url
196
219
 
197
220
  # %% ../nbs/00_core.ipynb #4b7a278c
198
- @patch
221
+ @gh_patch
199
222
  async def delete_release(self:GhApi, release):
200
223
  "Delete a release and its associated tag"
201
224
  await self.repos.delete_release(release.id)
@@ -211,7 +234,7 @@ async def upload_file(self:GhApi, rel, fn):
211
234
  return await self(url, 'POST', headers={'Content-Type':mime}, query = {'name':fn.name}, data=fn.read_bytes())
212
235
 
213
236
  # %% ../nbs/00_core.ipynb #3cad71a4
214
- @patch
237
+ @gh_patch
215
238
  async def create_release(self:GhApi, tag_name, branch='master', name=None, body='',
216
239
  draft=False, prerelease=False, files=None, make_latest=UNSET):
217
240
  "Wrapper for `GhApi.repos.create_release` which also uploads `files`"
@@ -222,13 +245,13 @@ async def create_release(self:GhApi, tag_name, branch='master', name=None, body=
222
245
  return rel
223
246
 
224
247
  # %% ../nbs/00_core.ipynb #2be73ae0
225
- @patch
248
+ @gh_patch
226
249
  async def list_tags(self:GhApi, prefix:str=''):
227
250
  "List all tags, optionally filtered to those starting with `prefix`"
228
251
  return await self.git.list_matching_refs(f'tags/{prefix}')
229
252
 
230
253
  # %% ../nbs/00_core.ipynb #303eeec6
231
- @patch
254
+ @gh_patch
232
255
  async def list_branches(self:GhApi, prefix:str=''):
233
256
  "List all branches, optionally filtered to those starting with `prefix`"
234
257
  return await self.git.list_matching_refs(f'heads/{prefix}')
@@ -238,7 +261,7 @@ async def list_branches(self:GhApi, prefix:str=''):
238
261
  EMPTY_TREE_SHA = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
239
262
 
240
263
  # %% ../nbs/00_core.ipynb #ba6ab941
241
- @patch
264
+ @gh_patch
242
265
  async def create_branch_empty(self:GhApi, branch):
243
266
  t = await self.git.create_tree(base_tree=EMPTY_TREE_SHA, tree = [dict(
244
267
  path='.dummy', content='ignore me', mode='100644', type='blob')])
@@ -246,38 +269,56 @@ async def create_branch_empty(self:GhApi, branch):
246
269
  return await self.git.create_ref(f'refs/heads/{branch}', c.sha)
247
270
 
248
271
  # %% ../nbs/00_core.ipynb #68b150fc
249
- @patch
272
+ @gh_patch
250
273
  async def delete_tag(self:GhApi, tag:str):
251
274
  "Delete a tag"
252
275
  return await self.git.delete_ref(f'tags/{tag}')
253
276
 
254
277
  # %% ../nbs/00_core.ipynb #75c168a1
255
- @patch
278
+ @gh_patch
256
279
  async def delete_branch(self:GhApi, branch:str):
257
280
  "Delete a branch"
258
281
  return await self.git.delete_ref(f'heads/{branch}')
259
282
 
260
283
  # %% ../nbs/00_core.ipynb #96053795
261
- @patch
284
+ @gh_patch
262
285
  async def get_branch(self:GhApi, branch=None):
263
286
  branch = branch or (await self.repos.get()).default_branch
264
287
  return (await self.list_branches(branch))[0]
265
288
 
289
+ # %% ../nbs/00_core.ipynb #9f70ac44
290
+ @gh_patch
291
+ async def commit_tree(self:GhApi, branch, message, tree, base=None):
292
+ "Commit `tree` entries to `branch`, creating it from `base` when needed"
293
+ try:
294
+ ref = await self.git.get_ref(f'heads/{branch}')
295
+ exists = True
296
+ except APIError as e:
297
+ if e.status_code != 404: raise
298
+ base = base or (await self.repos.get()).default_branch
299
+ ref,exists = await self.git.get_ref(f'heads/{base}'),False
300
+ parent = await self.git.get_commit(ref.object.sha)
301
+ new_tree = await self.git.create_tree(tree, parent.tree.sha)
302
+ commit = await self.git.create_commit(message, new_tree.sha, [parent.sha])
303
+ if exists: await self.git.update_ref(f'heads/{branch}', commit.sha)
304
+ else: await self.git.create_ref(f'refs/heads/{branch}', commit.sha)
305
+ return commit
306
+
266
307
  # %% ../nbs/00_core.ipynb #93b24881
267
- @patch
308
+ @gh_patch
268
309
  async def list_files(self:GhApi, branch=None):
269
310
  ref = await self.get_branch(branch)
270
311
  res = (await self.git.get_tree(ref.object.sha)).tree
271
312
  return {o.path:o for o in res}
272
313
 
273
314
  # %% ../nbs/00_core.ipynb #ffc347b2
274
- @patch
315
+ @gh_patch
275
316
  async def get_content(self:GhApi, path):
276
317
  res = await self.repos.get_content(path)
277
318
  return base64.b64decode(res.content)
278
319
 
279
320
  # %% ../nbs/00_core.ipynb #9582ee1f
280
- @patch
321
+ @gh_patch
281
322
  async def create_or_update_file(self:GhApi, path, message, committer, author, content=None, sha=None, branch=''):
282
323
  if not branch: branch = (await self.repos.get())['default_branch']
283
324
  if not isinstance(content,bytes): content = content.encode()
@@ -287,20 +328,20 @@ async def create_or_update_file(self:GhApi, path, message, committer, author, co
287
328
  branch=branch, committer=committer or {}, author=author or {}, **kwargs)
288
329
 
289
330
  # %% ../nbs/00_core.ipynb #5044445d
290
- @patch
331
+ @gh_patch
291
332
  async def create_file(self:GhApi, path, message, committer, author, content=None, branch=None):
292
333
  if not branch: branch = (await self.repos.get())['default_branch']
293
334
  return await self.create_or_update_file(path, message, branch=branch, committer=committer, content=content, author=author)
294
335
 
295
336
  # %% ../nbs/00_core.ipynb #4e74c337
296
- @patch
337
+ @gh_patch
297
338
  async def delete_file(self:GhApi, path, message, committer, author, sha=None, branch=None):
298
339
  if not branch: branch = (await self.repos.get())['default_branch']
299
340
  if sha is None: sha = (await self.list_files())[path].sha
300
341
  return await self.repos.delete_file(path, message=message, sha=sha, branch=branch, committer=committer, author=author)
301
342
 
302
343
  # %% ../nbs/00_core.ipynb #93f6b559
303
- @patch
344
+ @gh_patch
304
345
  async def update_contents(self:GhApi, path, message, committer, author, content, sha=None, branch=None):
305
346
  if not branch: branch = (await self.repos.get())['default_branch']
306
347
  if sha is None: sha = (await self.list_files())[path].sha
@@ -348,7 +389,7 @@ async def get_repo_contents(self:GhApi, owner, repo, branch='main',
348
389
  return L(await parallel_async(self.get_file_content, paths, owner=owner, repo=repo, branch=branch, n_workers=n_workers))
349
390
 
350
391
  # %% ../nbs/00_core.ipynb #ac4ab4e0
351
- @patch
392
+ @gh_patch
352
393
  async def enable_pages(self:GhApi, branch=None, path="/"):
353
394
  "Enable or update pages for a repo to point to a `branch` and `path`."
354
395
  if path not in ('/docs','/'): raise Exception("path not in ('/docs','/')")
@@ -370,7 +411,7 @@ class _IssueInfo(AttrDict):
370
411
  return '\n\n'.join(res)
371
412
  __repr__ = _repr_markdown_
372
413
 
373
- @patch
414
+ @gh_patch
374
415
  async def read_issue(self:GhApi, issue_number:int):
375
416
  "Fetch an issue or PR: title, body, comments, and (for PRs) diff, review comments, and reviews"
376
417
  try: pr = await self.pulls.get(issue_number)
@@ -463,7 +504,7 @@ def _parse_tmpl(name, txt):
463
504
  for b in d.get('body', []) if b['type']!='markdown']
464
505
  return dict2obj(dict(name=name, title=d.get('name'), description=d.get('description'), sections=secs))
465
506
 
466
- @patch
507
+ @gh_patch
467
508
  async def issue_template(self:GhApi):
468
509
  "Parsed issue templates for the repo (or the owner's `.github` repo as fallback); pass one to `issue_body`"
469
510
  for repo,path in ((None,'.github/ISSUE_TEMPLATE'), ('.github','ISSUE_TEMPLATE'), ('.github','.github/ISSUE_TEMPLATE')):
@@ -501,14 +542,14 @@ class _CheckStatus(AttrDict):
501
542
  return '\n'.join([f'**{verdict}**', ''] + [f"- {r.name}: {r.conclusion or r.status}" for r in runs])
502
543
  __repr__ = _repr_markdown_
503
544
 
504
- @patch
545
+ @gh_patch
505
546
  async def check_status(self:GhApi, ref:str):
506
547
  "Combined commit status and check-run results for `ref` (a SHA, branch, or tag)"
507
548
  combined = await self.repos.get_combined_status_for_ref(ref)
508
549
  checks = await self.checks.list_for_ref(ref)
509
550
  return _CheckStatus(dict2obj(dict(state=combined.state, statuses=combined.statuses, check_runs=checks.check_runs)))
510
551
 
511
- @patch
552
+ @gh_patch
512
553
  async def pr_status(self:GhApi, pull_number:int):
513
554
  "Combined status and check-run results for a PR's head commit"
514
555
  return await self.check_status((await self.pulls.get(pull_number)).head.sha)
@@ -9,9 +9,17 @@
9
9
  api = GhApi(owner='fastai', repo='ghapi')
10
10
  await api.issues.list_for_repo(state='open') # owner/repo already bound
11
11
 
12
+ These are only defaults: any endpoint or convenience method accepts `owner=`/`repo=` to override per call (e.g. `await api.check_status('v1.0', repo='other')`), so one client can serve a whole org; convenience methods propagate the override to everything they call internally.
13
+
12
14
  # Discovering the API
13
15
 
14
- `api.<group>` (displayed bare, or via `doc()`) lists every endpoint in a group (issues, pulls, repos, checks, actions, gists, git, search, ...) with its signature and a link to GitHub's docs. `api['/path/{owner}/{repo}', 'GET']` looks up an endpoint by path+verb if you'd rather not use the grouped names. Endpoint names follow `<verb>_<noun>`, e.g. `issues.list_for_repo`, `issues.create`, `pulls.merge`. Displaying `api` itself lists every available group with a docs link. `doc()` works at every level of a live instance: `doc(api)`, `doc(api.issues)`, `doc(api.issues.create)`. It must be a live instance, since the API is generated at construction time: inspecting the `GhApi` *class* shows only the convenience methods and none of the endpoint groups.
16
+ Displaying `api` lists every available group with a docs link. Endpoint names follow `<verb>_<noun>`, e.g. `issues.list_for_repo`, `issues.create`, `pulls.merge`. Groups can contain hundreds of endpoints, so search their names with `pyskills.xdir()` instead of rendering the whole group, then inspect the matching endpoint:
17
+
18
+ import pyskills
19
+ pyskills.xdir(api.actions, 'log')
20
+ doc(api.actions.download_job_logs_for_workflow_run)
21
+
22
+ The optional `pyskills.xdir` query is a case-insensitive regex. `doc()` works at every level of a live instance, but `doc(api.actions)` renders that group's complete endpoint list; use it only when the group is small enough to read. The instance must be live because the API is generated at construction time: inspecting the `GhApi` *class* shows only convenience methods, not generated groups. `api['/path/{owner}/{repo}', 'GET']` looks up an endpoint directly by path and verb.
15
23
 
16
24
  # Reading an issue or PR (including comments and reviews)
17
25
 
@@ -60,6 +68,16 @@ For anything that's about the local repo/working tree rather than GitHub itself
60
68
 
61
69
  `Git.__call__` prints (not raises) on a `CalledProcessError` unless you pass `mute_errors=True` -- check the return value, don't assume no exception means success.
62
70
 
71
+ # External PRs
72
+
73
+ For a PR to another owner's repo, use normal local Git through the commit, ensure your fork exists, then use one client scoped to the fork. `commit_tree` uploads GitHub tree entries as one commit without Git transport authentication; override `owner` only when creating the upstream PR:
74
+
75
+ api = GhApi('me', repo)
76
+ await api.commit_tree(branch, message, entries)
77
+ await api.pulls.create(owner='upstream', head=f'me:{branch}', base='main', title=title, body=body)
78
+
79
+ Once the PR exists, the fork branch is the source of truth, and the local clone is residue: reset it (`git restore .` and switch back to the main branch) rather than leaving the edits behind, where a later session will find an unexplained diff. In particular, never build a second PR's edits on a tree still dirty from the first: each PR gets its own branch from main with a clean tree, since layered uncommitted edits ship a merged diff to `commit_tree` and block `git switch` once shared files diverge.
80
+
63
81
  # Gotchas
64
82
 
65
83
  - A PR *is* an issue for general comments (`issues.list_comments`, not `pulls.*`) -- inline code-review comments are the separate `pulls.list_review_comments`.
@@ -85,8 +103,7 @@ class ReadOnlyGhPolicy(AllowPolicy):
85
103
  allow({OpFunc: [('__call__', ReadOnlyGhPolicy())]})
86
104
  class GitPolicy(AllowPolicy):
87
105
  "Allow only safecmd-default git subcommands"
88
- cmds = {
89
- 'blame','branch','cat-file','config','describe','diff','log','ls-files','ls-tree','merge-base',
106
+ cmds = {'blame','branch','cat-file','config','describe','diff','log','ls-files','ls-tree','merge-base',
90
107
  'remote','rev-parse','shortlog','show','stash','status','tag','fetch','add','commit','switch','checkout'}
91
108
 
92
109
  def __call__(self, obj, args, kwargs, data):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghapi
3
- Version: 2.0.2
3
+ Version: 2.0.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
@@ -16,7 +16,7 @@ Requires-Python: >=3.10
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: fastcore>=1.14.5
19
- Requires-Dist: fastspec>=0.1.0
19
+ Requires-Dist: fastspec>=0.1.2
20
20
  Provides-Extra: dev
21
21
  Requires-Dist: fastgit; extra == "dev"
22
22
  Requires-Dist: matplotlib; extra == "dev"
@@ -1,5 +1,5 @@
1
1
  fastcore>=1.14.5
2
- fastspec>=0.1.0
2
+ fastspec>=0.1.2
3
3
 
4
4
  [dev]
5
5
  fastgit
@@ -12,7 +12,7 @@ license = {text = "Apache-2.0"}
12
12
  authors = [{name = "Jeremy Howard", email = "info@fast.ai"}]
13
13
  keywords = ['github', 'api']
14
14
  classifiers = ["Natural Language :: English", "Intended Audience :: Developers", "Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only"]
15
- dependencies = ['fastcore>=1.14.5', 'fastspec>=0.1.0']
15
+ dependencies = ['fastcore>=1.14.5', 'fastspec>=0.1.2']
16
16
 
17
17
  [project.urls]
18
18
  Repository = "https://github.com/fastai/ghapi"
@@ -1 +0,0 @@
1
- __version__ = "2.0.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes