Skip to content

GitHub: use_commit_name option #213

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
26 changes: 19 additions & 7 deletions nvchecker/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ async def run(self) -> None:
'''Run the `tasks`. Subclasses should implement this method.'''
raise NotImplementedError

def _normalize(x: Any) -> Any:
if isinstance(x, list):
return tuple(sorted(_normalize(y) for y in x))
elif isinstance(x, dict):
return tuple(sorted((_normalize(k), _normalize(v)) for k, v in x.items()))
else:
return x
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not obvious what this function does. It needs a docstring or a better name.


class AsyncCache:
'''A cache for use with async functions.'''
cache: Dict[Hashable, Any]
Expand All @@ -156,28 +164,32 @@ def __init__(self) -> None:
self.lock = asyncio.Lock()

async def _get_json(
self, key: Tuple[str, str, Tuple[Tuple[str, str], ...]],
self, key: Tuple[str, str, Tuple[Tuple[str, str], ...], object], extra: Any,
) -> Any:
_, url, headers = key
res = await session.get(url, headers=dict(headers))
_, url, headers, json = key
json = extra # denormalizing json would be a pain, so we sneak it through
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about passing json as a str or bytes? We can set the Content-Type header and directly pass it to session as body

res = await (session.get(url=url, headers=dict(headers)) if json is None \
else session.post(url=url, headers=dict(headers), json=json))
return res.json()

async def get_json(
self, url: str, *,
headers: Dict[str, str] = {},
json: Optional[object] = None,
) -> Any:
'''Get specified ``url`` and return the response content as JSON.

The returned data will be cached for reuse.
'''
key = '_jsonurl', url, tuple(sorted(headers.items()))
key = '_jsonurl', url, _normalize(headers), _normalize(json)
return await self.get(
key , self._get_json) # type: ignore
key, self._get_json, extra=json) # type: ignore

async def get(
self,
key: Hashable,
func: Callable[[Hashable], Coroutine[Any, Any, Any]],
func: Callable[[Hashable, Optional[Any]], Coroutine[Any, Any, Any]],
extra: Optional[Any] = None,
) -> Any:
'''Run async ``func`` and cache its return value by ``key``.

Expand All @@ -189,7 +201,7 @@ async def get(
async with self.lock:
cached = self.cache.get(key)
if cached is None:
coro = func(key)
coro = func(key, extra)
fu = asyncio.create_task(coro)
self.cache[key] = fu

Expand Down
188 changes: 110 additions & 78 deletions nvchecker_source/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import itertools
import time
from urllib.parse import urlencode
from typing import Optional, Tuple
from typing import Any, Dict, Optional, Tuple

import structlog

Expand All @@ -30,6 +30,53 @@ async def get_version(name, conf, **kwargs):
except TemporaryError as e:
check_ratelimit(e, name)

async def query_graphql(
*,
cache: AsyncCache,
token: Optional[str] = None,
headers: Optional[Dict[str, str]] = None,
query: str,
variables: Optional[Dict[str, object]] = None,
json: Optional[Dict[str, object]] = None,
url: Optional[str] = None,
**kwargs,
) -> Any:
if not token:
raise GetVersionError('token not given but it is required')
if headers is None:
headers = {}
headers.setdefault('Authorization', f'bearer {token}')
headers.setdefault('Content-Type', 'application/json')

if json is None:
json = {}
json['query'] = query
if variables is not None:
json.setdefault('variables', {}).update(variables)

if url is None:
url = GITHUB_GRAPHQL_URL
return await cache.get_json(url = url, headers = headers, json = json)

async def query_rest(
*,
cache: AsyncCache,
token: Optional[str] = None,
headers: Optional[Dict[str, str]] = None,
parameters: Optional[Dict[str, str]] = None,
url: str,
) -> Any:
if headers is None:
headers = {}
if token:
headers.setdefault('Authorization', f'token {token}')
headers.setdefault('Accept', 'application/vnd.github.quicksilver-preview+json')

if parameters:
url += '?' + urlencode(parameters)

return await cache.get_json(url = url, headers = headers)

QUERY_LATEST_TAG = '''
query latestTag(
$owner: String!, $name: String!,
Expand All @@ -51,106 +98,91 @@ async def get_version(name, conf, **kwargs):
}
'''

async def get_latest_tag(key: Tuple[str, Optional[str], str, bool]) -> str:
repo, query, token, use_commit_name = key
owner, reponame = repo.split('/')
headers = {
'Authorization': f'bearer {token}',
'Content-Type': 'application/json',
}
variables = {
'owner': owner,
'name': reponame,
'includeCommitName': use_commit_name,
}
if query is not None:
variables['query'] = query

res = await session.post(
GITHUB_GRAPHQL_URL,
headers = headers,
json = {'query': QUERY_LATEST_TAG, 'variables': variables},
)
j = res.json()

refs = j['data']['repository']['refs']['edges']
if not refs:
raise GetVersionError('no tag found')

return next(add_commit_name(
ref['node']['name'],
ref['node']['target']['oid'] if use_commit_name else None,
) for ref in refs)

async def get_version_real(
name: str, conf: Entry, *,
cache: AsyncCache, keymanager: KeyManager,
**kwargs,
) -> VersionResult:
repo = conf['github']
use_commit_name = conf.get('use_commit_name', False)

# Load token from config
token = conf.get('token')
# Load token from keyman
if token is None:
token = keymanager.get_key('github')

use_latest_tag = conf.get('use_latest_tag', False)
use_commit_name = conf.get('use_commit_name', False)
if use_latest_tag:
if not token:
raise GetVersionError('token not given but it is required')

query = conf.get('query')
return await cache.get((repo, query, token, use_commit_name), get_latest_tag) # type: ignore

br = conf.get('branch')
path = conf.get('path')
use_latest_release = conf.get('use_latest_release', False)
use_max_tag = conf.get('use_max_tag', False)
if use_latest_release:
url = GITHUB_LATEST_RELEASE % repo
elif use_max_tag:
url = GITHUB_MAX_TAG % repo
else:
url = GITHUB_URL % repo
parameters = {}
if br:
parameters['sha'] = br
if path:
parameters['path'] = path
url += '?' + urlencode(parameters)
headers = {
'Accept': 'application/vnd.github.quicksilver-preview+json',
}
if token:
headers['Authorization'] = f'token {token}'

data = await cache.get_json(url, headers = headers)

if use_max_tag:
tags = [add_commit_name(
ref['ref'].split('/', 2)[-1],
ref['object']['sha'] if use_commit_name else None,
) for ref in data]
if conf.get('use_latest_tag', False):
owner, reponame = repo.split('/')
j = await query_graphql(
cache = cache,
token = token,
query = QUERY_LATEST_TAG,
variables = {
'owner': owner,
'name': reponame,
'query': conf.get('query'),
'includeCommitName': use_commit_name,
},
)
refs = j['data']['repository']['refs']['edges']
if not refs:
raise GetVersionError('no tag found')
ref = next(
add_commit_name(
ref['node']['name'],
ref['node']['target']['oid'] if use_commit_name else None,
)
for ref in refs
)
return ref
elif conf.get('use_latest_release', False):
data = await query_rest(
cache = cache,
token = token,
url = GITHUB_LATEST_RELEASE % repo,
)
if 'tag_name' not in data:
raise GetVersionError('No release found in upstream repository.')
tag = data['tag_name']
return tag
elif conf.get('use_max_tag', False):
data = await query_rest(
cache = cache,
token = token,
url = GITHUB_MAX_TAG % repo,
)
tags = [
add_commit_name(
ref['ref'].split('/', 2)[-1],
ref['object']['sha'] if use_commit_name else None,
)
for ref in data
]
if not tags:
raise GetVersionError('No tag found in upstream repository.')
return tags

if use_latest_release:
if 'tag_name' not in data:
raise GetVersionError('No release found in upstream repository.')
version = data['tag_name']

else:
br = conf.get('branch')
path = conf.get('path')
parameters = {}
if br is not None:
parameters['sha'] = br
if path is not None:
parameters['path'] = path
data = await query_rest(
cache = cache,
token = token,
url = GITHUB_URL % repo,
parameters = parameters,
)
# YYYYMMDD.HHMMSS
version = add_commit_name(
data[0]['commit']['committer']['date'] \
.rstrip('Z').replace('-', '').replace(':', '').replace('T', '.'),
data[0]['sha'] if use_commit_name else None,
)

return version
return version

def check_ratelimit(exc, name):
res = exc.response
Expand Down