From 5833e3bcba0199441e18aa6bf0b237bf19d87f41 Mon Sep 17 00:00:00 2001 From: MadinaB Date: Sun, 11 Nov 2018 12:19:17 +0900 Subject: [PATCH 1/8] active_data reordered for docopt --- adr/queries/activedata_usage.query | 44 +++++++++++++++++++++--------- adr/query.py | 16 +++++++---- adr/recipes/activedata_usage.py | 16 ++++------- 3 files changed, 48 insertions(+), 28 deletions(-) diff --git a/adr/queries/activedata_usage.query b/adr/queries/activedata_usage.query index 38e7365..4d5d891 100644 --- a/adr/queries/activedata_usage.query +++ b/adr/queries/activedata_usage.query @@ -1,14 +1,32 @@ { - "from":"activedata_requests", - "edges":{ - "value":"timestamp", - "domain":{ - "type":"time", - "min":{$eval: from_date}, - "max":{$eval: to_date}, - "interval":"day" - } - }, - "where":{"gt":{"timestamp":{"date":{$eval: from_date}}}}, - "format":"table" -} + "query": { + "from": "activedata_requests", + "edges": { + "value": "timestamp", + "domain": { + "type": "time", + "min": { + $eval: from + }, + "max": { + $eval: to + }, + "interval": "day" + } + }, + "where": { + "gt": { + "timestamp": { + "date": { + $eval: from + } + } + } + }, + "format": "table" + }, "argument_parser": "\n Usage:\n active_data [--from ] [--to ]\n\n Options:\n\ + \ --from= Starting date to pull data from, defaults to a week ago.\ + \ [default: today-week]\n --to= Ending date to pull data from, defaults\ + \ to now. [default: eod]\n" + +} \ No newline at end of file diff --git a/adr/query.py b/adr/query.py index fad30cd..144df8c 100644 --- a/adr/query.py +++ b/adr/query.py @@ -9,7 +9,7 @@ import jsone import requests import yaml - +from docopt import docopt from adr.formatter import all_formatters from adr.errors import MissingDataError @@ -76,11 +76,15 @@ def load_query(name): :yields dict query: dictionary representation of yaml query. """ with open(os.path.join(QUERY_DIR, name + '.query')) as fh: - for query in yaml.load_all(fh): - yield query + for record in yaml.load_all(fh): + try: + yield record["argument_parser"], record["query"] + except KeyError: + yield '', record["query"] + -def run_query(name, config, **context): +def run_query(name, config, args): """Loads and runs the specified query, yielding the result. Given name of a query, this method will first read the query @@ -97,9 +101,11 @@ def run_query(name, config, **context): :param dict context: dictionary of ActiveData configs. :yields str: json-formatted string. """ - for query in load_query(name): + for argument_parser, query in load_query(name): # If limit is in the context, override the queries' value. We do this # to keep the results down to a sane level when testing queries. + parsed_arguments = docopt(argument_parser, argv=args) + context = { k.replace('--', ''): v for k, v in parsed_arguments.items() } if 'limit' in context: query['limit'] = context['limit'] if 'format' in context: diff --git a/adr/recipes/activedata_usage.py b/adr/recipes/activedata_usage.py index 76595b5..c4ef7c0 100644 --- a/adr/recipes/activedata_usage.py +++ b/adr/recipes/activedata_usage.py @@ -1,10 +1,10 @@ """ -Show ActiveData query usage, by day - -.. code-block:: bash - + Show ActiveData query usage, by day + + .. code-block:: bash + adr activedata_usage [--from [--to ]] -""" + """ from __future__ import print_function, absolute_import @@ -13,9 +13,5 @@ def run(args, config): - parser = RecipeParser('date') - args = parser.parse_args(args) - - query_args = vars(args) - response = next(run_query('activedata_usage', config, **query_args)) + response = next(run_query('activedata_usage', config, args)) return [response['header']] + response['data'] From beee791818040790a7e9cf0237635173deecf52f Mon Sep 17 00:00:00 2001 From: MadinaB Date: Sun, 11 Nov 2018 14:39:15 +0900 Subject: [PATCH 2/8] intermediate functionality Supports both ways for argument processing --- adr/query.py | 12 +++++++----- adr/recipes/activedata_usage.py | 6 +++--- setup.py | 1 + 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/adr/query.py b/adr/query.py index 144df8c..f2ac4ea 100644 --- a/adr/query.py +++ b/adr/query.py @@ -80,11 +80,10 @@ def load_query(name): try: yield record["argument_parser"], record["query"] except KeyError: - yield '', record["query"] + yield '', record - -def run_query(name, config, args): +def run_query(name, config, *args, **kwargs): """Loads and runs the specified query, yielding the result. Given name of a query, this method will first read the query @@ -104,8 +103,11 @@ def run_query(name, config, args): for argument_parser, query in load_query(name): # If limit is in the context, override the queries' value. We do this # to keep the results down to a sane level when testing queries. - parsed_arguments = docopt(argument_parser, argv=args) - context = { k.replace('--', ''): v for k, v in parsed_arguments.items() } + if len(argument_parser) != 0: + parsed_arguments = docopt(argument_parser, argv=args) + context = {k.replace('--', ''): v for k, v in parsed_arguments.items()} + else: + context = kwargs if 'limit' in context: query['limit'] = context['limit'] if 'format' in context: diff --git a/adr/recipes/activedata_usage.py b/adr/recipes/activedata_usage.py index c4ef7c0..cb29251 100644 --- a/adr/recipes/activedata_usage.py +++ b/adr/recipes/activedata_usage.py @@ -1,8 +1,8 @@ """ Show ActiveData query usage, by day - + .. code-block:: bash - + adr activedata_usage [--from [--to ]] """ @@ -13,5 +13,5 @@ def run(args, config): - response = next(run_query('activedata_usage', config, args)) + response = next(run_query('activedata_usage', config, *args)) return [response['header']] + response['data'] diff --git a/setup.py b/setup.py index 72badfd..1c368f2 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,7 @@ 'json-e >= 2.3.2', 'requests >= 2.18.3', 'terminaltables >= 3.1.0', + 'docopt==0.6.2', 'pyyaml', 'beautifulsoup4', 'flask', From 4a07c7a3aaf90725681b0f1191d2d3e4ff2eb362 Mon Sep 17 00:00:00 2001 From: MadinaB Date: Sun, 11 Nov 2018 15:29:51 +0900 Subject: [PATCH 3/8] backout_rate added --- adr/queries/backout_rate.query | 119 +++++++++++++++++++++++++------- adr/recipes/activedata_usage.py | 2 - adr/recipes/backout_rate.py | 8 +-- 3 files changed, 96 insertions(+), 33 deletions(-) diff --git a/adr/queries/backout_rate.query b/adr/queries/backout_rate.query index 209a793..ee93fbf 100644 --- a/adr/queries/backout_rate.query +++ b/adr/queries/backout_rate.query @@ -1,24 +1,95 @@ ---- -# all push ids -from: repo -select: - - push.id -where: - and: - - in: {branch.name: ["autoland", "mozilla-inbound"]} - - gte: [push.date, {date: {$eval: from_date}}] - - lte: [push.date, {date: {$eval: to_date}}] -limit: 100000 - ---- -# backout push ids -from: repo -select: - - push.id -where: - and: - - in: {branch.name: ["autoland", "mozilla-inbound"]} - - gte: [push.date, {date: {$eval: from_date}}] - - lte: [push.date, {date: {$eval: to_date}}] - - exists: changeset.backedoutby -limit: 100000 +[ + { + "query": { + "from": "repo", + "select": [ + "push.id" + ], + "where": { + "and": [ + { + "in": { + "branch.name": [ + "autoland", + "mozilla-inbound" + ] + } + }, + { + "gte": [ + "push.date", + { + "date": { + $eval: from + } + } + ] + }, + { + "lte": [ + "push.date", + { + "date": { + $eval: to + } + } + ] + } + ] + }, + "limit": 100000 + }, + "argument_parser": "\n Usage:\n all_push_ids [--from ] [--to ]\n\n Options:\n\ + \ --from= Starting date to pull data from, defaults to a week ago.\ + \ [default: today-week]\n --to= Ending date to pull data from, defaults\ + \ to now. [default: eod]\n" + }, + { + "query": { + "from": "repo", + "select": [ + "push.id" + ], + "where": { + "and": [ + { + "in": { + "branch.name": [ + "autoland", + "mozilla-inbound" + ] + } + }, + { + "gte": [ + "push.date", + { + "date": { + $eval: from + } + } + ] + }, + { + "lte": [ + "push.date", + { + "date": { + $eval: to + } + } + ] + }, + { + "exists": "changeset.backedoutby" + } + ] + }, + "limit": 100000 + }, + "argument_parser": "\n Usage:\n backout_push_ids [--from ] [--to ]\n\n Options:\n\ + \ --from= Starting date to pull data from, defaults to a week ago.\ + \ [default: today-week]\n --to= Ending date to pull data from, defaults\ + \ to now. [default: eod]\n" + } +] \ No newline at end of file diff --git a/adr/recipes/activedata_usage.py b/adr/recipes/activedata_usage.py index cb29251..fe88f91 100644 --- a/adr/recipes/activedata_usage.py +++ b/adr/recipes/activedata_usage.py @@ -7,8 +7,6 @@ """ from __future__ import print_function, absolute_import - -from ..recipe import RecipeParser from ..query import run_query diff --git a/adr/recipes/backout_rate.py b/adr/recipes/backout_rate.py index 963d880..5b329c6 100644 --- a/adr/recipes/backout_rate.py +++ b/adr/recipes/backout_rate.py @@ -9,18 +9,12 @@ `View Results `__ """ from __future__ import print_function, absolute_import - -from ..recipe import RecipeParser from ..query import run_query def run(args, config): - parser = RecipeParser('date') - args = parser.parse_args(args) - - query_args = vars(args) - query = run_query('backout_rate', config, **query_args) + query = run_query('backout_rate', config, *args) pushes = len(set(next(query)['data']['push.id'])) backouts = len(set(next(query)['data']['push.id'])) backout_rate = round((float(backouts) / pushes) * 100, 2) From 2a64e27f341fef56ffe5df1f6c4e5fc8f7783fb8 Mon Sep 17 00:00:00 2001 From: MadinaB Date: Sun, 11 Nov 2018 15:32:47 +0900 Subject: [PATCH 4/8] backout_rate fix --- adr/queries/backout_rate.query | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/adr/queries/backout_rate.query b/adr/queries/backout_rate.query index ee93fbf..0c4fd79 100644 --- a/adr/queries/backout_rate.query +++ b/adr/queries/backout_rate.query @@ -1,4 +1,4 @@ -[ +--- { "query": { "from": "repo", @@ -43,7 +43,8 @@ \ --from= Starting date to pull data from, defaults to a week ago.\ \ [default: today-week]\n --to= Ending date to pull data from, defaults\ \ to now. [default: eod]\n" - }, + } +--- { "query": { "from": "repo", @@ -91,5 +92,4 @@ \ --from= Starting date to pull data from, defaults to a week ago.\ \ [default: today-week]\n --to= Ending date to pull data from, defaults\ \ to now. [default: eod]\n" - } -] \ No newline at end of file + } \ No newline at end of file From 7e96921d9b0c3637a9fc7db898d4109fcc9b27c1 Mon Sep 17 00:00:00 2001 From: MadinaB Date: Sun, 11 Nov 2018 18:04:42 +0900 Subject: [PATCH 5/8] adding docopt for further modifications --- adr/util/modified_docopt.py | 569 ++++++++++++++++++++++++++++++++++++ 1 file changed, 569 insertions(+) create mode 100644 adr/util/modified_docopt.py diff --git a/adr/util/modified_docopt.py b/adr/util/modified_docopt.py new file mode 100644 index 0000000..8ded647 --- /dev/null +++ b/adr/util/modified_docopt.py @@ -0,0 +1,569 @@ +"""Pythonic command-line interface parser that will make you smile. + * http://docopt.org + * Repository and issue-tracker: https://github.com/docopt/docopt + * Licensed under terms of MIT license (see LICENSE-MIT) + * Copyright (c) 2013 Vladimir Keleshev, vladimir@keleshev.com +""" +import sys +import re + + +__all__ = ['docopt'] +__version__ = '0.6.2' + + +class DocoptLanguageError(Exception): + + """Error in construction of usage-message by developer.""" + + +class DocoptExit(SystemExit): + + """Exit in case user invoked program with incorrect arguments.""" + + usage = '' + + def __init__(self, message=''): + SystemExit.__init__(self, (message + '\n' + self.usage).strip()) + + +class Pattern(object): + + def __eq__(self, other): + return repr(self) == repr(other) + + def __hash__(self): + return hash(repr(self)) + + def fix(self): + self.fix_identities() + self.fix_repeating_arguments() + return self + + def fix_identities(self, uniq=None): + """Make pattern-tree tips point to same object if they are equal.""" + if not hasattr(self, 'children'): + return self + uniq = list(set(self.flat())) if uniq is None else uniq + for i, child in enumerate(self.children): + if not hasattr(child, 'children'): + assert child in uniq + self.children[i] = uniq[uniq.index(child)] + else: + child.fix_identities(uniq) + + def fix_repeating_arguments(self): + """Fix elements that should accumulate/increment values.""" + either = [list(child.children) for child in transform(self).children] + for case in either: + for e in [child for child in case if case.count(child) > 1]: + if type(e) is Argument or type(e) is Option and e.argcount: + if e.value is None: + e.value = [] + elif type(e.value) is not list: + e.value = e.value.split() + if type(e) is Command or type(e) is Option and e.argcount == 0: + e.value = 0 + return self + + +def transform(pattern): + """Expand pattern into an (almost) equivalent one, but with single Either. + Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d) + Quirks: [-a] => (-a), (-a...) => (-a -a) + """ + result = [] + groups = [[pattern]] + while groups: + children = groups.pop(0) + parents = [Required, Optional, OptionsShortcut, Either, OneOrMore] + if any(t in map(type, children) for t in parents): + child = [c for c in children if type(c) in parents][0] + children.remove(child) + if type(child) is Either: + for c in child.children: + groups.append([c] + children) + elif type(child) is OneOrMore: + groups.append(child.children * 2 + children) + else: + groups.append(child.children + children) + else: + result.append(children) + return Either(*[Required(*e) for e in result]) + + +class LeafPattern(Pattern): + + """Leaf/terminal node of a pattern tree.""" + + def __init__(self, name, value=None): + self.name, self.value = name, value + + def __repr__(self): + return '%s(%r, %r)' % (self.__class__.__name__, self.name, self.value) + + def flat(self, *types): + return [self] if not types or type(self) in types else [] + + def match(self, left, collected=None): + collected = [] if collected is None else collected + pos, match = self.single_match(left) + if match is None: + return False, left, collected + left_ = left[:pos] + left[pos + 1:] + same_name = [a for a in collected if a.name == self.name] + if type(self.value) in (int, list): + if type(self.value) is int: + increment = 1 + else: + increment = ([match.value] if type(match.value) is str + else match.value) + if not same_name: + match.value = increment + return True, left_, collected + [match] + same_name[0].value += increment + return True, left_, collected + return True, left_, collected + [match] + + +class BranchPattern(Pattern): + + """Branch/inner node of a pattern tree.""" + + def __init__(self, *children): + self.children = list(children) + + def __repr__(self): + return '%s(%s)' % (self.__class__.__name__, + ', '.join(repr(a) for a in self.children)) + + def flat(self, *types): + if type(self) in types: + return [self] + return sum([child.flat(*types) for child in self.children], []) + + +class Argument(LeafPattern): + + def single_match(self, left): + for n, pattern in enumerate(left): + if type(pattern) is Argument: + return n, Argument(self.name, pattern.value) + return None, None + + @classmethod + def parse(class_, source): + name = re.findall('(<\S*?>)', source)[0] + value = re.findall('\[default: (.*)\]', source, flags=re.I) + return class_(name, value[0] if value else None) + + +class Command(Argument): + + def __init__(self, name, value=False): + self.name, self.value = name, value + + def single_match(self, left): + for n, pattern in enumerate(left): + if type(pattern) is Argument: + if pattern.value == self.name: + return n, Command(self.name, True) + else: + break + return None, None + + +class Option(LeafPattern): + + def __init__(self, short=None, long=None, argcount=0, value=False): + assert argcount in (0, 1) + self.short, self.long, self.argcount = short, long, argcount + self.value = None if value is False and argcount else value + + @classmethod + def parse(class_, option_description): + short, long, argcount, value = None, None, 0, False + options, _, description = option_description.strip().partition(' ') + options = options.replace(',', ' ').replace('=', ' ') + for s in options.split(): + if s.startswith('--'): + long = s + elif s.startswith('-'): + short = s + else: + argcount = 1 + if argcount: + matched = re.findall('\[default: (.*)\]', description, flags=re.I) + value = matched[0] if matched else None + return class_(short, long, argcount, value) + + def single_match(self, left): + for n, pattern in enumerate(left): + if self.name == pattern.name: + return n, pattern + return None, None + + @property + def name(self): + return self.long or self.short + + def __repr__(self): + return 'Option(%r, %r, %r, %r)' % (self.short, self.long, + self.argcount, self.value) + + +class Required(BranchPattern): + + def match(self, left, collected=None): + collected = [] if collected is None else collected + l = left + c = collected + for pattern in self.children: + matched, l, c = pattern.match(l, c) + if not matched: + return False, left, collected + return True, l, c + + +class Optional(BranchPattern): + + def match(self, left, collected=None): + collected = [] if collected is None else collected + for pattern in self.children: + m, left, collected = pattern.match(left, collected) + return True, left, collected + + +class OptionsShortcut(Optional): + + """Marker/placeholder for [options] shortcut.""" + + +class OneOrMore(BranchPattern): + + def match(self, left, collected=None): + assert len(self.children) == 1 + collected = [] if collected is None else collected + l = left + c = collected + l_ = None + matched = True + times = 0 + while matched: + # could it be that something didn't match but changed l or c? + matched, l, c = self.children[0].match(l, c) + times += 1 if matched else 0 + if l_ == l: + break + l_ = l + if times >= 1: + return True, l, c + return False, left, collected + + +class Either(BranchPattern): + + def match(self, left, collected=None): + collected = [] if collected is None else collected + outcomes = [] + for pattern in self.children: + matched, _, _ = outcome = pattern.match(left, collected) + if matched: + outcomes.append(outcome) + if outcomes: + return min(outcomes, key=lambda outcome: len(outcome[1])) + return False, left, collected + + +class Tokens(list): + + def __init__(self, source, error=DocoptExit): + self += source.split() if hasattr(source, 'split') else source + self.error = error + + @staticmethod + def from_pattern(source): + source = re.sub(r'([\[\]\(\)\|]|\.\.\.)', r' \1 ', source) + source = [s for s in re.split('\s+|(\S*<.*?>)', source) if s] + return Tokens(source, error=DocoptLanguageError) + + def move(self): + return self.pop(0) if len(self) else None + + def current(self): + return self[0] if len(self) else None + + +def parse_long(tokens, options): + """long ::= '--' chars [ ( ' ' | '=' ) chars ] ;""" + long, eq, value = tokens.move().partition('=') + assert long.startswith('--') + value = None if eq == value == '' else value + similar = [o for o in options if o.long == long] + if tokens.error is DocoptExit and similar == []: # if no exact match + similar = [o for o in options if o.long and o.long.startswith(long)] + if len(similar) > 1: # might be simply specified ambiguously 2+ times? + raise tokens.error('%s is not a unique prefix: %s?' % + (long, ', '.join(o.long for o in similar))) + elif len(similar) < 1: + argcount = 1 if eq == '=' else 0 + o = Option(None, long, argcount) + options.append(o) + if tokens.error is DocoptExit: + o = Option(None, long, argcount, value if argcount else True) + else: + o = Option(similar[0].short, similar[0].long, + similar[0].argcount, similar[0].value) + if o.argcount == 0: + if value is not None: + raise tokens.error('%s must not have an argument' % o.long) + else: + if value is None: + if tokens.current() in [None, '--']: + raise tokens.error('%s requires argument' % o.long) + value = tokens.move() + if tokens.error is DocoptExit: + o.value = value if value is not None else True + return [o] + + +def parse_shorts(tokens, options): + """shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;""" + token = tokens.move() + assert token.startswith('-') and not token.startswith('--') + left = token.lstrip('-') + parsed = [] + while left != '': + short, left = '-' + left[0], left[1:] + similar = [o for o in options if o.short == short] + if len(similar) > 1: + raise tokens.error('%s is specified ambiguously %d times' % + (short, len(similar))) + elif len(similar) < 1: + o = Option(short, None, 0) + options.append(o) + if tokens.error is DocoptExit: + o = Option(short, None, 0, True) + else: # why copying is necessary here? + o = Option(short, similar[0].long, + similar[0].argcount, similar[0].value) + value = None + if o.argcount != 0: + if left == '': + if tokens.current() in [None, '--']: + raise tokens.error('%s requires argument' % short) + value = tokens.move() + else: + value = left + left = '' + if tokens.error is DocoptExit: + o.value = value if value is not None else True + parsed.append(o) + return parsed + + +def parse_pattern(source, options): + tokens = Tokens.from_pattern(source) + result = parse_expr(tokens, options) + if tokens.current() is not None: + raise tokens.error('unexpected ending: %r' % ' '.join(tokens)) + return Required(*result) + + +def parse_expr(tokens, options): + """expr ::= seq ( '|' seq )* ;""" + seq = parse_seq(tokens, options) + if tokens.current() != '|': + return seq + result = [Required(*seq)] if len(seq) > 1 else seq + while tokens.current() == '|': + tokens.move() + seq = parse_seq(tokens, options) + result += [Required(*seq)] if len(seq) > 1 else seq + return [Either(*result)] if len(result) > 1 else result + + +def parse_seq(tokens, options): + """seq ::= ( atom [ '...' ] )* ;""" + result = [] + while tokens.current() not in [None, ']', ')', '|']: + atom = parse_atom(tokens, options) + if tokens.current() == '...': + atom = [OneOrMore(*atom)] + tokens.move() + result += atom + return result + + +def parse_atom(tokens, options): + """atom ::= '(' expr ')' | '[' expr ']' | 'options' + | long | shorts | argument | command ; + """ + token = tokens.current() + result = [] + if token in '([': + tokens.move() + matching, pattern = {'(': [')', Required], '[': [']', Optional]}[token] + result = pattern(*parse_expr(tokens, options)) + if tokens.move() != matching: + raise tokens.error("unmatched '%s'" % token) + return [result] + elif token == 'options': + tokens.move() + return [OptionsShortcut()] + elif token.startswith('--') and token != '--': + return parse_long(tokens, options) + elif token.startswith('-') and token not in ('-', '--'): + return parse_shorts(tokens, options) + elif token.startswith('<') and token.endswith('>') or token.isupper(): + return [Argument(tokens.move())] + else: + return [Command(tokens.move())] + + +def parse_argv(tokens, options, options_first=False): + """Parse command-line argument vector. + If options_first: + argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ; + else: + argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ; + """ + parsed = [] + while tokens.current() is not None: + if tokens.current() == '--': + return parsed + [Argument(None, v) for v in tokens] + elif tokens.current().startswith('--'): + parsed += parse_long(tokens, options) + elif tokens.current().startswith('-') and tokens.current() != '-': + parsed += parse_shorts(tokens, options) + elif options_first: + return parsed + [Argument(None, v) for v in tokens] + else: + parsed.append(Argument(None, tokens.move())) + return parsed + + +def parse_defaults(doc): + defaults = [] + for s in parse_section('options:', doc): + # FIXME corner case "bla: options: --foo" + _, _, s = s.partition(':') # get rid of "options:" + split = re.split('\n[ \t]*(-\S+?)', '\n' + s)[1:] + split = [s1 + s2 for s1, s2 in zip(split[::2], split[1::2])] + options = [Option.parse(s) for s in split if s.startswith('-')] + defaults += options + return defaults + + +def parse_section(name, source): + pattern = re.compile('^([^\n]*' + name + '[^\n]*\n?(?:[ \t].*?(?:\n|$))*)', + re.IGNORECASE | re.MULTILINE) + return [s.strip() for s in pattern.findall(source)] + + +def formal_usage(section): + _, _, section = section.partition(':') # drop "usage:" + pu = section.split() + return '( ' + ' '.join(') | (' if s == pu[0] else s for s in pu[1:]) + ' )' + + +def extras(help, version, options, doc): + if help and any((o.name in ('-h', '--help')) and o.value for o in options): + print(doc.strip("\n")) + sys.exit() + if version and any(o.name == '--version' and o.value for o in options): + print(version) + sys.exit() + + +class Dict(dict): + def __repr__(self): + return '{%s}' % ',\n '.join('%r: %r' % i for i in sorted(self.items())) + + +def docopt(doc, argv=None, help=True, version=None, options_first=False): + """Parse `argv` based on command-line interface described in `doc`. + `docopt` creates your command-line interface based on its + description that you pass as `doc`. Such description can contain + --options, , commands, which could be + [optional], (required), (mutually | exclusive) or repeated... + Parameters + ---------- + doc : str + Description of your command-line interface. + argv : list of str, optional + Argument vector to be parsed. sys.argv[1:] is used if not + provided. + help : bool (default: True) + Set to False to disable automatic help on -h or --help + options. + version : any object + If passed, the object will be printed if --version is in + `argv`. + options_first : bool (default: False) + Set to True to require options precede positional arguments, + i.e. to forbid options and positional arguments intermix. + Returns + ------- + args : dict + A dictionary, where keys are names of command-line elements + such as e.g. "--verbose" and "", and values are the + parsed values of those elements. + Example + ------- + >>> from docopt import docopt + >>> doc = ''' + ... Usage: + ... my_program tcp [--timeout=] + ... my_program serial [--baud=] [--timeout=] + ... my_program (-h | --help | --version) + ... + ... Options: + ... -h, --help Show this screen and exit. + ... --baud= Baudrate [default: 9600] + ... ''' + >>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30'] + >>> docopt(doc, argv) + {'--baud': '9600', + '--help': False, + '--timeout': '30', + '--version': False, + '': '127.0.0.1', + '': '80', + 'serial': False, + 'tcp': True} + See also + -------- + * For video introduction see http://docopt.org + * Full documentation is available in README.rst as well as online + at https://github.com/docopt/docopt#readme + """ + argv = sys.argv[1:] if argv is None else argv + + usage_sections = parse_section('usage:', doc) + if len(usage_sections) == 0: + raise DocoptLanguageError('"usage:" (case-insensitive) not found.') + if len(usage_sections) > 1: + raise DocoptLanguageError('More than one "usage:" (case-insensitive).') + DocoptExit.usage = usage_sections[0] + + options = parse_defaults(doc) + pattern = parse_pattern(formal_usage(DocoptExit.usage), options) + # [default] syntax for argument is disabled + #for a in pattern.flat(Argument): + # same_name = [d for d in arguments if d.name == a.name] + # if same_name: + # a.value = same_name[0].value + argv = parse_argv(Tokens(argv), list(options), options_first) + pattern_options = set(pattern.flat(Option)) + for options_shortcut in pattern.flat(OptionsShortcut): + doc_options = parse_defaults(doc) + options_shortcut.children = list(set(doc_options) - pattern_options) + #if any_options: + # options_shortcut.children += [Option(o.short, o.long, o.argcount) + # for o in argv if type(o) is Option] + extras(help, version, argv, doc) + matched, left, collected = pattern.fix().match(argv) + if matched and left == []: # better error message if left? + return Dict((a.name, a.value) for a in (pattern.flat() + collected)) + raise DocoptExit() From 8f05b257633e1e32300797243675d1c506ca2701 Mon Sep 17 00:00:00 2001 From: MadinaB Date: Sun, 11 Nov 2018 18:34:47 +0900 Subject: [PATCH 6/8] Modifying docopt to allow parsing with missing arguments --- adr/query.py | 2 +- adr/util/modified_docopt.py | 569 +----------------------------------- 2 files changed, 11 insertions(+), 560 deletions(-) diff --git a/adr/query.py b/adr/query.py index f2ac4ea..103689a 100644 --- a/adr/query.py +++ b/adr/query.py @@ -9,7 +9,7 @@ import jsone import requests import yaml -from docopt import docopt +from adr.util.modified_docopt import docopt from adr.formatter import all_formatters from adr.errors import MissingDataError diff --git a/adr/util/modified_docopt.py b/adr/util/modified_docopt.py index 8ded647..0667994 100644 --- a/adr/util/modified_docopt.py +++ b/adr/util/modified_docopt.py @@ -1,569 +1,20 @@ -"""Pythonic command-line interface parser that will make you smile. - * http://docopt.org - * Repository and issue-tracker: https://github.com/docopt/docopt - * Licensed under terms of MIT license (see LICENSE-MIT) - * Copyright (c) 2013 Vladimir Keleshev, vladimir@keleshev.com -""" -import sys -import re - - -__all__ = ['docopt'] -__version__ = '0.6.2' - - -class DocoptLanguageError(Exception): - - """Error in construction of usage-message by developer.""" - - -class DocoptExit(SystemExit): - - """Exit in case user invoked program with incorrect arguments.""" - - usage = '' - - def __init__(self, message=''): - SystemExit.__init__(self, (message + '\n' + self.usage).strip()) - - -class Pattern(object): - - def __eq__(self, other): - return repr(self) == repr(other) - - def __hash__(self): - return hash(repr(self)) - - def fix(self): - self.fix_identities() - self.fix_repeating_arguments() - return self - - def fix_identities(self, uniq=None): - """Make pattern-tree tips point to same object if they are equal.""" - if not hasattr(self, 'children'): - return self - uniq = list(set(self.flat())) if uniq is None else uniq - for i, child in enumerate(self.children): - if not hasattr(child, 'children'): - assert child in uniq - self.children[i] = uniq[uniq.index(child)] - else: - child.fix_identities(uniq) - - def fix_repeating_arguments(self): - """Fix elements that should accumulate/increment values.""" - either = [list(child.children) for child in transform(self).children] - for case in either: - for e in [child for child in case if case.count(child) > 1]: - if type(e) is Argument or type(e) is Option and e.argcount: - if e.value is None: - e.value = [] - elif type(e.value) is not list: - e.value = e.value.split() - if type(e) is Command or type(e) is Option and e.argcount == 0: - e.value = 0 - return self - - -def transform(pattern): - """Expand pattern into an (almost) equivalent one, but with single Either. - Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d) - Quirks: [-a] => (-a), (-a...) => (-a -a) - """ - result = [] - groups = [[pattern]] - while groups: - children = groups.pop(0) - parents = [Required, Optional, OptionsShortcut, Either, OneOrMore] - if any(t in map(type, children) for t in parents): - child = [c for c in children if type(c) in parents][0] - children.remove(child) - if type(child) is Either: - for c in child.children: - groups.append([c] + children) - elif type(child) is OneOrMore: - groups.append(child.children * 2 + children) - else: - groups.append(child.children + children) - else: - result.append(children) - return Either(*[Required(*e) for e in result]) - - -class LeafPattern(Pattern): - - """Leaf/terminal node of a pattern tree.""" - - def __init__(self, name, value=None): - self.name, self.value = name, value - - def __repr__(self): - return '%s(%r, %r)' % (self.__class__.__name__, self.name, self.value) - - def flat(self, *types): - return [self] if not types or type(self) in types else [] - - def match(self, left, collected=None): - collected = [] if collected is None else collected - pos, match = self.single_match(left) - if match is None: - return False, left, collected - left_ = left[:pos] + left[pos + 1:] - same_name = [a for a in collected if a.name == self.name] - if type(self.value) in (int, list): - if type(self.value) is int: - increment = 1 - else: - increment = ([match.value] if type(match.value) is str - else match.value) - if not same_name: - match.value = increment - return True, left_, collected + [match] - same_name[0].value += increment - return True, left_, collected - return True, left_, collected + [match] - - -class BranchPattern(Pattern): - - """Branch/inner node of a pattern tree.""" - - def __init__(self, *children): - self.children = list(children) - - def __repr__(self): - return '%s(%s)' % (self.__class__.__name__, - ', '.join(repr(a) for a in self.children)) - - def flat(self, *types): - if type(self) in types: - return [self] - return sum([child.flat(*types) for child in self.children], []) - - -class Argument(LeafPattern): - - def single_match(self, left): - for n, pattern in enumerate(left): - if type(pattern) is Argument: - return n, Argument(self.name, pattern.value) - return None, None - - @classmethod - def parse(class_, source): - name = re.findall('(<\S*?>)', source)[0] - value = re.findall('\[default: (.*)\]', source, flags=re.I) - return class_(name, value[0] if value else None) - - -class Command(Argument): - - def __init__(self, name, value=False): - self.name, self.value = name, value - - def single_match(self, left): - for n, pattern in enumerate(left): - if type(pattern) is Argument: - if pattern.value == self.name: - return n, Command(self.name, True) - else: - break - return None, None - - -class Option(LeafPattern): - - def __init__(self, short=None, long=None, argcount=0, value=False): - assert argcount in (0, 1) - self.short, self.long, self.argcount = short, long, argcount - self.value = None if value is False and argcount else value - - @classmethod - def parse(class_, option_description): - short, long, argcount, value = None, None, 0, False - options, _, description = option_description.strip().partition(' ') - options = options.replace(',', ' ').replace('=', ' ') - for s in options.split(): - if s.startswith('--'): - long = s - elif s.startswith('-'): - short = s - else: - argcount = 1 - if argcount: - matched = re.findall('\[default: (.*)\]', description, flags=re.I) - value = matched[0] if matched else None - return class_(short, long, argcount, value) - - def single_match(self, left): - for n, pattern in enumerate(left): - if self.name == pattern.name: - return n, pattern - return None, None - - @property - def name(self): - return self.long or self.short - - def __repr__(self): - return 'Option(%r, %r, %r, %r)' % (self.short, self.long, - self.argcount, self.value) - - -class Required(BranchPattern): - - def match(self, left, collected=None): - collected = [] if collected is None else collected - l = left - c = collected - for pattern in self.children: - matched, l, c = pattern.match(l, c) - if not matched: - return False, left, collected - return True, l, c - - -class Optional(BranchPattern): - - def match(self, left, collected=None): - collected = [] if collected is None else collected - for pattern in self.children: - m, left, collected = pattern.match(left, collected) - return True, left, collected - - -class OptionsShortcut(Optional): - - """Marker/placeholder for [options] shortcut.""" - - -class OneOrMore(BranchPattern): - - def match(self, left, collected=None): - assert len(self.children) == 1 - collected = [] if collected is None else collected - l = left - c = collected - l_ = None - matched = True - times = 0 - while matched: - # could it be that something didn't match but changed l or c? - matched, l, c = self.children[0].match(l, c) - times += 1 if matched else 0 - if l_ == l: - break - l_ = l - if times >= 1: - return True, l, c - return False, left, collected - - -class Either(BranchPattern): - - def match(self, left, collected=None): - collected = [] if collected is None else collected - outcomes = [] - for pattern in self.children: - matched, _, _ = outcome = pattern.match(left, collected) - if matched: - outcomes.append(outcome) - if outcomes: - return min(outcomes, key=lambda outcome: len(outcome[1])) - return False, left, collected - - -class Tokens(list): - - def __init__(self, source, error=DocoptExit): - self += source.split() if hasattr(source, 'split') else source - self.error = error - - @staticmethod - def from_pattern(source): - source = re.sub(r'([\[\]\(\)\|]|\.\.\.)', r' \1 ', source) - source = [s for s in re.split('\s+|(\S*<.*?>)', source) if s] - return Tokens(source, error=DocoptLanguageError) - - def move(self): - return self.pop(0) if len(self) else None - - def current(self): - return self[0] if len(self) else None - - -def parse_long(tokens, options): - """long ::= '--' chars [ ( ' ' | '=' ) chars ] ;""" - long, eq, value = tokens.move().partition('=') - assert long.startswith('--') - value = None if eq == value == '' else value - similar = [o for o in options if o.long == long] - if tokens.error is DocoptExit and similar == []: # if no exact match - similar = [o for o in options if o.long and o.long.startswith(long)] - if len(similar) > 1: # might be simply specified ambiguously 2+ times? - raise tokens.error('%s is not a unique prefix: %s?' % - (long, ', '.join(o.long for o in similar))) - elif len(similar) < 1: - argcount = 1 if eq == '=' else 0 - o = Option(None, long, argcount) - options.append(o) - if tokens.error is DocoptExit: - o = Option(None, long, argcount, value if argcount else True) - else: - o = Option(similar[0].short, similar[0].long, - similar[0].argcount, similar[0].value) - if o.argcount == 0: - if value is not None: - raise tokens.error('%s must not have an argument' % o.long) - else: - if value is None: - if tokens.current() in [None, '--']: - raise tokens.error('%s requires argument' % o.long) - value = tokens.move() - if tokens.error is DocoptExit: - o.value = value if value is not None else True - return [o] - - -def parse_shorts(tokens, options): - """shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;""" - token = tokens.move() - assert token.startswith('-') and not token.startswith('--') - left = token.lstrip('-') - parsed = [] - while left != '': - short, left = '-' + left[0], left[1:] - similar = [o for o in options if o.short == short] - if len(similar) > 1: - raise tokens.error('%s is specified ambiguously %d times' % - (short, len(similar))) - elif len(similar) < 1: - o = Option(short, None, 0) - options.append(o) - if tokens.error is DocoptExit: - o = Option(short, None, 0, True) - else: # why copying is necessary here? - o = Option(short, similar[0].long, - similar[0].argcount, similar[0].value) - value = None - if o.argcount != 0: - if left == '': - if tokens.current() in [None, '--']: - raise tokens.error('%s requires argument' % short) - value = tokens.move() - else: - value = left - left = '' - if tokens.error is DocoptExit: - o.value = value if value is not None else True - parsed.append(o) - return parsed - - -def parse_pattern(source, options): - tokens = Tokens.from_pattern(source) - result = parse_expr(tokens, options) - if tokens.current() is not None: - raise tokens.error('unexpected ending: %r' % ' '.join(tokens)) - return Required(*result) - - -def parse_expr(tokens, options): - """expr ::= seq ( '|' seq )* ;""" - seq = parse_seq(tokens, options) - if tokens.current() != '|': - return seq - result = [Required(*seq)] if len(seq) > 1 else seq - while tokens.current() == '|': - tokens.move() - seq = parse_seq(tokens, options) - result += [Required(*seq)] if len(seq) > 1 else seq - return [Either(*result)] if len(result) > 1 else result - - -def parse_seq(tokens, options): - """seq ::= ( atom [ '...' ] )* ;""" - result = [] - while tokens.current() not in [None, ']', ')', '|']: - atom = parse_atom(tokens, options) - if tokens.current() == '...': - atom = [OneOrMore(*atom)] - tokens.move() - result += atom - return result - - -def parse_atom(tokens, options): - """atom ::= '(' expr ')' | '[' expr ']' | 'options' - | long | shorts | argument | command ; - """ - token = tokens.current() - result = [] - if token in '([': - tokens.move() - matching, pattern = {'(': [')', Required], '[': [']', Optional]}[token] - result = pattern(*parse_expr(tokens, options)) - if tokens.move() != matching: - raise tokens.error("unmatched '%s'" % token) - return [result] - elif token == 'options': - tokens.move() - return [OptionsShortcut()] - elif token.startswith('--') and token != '--': - return parse_long(tokens, options) - elif token.startswith('-') and token not in ('-', '--'): - return parse_shorts(tokens, options) - elif token.startswith('<') and token.endswith('>') or token.isupper(): - return [Argument(tokens.move())] - else: - return [Command(tokens.move())] - - -def parse_argv(tokens, options, options_first=False): - """Parse command-line argument vector. - If options_first: - argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ; - else: - argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ; - """ - parsed = [] - while tokens.current() is not None: - if tokens.current() == '--': - return parsed + [Argument(None, v) for v in tokens] - elif tokens.current().startswith('--'): - parsed += parse_long(tokens, options) - elif tokens.current().startswith('-') and tokens.current() != '-': - parsed += parse_shorts(tokens, options) - elif options_first: - return parsed + [Argument(None, v) for v in tokens] - else: - parsed.append(Argument(None, tokens.move())) - return parsed - - -def parse_defaults(doc): - defaults = [] - for s in parse_section('options:', doc): - # FIXME corner case "bla: options: --foo" - _, _, s = s.partition(':') # get rid of "options:" - split = re.split('\n[ \t]*(-\S+?)', '\n' + s)[1:] - split = [s1 + s2 for s1, s2 in zip(split[::2], split[1::2])] - options = [Option.parse(s) for s in split if s.startswith('-')] - defaults += options - return defaults - - -def parse_section(name, source): - pattern = re.compile('^([^\n]*' + name + '[^\n]*\n?(?:[ \t].*?(?:\n|$))*)', - re.IGNORECASE | re.MULTILINE) - return [s.strip() for s in pattern.findall(source)] - - -def formal_usage(section): - _, _, section = section.partition(':') # drop "usage:" - pu = section.split() - return '( ' + ' '.join(') | (' if s == pu[0] else s for s in pu[1:]) + ' )' - - -def extras(help, version, options, doc): - if help and any((o.name in ('-h', '--help')) and o.value for o in options): - print(doc.strip("\n")) - sys.exit() - if version and any(o.name == '--version' and o.value for o in options): - print(version) - sys.exit() - - -class Dict(dict): - def __repr__(self): - return '{%s}' % ',\n '.join('%r: %r' % i for i in sorted(self.items())) +from docopt import DocoptExit, Option, AnyOptions, TokenStream, Dict +from docopt import printable_usage, parse_defaults, parse_pattern, formal_usage, parse_argv, extras def docopt(doc, argv=None, help=True, version=None, options_first=False): - """Parse `argv` based on command-line interface described in `doc`. - `docopt` creates your command-line interface based on its - description that you pass as `doc`. Such description can contain - --options, , commands, which could be - [optional], (required), (mutually | exclusive) or repeated... - Parameters - ---------- - doc : str - Description of your command-line interface. - argv : list of str, optional - Argument vector to be parsed. sys.argv[1:] is used if not - provided. - help : bool (default: True) - Set to False to disable automatic help on -h or --help - options. - version : any object - If passed, the object will be printed if --version is in - `argv`. - options_first : bool (default: False) - Set to True to require options precede positional arguments, - i.e. to forbid options and positional arguments intermix. - Returns - ------- - args : dict - A dictionary, where keys are names of command-line elements - such as e.g. "--verbose" and "", and values are the - parsed values of those elements. - Example - ------- - >>> from docopt import docopt - >>> doc = ''' - ... Usage: - ... my_program tcp [--timeout=] - ... my_program serial [--baud=] [--timeout=] - ... my_program (-h | --help | --version) - ... - ... Options: - ... -h, --help Show this screen and exit. - ... --baud= Baudrate [default: 9600] - ... ''' - >>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30'] - >>> docopt(doc, argv) - {'--baud': '9600', - '--help': False, - '--timeout': '30', - '--version': False, - '': '127.0.0.1', - '': '80', - 'serial': False, - 'tcp': True} - See also - -------- - * For video introduction see http://docopt.org - * Full documentation is available in README.rst as well as online - at https://github.com/docopt/docopt#readme - """ - argv = sys.argv[1:] if argv is None else argv - - usage_sections = parse_section('usage:', doc) - if len(usage_sections) == 0: - raise DocoptLanguageError('"usage:" (case-insensitive) not found.') - if len(usage_sections) > 1: - raise DocoptLanguageError('More than one "usage:" (case-insensitive).') - DocoptExit.usage = usage_sections[0] - + if argv is None: + argv = sys.argv[1:] + DocoptExit.usage = printable_usage(doc) options = parse_defaults(doc) pattern = parse_pattern(formal_usage(DocoptExit.usage), options) - # [default] syntax for argument is disabled - #for a in pattern.flat(Argument): - # same_name = [d for d in arguments if d.name == a.name] - # if same_name: - # a.value = same_name[0].value - argv = parse_argv(Tokens(argv), list(options), options_first) + argv = parse_argv(TokenStream(argv, DocoptExit), list(options), options_first) pattern_options = set(pattern.flat(Option)) - for options_shortcut in pattern.flat(OptionsShortcut): + for ao in pattern.flat(AnyOptions): doc_options = parse_defaults(doc) - options_shortcut.children = list(set(doc_options) - pattern_options) - #if any_options: - # options_shortcut.children += [Option(o.short, o.long, o.argcount) - # for o in argv if type(o) is Option] + ao.children = list(set(doc_options) - pattern_options) extras(help, version, argv, doc) matched, left, collected = pattern.fix().match(argv) - if matched and left == []: # better error message if left? + if matched: return Dict((a.name, a.value) for a in (pattern.flat() + collected)) - raise DocoptExit() + raise DocoptExit() From 96a42f5904de37c924022694836462c318e3d970 Mon Sep 17 00:00:00 2001 From: MadinaB Date: Sun, 11 Nov 2018 18:48:38 +0900 Subject: [PATCH 7/8] fixing flake8 violation --- adr/util/modified_docopt.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/adr/util/modified_docopt.py b/adr/util/modified_docopt.py index 0667994..df8f68e 100644 --- a/adr/util/modified_docopt.py +++ b/adr/util/modified_docopt.py @@ -1,5 +1,6 @@ from docopt import DocoptExit, Option, AnyOptions, TokenStream, Dict -from docopt import printable_usage, parse_defaults, parse_pattern, formal_usage, parse_argv, extras +from docopt import parse_defaults, parse_pattern, parse_argv, extras +from docopt import formal_usage, printable_usage, sys def docopt(doc, argv=None, help=True, version=None, options_first=False): @@ -17,4 +18,4 @@ def docopt(doc, argv=None, help=True, version=None, options_first=False): matched, left, collected = pattern.fix().match(argv) if matched: return Dict((a.name, a.value) for a in (pattern.flat() + collected)) - raise DocoptExit() + DocoptExit() From 55f62f2c6c4cbab6a451ec029779e5a2adffc1b3 Mon Sep 17 00:00:00 2001 From: MadinaB Date: Sun, 11 Nov 2018 20:12:25 +0900 Subject: [PATCH 8/8] added config_durations --- adr/queries/config_durations.query | 106 +++++++++++++++++++++++++---- adr/recipes/config_durations.py | 30 ++++---- 2 files changed, 109 insertions(+), 27 deletions(-) diff --git a/adr/queries/config_durations.query b/adr/queries/config_durations.query index 7492c25..a0856df 100644 --- a/adr/queries/config_durations.query +++ b/adr/queries/config_durations.query @@ -1,15 +1,91 @@ -from: task -select: - - {aggregate: count, name: tasks} - - {aggregate: avg, name: "average minutes", value: {div: {action.duration: 60}}} -groupby: - - "run.machine.platform" - - "build.type" -where: - and: - - in: {repo.branch.name: {$eval: branch}} - - gte: [repo.push.date, {date: {$eval: from_date}}] - - lte: [repo.push.date, {date: {$eval: to_date}}] - - in: {run.suite.name: ["mochitest", "reftest", "web-platform-tests", "web-platform-tests-reftests", "xpcshell", "marionette", "firefox-ui", "cppunittest", "gtest", "jittest", "awsy", "web-platform-tests-wdspec", "test-verification"]} - - in: {build.type: ["opt","debug","pgo","asan"]} -limit: 10000 +{ + "query": { + "from": "task", + "select": [ + { + "aggregate": "count", + "name": "tasks" + }, + { + "aggregate": "avg", + "name": "average minutes", + "value": { + "div": { + "action.duration": 60 + } + } + } + ], + "groupby": [ + "run.machine.platform", + "build.type" + ], + "where": { + "and": [ + { + "in": { + "repo.branch.name": { + $eval: branch + } + } + }, + { + "gte": [ + "repo.push.date", + { + "date": { + $eval: from + } + } + ] + }, + { + "lte": [ + "repo.push.date", + { + "date": { + $eval: to + } + } + ] + }, + { + "in": { + "run.suite.name": [ + "mochitest", + "reftest", + "web-platform-tests", + "web-platform-tests-reftests", + "xpcshell", + "marionette", + "firefox-ui", + "cppunittest", + "gtest", + "jittest", + "awsy", + "web-platform-tests-wdspec", + "test-verification" + ] + } + }, + { + "in": { + "build.type": [ + "opt", + "debug", + "pgo", + "asan" + ] + } + } + ] + }, + "limit": 10000 + }, + "argument_parser": "\n Argument parser for the config_durations query.\n \n Usage:\n config_durations\ + \ [--from FROM_DATE] [--to TO_DATE] [--branch BRANCH...]\n \n Options:\n \ + \ --from=FROM_DATE Starting date to pull data from, defaults to a week ago. [default:\ + \ today-week]\n --to=TO_DATE Ending date to pull data from, defaults to now.\ +\ [default: eod]\n --branch=BRANCH Branches to query results from. [default:\ +\ mozilla-central]\n " +} \ No newline at end of file diff --git a/adr/recipes/config_durations.py b/adr/recipes/config_durations.py index b7252d1..0c36342 100644 --- a/adr/recipes/config_durations.py +++ b/adr/recipes/config_durations.py @@ -6,23 +6,29 @@ adr config_durations [--branch ] """ from __future__ import print_function, absolute_import - -from ..recipe import RecipeParser +from adr.util.modified_docopt import docopt from ..query import run_query +argument_parser = """ + Argument parser for the config_durations recipe. + + Usage: + config_durations [--limit LIMIT] [--sort-key SORTKEY] + + Options: + --limit=LIMIT Maximum number of jobs to return. [default: 50] + --sort-key=SORTKEY Key to sort on (int, 0-based index). [default: 4] + """ + + def run(args, config): - parser = RecipeParser('date', 'branch') - parser.add_argument('--limit', type=int, default=50, - help="Maximum number of jobs to return") - parser.add_argument('--sort-key', type=int, default=4, - help="Key to sort on (int, 0-based index)") - args = parser.parse_args(args) - query_args = vars(args) - limit = query_args.pop('limit') + limit = int(docopt(argument_parser, args)['--limit']) + sort_key = int(docopt(argument_parser, args)['--sort-key']) + + data = next(run_query('config_durations', config, *args))['data'] - data = next(run_query('config_durations', config, **query_args))['data'] result = [] for record in data: if isinstance(record[1], list): @@ -35,6 +41,6 @@ def run(args, config): record.append(int(round(record[2] * record[3], 0))) result.append(record) - result = sorted(result, key=lambda k: k[args.sort_key], reverse=True)[:limit] + result = sorted(result, key=lambda k: k[sort_key], reverse=True)[:limit] result.insert(0, ['Platform', 'Type', 'Num Jobs', 'Average Hours', 'Total Hours']) return result