pivotalcli/commands/stories.py

158 lines
5.1 KiB
Python

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)