mirror of
https://github.com/lilydjwg/nvchecker.git
synced 2025-03-10 06:14:02 +00:00
version 2: MVP
This commit is contained in:
parent
fbdde9bf58
commit
14b3863f11
49 changed files with 537 additions and 403 deletions
21
NEW
Normal file
21
NEW
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
source file
|
||||||
|
| parse
|
||||||
|
entries: name, conf, global options
|
||||||
|
| dedupe
|
||||||
|
entries: names, conf, global options
|
||||||
|
| dispatch
|
||||||
|
one future per module
|
||||||
|
| run; token queue; result queue
|
||||||
|
result task, runner task
|
||||||
|
| receive
|
||||||
|
(names, result)
|
||||||
|
| transform back
|
||||||
|
(name, result)
|
||||||
|
| runner task done
|
||||||
|
| write record file
|
||||||
|
file
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
* dedupe & cache
|
||||||
|
* update tests
|
||||||
|
* update README
|
13
mypy.ini
Normal file
13
mypy.ini
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[mypy]
|
||||||
|
warn_unused_configs = True
|
||||||
|
warn_redundant_casts = True
|
||||||
|
warn_unused_ignores = True
|
||||||
|
show_error_context = True
|
||||||
|
show_column_numbers = True
|
||||||
|
no_implicit_optional = True
|
||||||
|
|
||||||
|
[mypy-structlog]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
[mypy-pyalpm]
|
||||||
|
ignore_missing_imports = True
|
57
nvchecker-old/get_version.py
Normal file
57
nvchecker-old/get_version.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# MIT licensed
|
||||||
|
# Copyright (c) 2013-2017 lilydjwg <lilydjwg@gmail.com>, et al.
|
||||||
|
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
import structlog
|
||||||
|
|
||||||
|
logger = structlog.get_logger(logger_name=__name__)
|
||||||
|
|
||||||
|
_handler_precedence = (
|
||||||
|
'github', 'aur', 'pypi', 'archpkg', 'debianpkg', 'ubuntupkg',
|
||||||
|
'gems', 'pacman',
|
||||||
|
'cmd', 'bitbucket', 'regex', 'manual', 'vcs',
|
||||||
|
'cratesio', 'npm', 'hackage', 'cpan', 'gitlab', 'packagist',
|
||||||
|
'repology', 'anitya', 'android_sdk', 'sparkle', 'gitea'
|
||||||
|
)
|
||||||
|
|
||||||
|
_Task = namedtuple('_Task', 'batch_mode main args names')
|
||||||
|
|
||||||
|
class Dispatcher:
|
||||||
|
def __init__(self):
|
||||||
|
self.futures = []
|
||||||
|
self.mods = {}
|
||||||
|
self.tasks_dedupe = {}
|
||||||
|
|
||||||
|
def add_task(self, name, conf, **kwargs):
|
||||||
|
for key in _handler_precedence:
|
||||||
|
if key not in conf:
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = self.mods.get(key)
|
||||||
|
if not value:
|
||||||
|
mod = import_module(
|
||||||
|
'.source.' + key, __package__)
|
||||||
|
batch_mode = getattr(mod, 'BATCH_MODE', False)
|
||||||
|
if batch_mode:
|
||||||
|
main = mod.Batcher()
|
||||||
|
else:
|
||||||
|
main = mod.get_version
|
||||||
|
get_cacheable_conf = getattr(mod, 'get_cacheable_conf', lambda name, conf: conf)
|
||||||
|
self.mods[key] = batch_mode, main, get_cacheable_conf
|
||||||
|
else:
|
||||||
|
batch_mode, main, get_cacheable_conf = value
|
||||||
|
|
||||||
|
cacheable_conf = get_cacheable_conf(name, conf)
|
||||||
|
cache_key = tuple(sorted(cacheable_conf.items()))
|
||||||
|
task = self.tasks_dedupe.get(cache_key)
|
||||||
|
if task is None:
|
||||||
|
self.tasks_dedupe[cache_key] = _Task(
|
||||||
|
batch_mode, main, (cacheable_conf, kwargs), [name])
|
||||||
|
else:
|
||||||
|
task.names.append(name)
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
'no idea to get version info.', name=name)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# MIT licensed
|
# MIT licensed
|
||||||
# Copyright (c) 2013-2020 lilydjwg <lilydjwg@gmail.com>, et al.
|
# Copyright (c) 2013-2020 lilydjwg <lilydjwg@gmail.com>, et al.
|
||||||
|
|
||||||
__version__ = '1.8dev'
|
__version__ = '2.0dev'
|
||||||
|
|
|
@ -1,33 +1,23 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# MIT licensed
|
# MIT licensed
|
||||||
# Copyright (c) 2013-2017 lilydjwg <lilydjwg@gmail.com>, et al.
|
# Copyright (c) 2013-2020 lilydjwg <lilydjwg@gmail.com>, et al.
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from typing import Coroutine
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
from .lib import notify
|
|
||||||
from . import core
|
from . import core
|
||||||
|
from .util import VersData, RawResult
|
||||||
|
|
||||||
logger = structlog.get_logger(logger_name=__name__)
|
logger = structlog.get_logger(logger_name=__name__)
|
||||||
|
|
||||||
notifications = []
|
def main() -> None:
|
||||||
args = None
|
|
||||||
|
|
||||||
class Source(core.Source):
|
|
||||||
def on_update(self, name, version, oldver):
|
|
||||||
if args.notify:
|
|
||||||
msg = '%s updated to version %s' % (name, version)
|
|
||||||
notifications.append(msg)
|
|
||||||
notify.update('nvchecker', '\n'.join(notifications))
|
|
||||||
|
|
||||||
def main():
|
|
||||||
global args
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='New version checker for software')
|
parser = argparse.ArgumentParser(description='New version checker for software')
|
||||||
parser.add_argument('-n', '--notify', action='store_true', default=False,
|
|
||||||
help='show desktop notifications when a new version is available')
|
|
||||||
parser.add_argument('-t', '--tries', default=1, type=int, metavar='N',
|
parser.add_argument('-t', '--tries', default=1, type=int, metavar='N',
|
||||||
help='try N times when errors occur')
|
help='try N times when errors occur')
|
||||||
core.add_common_arguments(parser)
|
core.add_common_arguments(parser)
|
||||||
|
@ -36,12 +26,47 @@ def main():
|
||||||
return
|
return
|
||||||
|
|
||||||
if not args.file:
|
if not args.file:
|
||||||
return
|
try:
|
||||||
|
file = open(core.get_default_config())
|
||||||
|
except FileNotFoundError:
|
||||||
|
sys.exit('version configuration file not given and default does not exist')
|
||||||
|
else:
|
||||||
|
file = args.file
|
||||||
|
|
||||||
s = Source(args.file, args.tries)
|
entries, options = core.load_file(file)
|
||||||
|
token_q = core.token_queue(options.max_concurrent)
|
||||||
|
result_q: asyncio.Queue[RawResult] = asyncio.Queue()
|
||||||
|
try:
|
||||||
|
futures = core.dispatch(
|
||||||
|
entries, token_q, result_q,
|
||||||
|
options.keymanager, args.tries,
|
||||||
|
)
|
||||||
|
except ModuleNotFoundError as e:
|
||||||
|
sys.exit(f'Error: {e}')
|
||||||
|
|
||||||
ioloop = asyncio.get_event_loop()
|
if options.ver_files is not None:
|
||||||
ioloop.run_until_complete(s.check())
|
oldvers = core.read_verfile(options.ver_files[0])
|
||||||
|
else:
|
||||||
|
oldvers = {}
|
||||||
|
result_coro = core.process_result(oldvers, result_q)
|
||||||
|
runner_coro = core.run_tasks(futures)
|
||||||
|
|
||||||
|
# asyncio.run doesn't work because it always creates new eventloops
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
newvers = loop.run_until_complete(run(result_coro, runner_coro))
|
||||||
|
|
||||||
|
if options.ver_files is not None:
|
||||||
|
core.write_verfile(options.ver_files[1], newvers)
|
||||||
|
|
||||||
|
async def run(
|
||||||
|
result_coro: Coroutine[None, None, VersData],
|
||||||
|
runner_coro: Coroutine[None, None, None],
|
||||||
|
) -> VersData:
|
||||||
|
result_fu = asyncio.create_task(result_coro)
|
||||||
|
runner_fu = asyncio.create_task(runner_coro)
|
||||||
|
await runner_fu
|
||||||
|
result_fu.cancel()
|
||||||
|
return await result_fu
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -1,25 +1,43 @@
|
||||||
# vim: se sw=2:
|
# vim: se sw=2:
|
||||||
# MIT licensed
|
# MIT licensed
|
||||||
# Copyright (c) 2013-2018 lilydjwg <lilydjwg@gmail.com>, et al.
|
# Copyright (c) 2013-2020 lilydjwg <lilydjwg@gmail.com>, et al.
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import configparser
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from asyncio import Queue
|
||||||
import logging
|
import logging
|
||||||
|
import argparse
|
||||||
|
from typing import (
|
||||||
|
TextIO, Tuple, NamedTuple, Optional, List, Union,
|
||||||
|
cast, Dict, Awaitable, Sequence,
|
||||||
|
)
|
||||||
|
import types
|
||||||
|
from pathlib import Path
|
||||||
|
from importlib import import_module
|
||||||
|
import re
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
import toml
|
||||||
|
|
||||||
from .lib import nicelogger
|
from .lib import nicelogger
|
||||||
from .get_version import get_version
|
|
||||||
from .source import session
|
|
||||||
from . import slogconf
|
from . import slogconf
|
||||||
|
from .util import (
|
||||||
|
Entry, Entries, KeyManager, RawResult, Result, VersData,
|
||||||
|
)
|
||||||
from . import __version__
|
from . import __version__
|
||||||
|
from .sortversion import sort_version_keys
|
||||||
|
|
||||||
logger = structlog.get_logger(logger_name=__name__)
|
logger = structlog.get_logger(logger_name=__name__)
|
||||||
|
|
||||||
def add_common_arguments(parser):
|
def get_default_config() -> str:
|
||||||
|
confdir = os.environ.get('XDG_CONFIG_DIR', os.path.expanduser('~/.config'))
|
||||||
|
file = os.path.join(confdir, 'nvchecker/nvchecker.toml')
|
||||||
|
return file
|
||||||
|
|
||||||
|
def add_common_arguments(parser: argparse.ArgumentParser) -> None:
|
||||||
parser.add_argument('-l', '--logging',
|
parser.add_argument('-l', '--logging',
|
||||||
choices=('debug', 'info', 'warning', 'error'), default='info',
|
choices=('debug', 'info', 'warning', 'error'), default='info',
|
||||||
help='logging level (default: info)')
|
help='logging level (default: info)')
|
||||||
|
@ -31,10 +49,11 @@ def add_common_arguments(parser):
|
||||||
help='specify fd to send json logs to. stdout by default')
|
help='specify fd to send json logs to. stdout by default')
|
||||||
parser.add_argument('-V', '--version', action='store_true',
|
parser.add_argument('-V', '--version', action='store_true',
|
||||||
help='show version and exit')
|
help='show version and exit')
|
||||||
parser.add_argument('file', metavar='FILE', nargs='?', type=open,
|
parser.add_argument('-c', '--file',
|
||||||
help='software version source file')
|
metavar='FILE', type=open,
|
||||||
|
help='software version configuration file [default: %s]' % get_default_config)
|
||||||
|
|
||||||
def process_common_arguments(args):
|
def process_common_arguments(args: argparse.Namespace) -> bool:
|
||||||
'''return True if should stop'''
|
'''return True if should stop'''
|
||||||
processors = [
|
processors = [
|
||||||
slogconf.exc_info,
|
slogconf.exc_info,
|
||||||
|
@ -71,8 +90,10 @@ def process_common_arguments(args):
|
||||||
progname = os.path.basename(sys.argv[0])
|
progname = os.path.basename(sys.argv[0])
|
||||||
print('%s v%s' % (progname, __version__))
|
print('%s v%s' % (progname, __version__))
|
||||||
return True
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def safe_overwrite(fname, data, *, method='write', mode='w', encoding=None):
|
def safe_overwrite(fname: str, data: Union[bytes, str], *,
|
||||||
|
method: str = 'write', mode: str = 'w', encoding: Optional[str] = None) -> None:
|
||||||
# FIXME: directory has no read perm
|
# FIXME: directory has no read perm
|
||||||
# FIXME: symlinks and hard links
|
# FIXME: symlinks and hard links
|
||||||
tmpname = fname + '.tmp'
|
tmpname = fname + '.tmp'
|
||||||
|
@ -85,7 +106,7 @@ def safe_overwrite(fname, data, *, method='write', mode='w', encoding=None):
|
||||||
# if the above write failed (because disk is full etc), the old data should be kept
|
# if the above write failed (because disk is full etc), the old data should be kept
|
||||||
os.rename(tmpname, fname)
|
os.rename(tmpname, fname)
|
||||||
|
|
||||||
def read_verfile(file):
|
def read_verfile(file: Path) -> VersData:
|
||||||
v = {}
|
v = {}
|
||||||
try:
|
try:
|
||||||
with open(file) as f:
|
with open(file) as f:
|
||||||
|
@ -96,134 +117,199 @@ def read_verfile(file):
|
||||||
pass
|
pass
|
||||||
return v
|
return v
|
||||||
|
|
||||||
def write_verfile(file, versions):
|
def write_verfile(file: Path, versions: VersData) -> None:
|
||||||
# sort using only alphanums, as done by the sort command, and needed by
|
# sort using only alphanums, as done by the sort command,
|
||||||
# comm command
|
# and needed by comm command
|
||||||
data = ['%s %s\n' % item
|
data = ['%s %s\n' % item
|
||||||
for item in sorted(versions.items(), key=lambda i: (''.join(filter(str.isalnum, i[0])), i[1]))]
|
for item in sorted(versions.items(), key=lambda i: (''.join(filter(str.isalnum, i[0])), i[1]))]
|
||||||
safe_overwrite(file, data, method='writelines')
|
safe_overwrite(
|
||||||
|
str(file), ''.join(data), method='writelines')
|
||||||
|
|
||||||
class Source:
|
class Options(NamedTuple):
|
||||||
oldver = newver = None
|
ver_files: Optional[Tuple[Path, Path]]
|
||||||
tries = 1
|
max_concurrent: int
|
||||||
|
keymanager: KeyManager
|
||||||
|
|
||||||
def __init__(self, file, tries=1):
|
def load_file(
|
||||||
self.config = config = configparser.ConfigParser(
|
file: TextIO,
|
||||||
dict_type=dict, allow_no_value=True, interpolation=None,
|
) -> Tuple[Entries, Options]:
|
||||||
|
config = toml.load(file)
|
||||||
|
ver_files: Optional[Tuple[Path, Path]] = None
|
||||||
|
|
||||||
|
if '__config__' in config:
|
||||||
|
c = config.pop('__config__')
|
||||||
|
d = Path(file.name).parent
|
||||||
|
|
||||||
|
if 'oldver' in c and 'newver' in c:
|
||||||
|
oldver_s = os.path.expandvars(
|
||||||
|
os.path.expanduser(c.get('oldver')))
|
||||||
|
oldver = d / oldver_s
|
||||||
|
newver_s = os.path.expandvars(
|
||||||
|
os.path.expanduser(c.get('newver')))
|
||||||
|
newver = d / newver_s
|
||||||
|
ver_files = oldver, newver
|
||||||
|
|
||||||
|
keyfile = c.get('keyfile')
|
||||||
|
if keyfile:
|
||||||
|
keyfile_s = os.path.expandvars(
|
||||||
|
os.path.expanduser(c.get('keyfile')))
|
||||||
|
keyfile = d / keyfile_s
|
||||||
|
|
||||||
|
max_concurrent = c.getint(
|
||||||
|
'max_concurrent', 20)
|
||||||
|
keymanager = KeyManager(keyfile)
|
||||||
|
|
||||||
|
else:
|
||||||
|
max_concurrent = 20
|
||||||
|
keymanager = KeyManager(None)
|
||||||
|
|
||||||
|
return cast(Entries, config), Options(
|
||||||
|
ver_files, max_concurrent, keymanager)
|
||||||
|
|
||||||
|
def token_queue(maxsize: int) -> Queue[bool]:
|
||||||
|
token_q: Queue[bool] = Queue(maxsize=maxsize)
|
||||||
|
|
||||||
|
for _ in range(maxsize):
|
||||||
|
token_q.put_nowait(True)
|
||||||
|
|
||||||
|
return token_q
|
||||||
|
|
||||||
|
def dispatch(
|
||||||
|
entries: Entries,
|
||||||
|
token_q: Queue[bool],
|
||||||
|
result_q: Queue[RawResult],
|
||||||
|
keymanager: KeyManager,
|
||||||
|
tries: int,
|
||||||
|
) -> List[asyncio.Future]:
|
||||||
|
mods: Dict[str, Tuple[types.ModuleType, List]] = {}
|
||||||
|
for name, entry in entries.items():
|
||||||
|
source = entry.get('source', 'none')
|
||||||
|
if source not in mods:
|
||||||
|
mod = import_module('nvchecker_source.' + source)
|
||||||
|
tasks: List[Tuple[str, Entry]] = []
|
||||||
|
mods[source] = mod, tasks
|
||||||
|
else:
|
||||||
|
tasks = mods[source][1]
|
||||||
|
tasks.append((name, entry))
|
||||||
|
|
||||||
|
ret = []
|
||||||
|
for mod, tasks in mods.values():
|
||||||
|
worker = mod.Worker( # type: ignore
|
||||||
|
token_q, result_q, tasks,
|
||||||
|
tries, keymanager,
|
||||||
)
|
)
|
||||||
self.name = file.name
|
ret.append(worker.run())
|
||||||
self.tries = tries
|
|
||||||
config.read_file(file)
|
|
||||||
if '__config__' in config:
|
|
||||||
c = config['__config__']
|
|
||||||
|
|
||||||
d = os.path.dirname(file.name)
|
return ret
|
||||||
if 'oldver' in c and 'newver' in c:
|
|
||||||
self.oldver = os.path.expandvars(os.path.expanduser(
|
|
||||||
os.path.join(d, c.get('oldver'))))
|
|
||||||
self.newver = os.path.expandvars(os.path.expanduser(
|
|
||||||
os.path.join(d, c.get('newver'))))
|
|
||||||
|
|
||||||
keyfile = c.get('keyfile')
|
def substitute_version(
|
||||||
if keyfile:
|
version: str, conf: Entry,
|
||||||
keyfile = os.path.expandvars(os.path.expanduser(
|
) -> str:
|
||||||
os.path.join(d, c.get('keyfile'))))
|
'''
|
||||||
|
Substitute the version string via defined rules in the configuration file.
|
||||||
|
See README.rst#global-options for details.
|
||||||
|
'''
|
||||||
|
prefix = conf.get('prefix')
|
||||||
|
if prefix:
|
||||||
|
if version.startswith(prefix):
|
||||||
|
version = version[len(prefix):]
|
||||||
|
return version
|
||||||
|
|
||||||
self.max_concurrent = c.getint('max_concurrent', 20)
|
from_pattern = conf.get('from_pattern')
|
||||||
self.keymanager = KeyManager(keyfile)
|
if from_pattern:
|
||||||
session.nv_config = config["__config__"]
|
to_pattern = conf.get('to_pattern')
|
||||||
|
if not to_pattern:
|
||||||
|
raise ValueError("from_pattern exists but to_pattern doesn't")
|
||||||
|
|
||||||
else:
|
return re.sub(from_pattern, to_pattern, version)
|
||||||
self.max_concurrent = 20
|
|
||||||
self.keymanager = KeyManager(None)
|
|
||||||
|
|
||||||
async def check(self):
|
# No substitution rules found. Just return the original version string.
|
||||||
if self.oldver:
|
return version
|
||||||
self.oldvers = read_verfile(self.oldver)
|
|
||||||
else:
|
|
||||||
self.oldvers = {}
|
|
||||||
self.curvers = self.oldvers.copy()
|
|
||||||
|
|
||||||
tries = self.tries
|
def apply_list_options(
|
||||||
token_q = asyncio.Queue(maxsize=self.max_concurrent)
|
versions: List[str], conf: Entry,
|
||||||
|
) -> Optional[str]:
|
||||||
|
pattern = conf.get('include_regex')
|
||||||
|
if pattern:
|
||||||
|
re_pat = re.compile(pattern)
|
||||||
|
versions = [x for x in versions
|
||||||
|
if re_pat.fullmatch(x)]
|
||||||
|
|
||||||
for _ in range(self.max_concurrent):
|
pattern = conf.get('exclude_regex')
|
||||||
await token_q.put(True)
|
if pattern:
|
||||||
|
re_pat = re.compile(pattern)
|
||||||
|
versions = [x for x in versions
|
||||||
|
if not re_pat.fullmatch(x)]
|
||||||
|
|
||||||
async def worker(name, conf):
|
ignored = set(conf.get('ignored', '').split())
|
||||||
await token_q.get()
|
if ignored:
|
||||||
try:
|
versions = [x for x in versions if x not in ignored]
|
||||||
for i in range(tries):
|
|
||||||
try:
|
|
||||||
ret = await get_version(
|
|
||||||
name, conf, keyman=self.keymanager)
|
|
||||||
return name, ret
|
|
||||||
except Exception as e:
|
|
||||||
if i + 1 < tries:
|
|
||||||
logger.warning('failed, retrying',
|
|
||||||
name=name, exc_info=e)
|
|
||||||
await asyncio.sleep(i)
|
|
||||||
else:
|
|
||||||
return name, e
|
|
||||||
finally:
|
|
||||||
await token_q.put(True)
|
|
||||||
|
|
||||||
config = self.config
|
if not versions:
|
||||||
futures = []
|
return None
|
||||||
for name in config.sections():
|
|
||||||
if name == '__config__':
|
sort_version_key = sort_version_keys[
|
||||||
|
conf.get("sort_version_key", "parse_version")]
|
||||||
|
versions.sort(key=sort_version_key)
|
||||||
|
|
||||||
|
return versions[-1]
|
||||||
|
|
||||||
|
def _process_result(r: RawResult) -> Optional[Result]:
|
||||||
|
version = r.version
|
||||||
|
conf = r.conf
|
||||||
|
name = r.name
|
||||||
|
|
||||||
|
if isinstance(version, Exception):
|
||||||
|
logger.error('unexpected error happened',
|
||||||
|
name=r.name, exc_info=r.version)
|
||||||
|
return None
|
||||||
|
elif isinstance(version, list):
|
||||||
|
version_str = apply_list_options(version, conf)
|
||||||
|
else:
|
||||||
|
version_str = version
|
||||||
|
|
||||||
|
if version_str:
|
||||||
|
version_str = version_str.replace('\n', ' ')
|
||||||
|
|
||||||
|
try:
|
||||||
|
version_str = substitute_version(version_str, conf)
|
||||||
|
return Result(name, version_str, conf)
|
||||||
|
except (ValueError, re.error):
|
||||||
|
logger.exception('error occurred in version substitutions', name=name)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_version_update(
|
||||||
|
oldvers: VersData, name: str, version: str,
|
||||||
|
) -> Optional[str]:
|
||||||
|
oldver = oldvers.get(name, None)
|
||||||
|
if not oldver or oldver != version:
|
||||||
|
logger.info('updated', name=name, version=version, old_version=oldver)
|
||||||
|
return version
|
||||||
|
else:
|
||||||
|
logger.debug('up-to-date', name=name, version=version)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def process_result(
|
||||||
|
oldvers: VersData,
|
||||||
|
result_q: Queue[RawResult],
|
||||||
|
) -> VersData:
|
||||||
|
ret = {}
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
r = await result_q.get()
|
||||||
|
r1 = _process_result(r)
|
||||||
|
if r1 is None:
|
||||||
continue
|
continue
|
||||||
|
v = check_version_update(
|
||||||
|
oldvers, r1.name, r1.version)
|
||||||
|
if v is not None:
|
||||||
|
ret[r1.name] = v
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return ret
|
||||||
|
|
||||||
conf = config[name]
|
async def run_tasks(
|
||||||
conf['oldver'] = self.oldvers.get(name, None)
|
futures: Sequence[Awaitable[None]]
|
||||||
fu = asyncio.ensure_future(worker(name, conf))
|
) -> None:
|
||||||
futures.append(fu)
|
for fu in asyncio.as_completed(futures):
|
||||||
|
await fu
|
||||||
for fu in asyncio.as_completed(futures):
|
|
||||||
name, result = await fu
|
|
||||||
if isinstance(result, Exception):
|
|
||||||
logger.error('unexpected error happened',
|
|
||||||
name=name, exc_info=result)
|
|
||||||
self.on_exception(name, result)
|
|
||||||
elif result is not None:
|
|
||||||
self.print_version_update(name, result)
|
|
||||||
else:
|
|
||||||
conf = config[name]
|
|
||||||
if not conf.getboolean('missing_ok', False):
|
|
||||||
logger.warning('no-result', name=name)
|
|
||||||
self.on_no_result(name)
|
|
||||||
|
|
||||||
if self.newver:
|
|
||||||
write_verfile(self.newver, self.curvers)
|
|
||||||
|
|
||||||
def print_version_update(self, name, version):
|
|
||||||
oldver = self.oldvers.get(name, None)
|
|
||||||
if not oldver or oldver != version:
|
|
||||||
logger.info('updated', name=name, version=version, old_version=oldver)
|
|
||||||
self.curvers[name] = version
|
|
||||||
self.on_update(name, version, oldver)
|
|
||||||
else:
|
|
||||||
logger.debug('up-to-date', name=name, version=version)
|
|
||||||
|
|
||||||
def on_update(self, name, version, oldver):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_no_result(self, name):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_exception(self, name, exc):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<Source from %r>' % self.name
|
|
||||||
|
|
||||||
class KeyManager:
|
|
||||||
def __init__(self, file):
|
|
||||||
self.config = config = configparser.ConfigParser(dict_type=dict)
|
|
||||||
if file is not None:
|
|
||||||
config.read([file])
|
|
||||||
else:
|
|
||||||
config.add_section('keys')
|
|
||||||
|
|
||||||
def get_key(self, name):
|
|
||||||
return self.config.get('keys', name, fallback=None)
|
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
# MIT licensed
|
|
||||||
# Copyright (c) 2013-2017 lilydjwg <lilydjwg@gmail.com>, et al.
|
|
||||||
|
|
||||||
import re
|
|
||||||
from importlib import import_module
|
|
||||||
|
|
||||||
import structlog
|
|
||||||
|
|
||||||
from .sortversion import sort_version_keys
|
|
||||||
|
|
||||||
logger = structlog.get_logger(logger_name=__name__)
|
|
||||||
|
|
||||||
handler_precedence = (
|
|
||||||
'github', 'aur', 'pypi', 'archpkg', 'debianpkg', 'ubuntupkg',
|
|
||||||
'gems', 'pacman',
|
|
||||||
'cmd', 'bitbucket', 'regex', 'manual', 'vcs',
|
|
||||||
'cratesio', 'npm', 'hackage', 'cpan', 'gitlab', 'packagist',
|
|
||||||
'repology', 'anitya', 'android_sdk', 'sparkle', 'gitea'
|
|
||||||
)
|
|
||||||
|
|
||||||
def substitute_version(version, name, conf):
|
|
||||||
'''
|
|
||||||
Substitute the version string via defined rules in the configuration file.
|
|
||||||
See README.rst#global-options for details.
|
|
||||||
'''
|
|
||||||
prefix = conf.get('prefix')
|
|
||||||
if prefix:
|
|
||||||
if version.startswith(prefix):
|
|
||||||
version = version[len(prefix):]
|
|
||||||
return version
|
|
||||||
|
|
||||||
from_pattern = conf.get('from_pattern')
|
|
||||||
if from_pattern:
|
|
||||||
to_pattern = conf.get('to_pattern')
|
|
||||||
if not to_pattern:
|
|
||||||
raise ValueError('%s: from_pattern exists but to_pattern doesn\'t', name)
|
|
||||||
|
|
||||||
return re.sub(from_pattern, to_pattern, version)
|
|
||||||
|
|
||||||
# No substitution rules found. Just return the original version string.
|
|
||||||
return version
|
|
||||||
|
|
||||||
def apply_list_options(versions, conf):
|
|
||||||
pattern = conf.get('include_regex')
|
|
||||||
if pattern:
|
|
||||||
pattern = re.compile(pattern)
|
|
||||||
versions = [x for x in versions
|
|
||||||
if pattern.fullmatch(x)]
|
|
||||||
|
|
||||||
pattern = conf.get('exclude_regex')
|
|
||||||
if pattern:
|
|
||||||
pattern = re.compile(pattern)
|
|
||||||
versions = [x for x in versions
|
|
||||||
if not pattern.fullmatch(x)]
|
|
||||||
|
|
||||||
ignored = set(conf.get('ignored', '').split())
|
|
||||||
if ignored:
|
|
||||||
versions = [x for x in versions if x not in ignored]
|
|
||||||
|
|
||||||
if not versions:
|
|
||||||
return
|
|
||||||
|
|
||||||
sort_version_key = sort_version_keys[
|
|
||||||
conf.get("sort_version_key", "parse_version")]
|
|
||||||
versions.sort(key=sort_version_key)
|
|
||||||
|
|
||||||
return versions[-1]
|
|
||||||
|
|
||||||
_cache = {}
|
|
||||||
|
|
||||||
async def get_version(name, conf, **kwargs):
|
|
||||||
for key in handler_precedence:
|
|
||||||
if key in conf:
|
|
||||||
mod = import_module('.source.' + key, __package__)
|
|
||||||
func = mod.get_version
|
|
||||||
get_cacheable_conf = getattr(mod, 'get_cacheable_conf', lambda name, conf: conf)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
logger.error('no idea to get version info.', name=name)
|
|
||||||
return
|
|
||||||
|
|
||||||
cacheable_conf = get_cacheable_conf(name, conf)
|
|
||||||
cache_key = tuple(sorted(cacheable_conf.items()))
|
|
||||||
if cache_key in _cache:
|
|
||||||
version = _cache[cache_key]
|
|
||||||
logger.debug('cache hit', name=name,
|
|
||||||
cache_key=cache_key, cached=version)
|
|
||||||
return version
|
|
||||||
|
|
||||||
version = await func(name, conf, **kwargs)
|
|
||||||
|
|
||||||
if isinstance(version, list):
|
|
||||||
version = apply_list_options(version, conf)
|
|
||||||
|
|
||||||
if version:
|
|
||||||
version = version.replace('\n', ' ')
|
|
||||||
try:
|
|
||||||
version = substitute_version(version, name, conf)
|
|
||||||
except (ValueError, re.error):
|
|
||||||
logger.exception('error occurred in version substitutions', name=name)
|
|
||||||
|
|
||||||
if version is not None:
|
|
||||||
_cache[cache_key] = version
|
|
||||||
|
|
||||||
return version
|
|
|
@ -19,10 +19,3 @@ m = __import__('%s_httpclient' % which, globals(), locals(), level=1)
|
||||||
__all__ = m.__all__
|
__all__ = m.__all__
|
||||||
for x in __all__:
|
for x in __all__:
|
||||||
globals()[x] = getattr(m, x)
|
globals()[x] = getattr(m, x)
|
||||||
|
|
||||||
def conf_cacheable_with_name(key):
|
|
||||||
def get_cacheable_conf(name, conf):
|
|
||||||
conf = dict(conf)
|
|
||||||
conf[key] = conf.get(key) or name
|
|
||||||
return conf
|
|
||||||
return get_cacheable_conf
|
|
|
@ -11,7 +11,7 @@ try:
|
||||||
import pycurl
|
import pycurl
|
||||||
AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient", max_clients=20)
|
AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient", max_clients=20)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pycurl = None
|
pycurl = None # type: ignore
|
||||||
|
|
||||||
from .httpclient import DEFAULT_USER_AGENT
|
from .httpclient import DEFAULT_USER_AGENT
|
||||||
|
|
||||||
|
@ -76,8 +76,8 @@ async def json_response(self, **kwargs):
|
||||||
async def read(self):
|
async def read(self):
|
||||||
return self.body
|
return self.body
|
||||||
|
|
||||||
HTTPResponse.json = json_response
|
HTTPResponse.json = json_response # type: ignore
|
||||||
HTTPResponse.read = read
|
HTTPResponse.read = read # type: ignore
|
||||||
session = Session()
|
session = Session()
|
||||||
|
|
||||||
NetworkErrors = ()
|
NetworkErrors = ()
|
|
@ -1 +1 @@
|
||||||
This directory belongs to modules from my [winterpy](https://github.com/lilydjwg/winterpy) and can be synced from there without care.
|
This directory contains modules from my [winterpy](https://github.com/lilydjwg/winterpy).
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
# MIT licensed
|
||||||
|
# Copyright (c) 2020 lilydjwg <lilydjwg@gmail.com>, et al.
|
||||||
|
|
|
@ -44,8 +44,7 @@ class TornadoLogFormatter(logging.Formatter):
|
||||||
record.message = "Bad message (%r): %r" % (e, record.__dict__)
|
record.message = "Bad message (%r): %r" % (e, record.__dict__)
|
||||||
record.asctime = time.strftime(
|
record.asctime = time.strftime(
|
||||||
"%m-%d %H:%M:%S", self.converter(record.created))
|
"%m-%d %H:%M:%S", self.converter(record.created))
|
||||||
record.asctime += '.%03d' % ((record.created % 1) * 1000)
|
prefix = '[%(levelname)1.1s %(asctime)s.%(msecs)03d %(module)s:%(lineno)d]' % \
|
||||||
prefix = '[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]' % \
|
|
||||||
record.__dict__
|
record.__dict__
|
||||||
if self._color:
|
if self._color:
|
||||||
prefix = (self._colors.get(record.levelno, self._normal) +
|
prefix = (self._colors.get(record.levelno, self._normal) +
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
# MIT licensed
|
|
||||||
# Copyright (c) 2013-2017 lilydjwg <lilydjwg@gmail.com>, et al.
|
|
||||||
|
|
||||||
'''
|
|
||||||
调用 libnotify
|
|
||||||
'''
|
|
||||||
|
|
||||||
__all__ = ["set", "show", "update", "set_timeout", "set_urgency"]
|
|
||||||
|
|
||||||
from ctypes import *
|
|
||||||
from threading import Lock
|
|
||||||
import atexit
|
|
||||||
|
|
||||||
NOTIFY_URGENCY_LOW = 0
|
|
||||||
NOTIFY_URGENCY_NORMAL = 1
|
|
||||||
NOTIFY_URGENCY_CRITICAL = 2
|
|
||||||
UrgencyLevel = {NOTIFY_URGENCY_LOW, NOTIFY_URGENCY_NORMAL, NOTIFY_URGENCY_CRITICAL}
|
|
||||||
|
|
||||||
libnotify = None
|
|
||||||
gobj = None
|
|
||||||
libnotify_lock = Lock()
|
|
||||||
libnotify_inited = False
|
|
||||||
|
|
||||||
class obj: pass
|
|
||||||
notify_st = obj()
|
|
||||||
|
|
||||||
def set(summary=None, body=None, icon_str=None):
|
|
||||||
with libnotify_lock:
|
|
||||||
init()
|
|
||||||
|
|
||||||
if summary is not None:
|
|
||||||
notify_st.summary = summary.encode()
|
|
||||||
notify_st.body = notify_st.icon_str = None
|
|
||||||
if body is not None:
|
|
||||||
notify_st.body = body.encode()
|
|
||||||
if icon_str is not None:
|
|
||||||
notify_st.icon_str = icon_str.encode()
|
|
||||||
|
|
||||||
libnotify.notify_notification_update(
|
|
||||||
notify_st.notify,
|
|
||||||
notify_st.summary,
|
|
||||||
notify_st.body,
|
|
||||||
notify_st.icon_str,
|
|
||||||
)
|
|
||||||
|
|
||||||
def show():
|
|
||||||
libnotify.notify_notification_show(notify_st.notify, c_void_p())
|
|
||||||
|
|
||||||
def update(summary=None, body=None, icon_str=None):
|
|
||||||
if not any((summary, body)):
|
|
||||||
raise TypeError('at least one argument please')
|
|
||||||
|
|
||||||
set(summary, body, icon_str)
|
|
||||||
show()
|
|
||||||
|
|
||||||
def set_timeout(self, timeout):
|
|
||||||
'''set `timeout' in milliseconds'''
|
|
||||||
libnotify.notify_notification_set_timeout(notify_st.notify, int(timeout))
|
|
||||||
|
|
||||||
def set_urgency(self, urgency):
|
|
||||||
if urgency not in UrgencyLevel:
|
|
||||||
raise ValueError
|
|
||||||
libnotify.notify_notification_set_urgency(notify_st.notify, urgency)
|
|
||||||
|
|
||||||
def init():
|
|
||||||
global libnotify_inited, libnotify, gobj
|
|
||||||
if libnotify_inited:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
libnotify = CDLL('libnotify.so')
|
|
||||||
except OSError:
|
|
||||||
libnotify = CDLL('libnotify.so.4')
|
|
||||||
gobj = CDLL('libgobject-2.0.so')
|
|
||||||
|
|
||||||
libnotify.notify_init('pynotify')
|
|
||||||
libnotify_inited = True
|
|
||||||
|
|
||||||
libnotify.notify_notification_new.restype = c_void_p
|
|
||||||
notify_st.notify = c_void_p(libnotify.notify_notification_new(
|
|
||||||
c_void_p(), c_void_p(), c_void_p(),
|
|
||||||
))
|
|
||||||
atexit.register(uninit)
|
|
||||||
|
|
||||||
def uninit():
|
|
||||||
global libnotify_inited
|
|
||||||
try:
|
|
||||||
if libnotify_inited:
|
|
||||||
gobj.g_object_unref(notify_st.notify)
|
|
||||||
libnotify.notify_uninit()
|
|
||||||
libnotify_inited = False
|
|
||||||
except AttributeError:
|
|
||||||
# libnotify.so 已被卸载
|
|
||||||
pass
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
from time import sleep
|
|
||||||
notify = __import__('__main__')
|
|
||||||
notify.set('This is a test', '测试一下。')
|
|
||||||
notify.show()
|
|
||||||
sleep(1)
|
|
||||||
notify.update(body='再测试一下。')
|
|
|
@ -10,7 +10,7 @@ import sys
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
from .source import HTTPError, NetworkErrors
|
from .httpclient import HTTPError, NetworkErrors # type: ignore
|
||||||
|
|
||||||
def _console_msg(event):
|
def _console_msg(event):
|
||||||
evt = event['event']
|
evt = event['event']
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
# MIT licensed
|
|
||||||
# Copyright (c) 2013-2017 lilydjwg <lilydjwg@gmail.com>, et al.
|
|
||||||
|
|
||||||
import structlog
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from . import session, conf_cacheable_with_name
|
|
||||||
|
|
||||||
logger = structlog.get_logger(logger_name=__name__)
|
|
||||||
|
|
||||||
AUR_URL = 'https://aur.archlinux.org/rpc/'
|
|
||||||
|
|
||||||
get_cacheable_conf = conf_cacheable_with_name('aur')
|
|
||||||
|
|
||||||
async def get_version(name, conf, **kwargs):
|
|
||||||
aurname = conf.get('aur') or name
|
|
||||||
use_last_modified = conf.getboolean('use_last_modified', False)
|
|
||||||
strip_release = conf.getboolean('strip-release', False)
|
|
||||||
async with session.get(AUR_URL, params={"v": 5, "type": "info", "arg[]": aurname}) as res:
|
|
||||||
data = await res.json()
|
|
||||||
|
|
||||||
if not data['results']:
|
|
||||||
logger.error('AUR upstream not found', name=name)
|
|
||||||
return
|
|
||||||
|
|
||||||
version = data['results'][0]['Version']
|
|
||||||
if use_last_modified:
|
|
||||||
version += '-' + datetime.utcfromtimestamp(data['results'][0]['LastModified']).strftime('%Y%m%d%H%M%S')
|
|
||||||
if strip_release and '-' in version:
|
|
||||||
version = version.rsplit('-', 1)[0]
|
|
||||||
return version
|
|
72
nvchecker/util.py
Normal file
72
nvchecker/util.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
# MIT licensed
|
||||||
|
# Copyright (c) 2020 lilydjwg <lilydjwg@gmail.com>, et al.
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from asyncio import Queue
|
||||||
|
import contextlib
|
||||||
|
from typing import (
|
||||||
|
Dict, Optional, List, AsyncGenerator, NamedTuple, Union,
|
||||||
|
Any, Tuple,
|
||||||
|
)
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import toml
|
||||||
|
|
||||||
|
Entry = Dict[str, Any]
|
||||||
|
Entries = Dict[str, Entry]
|
||||||
|
VersData = Dict[str, str]
|
||||||
|
|
||||||
|
class KeyManager:
|
||||||
|
def __init__(
|
||||||
|
self, file: Optional[Path],
|
||||||
|
) -> None:
|
||||||
|
if file is not None:
|
||||||
|
with file.open() as f:
|
||||||
|
keys = toml.load(f)['keys']
|
||||||
|
else:
|
||||||
|
keys = {}
|
||||||
|
self.keys = keys
|
||||||
|
|
||||||
|
def get_key(self, name: str) -> Optional[str]:
|
||||||
|
return self.keys.get(name)
|
||||||
|
|
||||||
|
class BaseWorker:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
token_q: Queue[bool],
|
||||||
|
result_q: Queue[RawResult],
|
||||||
|
tasks: List[Tuple[str, Entry]],
|
||||||
|
tries: int,
|
||||||
|
keymanager: KeyManager,
|
||||||
|
) -> None:
|
||||||
|
self.token_q = token_q
|
||||||
|
self.result_q = result_q
|
||||||
|
self.tries = tries
|
||||||
|
self.keymanager = keymanager
|
||||||
|
self.tasks = tasks
|
||||||
|
|
||||||
|
@contextlib.asynccontextmanager
|
||||||
|
async def acquire_token(self) -> AsyncGenerator[None, None]:
|
||||||
|
token = await self.token_q.get()
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
await self.token_q.put(token)
|
||||||
|
|
||||||
|
class RawResult(NamedTuple):
|
||||||
|
name: str
|
||||||
|
version: Union[Exception, List[str], str]
|
||||||
|
conf: Entry
|
||||||
|
|
||||||
|
class Result(NamedTuple):
|
||||||
|
name: str
|
||||||
|
version: str
|
||||||
|
conf: Entry
|
||||||
|
|
||||||
|
def conf_cacheable_with_name(key):
|
||||||
|
def get_cacheable_conf(name, conf):
|
||||||
|
conf = dict(conf)
|
||||||
|
conf[key] = conf.get(key) or name
|
||||||
|
return conf
|
||||||
|
return get_cacheable_conf
|
84
nvchecker_source/aur.py
Normal file
84
nvchecker_source/aur.py
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
# MIT licensed
|
||||||
|
# Copyright (c) 2013-2020 lilydjwg <lilydjwg@gmail.com>, et al.
|
||||||
|
|
||||||
|
import structlog
|
||||||
|
from datetime import datetime
|
||||||
|
import asyncio
|
||||||
|
from typing import Iterable, Dict, List, Tuple, Any
|
||||||
|
|
||||||
|
from nvchecker.util import (
|
||||||
|
Entry, BaseWorker, RawResult,
|
||||||
|
conf_cacheable_with_name,
|
||||||
|
)
|
||||||
|
from nvchecker.httpclient import session # type: ignore
|
||||||
|
|
||||||
|
get_cacheable_conf = conf_cacheable_with_name('aur')
|
||||||
|
|
||||||
|
logger = structlog.get_logger(logger_name=__name__)
|
||||||
|
|
||||||
|
AUR_URL = 'https://aur.archlinux.org/rpc/'
|
||||||
|
|
||||||
|
class Worker(BaseWorker):
|
||||||
|
# https://wiki.archlinux.org/index.php/Aurweb_RPC_interface#Limitations
|
||||||
|
batch_size = 100
|
||||||
|
|
||||||
|
async def run(self) -> None:
|
||||||
|
tasks = self.tasks
|
||||||
|
n_batch, left = divmod(len(tasks), self.batch_size)
|
||||||
|
if left > 0:
|
||||||
|
n_batch += 1
|
||||||
|
|
||||||
|
ret = []
|
||||||
|
for i in range(n_batch):
|
||||||
|
s = i * self.batch_size
|
||||||
|
batch = tasks[s : s+self.batch_size]
|
||||||
|
fu = self._run_batch(batch)
|
||||||
|
ret.append(fu)
|
||||||
|
|
||||||
|
await asyncio.wait(ret)
|
||||||
|
|
||||||
|
async def _run_batch(self, batch: List[Tuple[str, Entry]]) -> None:
|
||||||
|
task_by_name: Dict[str, Entry] = dict(self.tasks)
|
||||||
|
async with self.acquire_token():
|
||||||
|
results = await _run_batch_impl(batch)
|
||||||
|
for name, version in results.items():
|
||||||
|
r = RawResult(name, version, task_by_name[name])
|
||||||
|
await self.result_q.put(r)
|
||||||
|
|
||||||
|
async def _run_batch_impl(batch: List[Tuple[str, Entry]]) -> Dict[str, str]:
|
||||||
|
aurnames = {conf.get('aur', name) for name, conf in batch}
|
||||||
|
results = await _aur_get_multiple(aurnames)
|
||||||
|
|
||||||
|
ret = {}
|
||||||
|
|
||||||
|
for name, conf in batch:
|
||||||
|
aurname = conf.get('aur', name)
|
||||||
|
use_last_modified = conf.get('use_last_modified', False)
|
||||||
|
strip_release = conf.get('strip-release', False)
|
||||||
|
|
||||||
|
result = results.get(aurname)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
logger.error('AUR upstream not found', name=name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
version = result['Version']
|
||||||
|
if use_last_modified:
|
||||||
|
version += '-' + datetime.utcfromtimestamp(result['LastModified']).strftime('%Y%m%d%H%M%S')
|
||||||
|
if strip_release and '-' in version:
|
||||||
|
version = version.rsplit('-', 1)[0]
|
||||||
|
|
||||||
|
ret[name] = version
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
async def _aur_get_multiple(
|
||||||
|
aurnames: Iterable[str],
|
||||||
|
) -> Dict[str, Dict[str, Any]]:
|
||||||
|
params = [('v', '5'), ('type', 'info')]
|
||||||
|
params.extend(('arg[]', name) for name in aurnames)
|
||||||
|
async with session.get(AUR_URL, params=params) as res:
|
||||||
|
data = await res.json()
|
||||||
|
results = {r['Name']: r for r in data['results']}
|
||||||
|
return results
|
||||||
|
|
15
nvchecker_source/none.py
Normal file
15
nvchecker_source/none.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# MIT licensed
|
||||||
|
# Copyright (c) 2020 lilydjwg <lilydjwg@gmail.com>, et al.
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import structlog
|
||||||
|
from nvchecker.util import BaseWorker
|
||||||
|
|
||||||
|
logger = structlog.get_logger(logger_name=__name__)
|
||||||
|
|
||||||
|
class Worker(BaseWorker):
|
||||||
|
async def run(self) -> None:
|
||||||
|
async with self.acquire_token():
|
||||||
|
for name, _ in self.tasks:
|
||||||
|
logger.error('no source specified', name=name)
|
4
setup.py
4
setup.py
|
@ -21,7 +21,7 @@ setup(
|
||||||
zip_safe = False,
|
zip_safe = False,
|
||||||
|
|
||||||
packages = find_packages(exclude=["tests"]),
|
packages = find_packages(exclude=["tests"]),
|
||||||
install_requires = ['setuptools', 'structlog', 'tornado>=6', 'pycurl'],
|
install_requires = ['setuptools', 'toml', 'structlog', 'tornado>=6', 'pycurl'],
|
||||||
extras_require = {
|
extras_require = {
|
||||||
'vercmp': ['pyalpm'],
|
'vercmp': ['pyalpm'],
|
||||||
},
|
},
|
||||||
|
@ -50,8 +50,6 @@ setup(
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
"Programming Language :: Python :: 3.5",
|
|
||||||
"Programming Language :: Python :: 3.6",
|
|
||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.7",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
"Topic :: Internet",
|
"Topic :: Internet",
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
# MIT licensed
|
||||||
|
# Copyright (c) 2020 lilydjwg <lilydjwg@gmail.com>, et al.
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
# MIT licensed
|
||||||
|
# Copyright (c) 2020 lilydjwg <lilydjwg@gmail.com>, et al.
|
||||||
|
|
||||||
import configparser
|
import configparser
|
||||||
import pytest
|
import pytest
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
Loading…
Add table
Reference in a new issue