Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
488 changes: 433 additions & 55 deletions 00_core.ipynb

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions ghapi/_modidx.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
'ghapi.core.GhApi.__getattr__': ('core.html#ghapi.__getattr__', 'ghapi/core.py'),
'ghapi.core.GhApi.__getitem__': ('core.html#ghapi.__getitem__', 'ghapi/core.py'),
'ghapi.core.GhApi.__init__': ('core.html#ghapi.__init__', 'ghapi/core.py'),
'ghapi.core.GhApi._get_repo_files': ('core.html#ghapi._get_repo_files', 'ghapi/core.py'),
'ghapi.core.GhApi._repr_markdown_': ('core.html#ghapi._repr_markdown_', 'ghapi/core.py'),
'ghapi.core.GhApi.create_branch_empty': ('core.html#ghapi.create_branch_empty', 'ghapi/core.py'),
'ghapi.core.GhApi.create_file': ('core.html#ghapi.create_file', 'ghapi/core.py'),
Expand All @@ -64,6 +65,9 @@
'ghapi.core.GhApi.full_docs': ('core.html#ghapi.full_docs', 'ghapi/core.py'),
'ghapi.core.GhApi.get_branch': ('core.html#ghapi.get_branch', 'ghapi/core.py'),
'ghapi.core.GhApi.get_content': ('core.html#ghapi.get_content', 'ghapi/core.py'),
'ghapi.core.GhApi.get_file_content': ('core.html#ghapi.get_file_content', 'ghapi/core.py'),
'ghapi.core.GhApi.get_repo_contents': ('core.html#ghapi.get_repo_contents', 'ghapi/core.py'),
'ghapi.core.GhApi.get_repo_files': ('core.html#ghapi.get_repo_files', 'ghapi/core.py'),
'ghapi.core.GhApi.list_branches': ('core.html#ghapi.list_branches', 'ghapi/core.py'),
'ghapi.core.GhApi.list_files': ('core.html#ghapi.list_files', 'ghapi/core.py'),
'ghapi.core.GhApi.list_tags': ('core.html#ghapi.list_tags', 'ghapi/core.py'),
Expand All @@ -82,6 +86,8 @@
'ghapi.core._GhVerbGroup.__str__': ('core.html#_ghverbgroup.__str__', 'ghapi/core.py'),
'ghapi.core._GhVerbGroup._repr_markdown_': ('core.html#_ghverbgroup._repr_markdown_', 'ghapi/core.py'),
'ghapi.core._decode_response': ('core.html#_decode_response', 'ghapi/core.py'),
'ghapi.core._find_matches': ('core.html#_find_matches', 'ghapi/core.py'),
'ghapi.core._include': ('core.html#_include', 'ghapi/core.py'),
'ghapi.core._mk_param': ('core.html#_mk_param', 'ghapi/core.py'),
'ghapi.core._mk_sig': ('core.html#_mk_sig', 'ghapi/core.py'),
'ghapi.core._mk_sig_detls': ('core.html#_mk_sig_detls', 'ghapi/core.py'),
Expand Down
46 changes: 23 additions & 23 deletions ghapi/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
'context_github', 'context_env', 'context_job', 'context_steps', 'context_runner', 'context_secrets',
'context_strategy', 'context_matrix', 'context_needs']

# %% ../01_actions.ipynb #0f28e91f
# %% ../01_actions.ipynb #e4187aeb
from fastcore.all import *
from .core import *
from .templates import *
Expand All @@ -18,7 +18,7 @@
from contextlib import contextmanager
from enum import Enum

# %% ../01_actions.ipynb #f1d8838a
# %% ../01_actions.ipynb #fb69ba90
# So we can run this outside of GitHub actions too, read from file if needed
for a,b in (('CONTEXT_GITHUB',context_example), ('CONTEXT_NEEDS',needs_example), ('GITHUB_REPOSITORY','octocat/Hello-World')):
if a not in os.environ: os.environ[a] = b
Expand All @@ -27,18 +27,18 @@
for context in contexts:
globals()[f'context_{context}'] = dict2obj(loads(os.getenv(f"CONTEXT_{context.upper()}", "{}")))

# %% ../01_actions.ipynb #e842a54d
# %% ../01_actions.ipynb #393990e1
_all_ = ['context_github', 'context_env', 'context_job', 'context_steps', 'context_runner', 'context_secrets', 'context_strategy', 'context_matrix', 'context_needs']

# %% ../01_actions.ipynb #1f119280
# %% ../01_actions.ipynb #02ca0806
env_github = dict2obj({k[7:].lower():v for k,v in os.environ.items() if k.startswith('GITHUB_')})

# %% ../01_actions.ipynb #83cc44c3
# %% ../01_actions.ipynb #3170b688
def user_repo():
"List of `user,repo` from `env_github.repository"
return env_github.repository.split('/')

# %% ../01_actions.ipynb #baba5eb9
# %% ../01_actions.ipynb #deb389d7
Event = str_enum('Event',
'page_build','content_reference','repository_import','create','workflow_run','delete','organization','sponsorship',
'project_column','push','context','milestone','project_card','project','package','pull_request','repository_dispatch',
Expand All @@ -48,14 +48,14 @@ def user_repo():
'installation','release','issues','repository','gollum','membership','deployment','deploy_key','issue_comment','ping',
'deployment_status','fork','schedule')

# %% ../01_actions.ipynb #e42ac247
# %% ../01_actions.ipynb #bf0b5888
def _create_file(path:Path, fname:str, contents):
if contents and not (path/fname).exists(): (path/fname).write_text(contents)

def _replace(s:str, find, repl, i:int=0, suf:str=''):
return s.replace(find, textwrap.indent(repl, ' '*i)+suf)

# %% ../01_actions.ipynb #db6f1964
# %% ../01_actions.ipynb #26eb64e5
def create_workflow_files(fname:str, workflow:str, build_script:str, prebuild:bool=False):
"Create workflow and script files in suitable places in `github` folder"
if not os.path.exists('.git'): return print('This does not appear to be the root of a git repo')
Expand All @@ -67,7 +67,7 @@ def create_workflow_files(fname:str, workflow:str, build_script:str, prebuild:bo
_create_file(scr_path, f'build-{fname}.py', build_script)
if prebuild: _create_file(scr_path, f'prebuild-{fname}.py', build_script)

# %% ../01_actions.ipynb #f8d6bf37
# %% ../01_actions.ipynb #3cf450a0
def fill_workflow_templates(name:str, event, run, context, script, opersys='ubuntu', prebuild=False):
"Function to create a simple Ubuntu workflow that calls a Python `ghapi` script"
c = wf_tmpl
Expand All @@ -78,23 +78,23 @@ def fill_workflow_templates(name:str, event, run, context, script, opersys='ubun
c = _replace(c, f'${find}', str(repl), i)
create_workflow_files(name, c, script, prebuild=prebuild)

# %% ../01_actions.ipynb #3bfae412
# %% ../01_actions.ipynb #bc2f80a5
def env_contexts(contexts):
"Create a suitable `env:` line for a workflow to make a context available in the environment"
contexts = uniqueify(['github'] + listify(contexts))
return "\n".join("CONTEXT_" + o.upper() + ": ${{ toJson(" + o.lower() + ") }}" for o in contexts)

# %% ../01_actions.ipynb #1068e627
# %% ../01_actions.ipynb #561125fa
def_pipinst = 'pip install -Uq ghapi'

# %% ../01_actions.ipynb #dd605316
# %% ../01_actions.ipynb #39c5d429
def create_workflow(name:str, event:Event, contexts:list=None, opersys='ubuntu', prebuild=False):
"Function to create a simple Ubuntu workflow that calls a Python `ghapi` script"
script = "from fastcore.all import *\nfrom ghapi import *"
fill_workflow_templates(name, f'{event}:', def_pipinst, env_contexts(contexts),
script=script, opersys=opersys, prebuild=prebuild)

# %% ../01_actions.ipynb #b5e64efa
# %% ../01_actions.ipynb #e8482286
@call_parse
def gh_create_workflow(
name:str, # Name of the workflow file
Expand All @@ -104,53 +104,53 @@ def gh_create_workflow(
"Supports `gh-create-workflow`, a CLI wrapper for `create_workflow`."
create_workflow(name, Event[event], contexts.split())

# %% ../01_actions.ipynb #c69fe43a
# %% ../01_actions.ipynb #a85ea1f8
_example_url = 'https://raw.githubusercontent.com/fastai/ghapi/master/examples/{}.json'

# %% ../01_actions.ipynb #ad31da47
# %% ../01_actions.ipynb #47cfc8b9
def example_payload(event):
"Get an example of a JSON payload for `event`"
return dict2obj(urljson(_example_url.format(event)))

# %% ../01_actions.ipynb #9ffb1dce
# %% ../01_actions.ipynb #778d7cc7
def github_token():
"Get GitHub token from `GITHUB_TOKEN` env var if available, or from `github` context"
return os.getenv('GITHUB_TOKEN', context_github.get('token', None))

# %% ../01_actions.ipynb #20832f3c
# %% ../01_actions.ipynb #88716812
def actions_output(name, value):
"Print the special GitHub Actions `::set-output` line for `name::value`"
print(f"::set-output name={name}::{value}")

# %% ../01_actions.ipynb #497020d4
# %% ../01_actions.ipynb #50cad4c2
def actions_debug(message):
"Print the special `::debug` line for `message`"
print(f"::debug::{message}")

# %% ../01_actions.ipynb #3e75ffe1
# %% ../01_actions.ipynb #5b6d15ab
def actions_warn(message, details=''):
"Print the special `::warning` line for `message`"
print(f"::warning {details}::{message}")

# %% ../01_actions.ipynb #886ede87
# %% ../01_actions.ipynb #e97ba982
def actions_error(message, details=''):
"Print the special `::error` line for `message`"
print(f"::error {details}::{message}")

# %% ../01_actions.ipynb #da04d233
# %% ../01_actions.ipynb #a7ff989d
@contextmanager
def actions_group(title):
"Context manager to print the special `::group`/`::endgroup` lines for `title`"
print(f"::group::{title}")
yield
print(f"::endgroup::")

# %% ../01_actions.ipynb #95fb29a7
# %% ../01_actions.ipynb #3405cb7f
def actions_mask(value):
"Print the special `::add-mask` line for `value`"
print(f"::add-mask::{value}")

# %% ../01_actions.ipynb #a1adceae
# %% ../01_actions.ipynb #8682ff46
def set_git_user(api=None):
"Set git user name/email to authenticated user (if `api`) or GitHub Actions bot (otherwise)"
if api:
Expand Down
22 changes: 11 additions & 11 deletions ghapi/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,33 @@
# %% auto #0
__all__ = ['Scope', 'scope_str', 'GhDeviceAuth', 'github_auth_device']

# %% ../02_auth.ipynb #10a8702d
# %% ../02_auth.ipynb #e1ccb974
from fastcore.all import *
from .core import *

import webbrowser,time
from urllib.parse import parse_qs,urlsplit

# %% ../02_auth.ipynb #6b060627
# %% ../02_auth.ipynb #46620407
_scopes =(
'repo','repo:status','repo_deployment','public_repo','repo:invite','security_events','admin:repo_hook','write:repo_hook',
'read:repo_hook','admin:org','write:org','read:org','admin:public_key','write:public_key','read:public_key','admin:org_hook',
'gist','notifications','user','read:user','user:email','user:follow','delete_repo','write:discussion','read:discussion',
'write:packages','read:packages','delete:packages','admin:gpg_key','write:gpg_key','read:gpg_key','workflow'
)

# %% ../02_auth.ipynb #0870167c
# %% ../02_auth.ipynb #0cbc503b
Scope = AttrDict({o.replace(':','_'):o for o in _scopes})

# %% ../02_auth.ipynb #587e8748
# %% ../02_auth.ipynb #404b7d4f
def scope_str(*scopes)->str:
"Convert `scopes` into a comma-separated string"
return ','.join(str(o) for o in scopes if o)

# %% ../02_auth.ipynb #f0acfe01
# %% ../02_auth.ipynb #7b224a0a
_def_clientid = '71604a89b882ab8c8634'

# %% ../02_auth.ipynb #42b9eb35
# %% ../02_auth.ipynb #2a614e72
class GhDeviceAuth(GetAttrBase):
"Get an oauth token using the GitHub API device flow"
_attr="params"
Expand All @@ -42,20 +42,20 @@ def __init__(self, client_id=_def_clientid, *scopes):

def _getattr(self,v): return v[0]

# %% ../02_auth.ipynb #ee32c6f1
# %% ../02_auth.ipynb #d3764621
@patch
def url_docs(self:GhDeviceAuth)->str:
"Default instructions on how to authenticate"
return f"""First copy your one-time code: {self.user_code}
Then visit {self.verification_uri} in your browser, and paste the code when prompted."""

# %% ../02_auth.ipynb #dcbc8387
# %% ../02_auth.ipynb #255b299c
@patch
def open_browser(self:GhDeviceAuth):
"Open a web browser with the verification URL"
webbrowser.open(self.verification_uri)

# %% ../02_auth.ipynb #edaddf67
# %% ../02_auth.ipynb #5091b2c8
@patch
def auth(self:GhDeviceAuth)->str:
"Return token if authentication complete, or `None` otherwise"
Expand All @@ -68,7 +68,7 @@ def auth(self:GhDeviceAuth)->str:
if err: raise Exception(resp['error_description'][0])
return resp['access_token'][0]

# %% ../02_auth.ipynb #e114ebb3
# %% ../02_auth.ipynb #a7d55794
@patch
def wait(self:GhDeviceAuth, cb:callable=None, n_polls=9999)->str:
"Wait up to `n_polls` times for authentication to complete, calling `cb` after each poll, if passed"
Expand All @@ -80,7 +80,7 @@ def wait(self:GhDeviceAuth, cb:callable=None, n_polls=9999)->str:
if cb: cb()
time.sleep(interval)

# %% ../02_auth.ipynb #fd873541
# %% ../02_auth.ipynb #e329a3d2
def github_auth_device(wb='', n_polls=9999):
"Authenticate with GitHub, polling up to `n_polls` times to wait for completion"
auth = GhDeviceAuth()
Expand Down
10 changes: 5 additions & 5 deletions ghapi/build_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
# %% auto #0
__all__ = ['GH_OPENAPI_URL', 'GhMeta', 'build_funcs']

# %% ../90_build_lib.ipynb #73ed725e
# %% ../90_build_lib.ipynb #cdbcf258
from fastcore.all import *

import pprint
# from json import loads
from jsonref import loads
from collections import namedtuple

# %% ../90_build_lib.ipynb #c322ea0b
# %% ../90_build_lib.ipynb #c215ed8d
GH_OPENAPI_URL = 'https://github.com/github/rest-api-description/raw/main/descriptions/api.github.com/api.github.com.json?raw=true'
_DOC_URL = 'https://docs.github.com/'

# %% ../90_build_lib.ipynb #a8fd97f6
# %% ../90_build_lib.ipynb #ce33036d
_lu_type = dict(zip(
'NA string object array boolean number integer'.split(),
map(PrettyString,'object str dict list bool int int'.split())
Expand All @@ -34,7 +34,7 @@ def _find_data(d):
if 'properties' in o: return o['properties']
return {}

# %% ../90_build_lib.ipynb #7b211864
# %% ../90_build_lib.ipynb #cf7204d9
def build_funcs(nm='ghapi/metadata.py', url=GH_OPENAPI_URL, docurl=_DOC_URL):
"Build module metadata.py from an Open API spec and optionally filter by a path `pre`"
def _get_detls(o):
Expand All @@ -53,5 +53,5 @@ def _get_detls(o):
if 'externalDocs' in detls]
Path(nm).write_text("funcs = " + pprint.pformat(_funcs, width=360))

# %% ../90_build_lib.ipynb #495861d0
# %% ../90_build_lib.ipynb #9d2307fc
GhMeta = namedtuple('GhMeta', 'path verb oper_id summary doc_url params data preview'.split())
18 changes: 9 additions & 9 deletions ghapi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
# %% auto #0
__all__ = ['ghapi', 'ghpath', 'ghraw', 'completion_ghapi']

# %% ../10_cli.ipynb #6adcc120
# %% ../10_cli.ipynb #e10a0628
from fastcore.all import *
import ghapi.core as gh,inspect
from .core import *
from collections import defaultdict

# %% ../10_cli.ipynb #295d196c
# %% ../10_cli.ipynb #44ebb54f
def _parse_args(a):
"Extract positional and keyword arguments from `a`=`sys.argv`"
pos,kw = [],{}
Expand Down Expand Up @@ -43,34 +43,34 @@ def _call_api(f):
call = f(pos, api)
return call if kw.get('help', None) else call(*pos, **kw)

# %% ../10_cli.ipynb #1f1a4f1a
# %% ../10_cli.ipynb #55f9002f
def _ghapi(arg, api):
for part in arg.pop(0).split('.'): api = getattr(api,part)
return api

# %% ../10_cli.ipynb #1f166d73
# %% ../10_cli.ipynb #7557c309
def ghapi():
"Python backend for the `ghapi` command, which calls an endpoint by operation name"
res = _call_api(_ghapi)
if isinstance(res, (gh._GhObj,dict,L)): print(res)
elif res: print(inspect.signature(res))

# %% ../10_cli.ipynb #aa44c461
# %% ../10_cli.ipynb #524623ba
def _ghpath(arg, api): return api[arg.pop(0),arg.pop(0)]

# %% ../10_cli.ipynb #e6465efb
# %% ../10_cli.ipynb #723624b0
def ghpath():
"Python backend for the `ghpath` command, which calls an endpoint by path"
print(_call_api(_ghpath) or '')

# %% ../10_cli.ipynb #fc26e8f5
# %% ../10_cli.ipynb #93dbad3e
def ghraw():
"Python backend for the `ghraw` command, which calls a fully-specified endpoint"
cmd,api,pos,kw = _api()
if not pos: return print(f"Usage: `{cmd}` operation <params>")
print(api(*pos, **kw))

# %% ../10_cli.ipynb #bf4322ee
# %% ../10_cli.ipynb #21033494
_TAB_COMPLETION="""
_do_ghapi_completions()
{
Expand All @@ -81,7 +81,7 @@ def ghraw():
complete -F _do_ghapi_completions ghapi
"""

# %% ../10_cli.ipynb #007b595b
# %% ../10_cli.ipynb #9fe362ab
def completion_ghapi():
"Python backend for `completion-ghapi` command"
if len(sys.argv) == 2 and sys.argv[1] == '--install':
Expand Down
Loading