148 lines
4.7 KiB
Python
148 lines
4.7 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]
|
||
|
|
||
|
for date, *counts in history['data'][1:]:
|
||
|
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)
|