diff --git a/cznicinfo/cli.py b/cznicinfo/cli.py index 696ce61425cf24192a3f29461004bbb8bbc62a39..b16b9ddaafe3aa63ee55ac86cd6dcca8953020cc 100644 --- a/cznicinfo/cli.py +++ b/cznicinfo/cli.py @@ -6,7 +6,8 @@ Usage: cznicinfo <command> [<args>...] Commands: list List available projects - show Show project information + show Show project(s) information + find Find projects using regexp versions Show versions of project and its packages Options: @@ -18,7 +19,7 @@ from docopt import docopt import sys from . import __version__ -from . import commands +from . import commands # noqa: F401 (dynamic import) from . import exception from .util import log @@ -56,7 +57,6 @@ def cznicinfo(*cargs): print(log.T.bold_yellow(str(ex))) return ex.exit_code - return 0 diff --git a/cznicinfo/commands/find.py b/cznicinfo/commands/find.py new file mode 100644 index 0000000000000000000000000000000000000000..d6d78f958a588af0ff5aaf29224c0b0d0482059e --- /dev/null +++ b/cznicinfo/commands/find.py @@ -0,0 +1,44 @@ +""" +Find projects using regexp + +Usage: cznicinfo find [-h] [-f pretty|yaml|list] <query>... + +Arguments: + <query> can be either: + * python regex to match again relevant project attributes + * ATTR:REGEX string where ATTR is a project attribute + to match REGEX against. Lists kind of supported. + +Options: + -f, --format=<fmt> Output format: + 'pretty', 'yaml' or 'list' [default: pretty] +""" + +from docopt import docopt + +from cznicinfo import infocore +# from cznicinfo.util import log + + +def run_command(*args, **kwargs): + cargs = docopt(__doc__) + fmt = cargs['--format'] + + info = infocore.get_info() + projs = infocore.filter_projects(info, cargs['<query>']) + + if fmt == 'list': + infocore.pretty_list_projects(projs) + elif fmt == 'yaml': + infocore.pretty_print_data(projs) + else: + # default --format is pretty print + first = True + for proj in projs: + if first: + first = False + else: + print() + infocore.pretty_print_project(proj) + + return 0 diff --git a/cznicinfo/commands/list.py b/cznicinfo/commands/list.py index 2556de8b4addeb22b4e8eab10dacec1e633c85aa..d0987f285033e75d8a6fcf837c2aed6049cf2210 100644 --- a/cznicinfo/commands/list.py +++ b/cznicinfo/commands/list.py @@ -4,17 +4,15 @@ List available projects Usage: cznicinfo list [-h] """ -from docopt import docopt +# from docopt import docopt -from cznicinfo import infoparse -from cznicinfo.util import log +from cznicinfo import infocore def run_command(*args, **kwargs): - cargs = docopt(__doc__) + # cargs = docopt(__doc__) - info = infoparse.get_info(); - for proj in info['projects']: - print("%s: %s" % (proj['short-name'], proj['full-name'])) + info = infocore.get_info() + infocore.pretty_list_projects(info) return 0 diff --git a/cznicinfo/commands/show.py b/cznicinfo/commands/show.py index e2f36bf65424760b73d45400f2c6b3b49ba81405..c76aefc065c6a690faf503545fb4a5a1ed0c7e3d 100644 --- a/cznicinfo/commands/show.py +++ b/cznicinfo/commands/show.py @@ -9,7 +9,7 @@ Options: from docopt import docopt -from cznicinfo import infoparse +from cznicinfo import infocore from cznicinfo.util import log @@ -17,8 +17,8 @@ def run_command(*args, **kwargs): cargs = docopt(__doc__) fmt = cargs['--format'] - info = infoparse.get_info(); - projs, not_found = infoparse.get_projects_by_name(info, cargs['<project>']) + info = infocore.get_info() + projs, not_found = infocore.get_projects_by_name(info, cargs['<project>']) if not_found: nf_str = ", ".join(not_found) @@ -26,7 +26,7 @@ def run_command(*args, **kwargs): return 4 if fmt == 'yaml': - infoparse.pretty_print_data(projs) + infocore.pretty_print_data(projs) else: # default --format is pretty print first = True @@ -35,6 +35,6 @@ def run_command(*args, **kwargs): first = False else: print() - infoparse.pretty_print_project(proj) + infocore.pretty_print_project(proj) return 0 diff --git a/cznicinfo/commands/versions.py b/cznicinfo/commands/versions.py index 21d84060a061d51b1f57e27814234d0da8108e54..62255df14d71e8969f162c1f38cc811810fc8e8c 100644 --- a/cznicinfo/commands/versions.py +++ b/cznicinfo/commands/versions.py @@ -6,7 +6,7 @@ Usage: cznicinfo versions [-h] <project>... from docopt import docopt -from cznicinfo import infoparse +from cznicinfo import infocore from cznicinfo import versions from cznicinfo.util import log @@ -14,9 +14,9 @@ from cznicinfo.util import log def run_command(*args, **kwargs): cargs = docopt(__doc__) - info = infoparse.get_info(); + info = infocore.get_info() - projs, not_found = infoparse.get_projects_by_name(info, cargs['<project>']) + projs, not_found = infocore.get_projects_by_name(info, cargs['<project>']) if not_found: nf_str = ", ".join(not_found) diff --git a/cznicinfo/exception.py b/cznicinfo/exception.py index a479b75a685c817717ac761c3c626d7043e3c887..fecdc9eaddc7ce398c45a2510ca7eb72b4a7ed3c 100644 --- a/cznicinfo/exception.py +++ b/cznicinfo/exception.py @@ -31,3 +31,8 @@ class DuplicateInfoItem(InvalidInfoStructure): class DuplicateInfoProjects(InvalidInfoStructure): msg_fmt = "Duplicate info projects: %(item)s" + + +class InvalidProjectFilter(CZNICInfoException): + msg_fmt = "Invalid filter: %(why)" + exit_code = 12 diff --git a/cznicinfo/infoparse.py b/cznicinfo/infocore.py similarity index 62% rename from cznicinfo/infoparse.py rename to cznicinfo/infocore.py index 83099f36faf98f02a0d69a62cc148d1f3543e588..b9491da0fa580b91d6b37cadd8be5a2d9767e0fa 100644 --- a/cznicinfo/infoparse.py +++ b/cznicinfo/infocore.py @@ -1,6 +1,9 @@ import yaml import os +import re from collections import defaultdict +from collections.abc import Iterable +from functools import partial from cznicinfo.util import log from cznicinfo import exception @@ -11,6 +14,13 @@ DEFAULT_RELEASE = { 'branch': 'master' } + +# XXX: if thise file gets big, consider splitting: +# * cznicinfo.info.core (parsing) +# * cznicinfo.info.query (querying) +# * cznicinfo.info.print (pretty printing & other output) + + def get_info_fn(module_path=None): if module_path: path = module_path @@ -32,7 +42,7 @@ def load_info(info_fn=None): def parse_projects(info): if 'projects' not in info: - raise exception.MissingRequiredInfoItem(item='projects list' % item) + raise exception.MissingRequiredInfoItem(item='projects list') unique_items = ['short-name', 'full-name'] unique_lists = defaultdict(set) @@ -41,7 +51,8 @@ def parse_projects(info): for item_name in unique_items: item_value = proj.get(item_name) if not item_value: - raise exception.MissingRequiredInfoItem(item='projects[%d].%s' % (i, item_name)) + raise exception.MissingRequiredInfoItem( + item='projects[%d].%s' % (i, item_name)) if item_value in unique_lists[item_name]: item_str = 'projects[%d].%s = %s' % (i, item_name, item_value) raise exception.DuplicateInfoProjects(item=item_str) @@ -71,15 +82,78 @@ def get_projects_by_name(info, names): for proj in info['projects']: if (proj['short-name'] == pname or proj['full-name'] == pname): - found_projs.append(proj) - found = True - break + found_projs.append(proj) + found = True + break if not found: not_found.append(pname) return found_projs, not_found +def split_query(query): + attr, _, rex = query.partition(':') + if rex: + return attr, rex + return None, attr + + +def filter_projects(info, queries): + projs = info.get('projects', []) + qs = list(map(split_query, queries)) + filter_wrapper = partial(_match_project, qs) + filtered_projs = list(filter(filter_wrapper, projs)) + return filtered_projs + + +def match_project_by_attr_rex(project, attr, rex): + val = project.get(attr) + if val is None: + return False + if isinstance(val, str): + if not re.search(rex, val): + return False + elif isinstance(val, Iterable): + # collection matches if any item of collection matches + found = False + for e in val: + if re.search(rex, e): + found = True + break + if not found: + return False + else: + raise exception.InvalidProjectFilter( + why=("Can only filter strings but '%s' is %s" + % (attr, type(rex).__name__))) + return True + + +def match_project_by_smart_rex(project, rex): + if re.search(rex, project['short-name']): + return True + if re.search(rex, project['full-name']): + return True + # TODO: match more stuff like URLs + return None + + +def _match_project(queries, project): + for attr, rex in queries: + if attr: + m = match_project_by_attr_rex(project, attr, rex) + else: + m = match_project_by_smart_rex(project, rex) + if not m: + return False + return True + + +def pretty_list_projects(projects_info): + for proj in projects_info: + print("%s: %s" % (proj['short-name'], proj['full-name'])) + + def pretty_print_data(d): print(yaml.dump(d)) @@ -130,5 +204,3 @@ def pretty_print_project(project_info, indent=2): n=cp['name'], v=cp['build-status-url'], spc=space, t=log.T)) - # optional scalars - release_style = p.get('release-style') diff --git a/cznicinfo/versions.py b/cznicinfo/versions.py index 9859347d489de09c9807e9c93debbb4e5de1b41b..8858bd9c95f3c2eb977b63a11f49a5b42503c256 100644 --- a/cznicinfo/versions.py +++ b/cznicinfo/versions.py @@ -2,14 +2,14 @@ def get_project_versions_list(proj): vl = [] upstream = proj.get('upstream') if upstream: - nvchecker_options = upstream.get('nvchecker-options') - if nvchecker_options: - base_nvopts = nvchecker_options - for rls in proj.get('releases', []): - nvopts = base_nvopts.copy() - if 'branch' in rls: - nvopts['branch'] = rls['branch'] - vl.append((rls['release'], nvopts)) + nvchecker_options = upstream.get('nvchecker-options') + if nvchecker_options: + base_nvopts = nvchecker_options + for rls in proj.get('releases', []): + nvopts = base_nvopts.copy() + if 'branch' in rls: + nvopts['branch'] = rls['branch'] + vl.append((rls['release'], nvopts)) return vl