Example #1
0
    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))
Example #2
0
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)')
Example #3
0
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)')
Example #4
0
    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
Example #5
0
    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))
Example #6
0
    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)
Example #7
0
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
Example #8
0
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)'
Example #9
0
    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)
Example #10
0
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)'
Example #11
0
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'
Example #12
0
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")
Example #13
0
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)
Example #14
0
def parse_choice(choice, **kwargs):
    argh.io._input = lambda prompt: choice
    return argh.confirm('test', **kwargs)
Example #15
0
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.')
Example #16
0
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
Example #17
0
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!"
Example #18
0
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")
Example #19
0
def parse_choice(choice, **kwargs):
    argh.io._input = lambda prompt: choice
    return argh.confirm('test', **kwargs)
Example #20
0
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")
Example #21
0
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'
Example #22
0
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")