import argparse from beeprint import pp from collections import defaultdict from datetime import datetime, timedelta from typing import Any, DefaultDict, Dict, List, Sequence, Tuple import base32_crockford import tabulate import api.projects import api.stories from config import Config from util import require_login from . import COLOR_HEADER, _format_state, _get_persons from ._stories_info import _stories_info STATES = 'unstarted', 'planned', 'started', 'finished', 'delivered', 'accepted' Persons = Dict[int, Dict[str, Any]] Totals = DefaultDict[int, Dict[str, int]] def __get_row(item: Tuple[int, Dict[str, int]], persons: Persons) -> Sequence: owner_id, points = item name = persons[owner_id]['name'] estimates = [points[state] for state in STATES] progress = '[' progress += f'\033[90m' + 'P' * round(points['planned']) progress += f'\033[38;5;226m' + 'S' * round(points['started']) progress += f'\033[94m' + 'F' * round(points['finished']) progress += f'\033[38;5;208m' + 'D' * round(points['delivered']) progress += f'\033[92m' + 'A' * round(points['accepted']) progress += '\033[0m]' return (name, *estimates, sum(estimates), progress) def __format_story(story: Dict[str, Any], persons: Persons, totals: Totals) \ -> Tuple[str, str, str, str, str, int]: code = base32_crockford.encode(story['id']) is_owner = False initials = [] owner_ids = story.get('owner_ids', []) for owner_id in owner_ids: value = persons[owner_id]['initials'] if value == Config['user']['initials']: is_owner = True value = f'\033[96m{value}\033[97m' initials.append(value) estimate = story.get('estimate') if estimate: for owner_id in owner_ids: state = story['current_state'] totals[owner_id][state] += estimate / len(owner_ids) if is_owner: code = f'\033[1;97m{code}' estimate = f'\033[97m{estimate}\033[0m' if estimate else None owners = ', '.join(initials) state = _format_state(story['current_state']) return code, story['story_type'], story['name'], owners, state, estimate def __print_stories(stories: List[Dict[str, Any]], persons: Persons, totals: Totals) -> None: table = [__format_story(story, persons, totals) for story in stories] headers = 'Code', 'Type', 'Story name', 'Owners', 'State', 'Pts' print(tabulate.tabulate(table, headers=headers), end='\n\n') def __print_totals(totals: Totals, persons: Persons) -> None: COLOR_HEADER.print('Point totals:', end='\n\n') state_headers = [_format_state(state) for state in STATES] headers = ('Owner', *state_headers, 'Total', 'Progress') data = [__get_row(item, persons) for item in totals.items()] data.sort(key=lambda row: row[-2]) print(tabulate.tabulate(data, headers), end='\n\n') def __print_burndown(token: str, iteration: Dict[str, Any], persons: Persons, hide_accepted: bool = False) -> None: COLOR_HEADER.print('Burndown:', end='\n\n') start = datetime.strptime(iteration['start'], '%Y-%m-%dT%H:%M:%SZ') history = api.projects.get_history_days( token, iteration['project_id'], start - timedelta(days=1)) accepted_points = history['data'][0][1] last_counts: List[int] = [] for date, *counts in history['data'][1:]: if len(counts) > 0: # Update the last_counts variable if an update is available. last_counts = counts elif len(last_counts) > 0: # If no counts are available, use those of the previous day. counts = last_counts else: # If there are no last_counts either, just skip the day continue progress = '' if not hide_accepted: progress += '\033[92m' + 'A' * round(counts[0] - accepted_points) progress += '\033[38;5;208m' + 'D' * round(counts[1]) progress += '\033[94m' + 'F' * round(counts[2]) progress += '\033[38;5;226m' + 'S' * round(counts[3]) progress += '\033[90m' + 'P' * round(counts[5]) progress += '\033[0m' print(f'{date}: {progress}') print() def _stories_current(arguments: argparse.Namespace) -> None: try: project_id = int(Config['project_aliases'][arguments.project]) except KeyError: project_id = base32_crockford.decode(arguments.project) token = Config['user']['api_token'] iterations = api.projects.get_iterations( token, project_id, scope=arguments.scope) if not iterations: print('No current iteration.') return 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) @require_login def stories(arguments: argparse.Namespace) -> None: if arguments.story: _stories_info(arguments) else: _stories_current(arguments)