diff --git a/CHANGES.rst b/CHANGES.rst index c08a28646..7396fb16f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,6 +22,13 @@ The Dice: timestamping_addresses --> tstamper_address ## Not a list anymore! TstampReceiver.subject --> TstampSpec.subject_prefix ## Also used by `recv` cmd. +- feat: renamed command: ``project tstamp -- > project tsend``. + Now there is symmetricity between ``co2dice tstamp`` and ``co2dice project`` + cmds:: + + tstamp send <--> project tsend + tstamp recv <--> project recv + - feat: new commands: - ``tstamp recv``: Fetch tstamps from IMAP server and derive *decisions* diff --git a/README.rst b/README.rst index 3973595ee..a373eb8df 100644 --- a/README.rst +++ b/README.rst @@ -7,8 +7,8 @@ |co2mpas|: Vehicle simulator predicting NEDC |CO2| emissions from WLTP ###################################################################### -:release: 1.5.7.dev1 -:date: 2017-05-13 01:55:07 +:release: 1.5.7.b3 +:date: 2017-05-14 08:16:03 :home: http://co2mpas.io/ :repository: https://github.com/JRCSTU/CO2MPAS-TA :pypi-repo: https://pypi.org/project/co2mpas/ @@ -346,7 +346,7 @@ Alternatively, open the CONSOLE and type the following command: ## Check co2mpas version. $ co2mpas -V - co2mpas-1.5.7.dev1 + co2mpas-1.5.7.b3 |co2mpas| command syntax @@ -1348,7 +1348,7 @@ Install |co2mpas| package Downloading http://pypi.co2mpas.io/packages/co2mpas-... ... Installing collected packages: co2mpas - Successfully installed co2mpas-1.5.7.dev1 + Successfully installed co2mpas-1.5.7.b3 .. Warning:: **Installation failures:** @@ -1368,8 +1368,8 @@ Install |co2mpas| package .. code-block:: console > co2mpas -vV - co2mpas_version: 1.5.7.dev1 - co2mpas_rel_date: 2017-05-13 01:55:07 + co2mpas_version: 1.5.7.b3 + co2mpas_rel_date: 2017-05-14 08:16:03 co2mpas_path: d:\co2mpas_ALLINONE-64bit-v1.4.1\Apps\WinPython\python-3.4.3\lib\site-packages\co2mpas python_path: D:\co2mpas_ALLINONE-64bit-v1.4.1\WinPython\python-3.4.3 python_version: 3.4.3 (v3.4.3:9b73f1c3e601, Feb 24 2015, 22:44:40) [MSC v.1600 XXX] diff --git a/co2mpas/_version.py b/co2mpas/_version.py index d7e236265..17613de8c 100755 --- a/co2mpas/_version.py +++ b/co2mpas/_version.py @@ -9,7 +9,7 @@ #: Authoritative project's PEP 440 version. -__version__ = version = "1.5.7.dev1" # Also update README.rst, CHANGES.rst, +__version__ = version = "1.5.7.b3" # Also update README.rst, CHANGES.rst, #: Input/Output file's version. __file_version__ = "2.2.6" @@ -21,4 +21,4 @@ # Please UPDATE TIMESTAMP WHEN BUMPING VERSIONS AND BEFORE RELEASE. #: Release date. -__updated__ = "2017-05-13 01:55:07" +__updated__ = "2017-05-14 08:16:03" diff --git a/co2mpas/sampling/cfgcmd.py b/co2mpas/sampling/cfgcmd.py index 94cc254a1..f76187da0 100644 --- a/co2mpas/sampling/cfgcmd.py +++ b/co2mpas/sampling/cfgcmd.py @@ -14,9 +14,26 @@ from .._vendor import traitlets as trt +def prepare_matcher(terms, is_regex): + import re + + def matcher(r): + if is_regex: + return re.compile(r, re.I).search + else: + return lambda w: r.lower() in w.lower() + + matchers = [matcher(t) for t in terms] + + def match(word): + return any(m(word) for m in matchers) + + return match + + class ConfigCmd(baseapp.Cmd): """ - Commands to manage configuration-options loaded from filesystem. + Commands to manage configuration-options loaded from filesystem, cmd-line or defaults. Read also the help message for `--config-paths` generic option. """ @@ -222,7 +239,9 @@ class DescCmd(baseapp.Cmd): Describe config-params with their name '.' containing search-strings (case-insensitive). SYNTAX - %(cmd_chain)s [OPTIONS] [] ... + %(cmd_chain)s [OPTIONS] [ ...] + + - Use --verbose to view config-params on all intermediate classes. """ examples = trt.Unicode(""" @@ -230,15 +249,14 @@ class DescCmd(baseapp.Cmd): %(cmd_chain)s --list 'criteria' %(cmd_chain)s -l --cls 'config' %(cmd_chain)s -l --regex '^t.+cmd' - To view help on specific parameters: %(cmd_chain)s wait %(cmd_chain)s -e 'rec.+wait' - + To view help on full classes: %(cmd_chain)s -ecl 'rec.+wait' """) - + list = trt.Bool( help="Just list any matches." ).tag(config=True) @@ -254,8 +272,8 @@ class DescCmd(baseapp.Cmd): def __init__(self, **kwds): import pandalone.utils as pndlu - super().__init__( - cmd_flags={ + kwds.setdefault( + 'cmd_flags', { ('l', 'list'): ( {type(self).__name__: {'list': True}}, pndlu.first_line(type(self).list.help) @@ -270,21 +288,14 @@ def __init__(self, **kwds): ), } ) + super().__init__(**kwds) def run(self, *args): - import re from toolz import dicttoolz as dtz - def matcher(r): - if self.regex: - return re.compile(r, re.I).search - else: - return lambda w: r.lower() in w.lower() - - matchers = [matcher(t) for t in args] - - def is_matching(word): - return any(m(word) for m in matchers) + if len(args) == 0: + raise CmdException('Cmd %r takes at least one !' + % self.name) ## Prefer to modify `class_names` after `initialize()`, or else, # the cmd options would be irrelevant and fatty :-) @@ -299,15 +310,20 @@ def printer(ne, cls): return cls.class_get_help() else: - search_map = {'%s.%s' % (cls.__name__, attr): (cls, trait) - for cls in all_classes - for attr, trait in cls.class_traits(config=True).items()} - + search_map = { + '%s.%s' % (cls.__name__, attr): (cls, trait) + for cls in all_classes + for attr, trait in + (cls.class_traits + if self.verbose + else cls.class_own_traits)(config=True).items()} + def printer(name, v): cls, attr = v return cls.class_get_trait_help(attr) - - res_map = dtz.keyfilter(is_matching, search_map) + + match = prepare_matcher(args, self.regex) + res_map = dtz.keyfilter(match, search_map) for name, v in sorted(res_map.items()): if self.list: diff --git a/co2mpas/sampling/project.py b/co2mpas/sampling/project.py index a6d6ba2ff..47f99fd9a 100644 --- a/co2mpas/sampling/project.py +++ b/co2mpas/sampling/project.py @@ -1570,11 +1570,11 @@ class OpenCmd(_SubCmd): %(cmd_chain)s [OPTIONS] """ def run(self, *args): - self.log.info('Opening project %r...', args) if len(args) != 1: raise CmdException( "Cmd %r takes exactly one argument as the project-name, received %r!" % (self.name, args)) + self.log.info('Opening project %r...', args) projDB = self.projects_db proj = projDB.proj_open(args[0]) @@ -1738,7 +1738,9 @@ class ReportCmd(_SubCmd): - Eventually the *Dice Report* parameters will be time-stamped and disseminated to TA authorities & oversight bodies with an email, to receive back the sampling decision. - - To get report ready for sending it MANUALLY, use tstamp` sub-command. + - To send the report to the stamper, use `tsend` sub-command. + - To get report ready for sending it MANUALLY, use `tsend --dry-run` + instead. """ @@ -1773,6 +1775,13 @@ def run(self, *args): class TstampCmd(_SubCmd): + """Deprecated: renamed as `tsend`!""" + def run(self, *args): + raise CmdException("Cmd %r has been renamed to %r!" + % (self.name, 'tsend')) + + +class TsendCmd(_SubCmd): """ IRREVOCABLY send report to the time-stamp service, or print it for sending it manually (--dry-run). @@ -1947,6 +1956,7 @@ def __init__(self, **kwds): def run(self, *args): from . import tstamp + self.log.info("Receiving emails for projects(s) %s: ...", args) default_flow_style = None if self.verbose else False warn = self.log.warning rcver = tstamp.TstampReceiver(config=self.config) @@ -1977,6 +1987,10 @@ def run(self, *args): mid, pname) continue + ## Respect verbose flag for print-outs. + infos = rcver._get_recved_email_infos(mail, verdict) + yield _mydump({mid: infos}, default_flow_style=default_flow_style) + try: proj.do_storedice(verdict=verdict) #report = proj.result # Not needed, we already have verdict. @@ -1984,10 +1998,6 @@ def run(self, *args): self.log.error('Failed storing %s email, due to: %s', mid, ex) - ## Respect verbose flag for print-outs. - infos2 = rcver._get_recved_email_infos(mail, verdict) - yield _mydump({mid: infos2}, default_flow_style=default_flow_style) - class ExportCmd(_SubCmd): """ @@ -2154,10 +2164,11 @@ class BackupCmd(_SubCmd): ).tag(config=True) def run(self, *args): - self.log.info('Archiving repo into %r...', args) if len(args) > 1: raise CmdException('Cmd %r takes one optional filepath, received %d: %r!' % (self.name, len(args), args)) + self.log.info('Archiving repo into %r...', args) + archive_fpath = args and args[0] or None kwds = {} if archive_fpath: @@ -2178,6 +2189,8 @@ def run(self, *args): all_subcmds = (LsCmd, InitCmd, OpenCmd, AppendCmd, ReportCmd, - TstampCmd, TrecvCmd, TparseCmd, + TstampCmd, ## TODO: delete deprecated `projext tsend` cmd + TsendCmd, + TrecvCmd, TparseCmd, StatusCmd, ExportCmd, ImportCmd, BackupCmd) diff --git a/co2mpas/sampling/tstamp.py b/co2mpas/sampling/tstamp.py index 478cf2f01..9f6096623 100644 --- a/co2mpas/sampling/tstamp.py +++ b/co2mpas/sampling/tstamp.py @@ -181,8 +181,8 @@ def _is_all_latin(self, proposal): value = proposal.value if any(ord(c) >= 128 for c in value): myname = type(self).__name__ - raise trt.TraitError('%s.%s must not contain non-ASCII chars!' - % (myname, proposal.trait.name)) + raise trt.TraitError('%s.%s must not contain non-ASCII chars: %s' + % (myname, proposal.trait.name, value)) return value @property @@ -478,7 +478,7 @@ class TstampReceiver(TstampSpec): ).tag(config=True) email_criteria = trt.List( - trt.Unicode(), + trt.Unicode(), allow_none=True, default_value=[ 'From "mailer@stamper.itconsult.co.uk"', 'Subject "Proof of Posting Certificate"', @@ -500,7 +500,7 @@ class TstampReceiver(TstampSpec): are both compulsory; - see https://tools.ietf.org/html/rfc3501#page-49 for more. - More criteria are appended on runtime, ie `TstampSpec.subject_prefix`, - `wait_criteria` if --wait, and any args to `recv` command as ORed + `wait_criterio` if --wait, and any args to `recv` command as ORed and searched as subject terms (i.e. the (projects-ids"). - If you want to fetch tstamps sent to `tstamp_recipients`, either leave this empty, or set it to email-address of the sender: @@ -509,7 +509,7 @@ class TstampReceiver(TstampSpec): """ ).tag(config=True) - wait_criteria = trt.Unicode( + wait_criterio = trt.Unicode( 'NEW', allow_none=True, help="""The RFC3501 IMAP search criteria for when IDLE-waiting, usually RECENT+UNSEEN messages.""" ).tag(config=True) @@ -561,6 +561,11 @@ class TstampReceiver(TstampSpec): """ ).tag(config=True) + @trt.validate('subject_prefix', 'wait_criterio', 'before_date', 'after_date') + def _strip_trait(self, p): + v = p.value + return v and v.strip() + def _capture_stamper_msg_and_id(self, ts_msg: Text, ts_heads: Text) -> int: stamper_id = msg = None m = _stamper_id_regex.search(ts_heads) @@ -779,32 +784,44 @@ def list_mailbox(self, directory='""', pattern='*'): return ["Found %i mailboxes:" % len(res)] + res def _prepare_search_criteria(self, is_wait, projects): - criteria = list(self.email_criteria) - if self.subject_prefix: - criteria.append('Subject "%s"' % self.subject_prefix) - if is_wait: - criteria.append(self.wait_criteria) - + subj = self.subject_prefix before, after = [self.before_date, self.after_date] + waitcrt = self.wait_criterio + + if not self.email_criteria: + criteria = [] + else: + criteria = [c and c.strip() for c in self.email_criteria] + criteria = [c for c in criteria if c] + + if subj: + criteria.append('SUBJECT "%s"' % subj) + + if is_wait and waitcrt: + criteria.append(waitcrt) + if before or after: import parsedatetime as pdt c = self.dates_locale and pdt.Constants(self.dates_locale) cal = pdt.Calendar(c) - if before: + if before and before.strip(): criteria.append('SENTBEFORE "%s"' % parse_as_RFC3501_date(cal, before)) - if after: + if after and after.strip(): criteria.append('SINCE "%s"' % parse_as_RFC3501_date(cal, after)) + projects = [c and c.strip() for c in projects] + projects = list(set(c for c in projects if c)) if projects: criteria.append(pairwise_ORed(projects, lambda i: '(SUBJECT "%s")' % i)) criteria = [c.strip() for c in criteria] - criteria = [c if c.startswith('(') else '(%s)' % c for c in criteria] + criteria = [c if c.startswith('(') else '(%s)' % c + for c in criteria] criteria = ' '.join(criteria) return criteria @@ -1105,7 +1122,8 @@ class SendCmd(baseapp.Cmd): SYNTAX %(cmd_chain)s [OPTIONS] [ ...] - - Do not use this command directly (unless experimenting) - prefer the `project tstamp` sub-command. + - Do not use this command directly (unless experimenting) - prefer + the `project tsend` sub-command. - If '-' is given or no files at all, it reads from STDIN. - Many options related to sending & receiving the email are expected to be stored in the config-file. - Use --verbose to print the timestamped email. diff --git a/tests/sampling/test_tstamp.py b/tests/sampling/test_tstamp.py index 855ce43c0..72cec2e29 100644 --- a/tests/sampling/test_tstamp.py +++ b/tests/sampling/test_tstamp.py @@ -615,6 +615,39 @@ def test_parse_timestamps(self, case): rcv.force = True self.check_timestamp(rcv, *verdicts) + @ddt.data( + (None, None, []), + (None, '', []), + (None, ' ', []), + + ([], None, []), + ([], '', []), + ([], ' ', []), + + (['', ' '], None, ['', ' ']), + (['', ' '], '', ['', ' ']), + (['', ' '], ' ', ['', ' ']), + ) + def test_criteria_stripping(self, case): + ecrts, one, projects = case + rcv = tstamp.TstampReceiver(email_criteria=ecrts, + wait_criterio=one, + subject_prefix=one) + crt = rcv._prepare_search_criteria(True, projects) + self.assertEqual(crt, '') + + def test_criteria_dupe_projects(self): + rcv = tstamp.TstampReceiver(email_criteria=[], + subject_prefix='') + crt = rcv._prepare_search_criteria(False, []) # sanity + self.assertEqual(crt, '') + + crt = rcv._prepare_search_criteria(False, ['ab', 'ab']) + self.assertNotIn('OR', crt) + + crt = rcv._prepare_search_criteria(False, ['ab', 'foo', 'ab']) + self.assertEqual(crt.count('OR'), 1) + @ddt.ddt class TstampShell(unittest.TestCase):