def insert(self, date, when, what, tags=None, yes_to_all=False, *note): """ Inserts a fact starting on given date. Different from `add` in that it seeks a gap in existing facts instead of relating to the tail of the timeline. The timespec is also interpreted in a specific manner: * instead of the previous fact's end, given `date` is used at 00:00; * instead of `now`, the date next to given one is used at 00:00. After the `since` and `until` are obtained from the parser, the storage is checked for overlapping facts. If they exist, the user is asked for confirmation. """ date_parsed = utils.parse_date(date) date_time = datetime.datetime.combine(date_parsed, datetime.time()) now = date_time + datetime.timedelta(days=1) last = date_time since, until = utils.parse_date_time_bounds(when, last, now=now) fact = { 'activity': what, 'since': since, 'until': until, 'description': ' '.join(note) if note else None, 'tags': tags.split(',') if tags else [], } delta_sec = (until - since).total_seconds() ## sanity checks: # 1. ask for confirmation if the fact duration is over a threshold # (XXX code copied and pasted from add(), refactoring needed) if not yes_to_all and FISHY_FACT_DURATION_THRESHOLD <= delta_sec: msg = 'Did you really {} for {:.1f}h'.format(what, delta_sec / 60 / 60.) if not argh.confirm(t.yellow(msg)): return t.red('CANCELLED') # 2. search for overlapping facts (incl. prev or next day if the newly # created fact doesn't fit one day) overlaps = list(self.storage.find_overlapping_facts(since, until)) if overlaps: _item_tmpl = '{f.since} +{f.duration} {f.activity}' _items = '\n* '.join(_item_tmpl.format(f=f) for f in overlaps) msg = ('This fact would overlap {} existing records:\n* {}\n' 'Are you sure to add it'.format(len(overlaps), _items)) if not argh.confirm(t.yellow(msg)): return t.red('CANCELLED') file_path = self.storage.add(fact) return ('Added {} +{:.0f}m to {}'.format(since.strftime('%Y-%m-%d %H:%M'), delta_sec / 60, file_path))
def test_encoding(): "Unicode is accepted as prompt message" raw_input_mock = mock.MagicMock() argh.io._input = raw_input_mock msg = 'привет' if sys.version_info <= (3,0): msg = msg.decode('utf-8') argh.confirm(msg) # bytes in Python 2.x, Unicode in Python 3.x raw_input_mock.assert_called_once_with('привет? (y/n)')
def test_encoding(): "Unicode is accepted as prompt message" raw_input_mock = mock.MagicMock() argh.io._input = raw_input_mock msg = 'привет' if sys.version_info <= (3, 0): msg = msg.decode('utf-8') argh.confirm(msg) # bytes in Python 2.x, Unicode in Python 3.x raw_input_mock.assert_called_once_with('привет? (y/n)')
def _validate_activity_interactively(self, pattern): known_activities = self._collect_activities() if pattern in known_activities: return pattern candidates = [x for x in known_activities if pattern in x] if len(candidates) == 1: candidate = candidates[0] if argh.confirm('Did you mean {}'.format(t.yellow(candidate))): return candidate elif candidates: print('You probably mean one of these:\n * {}'.format('\n * '.join(candidates))) if argh.confirm('Add {} as a new kind of activity' .format(t.red(pattern))): return pattern return None
def add(self, when, what, tags=None, yes_to_all=False, *note): """ Adds a fact somewhere near the end of the timeline. """ what = self._validate_activity_interactively(what) if not what: return t.red('CANCELLED') prev = self['storage'].get_latest() last = prev.until since, until = utils.parse_date_time_bounds(when, last) fact = { 'activity': what, 'since': since, 'until': until, 'description': ' '.join(note) if note else None, 'tags': tags.split(',') if tags else [], } # sanity check delta_sec = (until - since).total_seconds() if not yes_to_all and FISHY_FACT_DURATION_THRESHOLD <= delta_sec: msg = 'Did you really {} for {:.1f}h'.format(what, delta_sec / 60 / 60.) if not argh.confirm(t.yellow(msg)): return t.red('CANCELLED') file_path = self.storage.add(fact) return ('Added {} +{:.0f}m to {}'.format(since.strftime('%Y-%m-%d %H:%M'), delta_sec / 60, file_path))
def import_gramps_xml(self, path=None, db_name=MONGO_DB_NAME, replace=False): if db_name in self.mongo_client.database_names(): if replace or argh.confirm('DROP and replace existing DB "{}"' .format(db_name)): self.mongo_client.drop_database(db_name) else: yield 'Not replacing the existing database.' return else: yield 'Importing into a new DB "{}"'.format(db_name) db = self.mongo_client[db_name] return import_from_xml(path or self.gramps_xml_path, db)
def batch_update(args): """Update a batch of stacks sequentially.""" args.template = None stacks = find_stacks(args.stack_name) yield format_stacks(stacks) confirm_action(arg, default=False) for stack in stacks: args.stack_name = stack.stack_name try: update(args) except CommandError as error: print error if not confirm('Continue anyway?', default=True): raise
def test_prompt(): "Prompt is properly formatted" prompts = [] def raw_input_mock(prompt): prompts.append(prompt) argh.io._input = raw_input_mock argh.confirm('do smth') assert prompts[-1] == 'do smth? (y/n)' argh.confirm('do smth', default=None) assert prompts[-1] == 'do smth? (y/n)' argh.confirm('do smth', default=True) assert prompts[-1] == 'do smth? (Y/n)' argh.confirm('do smth', default=False) assert prompts[-1] == 'do smth? (y/N)'
def import_gramps_xml(self, path=None, db_name=MONGO_DB_NAME, replace=False): if db_name in self.mongo_client.database_names(): if replace or argh.confirm( 'DROP and replace existing DB "{}"'.format(db_name)): self.mongo_client.drop_database(db_name) else: yield 'Not replacing the existing database.' return else: yield 'Importing into a new DB "{}"'.format(db_name) db = self.mongo_client[db_name] return import_from_xml(path or self.gramps_xml_path, db)
def connect(args): """SSH to multiple EC2 instances by name, instance-id or private ip.""" if args.completion_list: try: yield " ".join(read_completion_list()) except IOError: pass elif args.completion_script: yield BASH_COMPLETION_INSTALL_SCRIPT elif args.list: instances = ec2.get_instances() names = sorted([ec2.get_name(i) for i in instances]) yield '\n'.join(names) elif args.instance is None: raise CommandError("No instances specified.") else: if args.confirm and args.yes: raise CommandError("Option confirm and yes are not compatible") try: instances = ec2.get_instances() write_completion_list(instances) specifiers = args.instance.lower().strip().split(',') instances = ec2.filter_instances(specifiers, instances) if len(instances) == 0: raise CommandError("No instances found.") except KeyboardInterrupt: raise CommandError("Killed while accessing AWS api.") if args.one: instances = instances[0:1] if len(instances) > 1 or args.confirm: args.verbose = True if len(instances) > 1 and not args.yes: args.confirm = True if args.verbose and args.command: yield '----- Command: %s' % ' '.join(args.command) if args.verbose: names = sorted([ec2.get_name(i) for i in instances]) yield '----- Instances(%s): %s' % (len(names), ",".join(names)) if args.confirm: if not argh.confirm('Connect to all instances (y) or just one (n)', default=True): instances = [instances[0]] if len(instances) == 1: host = instances[0].public_dns_name try: os.execvp('ssh', ['ec2ssh', host] + args.command) except OSError as error: raise Exception("Failed to call the ssh command: %s" % error) else: for instance in instances: if args.verbose: yield "----- %s: %s %s" % ( instance.id, instance.public_dns_name, instance.private_ip_address, ) host = instance.public_dns_name subprocess.call(['ssh', host] + args.command) if args.verbose: yield '----- DONE'
def confirm_action(arg, action="action", default=False): if hasattr(arg, 'force') and arg.force: return if not confirm('Confirm %s? ' % action, default=default): raise CommandError("Aborted")
def punch_in(args): """Starts tracking given activity in Hamster. Stops tracking on C-c. :param continued: The start time is taken from the last logged fact's end time. If that fact is not marked as finished, it is ended now. If it describes the same activity and is not finished, it is continued; if it is already finished, user is prompted for action. :param interactive: In this mode the application prompts for user input, adds it to the fact description (with timestamp) and displays the prompt again. The first empty comment stops current activitp and terminates the app. Useful for logging work obstacles, decisions, ideas, etc. """ # TODO: # * smart "-c": # * "--upto DURATION" modifier (avoids overlapping) assert hamster_storage activity, category = _parse_activity(args.activity) h_act = u'{activity}@{category}'.format(**locals()) start = None fact = None if args.continued: prev = get_latest_fact() if prev: if prev.activity == activity and prev.category == category: do_cont = True #comment = None if prev.end_time: delta = datetime.datetime.now() - prev.end_time question = (u'Merge with previous entry filling {0} of ' 'inactivity'.format(_format_delta(delta))) if not confirm(question, default=True): do_cont = False #comment = question if do_cont: fact = prev update_fact(fact, end_time=None)#, extra_description=comment) # if the last activity has not ended yet, it's ok: the `start` # variable will be `None` start = prev.end_time if start: yield u'Logging activity as started at {0}'.format(start) if not fact: fact = Fact(h_act, tags=[HAMSTER_TAG], start_time=start) hamster_storage.add_fact(fact) yield u'Started {0}'.format(h_act) if not args.interactive: return yield u'Type a comment and hit Enter. Empty comment ends activity.' try: while True: comment = raw_input(u'-> ').strip() if not comment: break fact = get_current_fact() assert fact, 'all logged activities are already closed' update_fact(fact, extra_description=comment) except KeyboardInterrupt: pass fact = get_current_fact() hamster_storage.stop_tracking() yield u'Stopped (total {0.delta}).'.format(fact)
def parse_choice(choice, **kwargs): argh.io._input = lambda prompt: choice return argh.confirm('test', **kwargs)
def check_overlap(start, end, activity='NEW ACTIVITY', amend_fact=None): """ Interactive check for overlapping facts. To be used from other commands as generator. """ def overlaps(fact, start_time, end_time): if not fact.end_time: # previous activity is still open return True if start_time >= fact.end_time or end_time <= fact.start_time: return False return True # check if we aren't going to overwrite any previous facts # FIXME not today but start.date() .. end.date() todays_facts = storage.get_facts_for_day( date = (start - datetime.timedelta(days=1)).date(), end_date = (end + datetime.timedelta(days=1)).date()) overlap = [f for f in todays_facts if overlaps(f, start, end)] if amend_fact: # do not count last fact as overlapping if we are about to change it. # using unicode(fact) because Hamster's Fact objects cannot be compared # directly for some reason. overlap = [f for f in overlap if not unicode(f) == unicode(amend_fact)] if not overlap: return if 1 < len(overlap): raise TooManyOverlappingFacts('FAIL: too many overlapping facts') prev_fact = overlap[-1] if start <= prev_fact.start_time and prev_fact.end_time <= end: # new fact devours an older one; this cannot be handled "properly" # FIXME: should count deltas <1min as equality raise FactOverlapsReplacement('FAIL: new fact would replace an older one') # FIXME: probably time should be rounded to seconds or even minutes # for safer comparisons (i.e. 15:30:15 == 15:30:20) #--- begin vision (pure visualization; backend will make decisions # on its own, hopefully in the same vein) outcome = [] old = prev_fact.activity new = activity if prev_fact.start_time < start: outcome.append((warning(old), start - prev_fact.start_time)) outcome.append((success(new), end - start)) if end < prev_fact.end_time: outcome.append((warning(old), prev_fact.end_time - end)) vision = ' '.join(u'[{0} +{1}]'.format(x[0], utils.format_delta(x[1])) for x in outcome) yield u'Before: [{0} +{1}]'.format(failure(prev_fact.activity), utils.format_delta(prev_fact.delta)) yield u' After: {0}'.format(vision) # #--- end vision if not confirm(u'OK', default=False): raise CommandError('Operation cancelled.')
def punch_in(storage, activity, continued=False, interactive=False): """Starts tracking given activity. Stops tracking on C-c. :param continued: The start time is taken from the last logged fact's end time. If that fact is not marked as finished, it is ended now. If it describes the same activity and is not finished, it is continued; if it is already finished, user is prompted for action. :param interactive: In this mode the application prompts for user input, adds it to the fact description (with timestamp) and displays the prompt again. The first empty comment stops current activity and terminates the app. Useful for logging work obstacles, decisions, ideas, etc. """ # TODO: # * smart "-c": # * "--upto DURATION" modifier (avoids overlapping) activity, category = parse_activity(activity) start = None fact = None if continued: prev = storage.get_latest() if prev: if prev.activity == activity and prev.category == category: do_cont = True #comment = None if prev.end_time: delta = datetime.datetime.now() - prev.end_time question = (u'Merge with previous entry filling {0} of ' 'inactivity'.format(utils.format_delta(delta))) if not confirm(question, default=True): do_cont = False #comment = question if do_cont: fact = prev storage.update(fact, {'until': None}) # if the last activity has not ended yet, it's ok: the `start` # variable will be `None` start = prev.end_time if start: yield u'Logging activity as started at {0}'.format(start) if not fact: fact = models.Fact(activity=activity, category=category, tags=[TIMETRA_TAG], since=start) storage.add(fact) for line in show_last_fact(): yield line if not interactive: return yield u'Type a comment and hit Enter. Empty comment ends activity.' try: while True: comment = raw_input(u'-> ').strip() if not comment: break # FIXME this is currently broken fact = storage.get_current_fact() assert fact, 'all logged activities are already closed' storage.update(fact, extra_description=comment) except KeyboardInterrupt: pass # FIXME this is currently broken storage.stop_tracking() for line in show_last_fact(): yield line
def migrate_cfg(args): """Migrate the stack: re-instantiate all instances.""" config, settings, sinfo = initialize_from_cli(args) stack = find_one_stack(args.stack_name, summary=False) print(format_stack_summary(stack)) asg = find_one_resource(stack, RES_TYPE_ASG) yield format_autoscale(asg) orig_min = asg.min_size orig_max = asg.max_size orig_desired = asg.desired_capacity orig_term_pol = asg.termination_policies mig_min = orig_desired * 2 mig_max = orig_desired * 2 mig_desired = orig_desired * 2 mig_term_pol = [u'OldestLaunchConfiguration', u'OldestInstance'] if orig_desired != len(asg.instances): raise CommandError("The ASG is not stable (desired != instances)") for instance in asg.instances: if instance.health_status != 'Healthy': raise CommandError("The ASG is not stable (instance not healthy)") warn_for_live(sinfo) confirm_action(arg, default=True) yield "\n <> Setting termination policy to %s" % mig_term_pol asg.termination_policies = mig_term_pol asg.update() yield "\n <> Growing the desired capacity from %s to %s" % ( orig_desired, mig_desired) asg.min_size = mig_min asg.max_size = mig_max asg.desired_capacity = mig_desired asg.update() yield "\n <> Waiting instances to stabilize..." while True: sleep(30) asg = find_one_resource(stack, RES_TYPE_ASG) res_elb_id = find_one_resource(stack, RES_TYPE_ELB, only_id=True) elbinstances = boto.connect_elb().describe_instance_health(res_elb_id) if len(asg.instances) < mig_desired: yield " NOTYET: only %i instances created" % len(asg.instances) continue elif [i for i in asg.instances if i.health_status != 'Healthy']: yield " NOTYET: still unhealthy instances" continue elif len(elbinstances) < mig_desired: yield " NOTYET: only %i instances in ELB" % len(elbinstances) continue elif [i for i in elbinstances if i.state != 'InService']: yield " NOTYET: not all instances are ELB:InService" continue else: yield " OK: %s healthy instances in ELB" % len(asg.instances) break yield "\n <> Checking new ASG state..." asg = find_one_resource(stack, RES_TYPE_ASG) yield format_autoscale(asg) yield format_autoscale_instances(stack) yield "\n <> Restoring previous ASG control:" asg.termination_policies = orig_term_pol asg.min_size = orig_min asg.max_size = orig_max asg.desired_capacity = orig_desired yield format_autoscale(asg) if confirm('Restoring ASG config?', default=True): try: asg.update() except BotoServerError as error: yield "\n <> Restoration failed!" yield error else: yield "\n <> ASG control restored." else: yield "WARNING: The ASG desired capacity was doubled!"
def warn_for_live(sinfo): if sinfo['live'] and sinfo['Environment'] == 'production': if not confirm("WARNING: Updating a live stack! Are you sure? "): raise CommandError("Aborted")