diff --git a/commands/_stories_info.py b/commands/_stories_info.py index cb23321..13961aa 100644 --- a/commands/_stories_info.py +++ b/commands/_stories_info.py @@ -130,13 +130,13 @@ def __print_blockers(token: str, project_id: int, story_id: int) -> None: print(f' [{resolved}] {desc}') -def _stories_info(args: argparse.Namespace) -> None: +def _stories_info(story: str) -> None: try: token = Config['user']['api_token'] except KeyError: sys.exit(1) - story_id = base32_crockford.decode(args.story) + story_id = base32_crockford.decode(story) story = api.stories.get(token, story_id) project_id = story['project_id'] diff --git a/commands/cli.py b/commands/cli.py new file mode 100644 index 0000000..a7929b6 --- /dev/null +++ b/commands/cli.py @@ -0,0 +1,5 @@ +import click + +@click.group() +def cli(): + pass diff --git a/commands/login.py b/commands/login.py index dce20d9..7ac47a5 100644 --- a/commands/login.py +++ b/commands/login.py @@ -1,16 +1,19 @@ -import argparse -import getpass +import click import api.me from config import Config +from .cli import cli -def login(arguments: argparse.Namespace) -> None: - username = input('E-mail: ') - password = getpass.getpass() +@cli.command('login') +@click.option('--email', prompt=True) +@click.option('--password', prompt=True, hide_input=True) +def login(email: str, password: str) -> None: + user = api.me.get(email, password) - me = api.me.get(username, password) + print() + print(f"Logged in successfully as {user['name']} (@{user['username']}).") - Config['user']['api_token'] = me['api_token'] - Config['user']['initials'] = me['initials'] + Config['user']['api_token'] = user['api_token'] + Config['user']['initials'] = user['initials'] Config.write() diff --git a/commands/projects.py b/commands/projects.py index 67f2c70..07b2fff 100644 --- a/commands/projects.py +++ b/commands/projects.py @@ -1,17 +1,25 @@ -import argparse import sys from typing import Dict import base32_crockford +import click import tabulate import api.projects from config import Config from util import require_login +from .cli import cli +@cli.group('projects', invoke_without_command=True) +@click.pass_context @require_login -def list_projects(arguments: argparse.Namespace) -> None: +def projects(context: click.Context) -> None: + if context.invoked_subcommand is not None: + # click calls this function when a subcommand is + # invoked as well. In this case, do nothing. + return + projects = api.projects.get(Config['user']['api_token']) projects.sort(key=lambda project: project['name']) @@ -28,24 +36,36 @@ def list_projects(arguments: argparse.Namespace) -> None: print(tabulate.tabulate(table, headers=('Code', 'Name', 'Alias'))) -def alias(arguments: argparse.Namespace) -> None: - project_id = base32_crockford.decode(arguments.code) - Config['project_aliases'][arguments.alias] = str(project_id) +@projects.group('alias') +def alias() -> None: + pass + + +@alias.command('add') +@click.argument('code') +@click.argument('name') +def alias_add(code: str, name: str) -> None: + project_id = base32_crockford.decode(code) + Config['project_aliases'][name] = str(project_id) Config.write() -def rmalias(arguments: argparse.Namespace) -> None: - del Config['project_aliases'][arguments.alias] +@alias.command('rm') +@click.argument('name') +def alias_rm(name: str) -> None: + del Config['project_aliases'][name] Config.write() -def info(arguments: argparse.Namespace) -> None: +@projects.command('info') +@click.argument('name') +def info(name: str) -> None: try: token = Config['user']['api_token'] - project_id = int(Config['project_aliases'][arguments.alias]) + project_id = int(Config['project_aliases'][name]) except KeyError: - print(f'unknown alias {arguments.alias}') + print(f'unknown alias {name}') sys.exit(1) - projects = api.projects.get_project(token, project_id).items() - print(tabulate.tabulate(projects)) + project_info = api.projects.get_project(token, project_id) + print(tabulate.tabulate(project_info.items())) diff --git a/commands/stories.py b/commands/stories.py index 514cdf3..116ab62 100644 --- a/commands/stories.py +++ b/commands/stories.py @@ -1,7 +1,8 @@ import argparse +import click from collections import defaultdict from datetime import datetime, timedelta -from typing import Any, DefaultDict, Dict, List, Sequence, Tuple +from typing import Any, DefaultDict, Dict, List, Sequence, Tuple, Optional import base32_crockford import tabulate @@ -13,6 +14,7 @@ from util import require_login from . import COLOR_HEADER, _format_state, _get_persons from ._stories_info import _stories_info +from .cli import cli STATES = 'unstarted', 'planned', 'started', 'finished', 'delivered', 'accepted' @@ -126,34 +128,50 @@ def __print_burndown(token: str, iteration: Dict[str, Any], persons: Persons, print() -def _stories_current(arguments: argparse.Namespace) -> None: +def _stories_current(project: str, scope: str, show_accepted: bool) -> None: try: - project_id = int(Config['project_aliases'][arguments.project]) + project_id = int(Config['project_aliases'][project]) except KeyError: - project_id = base32_crockford.decode(arguments.project) + project_id = base32_crockford.decode(project) token = Config['user']['api_token'] iterations = api.projects.get_iterations( - token, project_id, scope=arguments.scope) + token, project_id, scope=scope) if not iterations: print('No current iteration.') return + iteration = iterations[0] persons = _get_persons(token, project_id=project_id) totals: DefaultDict[int, Dict[str, int]] = \ defaultdict(lambda: dict((state, 0) for state in STATES)) - iteration = iterations[0] - __print_stories(iteration['stories'], persons, totals) __print_totals(totals, persons) - __print_burndown(token, iteration, persons, arguments.hide_accepted) + __print_burndown(token, iteration, persons, not show_accepted) +def _set_story_state(story: str, state: str) -> None: + token = Config['user']['api_token'] + story_id = base32_crockford.decode(story) + api.stories.put_story(token, story_id, current_state=state) + + +@cli.command('stories') +@click.argument('project') +@click.argument('story', required=False) +@click.option('--scope', default='current') +@click.option('--show-accepted/--hide-accepted', default=True) +@click.option('--set-state', type=click.Choice([ + 'started', 'finished', 'delivered', 'rejected', 'accepted'])) @require_login -def stories(arguments: argparse.Namespace) -> None: - if arguments.story: - _stories_info(arguments) +def stories(project: str, story: Optional[str], scope: str, + show_accepted: bool, set_state: str) -> None: + if story is not None: + if set_state is not None: + _set_story_state(story, set_state) + else: + _stories_info(story) else: - _stories_current(arguments) + _stories_current(project, scope, show_accepted) diff --git a/pivotalcli.py b/pivotalcli.py index b6ab9dc..e138932 100755 --- a/pivotalcli.py +++ b/pivotalcli.py @@ -1,96 +1,8 @@ #!/usr/bin/env python3 -import argparse -import commands.login as cmd_login -import commands.projects as cmd_projects -import commands.stories as cmd_stories -import api.stories - +from commands.cli import cli from config import Config -from base32_crockford import decode as b32_decode - - -def start_story(args) -> None: - story_set_state(args.story, 'finished') - - -def finish_story(args) -> None: - story_set_state(args.story, 'finished') - - -def deliver_story(args) -> None: - story_set_state(args.story, 'delivered') - - -def accept_story(args) -> None: - story_set_state(args.story, 'accepted') - - -def story_set_state(args, state: str) -> None: - token = Config['user']['api_token'] - story_id = b32_decode(args.story) - api.stories.put_story(token, story_id, current_state=state) - - -def parse_arguments() -> None: - parser = argparse.ArgumentParser() - parser.set_defaults(func=lambda _: parser.print_help()) - commands = parser.add_subparsers(title='commands') - - login_parser = commands.add_parser('login') - login_parser.set_defaults(func=cmd_login.login) - - projects_parser = commands.add_parser('projects') - projects_parser.set_defaults(func=cmd_projects.list_projects) - - projects_commands = projects_parser.add_subparsers(title='commands') - projects_list_parser = projects_commands.add_parser('list') - projects_list_parser.set_defaults(func=cmd_projects.list_projects) - - projects_alias_parser = projects_commands.add_parser('alias') - projects_alias_parser.add_argument('code', type=str) - projects_alias_parser.add_argument('alias', type=str) - projects_alias_parser.set_defaults(func=cmd_projects.alias) - - projects_rmalias_parser = projects_commands.add_parser('rmalias') - projects_rmalias_parser.add_argument('alias', type=str) - projects_rmalias_parser.set_defaults(func=cmd_projects.rmalias) - - projects_info_parser = projects_commands.add_parser('info') - projects_info_parser.add_argument('alias', type=str) - projects_info_parser.set_defaults(func=cmd_projects.info) - - stories_parser = commands.add_parser('stories', description='story stuff') - stories_parser.set_defaults(func=cmd_stories.stories) - stories_parser.add_argument('project', type=str) - stories_parser.add_argument('story', type=str, nargs='?', default=None) - stories_parser.add_argument('--scope', type=str, default='current') - stories_parser.add_argument('--hide-accepted', nargs='?', type=bool, - const=True, default=False) - - story_start_parser = commands.add_parser('start') - story_start_parser.set_defaults( - func=lambda args: story_set_state(args, 'started')) - story_start_parser.add_argument('story', type=str) - - story_finish_parser = commands.add_parser('finish') - story_finish_parser.set_defaults( - func=lambda args: story_set_state(args, 'finished')) - story_finish_parser.add_argument('story', type=str) - - story_deliver_parser = commands.add_parser('deliver') - story_deliver_parser.set_defaults( - func=lambda args: story_set_state(args, 'delivered')) - story_deliver_parser.add_argument('story', type=str) - - story_accept_parser = commands.add_parser('accept') - story_accept_parser.set_defaults( - func=lambda args: story_set_state(args, 'accepted')) - story_accept_parser.add_argument('story', type=str) - - args = parser.parse_args() - args.func(args) if __name__ == '__main__': Config.read() - parse_arguments() + cli() diff --git a/requirements.txt b/requirements.txt index 5447dcc..bb968f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,3 +35,4 @@ urllib3==1.22 urwid==2.0.1 wcwidth==0.1.7 wrapt==1.10.11 +click=6.7