def confirm(action, default=None, skip=False): """A shortcut for typical confirmation prompt. :param action: a string describing the action, e.g. "Apply changes". A question mark will be appended. :param default: `bool` or `None`. Determines what happens when user hits :kbd:`Enter` without typing in a choice. If `True`, default choice is "yes". If `False`, it is "no". If `None` the prompt keeps reappearing until user types in a choice (not necessarily acceptable) or until the number of iteration reaches the limit. Default is `None`. :param skip: `bool`; if `True`, no interactive prompt is used and default choice is returned (useful for batch mode). Default is `False`. Usage:: def delete(key, silent=False): item = db.get(Item, args.key) if confirm('Delete '+item.title, default=True, skip=silent): item.delete() print('Item deleted.') else: print('Operation cancelled.') Returns `None` on `KeyboardInterrupt` event. """ MAX_ITERATIONS = 3 if skip: return default else: defaults = {None: ("y", "n"), True: ("Y", "n"), False: ("y", "N")} y, n = defaults[default] prompt = text_type("{action}? ({y}/{n})").format(**locals()) choice = None try: if default is None: cnt = 1 while not choice and cnt < MAX_ITERATIONS: choice = safe_input(prompt) cnt += 1 else: choice = safe_input(prompt) except KeyboardInterrupt: return None if choice in ("yes", "y", "Y"): return True if choice in ("no", "n", "N"): return False if default is not None: return default return None
def confirm(action, default=None, skip=False): """A shortcut for typical confirmation prompt. :param action: a string describing the action, e.g. "Apply changes". A question mark will be appended. :param default: `bool` or `None`. Determines what happens when user hits :kbd:`Enter` without typing in a choice. If `True`, default choice is "yes". If `False`, it is "no". If `None` the prompt keeps reappearing until user types in a choice (not necessarily acceptable) or until the number of iteration reaches the limit. Default is `None`. :param skip: `bool`; if `True`, no interactive prompt is used and default choice is returned (useful for batch mode). Default is `False`. Usage:: def delete(key, silent=False): item = db.get(Item, args.key) if confirm('Delete '+item.title, default=True, skip=silent): item.delete() print('Item deleted.') else: print('Operation cancelled.') Returns `None` on `KeyboardInterrupt` event. """ MAX_ITERATIONS = 3 if skip: return default else: defaults = { None: ('y', 'n'), True: ('Y', 'n'), False: ('y', 'N'), } y, n = defaults[default] prompt = text_type('{action}? ({y}/{n})').format(**locals()) choice = None try: if default is None: cnt = 1 while not choice and cnt < MAX_ITERATIONS: choice = safe_input(prompt) cnt += 1 else: choice = safe_input(prompt) except KeyboardInterrupt: return None if choice in ('yes', 'y', 'Y'): return True if choice in ('no', 'n', 'N'): return False if default is not None: return default return None
def log_activity(args): "Logs a past activity (since last logged until now)" # TODO: split all changes into steps and apply each using a separate, # well-tested function since = args.since until = args.until duration = args.duration if not args.activity and not args.amend: raise CommandError(failure('activity must be specified ' 'unless --amend is set')) if args.between: assert not (since or until or duration), ( '--since, --until and --duration must not be used with --between') since, until = args.between.split('-') if args.date: rel_date = datetime.datetime.strptime(args.date, '%Y-%m-%d') since = utils.parse_time_to_datetime(since, relative_to=rel_date, ensure_past_time=False) until = utils.parse_time_to_datetime(until, relative_to=rel_date, ensure_past_time=False) else: since = utils.parse_time_to_datetime(since) until = utils.parse_time_to_datetime(until) delta = utils.parse_delta(duration) tags = [TIMETRA_TAG_LOG] if args.tags: tags = list(set(tags + args.tags.split(','))) if args.with_person: tags.extend(['with-{0}'.format(x) for x in args.with_person.split(',')]) if args.pick and not args.amend: raise CommandError(failure('--pick only makes sense with --amend')) prev = _get_last_fact(args.pick) if args.amend: if not prev: raise CommandError('Cannot amend: no fact found') # FIXME this disables --duration since = since or prev.start_time until = until or prev.end_time start, end = storage.get_start_end(since, until, delta) if end < start: raise CommandError('--since must be earlier than --until') if datetime.datetime.now() < end: raise CommandError('--until must not be in the future') # check if we aren't going to overwrite any previous facts try: for line in check_overlap(start, end, activity=(args.pick or args.activity), amend_fact=prev if args.amend else None): yield line except OverlapError as e: raise CommandError(failure(e)) if args.activity: activity, category = parse_activity(args.activity) else: activity = category = None description = None if args.description: description = args.description elif args.no_input: pass elif args.amend and prev.description: # updating a fact that already has a description pass else: # collect multi-line description from interactive user input lines = [] num = 0 try: while True: line = safe_input(' > ' if num else warning('Describe> ')) if line: lines.append(line) else: yield '' break num += 1 except (KeyboardInterrupt, EOFError): raise CommandError(failure('Operation cancelled.')) else: description = '\n'.join(lines) if lines else None if args.amend: #template = u'Updated {fact.activity}@{fact.category} ({delta_minutes} min)' assert prev fact = prev kwargs = dict( start_time=start, end_time=end, dry_run=args.dry_run, ) if activity: kwargs.update(activity=activity, category=category) if description is not None: kwargs.update(description=description) if args.tags is not None or args.with_person is not None: kwargs.update(tags=tags) changed = [] for key, value in kwargs.iteritems(): if hasattr(fact, key) and getattr(fact, key) != kwargs[key]: changed.append(key) old_value = getattr(fact, key) if hasattr(old_value, '__iter__'): # convert DBus strings to proper pythonic ones (for tags) old_value = [str(x) for x in old_value] note = u'' if isinstance(old_value, datetime.datetime) and value: if old_value < value: note = u'(+{0})'.format(value - old_value) else: note = u'(-{0})'.format(old_value - value) yield u'* {0}: {1} → {2} {3}'.format( key, failure(unicode(old_value)), success(unicode(value)), note) if not changed: yield failure(u'Nothing changed.') return storage.update_fact(fact, **kwargs) else: #template = u'Logged {fact.activity}@{fact.category} ({delta_minutes} min)' try: fact = storage.add_fact( args.activity, # NOTE: not using parsed activity + category because hamster wants the foo@bar thing start_time=start, end_time=end, description=description, tags=tags, dry_run=args.dry_run) except (storage.ActivityMatchingError, storage.CannotCreateFact) as e: raise CommandError(failure(e)) # report #delta = fact.end_time - start # почему-то сам факт "не знает" времени начала #delta_minutes = delta.seconds / 60 #yield success(template.format(fact=fact, delta_minutes=delta_minutes)) for output in show_last_fact(args.activity or args.pick): yield output if args.dry_run: yield warning(u'(Dry run, nothing changed.)')