コード例 #1
0
ファイル: ui.py プロジェクト: rossbayer/maxify
 def __init__(self, stdin=None, stdout=None, use_color=True):
     cmd.Cmd.__init__(self, stdin=stdin, stdout=stdout)
     self.intro = "Maxify programmer time tracker client"
     self.prompt = "> "
     self.current_project = None
     self.use_color = use_color
     self.projects = Projects()
     self._generate_help_funcs()
コード例 #2
0
ファイル: ui.py プロジェクト: rossbayer/maxify
class MaxifyCmd(cmd.Cmd):
    """Command interpreter used for accepting commands from the user to
    manage a Maxify project.

    """

    _stopwatch_status_colors = {
        StopWatch.STATUS_RUNNING: "green",
        StopWatch.STATUS_PAUSED: "yellow",
        StopWatch.STATUS_RESET: "magenta",
        StopWatch.STATUS_STOPPED: "white",
    }

    def __init__(self, stdin=None, stdout=None, use_color=True):
        cmd.Cmd.__init__(self, stdin=stdin, stdout=stdout)
        self.intro = "Maxify programmer time tracker client"
        self.prompt = "> "
        self.current_project = None
        self.use_color = use_color
        self.projects = Projects()
        self._generate_help_funcs()

    def _generate_help_funcs(self):
        for command in help_texts:
            self._generate_help_func(command)

    def _generate_help_func(self, command):
        func_name = "help_" + command

        def help_func():
            self._title("Help - " + command)
            self._print(help_texts[command])

        setattr(self, func_name, help_func)

    def cmdloop(self, args=None):
        if args and args.project:
            self._set_current_project(args.project)
            if self.current_project:
                self.intro = self.intro + "\n\n" + "Switched to project '{0}'\n".format(args.project)
            else:
                self.intro = self.intro + "\n\nNo project found named '{0}'\n".format(args.project)

        if args and args.command and len(args.command) > 0:
            stdin = StringIO()
            self.stdin = stdin
            self.prompt = ""
            self.use_rawinput = False
            stdin.write(" ".join(args.command) + "\nexit\n")
            stdin.seek(0)

        try:
            cmd.Cmd.cmdloop(self)
        except KeyboardInterrupt:
            self._print("\nExiting\n")
            return

    def _set_current_project(self, project_name):
        self.current_project = self.projects.get(project_name)

    def emptyline(self):
        """Handles an empty line (does nothing)."""
        pass

    ########################################
    # Command - quit/exit
    ########################################

    def do_exit(self, line):
        """Exit the application."""
        return True

    def do_quit(self, line):
        """Exit the application."""
        return True

    ########################################
    # Command - switch
    ########################################

    def do_switch(self, line):
        """Switch to a project with the provided name."""
        self._set_current_project(line.strip())

        if not self.current_project:
            self._error("No project found named '{0}'".format(line))
        else:
            self._success("Switched to project '{0}'".format(self.current_project.name))

    def complete_switch(self, text, line, beginx, endidx):
        start_index = len("switch ")
        if beginx == start_index:
            return self.projects.matching_name(text)

        # If beginning index not immediately after command keyword, then
        # readline found a organization delimiter, so handle it correctly
        # in search for matches, then strip it out of returned results for
        # readline tokenized completion logic.
        organization = line[start_index:beginx]
        partial_name = organization + text
        matches = self.projects.matching_name(partial_name)
        return [m.replace(organization, "") for m in matches]

    ########################################
    # Command - projects
    ########################################

    def do_projects(self, line):
        """Lists all projects current defined in the user's data file.
        """
        projects = self.projects.all()

        if not len(projects):
            self._print("\nNo projects found\n")
            return

        orgs = {project.organization for project in projects}
        by_org = {org: [p for p in projects if p.organization == org] for org in orgs}

        for org in sorted(orgs, key=lambda o: o if o is not None else ""):
            if org:
                self._title(org)
            else:
                self._title("default")

            for project in by_org[org]:
                self._print_project_summary(project)

        self._print()

    def _print_project_summary(self, project):
        project_str = " * {name} - {desc}".format(
            name=project.qualified_name, desc=project.desc if project.desc else "No description provided"
        )
        self._print(project_str)

    ########################################
    # Command - import
    ########################################

    def do_import(self, line):
        """Import projects from a configuration file.

        """
        # First, attempt an import and abort if a conflict happens
        file_path = line.strip()
        try:
            projects = import_config(file_path, ImportStrategy.abort)
            conflict = False
        except ProjectConflictError:
            conflict = True
        except ConfigError as e:
            self._error(str(e))
            return

        if conflict:
            self._warning("Conflicts found between current projects and " "projects defined in '{}'.".format(file_path))

            self._print(
                "\nYou can select one of the following options for "
                "continuing with the import:\n"
                " - (A)bort -  Stops the import and makes no changes.\n"
                " - (M)erge - Merges current projects with those "
                "being imported.\n"
                " - (R)eplace - Replaces current projects with the "
                "ones being imported. Any existing conflicting "
                "projects will be deleted along with their data.\n\n"
            )

            response = input("What would you like to do?: ")

            if response.upper() == "M":
                self._print("Merging projects...\n")
                projects = import_config(file_path, ImportStrategy.merge)
            elif response.upper() == "R":
                self._print("Replacing projects...\n")
                projects = import_config(file_path, ImportStrategy.overwrite)
            else:
                self._print("Import aborted.\n")
                projects = None

        if not projects:
            return

        self._print("\nThe following projects were imported:")
        for project in projects:
            self._print_project_summary(project)

        self._print("\n")

    ########################################
    # Command - metrics
    ########################################

    def do_metrics(self, line):
        """Print out metrics available for the current project."""
        if not self.current_project:
            self._error("Please select a project first using the 'switch' " "command")
            return

        self._title(self.current_project.name + " Metrics:")
        for metric in sorted(self.current_project.metrics, key=lambda m: m.name):
            self._print(" * {0} ({1})".format(metric.name, metric.metric_type.display_name()))

            if metric.desc:
                self._print("   - Description: " + metric.desc)

            if metric.value_range:
                self._print("   - Possible Values: " + ", ".join(map(str, metric.value_range)))

            if metric.default_value:
                self._print("   - Default Value: " + str(metric.default_value))

        self._print()

    ########################################
    # Command - tasks
    ########################################

    def do_tasks(self, line):
        """Print out a list of tasks for the current project and accumulated
         metrics for each task.
        """
        parser = ArgumentParser(stdout=self.stdout, prog="tasks", add_help=False)
        parser.add_argument("--details", action="store_true")
        parser.add_argument("pattern", metavar="PATTERN", nargs="?")

        args = parser.parse_args(line.split())
        if not args:
            self._error("Invalid arguments")
            return

        details = args.details
        pattern = args.pattern if args.pattern else "*"

        self._title("Tasks")

        # align printed values for details by finding longest metric name
        metric_names = [m.name for m in self.current_project.metrics]
        metric_names.append("Created"),
        metric_names.append("Last Updated")
        max_name_len = len(max(metric_names, key=len))
        detail_fmt = "    {0:" + str(max_name_len) + "} | {1}"

        for task in sorted(
            filter(lambda t: fnmatch.fnmatch(t.name, pattern), self.current_project.tasks), key=lambda t: t.name
        ):
            self._info(" * " + task.name, extra_newline=False)
            if details:
                self._print(" " + "-" * 51)
                for metric in self.current_project.metrics:
                    metric_value = task.value(metric)
                    value = metric.metric_type.to_str(metric_value) if metric_value else "----"
                    self._print(detail_fmt.format(metric.name, value))
                self._print()
                self._print(detail_fmt.format("Created", task.created))
                self._print(detail_fmt.format("Last Updated", task.last_updated))
                self._print()

        self._print()

    ########################################
    # Command - task
    ########################################

    def do_task(self, line):
        """Create a task or edit an existing task."""
        line = line.strip()
        if not line:
            self._error("You must specify a task to create or update.\n" "Usage: task [TASK_NAME]")
            return

        tokens = shlex.split(line)
        task_name = tokens[0]
        args = tokens[1:]

        if len(args):
            success = self._update_task(task_name, args)
        else:
            success = self._update_task_interactive(task_name)

        if success:
            self._success("Task updated")

    def _update_task_interactive(self, task_name):
        self._error("Interactive task input is not implemented yet!")
        return False

    def _update_task(self, task_name, args):
        metrics = []
        args_len = len(args)
        for i in range(0, args_len, 2):
            metric_name = args[i]

            # Determine value string
            val_idx = i + 1
            if val_idx >= args_len:
                self._error("Invalid expression. Missing value for: " + metric_name)
                return

            value_str = args[val_idx]

            # Determine metric
            metric = self.current_project.metric(metric_name)
            if not metric:
                self._error("Invalid metric: " + metric_name)
                return

            try:
                value = metric.metric_type.parse(value_str)
            except ParsingError as e:
                self._error(str(e))
                return

            metrics.append((metric, value))

        task = self.current_project.task(task_name)
        for metric, value in metrics:
            try:
                task.record(metric, value)
            except ValueError as e:
                self._error(str(e))
                self.projects.revert()
                return False

        self.projects.save(self.current_project)

        return True

    def _print_task(self, task):
        output = ["Created: " + str(task.created), "Last Updated: " + str(task.last_updated), "\n"]
        for data_point in task.data_points:
            output.append(" {0} -> {1}".format(data_point.metric, data_point.value))

        self._print("\n".join(output) + "\n")

    ########################################
    # Command - stopwatch
    ########################################

    def do_stopwatch(self, line):
        """Creates a new stopwatch for recording time for a particular task.
        """
        parser = ArgumentParser(stdout=self.stdout, prog="stopwatch", add_help=False)
        parser.add_argument("task", metavar="TASK")
        parser.add_argument("metric", metavar="METRIC", nargs="?")

        args = parser.parse_args(line.split())
        if not args.task:
            self._print()
            self._error("Invalid arguments")
            return

        # Get task with specified name and optional metric
        task = self.current_project.task(args.task, create=False)
        if not task:
            self._error("Task {} does not exist.".format(args.task))
            return

        if args.metric:
            metric = self.current_project.metric(args.metric)
            if not metric:
                self._error("Metric {} does not exist.".format(args.metric))
                return
        else:
            metric = None

        # Create a stop watch and UI
        self._print("\n  (R)eset | (S)tart | (P)ause | S(t)op\n")

        self.stdout.write("  Stopped\t--:--:--\r")
        self.stdout.flush()

        stopwatch_active = True
        stopwatch = StopWatch()
        with cbreak():
            while stopwatch_active:
                user_input_int = ord(self.stdin.read(1))
                if 0 <= user_input_int <= 256:
                    user_input = chr(user_input_int).upper()
                    if user_input == "S":
                        stopwatch.start(tick_callback=self._update_printout)
                    elif user_input == "P":
                        stopwatch.pause()
                    elif user_input == "R":
                        stopwatch.reset()
                    elif user_input == "T":
                        stopwatch.stop()
                        stopwatch_active = False

        # At this point, stopwatch has been stopped, so now attempt to assign
        # its total duration to the task.
        if metric:
            self._assign_time(task, metric, stopwatch.total)
        else:
            self._assign_time_interactive(task, stopwatch.total)

        self.projects.save(self.current_project)

    def _assign_time(self, task, metric, total):
        task.record(metric, total)
        self._print('  \n\n  Added {} to "{}"\n'.format(total, metric.name))

    def _assign_time_interactive(self, task, total):
        self._title("\n\nAssign Time")
        self._print("The stop watch recorded {}. Assign that time to the " "metrics in this task:\n\n".format(total))

        remainder = total
        duration_metrics = filter(lambda m: m.metric_type is Duration, self.current_project.metrics)
        for metric in sorted(duration_metrics, key=lambda m: m.name):
            parsed_val = None
            while parsed_val is None:
                value = input("  {} ({} remaining): ".format(metric.name, remainder))
                if value.lower() == "rest":
                    parsed_val = remainder
                else:
                    try:
                        parsed_val = Duration.parse(value)
                    except ParsingError as e:
                        self._error(str(e))
                    except:
                        self._error("Invalid duration")

                if parsed_val > remainder:
                    self._error(
                        "{} is greater than remaining time from " "stopwatch ({})".format(parsed_val, remainder)
                    )
                    parsed_val = None

            task.record(metric, parsed_val)
            remainder -= parsed_val

            if remainder.total_seconds() <= 0:
                break

        self._print()

    def _update_printout(self, total, status):
        """Update printout of current stopwatch value to screen.

        :param total: The total as a :class:`datetime.timedelta`.
        :param status: The current stopwatch status as a string.

        """
        self.stdout.write(" " * 80 + "\r")
        self.stdout.write(colored("  {:7}\t{}\r".format(status, total), self._stopwatch_status_colors[status]))
        self.stdout.flush()

    def complete_stopwatch(self, text, line, beginx, endidx):
        """Provides support for auto-complete of task name in stopwatch command.
        """
        tasks = Tasks(self.current_project)
        start_index = len("stopwatch ")
        if beginx == start_index:
            return [t.name for t in tasks.starts_with(text)]

        beginning = line[start_index:beginx]
        partial_name = beginning + text
        matches = tasks.starts_with(partial_name)
        return [t.name.replace(beginning, "") for t in matches]

    ########################################
    # Utility methods
    ########################################

    def _title(self, line):
        self._print("\n" + line)
        self._print("-" * min(len(line), 80) + "\n")

    def _success(self, msg, extra_newline=True):
        self._print(msg, "green", extra_newline)

    def _info(self, msg, extra_newline=True):
        self._print(msg, "cyan", extra_newline)

    def _warning(self, msg, extra_newline=True, indent=0):
        self._print(" " * indent + "Warning: " + msg, "yellow", extra_newline)

    def _error(self, msg, extra_newline=True, indent=0):
        self._print(" " * indent + "Error: " + msg, "red", extra_newline)

    def _print(self, msg=None, color=None, extra_newline=False):
        if msg and color and self.use_color:
            msg = colored(msg, color)

        if not msg:
            msg = ""

        print(msg, file=self.stdout)
        if extra_newline:
            print(file=self.stdout)
コード例 #3
0
ファイル: config.py プロジェクト: pombredanne/maxify
def import_config(path, import_strategy=ImportStrategy.abort):
    """Load project and metric configuration from the file specified and
    import into user's data file.

    :param path: `str` containing path to the configuration file.
    :param import_strategy: :class:`ImportStrategy` enum value indicating the
        import strategy to use in case a conflict is detected.

    :returns: List of projects that were imported

    :raise `ConfigError` Raised if invalid configuration file is found.

    """
    log.info("Attempting configuration import. Path: {} Stategy: {}",
             path,
             import_strategy)

    if not path:
        raise ConfigError("A path must be provided to import from.")

    if not os.path.exists(path):
        raise ConfigError("File {} does not exist".format(path))

    # Load projects from file
    if path.endswith(".py"):
        projects = _load_python_config(path)
    elif path.endswith(".yaml") or path.endswith(".json"):
        projects = _load_yaml_config(path)
    else:
        raise ConfigError("Unsupported filed format: {}.  Value formats are "
                          "Python file (.py), YAML file (.yaml), or a "
                          "JSON file (.json).")

    # For python config, allow a single return value instead of a collection.
    if isinstance(projects, Project):
        projects = [projects]

    # Check for conflicts
    project_names = [p.qualified_name for p in projects]
    projects_repo = Projects()

    with projects_repo.transaction():
        existing_projects = projects_repo.all_named(*project_names)
        conflicts_found = len(existing_projects)

        if conflicts_found and import_strategy == ImportStrategy.abort:
            existing_project_names = [p.name for p in existing_projects]
            raise ProjectConflictError("The following projects already exist in "
                                       "your data file: " +
                                       ", ".join(existing_project_names))

        if conflicts_found and import_strategy == ImportStrategy.overwrite:
            projects_repo.delete(*existing_projects)

        if conflicts_found and import_strategy == ImportStrategy.merge:
            _do_merge(projects_repo, projects)
        else:
            for project in projects:
                projects_repo.save(project)

    return projects