Style improvements and small bugfixes
This commit is contained in:
parent
7c98ed05d2
commit
2311ae4919
6 changed files with 265 additions and 182 deletions
|
@ -8,59 +8,70 @@ from . import _BASE_URL, _headers, _with_token
|
|||
|
||||
@_with_token
|
||||
def get(token: str) -> List[Dict[str, Any]]:
|
||||
r = requests.get(f'{_BASE_URL}/projects', headers=_headers(token))
|
||||
r = requests.get(f"{_BASE_URL}/projects", headers=_headers(token))
|
||||
return r.json()
|
||||
|
||||
|
||||
@_with_token
|
||||
def get_project(project_id: int, token: str) -> Dict[str, Any]:
|
||||
r = requests.get(f'{_BASE_URL}/projects/{project_id}',
|
||||
headers=_headers(token))
|
||||
r = requests.get(f"{_BASE_URL}/projects/{project_id}", headers=_headers(token))
|
||||
return r.json()
|
||||
|
||||
|
||||
@_with_token
|
||||
def get_stories(project_id: int, token: str) -> List[Dict[str, Any]]:
|
||||
r = requests.get(f'{_BASE_URL}/projects/{project_id}/stories',
|
||||
headers=_headers(token))
|
||||
r = requests.get(
|
||||
f"{_BASE_URL}/projects/{project_id}/stories", headers=_headers(token)
|
||||
)
|
||||
return r.json()
|
||||
|
||||
|
||||
@_with_token
|
||||
def get_memberships(project_id: int, token: str) -> List[Dict[str, Any]]:
|
||||
r = requests.get(f'{_BASE_URL}/projects/{project_id}/memberships',
|
||||
headers=_headers(token))
|
||||
r = requests.get(
|
||||
f"{_BASE_URL}/projects/{project_id}/memberships", headers=_headers(token)
|
||||
)
|
||||
return r.json()
|
||||
|
||||
|
||||
@_with_token
|
||||
def get_iterations(project_id: int, scope: str, token: str) \
|
||||
-> List[Dict[str, Any]]:
|
||||
r = requests.get(f'{_BASE_URL}/projects/{project_id}/iterations',
|
||||
headers=_headers(token), params={'scope': scope})
|
||||
def get_iterations(project_id: int, scope: str, token: str) -> List[Dict[str, Any]]:
|
||||
r = requests.get(
|
||||
f"{_BASE_URL}/projects/{project_id}/iterations",
|
||||
headers=_headers(token),
|
||||
params={"scope": scope},
|
||||
)
|
||||
return r.json()
|
||||
|
||||
|
||||
@_with_token
|
||||
def get_story_transitions(str, project_id: int, token: str,
|
||||
after: Optional[datetime] = None,
|
||||
before: Optional[datetime] = None) \
|
||||
-> List[Dict[str, Any]]:
|
||||
def get_story_transitions(
|
||||
str,
|
||||
project_id: int,
|
||||
token: str,
|
||||
after: Optional[datetime] = None,
|
||||
before: Optional[datetime] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
parameters = {}
|
||||
if after:
|
||||
parameters['occurred_after'] = after.isoformat()
|
||||
parameters["occurred_after"] = after.isoformat()
|
||||
if before:
|
||||
parameters['occurred_before'] = before.isoformat()
|
||||
parameters["occurred_before"] = before.isoformat()
|
||||
|
||||
r = requests.get(f'{_BASE_URL}/projects/{project_id}/story_transitions',
|
||||
headers=_headers(token), params=parameters)
|
||||
r = requests.get(
|
||||
f"{_BASE_URL}/projects/{project_id}/story_transitions",
|
||||
headers=_headers(token),
|
||||
params=parameters,
|
||||
)
|
||||
return r.json()
|
||||
|
||||
|
||||
@_with_token
|
||||
def get_history_days(project_id: int, start: datetime, token: str) \
|
||||
-> Dict[str, Any]:
|
||||
r = requests.get(f'{_BASE_URL}/projects/{project_id}/history/days',
|
||||
headers=_headers(token),
|
||||
params={'start_date': start.isoformat()})
|
||||
def get_history_days(project_id: int, start: datetime, token: str) -> Dict[str, Any]:
|
||||
r = requests.get(
|
||||
f"{_BASE_URL}/projects/{project_id}/history/days",
|
||||
headers=_headers(token),
|
||||
params={"start_date": start.isoformat()},
|
||||
)
|
||||
|
||||
return r.json()
|
||||
|
|
|
@ -6,37 +6,57 @@ from . import _BASE_URL, _headers, _with_token
|
|||
|
||||
@_with_token
|
||||
def get(token: str, story_id: int = None) -> Dict[str, Any]:
|
||||
r = requests.get(f'{_BASE_URL}/stories/{story_id}',
|
||||
headers=_headers(token))
|
||||
r = requests.get(f"{_BASE_URL}/stories/{story_id}", headers=_headers(token))
|
||||
return r.json()
|
||||
|
||||
|
||||
@_with_token
|
||||
def put_story(story_id: int, token: str, **kwargs: Any) -> Dict[str, Any]:
|
||||
r = requests.put(f'{_BASE_URL}/stories/{story_id}',
|
||||
headers=_headers(token), json=kwargs)
|
||||
r = requests.put(
|
||||
f"{_BASE_URL}/stories/{story_id}", headers=_headers(token), json=kwargs
|
||||
)
|
||||
return r.json()
|
||||
|
||||
|
||||
@_with_token
|
||||
def get_tasks(project_id: int, story_id: int, token: str) \
|
||||
-> List[Dict[str, Any]]:
|
||||
url = f'{_BASE_URL}/projects/{project_id}/stories/{story_id}/tasks'
|
||||
def get_tasks(project_id: int, story_id: int, token: str) -> List[Dict[str, Any]]:
|
||||
url = f"{_BASE_URL}/projects/{project_id}/stories/{story_id}/tasks"
|
||||
r = requests.get(url, headers=_headers(token))
|
||||
return r.json()
|
||||
|
||||
|
||||
@_with_token
|
||||
def get_comments(project_id: int, story_id: int, token: str) \
|
||||
-> List[Dict[str, Any]]:
|
||||
url = f'{_BASE_URL}/projects/{project_id}/stories/{story_id}/comments'
|
||||
def get_comments(project_id: int, story_id: int, token: str) -> List[Dict[str, Any]]:
|
||||
url = f"{_BASE_URL}/projects/{project_id}/stories/{story_id}/comments"
|
||||
r = requests.get(url, headers=_headers(token))
|
||||
return r.json()
|
||||
|
||||
|
||||
@_with_token
|
||||
def get_blockers(project_id: int, story_id: int, token: str) \
|
||||
-> List[Dict[str, Any]]:
|
||||
url = f'{_BASE_URL}/projects/{project_id}/stories/{story_id}/blockers'
|
||||
def get_blockers(project_id: int, story_id: int, token: str) -> List[Dict[str, Any]]:
|
||||
url = f"{_BASE_URL}/projects/{project_id}/stories/{story_id}/blockers"
|
||||
r = requests.get(url, headers=_headers(token))
|
||||
|
||||
|
||||
@_with_token
|
||||
def post(project_id: int, token: str, **kwargs: Any) -> Dict[str, Any]:
|
||||
r = requests.post(
|
||||
f"{_BASE_URL}/projects/{project_id}/stories",
|
||||
headers={"X-TrackerToken": token},
|
||||
json=kwargs,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
@_with_token
|
||||
def post_blocker(
|
||||
token: str, project_id: int, story_id: int, token: str, **kwargs
|
||||
) -> Dict[str, Any]:
|
||||
r = requests.post(
|
||||
f"{_BASE_URL}/projects/{project_id}/stories/{story_id}/blockers",
|
||||
headers={"X-TrackerToken": token},
|
||||
json=kwargs,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
|
|
@ -2,15 +2,14 @@ import re
|
|||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict
|
||||
from consolemd import Renderer
|
||||
|
||||
import base32_crockford as base32
|
||||
|
||||
import api.stories
|
||||
from config import Config
|
||||
from util import print_wrap
|
||||
|
||||
from . import (COLOR_HEADER, COLOR_TITLE, COLOR_WHITE, _format_state,
|
||||
_get_persons)
|
||||
from . import COLOR_HEADER, COLOR_TITLE, COLOR_WHITE, _format_state, _get_persons
|
||||
|
||||
|
||||
def __print_story(story: Dict[str, Any]) -> None:
|
||||
|
@ -20,62 +19,63 @@ def __print_story(story: Dict[str, Any]) -> None:
|
|||
|
||||
TODO: Split up in functions.
|
||||
"""
|
||||
COLOR_TITLE.print(story['name'], end='\n\n')
|
||||
COLOR_TITLE.print(story["name"])
|
||||
print(story["url"], end="\n\n")
|
||||
|
||||
if 'current_state' in story:
|
||||
state = _format_state(story['current_state'])
|
||||
COLOR_HEADER.print('State:', state, end='')
|
||||
if "current_state" in story:
|
||||
state = _format_state(story["current_state"])
|
||||
COLOR_HEADER.print("State:", state, end="")
|
||||
|
||||
if story['current_state'] == 'accepted':
|
||||
print(f" (at {story['accepted_at']})", end='')
|
||||
if story["current_state"] == "accepted":
|
||||
print(f" (at {story['accepted_at']})", end="")
|
||||
|
||||
print(end='\n\n')
|
||||
print(end="\n\n")
|
||||
|
||||
if 'estimate' in story:
|
||||
COLOR_HEADER.print('Estimate: ', end='')
|
||||
print(story['estimate'], 'points', end='')
|
||||
if len(story.get('owner_ids', [])) > 1:
|
||||
points = story['estimate'] / len(story['owner_ids'])
|
||||
print(f' ({points} each)', end='')
|
||||
print(end='\n\n')
|
||||
if "estimate" in story:
|
||||
COLOR_HEADER.print("Estimate: ", end="")
|
||||
print(story["estimate"], "points", end="")
|
||||
if len(story.get("owner_ids", [])) > 1:
|
||||
points = story["estimate"] / len(story["owner_ids"])
|
||||
print(f" ({points} each)", end="")
|
||||
print(end="\n\n")
|
||||
|
||||
|
||||
def __print_owners(story: Dict[str, Any], persons: Dict[int, Any]) -> None:
|
||||
"""Prints the owners of the story, if available."""
|
||||
if story.get('owner_ids'):
|
||||
COLOR_HEADER.print('Owners:')
|
||||
if story.get("owner_ids"):
|
||||
COLOR_HEADER.print("Owners:")
|
||||
|
||||
owners = []
|
||||
for owner_id in story['owner_ids']:
|
||||
name = persons[owner_id]['name']
|
||||
initials = persons[owner_id]['initials']
|
||||
owners.append(f' - {name} ({initials})')
|
||||
for owner_id in story["owner_ids"]:
|
||||
name = persons[owner_id]["name"]
|
||||
initials = persons[owner_id]["initials"]
|
||||
owners.append(f" - {name} ({initials})")
|
||||
|
||||
print('\n'.join(owners), end='\n\n')
|
||||
print("\n".join(owners), end="\n\n")
|
||||
|
||||
|
||||
def __print_description(story: Dict[str, Any]) -> None:
|
||||
"""Prints the description of the story, if available."""
|
||||
COLOR_HEADER.print('Description:')
|
||||
if 'description' in story:
|
||||
description = story['description'].strip()
|
||||
print_wrap(description, indent=' ', end='\n\n')
|
||||
COLOR_HEADER.print("Description:")
|
||||
if "description" in story:
|
||||
description = story["description"].strip()
|
||||
Renderer().render(description, width=80)
|
||||
print()
|
||||
else:
|
||||
print(' (No description)', end='\n\n')
|
||||
print(" (No description)", end="\n\n")
|
||||
|
||||
|
||||
def __print_labels(story: Dict[str, Any]) -> None:
|
||||
"""Prints the labels of the story, if available."""
|
||||
if not story.get('labels'):
|
||||
if not story.get("labels"):
|
||||
return
|
||||
|
||||
COLOR_HEADER.print('Labels:')
|
||||
COLOR_HEADER.print("Labels:")
|
||||
|
||||
template = '\033[97;48;5;22m {} \033[0m'
|
||||
labels = ' '.join(template.format(label['name'])
|
||||
for label in story['labels'])
|
||||
template = "\033[97;48;5;22m {} \033[0m"
|
||||
labels = " ".join(template.format(label["name"]) for label in story["labels"])
|
||||
|
||||
print(f' {labels}', end='\n\n')
|
||||
print(f" {labels}", end="\n\n")
|
||||
|
||||
|
||||
def __print_tasks(project_id: int, story_id: int) -> None:
|
||||
|
@ -83,36 +83,37 @@ def __print_tasks(project_id: int, story_id: int) -> None:
|
|||
tasks = api.stories.get_tasks(project_id, story_id)
|
||||
|
||||
if tasks:
|
||||
COLOR_HEADER.print('Tasks:')
|
||||
COLOR_HEADER.print("Tasks:")
|
||||
|
||||
for task in tasks:
|
||||
print(end=' ')
|
||||
print('[X]' if task['complete'] else '[ ]', end=' \033[34m')
|
||||
print(base32.encode(task['id']), end=':\033[0m ')
|
||||
print(task['description'])
|
||||
print(end=" ")
|
||||
print("[X]" if task["complete"] else "[ ]", end=" \033[34m")
|
||||
print(base32.encode(task["id"]), end=":\033[0m ")
|
||||
print(task["description"])
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def __print_comments(project_id: int, story_id: int,
|
||||
persons: Dict[int, Dict[str, Any]]) -> None:
|
||||
def __print_comments(
|
||||
project_id: int, story_id: int, persons: Dict[int, Dict[str, Any]]
|
||||
) -> None:
|
||||
"""Prints the comments on the story, if available."""
|
||||
comments = api.stories.get_comments(project_id, story_id)
|
||||
|
||||
if comments:
|
||||
COLOR_HEADER.print('Comments:')
|
||||
COLOR_HEADER.print("Comments:")
|
||||
|
||||
for comment in comments:
|
||||
text = comment['text'].strip()
|
||||
print_wrap(text, indent=' ')
|
||||
text = comment.get("text", "[Empty comment]").strip()
|
||||
print_wrap(text, indent=" ")
|
||||
|
||||
person_id = comment['person_id']
|
||||
name = persons[person_id]['name']
|
||||
COLOR_WHITE.print(' -', name, end=' ')
|
||||
person_id = comment["person_id"]
|
||||
name = persons[person_id]["name"]
|
||||
COLOR_WHITE.print(" -", name, end=" ")
|
||||
|
||||
date = datetime.strptime(comment['created_at'], '%Y-%m-%dT%H:%M:%SZ')
|
||||
date_str = date.strftime('on %a %Y-%m-%d at %H:%M')
|
||||
print(date_str, end='\n\n')
|
||||
date = datetime.strptime(comment["created_at"], "%Y-%m-%dT%H:%M:%SZ")
|
||||
date_str = date.strftime("on %a %Y-%m-%d at %H:%M")
|
||||
print(date_str, end="\n\n")
|
||||
|
||||
|
||||
def __print_blockers(project_id: int, story_id: int) -> None:
|
||||
|
@ -120,25 +121,25 @@ def __print_blockers(project_id: int, story_id: int) -> None:
|
|||
blockers = api.stories.get_blockers(project_id, story_id)
|
||||
|
||||
if blockers:
|
||||
COLOR_HEADER.print('Blockers:')
|
||||
COLOR_HEADER.print("Blockers:")
|
||||
|
||||
def blocker_repl(matchgroup: Any) -> str:
|
||||
id = int(matchgroup.group(1))
|
||||
code = base32.encode(id)
|
||||
return COLOR_HEADER.format(code)
|
||||
|
||||
pattern = re.compile(r'#(\d+)')
|
||||
pattern = re.compile(r"#(\d+)")
|
||||
for blocker in blockers:
|
||||
resolved = 'X' if blocker['resolved'] else ' '
|
||||
desc = pattern.sub(blocker_repl, blocker['description'])
|
||||
print(f' [{resolved}] {desc}')
|
||||
resolved = "X" if blocker["resolved"] else " "
|
||||
desc = pattern.sub(blocker_repl, blocker["description"])
|
||||
print(f" [{resolved}] {desc}")
|
||||
|
||||
|
||||
def stories_info(story_b32: str) -> None:
|
||||
story_id = base32.decode(story_b32)
|
||||
story = api.stories.get(story_id)
|
||||
|
||||
project_id = story['project_id']
|
||||
project_id = story["project_id"]
|
||||
persons = _get_persons(project_id)
|
||||
|
||||
__print_story(story)
|
||||
|
|
|
@ -5,15 +5,16 @@ from config import Config
|
|||
from .cli import cli
|
||||
|
||||
|
||||
@cli.command('login')
|
||||
@click.option('--email', prompt=True)
|
||||
@click.option('--password', prompt=True, hide_input=True)
|
||||
@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)
|
||||
|
||||
print()
|
||||
print(f"Logged in successfully as {user['name']} (@{user['username']}).")
|
||||
|
||||
Config['user']['api_token'] = user['api_token']
|
||||
Config['user']['initials'] = user['initials']
|
||||
Config["user"]["api_token"] = user["api_token"]
|
||||
Config["user"]["initials"] = user["initials"]
|
||||
Config["user"]["email"] = user["email"]
|
||||
Config.write()
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import math
|
||||
import inquirer
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, DefaultDict, Dict, List, Optional, Sequence, Tuple
|
||||
from typing import Any, DefaultDict, Dict, List, Sequence, Tuple, Optional
|
||||
from tqdm import tqdm
|
||||
|
||||
import base32_crockford as base32
|
||||
import click
|
||||
|
@ -11,15 +15,31 @@ import api.projects
|
|||
import api.stories
|
||||
from config import Config
|
||||
from util import require_login
|
||||
from color import Color
|
||||
|
||||
from . import (COLOR_ACCEPTED, COLOR_DELIVERED, COLOR_FINISHED, COLOR_HEADER,
|
||||
COLOR_PLANNED, COLOR_REJECTED, COLOR_STARTED, Color,
|
||||
_format_state, _get_persons)
|
||||
from . import (
|
||||
COLOR_HEADER,
|
||||
COLOR_PLANNED,
|
||||
COLOR_STARTED,
|
||||
COLOR_FINISHED,
|
||||
COLOR_DELIVERED,
|
||||
COLOR_ACCEPTED,
|
||||
COLOR_REJECTED,
|
||||
_format_state,
|
||||
_get_persons,
|
||||
)
|
||||
from ._stories_info import stories_info
|
||||
from .cli import cli
|
||||
|
||||
STATES = 'unstarted', 'planned', 'started', 'finished', 'delivered', \
|
||||
'accepted', 'rejected'
|
||||
STATES = (
|
||||
"unstarted",
|
||||
"planned",
|
||||
"started",
|
||||
"finished",
|
||||
"delivered",
|
||||
"accepted",
|
||||
"rejected",
|
||||
)
|
||||
|
||||
|
||||
Persons = Dict[int, Dict[str, Any]]
|
||||
|
@ -30,92 +50,107 @@ def __burndown(color: Color, letter: str, points: float) -> str:
|
|||
return color.format(letter * int(math.ceil(points)))
|
||||
|
||||
|
||||
def __get_row(item: Tuple[int, Dict[str, int]], persons: Persons,
|
||||
show_accepted: bool) -> Sequence:
|
||||
def __get_row(
|
||||
item: Tuple[int, Dict[str, int]], persons: Persons, show_accepted: bool
|
||||
) -> Sequence:
|
||||
owner_id, points = item
|
||||
name = persons[owner_id]['name']
|
||||
name = persons[owner_id]["name"]
|
||||
|
||||
estimates = [points[state] for state in STATES]
|
||||
estimates = [round(points[state], 1) for state in STATES[1:]]
|
||||
|
||||
progress = '['
|
||||
progress = "["
|
||||
if show_accepted:
|
||||
progress += __burndown(COLOR_ACCEPTED, 'A', points['accepted'])
|
||||
progress += __burndown(COLOR_ACCEPTED, "A", points["accepted"])
|
||||
|
||||
progress += \
|
||||
__burndown(COLOR_DELIVERED, 'D', points['delivered']) + \
|
||||
__burndown(COLOR_FINISHED, 'F', points['finished']) + \
|
||||
__burndown(COLOR_STARTED, 'S', points['started']) + \
|
||||
__burndown(COLOR_REJECTED, 'R', points['rejected']) + \
|
||||
__burndown(COLOR_PLANNED, 'P', points['planned']) + \
|
||||
']'
|
||||
progress += (
|
||||
__burndown(COLOR_DELIVERED, "D", points["delivered"])
|
||||
+ __burndown(COLOR_FINISHED, "F", points["finished"])
|
||||
+ __burndown(COLOR_STARTED, "S", points["started"])
|
||||
+ __burndown(COLOR_REJECTED, "R", points["rejected"])
|
||||
+ __burndown(COLOR_PLANNED, "P", points["planned"])
|
||||
+ "]"
|
||||
)
|
||||
|
||||
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.encode(story['id'])
|
||||
def __format_story(
|
||||
story: Dict[str, Any], persons: Persons, totals: Totals
|
||||
) -> Tuple[str, str, str, str, str, int]:
|
||||
code = base32.encode(story["id"])
|
||||
|
||||
is_owner = False
|
||||
initials = []
|
||||
owner_ids = story.get('owner_ids', [])
|
||||
owner_ids = story.get("owner_ids", [])
|
||||
for owner_id in owner_ids:
|
||||
value = persons[owner_id]['initials']
|
||||
if value == Config['user']['initials']:
|
||||
email = persons[owner_id]["email"]
|
||||
value = persons[owner_id]["initials"]
|
||||
if email == Config["user"]["email"]:
|
||||
is_owner = True
|
||||
value = f'\033[96m{value}\033[97m'
|
||||
value = Color(Color.CYAN).format(value)
|
||||
initials.append(value)
|
||||
|
||||
type_ = story['story_type']
|
||||
type_ = story["story_type"]
|
||||
if type_ == "release":
|
||||
code = f"\033[44m{code}"
|
||||
|
||||
estimate = story.get('estimate')
|
||||
estimate = story.get("estimate")
|
||||
if estimate:
|
||||
for owner_id in owner_ids:
|
||||
state = story['current_state']
|
||||
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
|
||||
code = f"\033[1;97m{code}"
|
||||
estimate = f"\033[97m{estimate}\033[0m" if estimate else None
|
||||
|
||||
owners = ', '.join(initials)
|
||||
owners = ", ".join(initials)
|
||||
|
||||
state = _format_state(story['current_state'])
|
||||
return code, type_, story['name'], owners, state, estimate
|
||||
state = _format_state(story["current_state"])
|
||||
return code, type_, story["name"], owners, state, estimate, "\033[0m"
|
||||
|
||||
|
||||
def __print_stories(stories_: List[Dict[str, Any]], persons: Persons,
|
||||
totals: Totals, show_accepted: bool) -> None:
|
||||
table = (__format_story(story, persons, totals) for story in stories_
|
||||
if show_accepted or story['current_state'] != 'accepted')
|
||||
headers = 'Code', 'Type', 'Story name', 'Owners', 'State', 'Pts'
|
||||
print(tabulate.tabulate(table, headers=headers), end='\n\n')
|
||||
def __print_stories(
|
||||
stories_: List[Dict[str, Any]],
|
||||
persons: Persons,
|
||||
totals: Totals,
|
||||
show_accepted: bool,
|
||||
) -> None:
|
||||
table = (
|
||||
__format_story(story, persons, totals)
|
||||
for story in stories_
|
||||
if show_accepted or story["current_state"] != "accepted"
|
||||
)
|
||||
headers = "Code", "Type", "Story name", "Owners", "State", "Pts"
|
||||
print(tabulate.tabulate(table, headers=headers), end="\n\n")
|
||||
|
||||
|
||||
def __print_totals(totals: Totals, persons: Persons, show_accepted: bool) \
|
||||
-> None:
|
||||
COLOR_HEADER.print('Point totals:', end='\n\n')
|
||||
def __print_totals(totals: Totals, persons: Persons, show_accepted: bool) -> None:
|
||||
COLOR_HEADER.print("Point totals:", end="\n\n")
|
||||
|
||||
state_headers = [_format_state(state, header=True) for state in STATES]
|
||||
headers = ('Owner', *state_headers, 'Total', 'Progress')
|
||||
state_headers = [_format_state(state) for state in STATES]
|
||||
headers = ("Owner", *state_headers[1:], "Total", "Progress")
|
||||
|
||||
data = sorted((__get_row(item, persons, show_accepted)
|
||||
for item in totals.items()),
|
||||
key=lambda row: row[-2], reverse=True)
|
||||
print(tabulate.tabulate(data, headers), end='\n\n')
|
||||
data = sorted(
|
||||
(__get_row(item, persons, show_accepted) for item in totals.items()),
|
||||
key=lambda row: row[-2],
|
||||
reverse=True,
|
||||
)
|
||||
print(tabulate.tabulate(data, headers), end="\n\n")
|
||||
|
||||
|
||||
def __print_burndown(iteration: Dict[str, Any], show_accepted: bool) -> None:
|
||||
COLOR_HEADER.print('Burndown:', end='\n\n')
|
||||
COLOR_HEADER.print("Burndown:", end="\n\n")
|
||||
|
||||
start = datetime.strptime(iteration['start'], '%Y-%m-%dT%H:%M:%SZ')
|
||||
start = datetime.strptime(iteration["start"], "%Y-%m-%dT%H:%M:%SZ")
|
||||
history = api.projects.get_history_days(
|
||||
iteration['project_id'], start - timedelta(days=1))
|
||||
iteration["project_id"], start - timedelta(days=1)
|
||||
)
|
||||
|
||||
accepted_points = history['data'][0][1]
|
||||
accepted_points = history["data"][0][1]
|
||||
|
||||
last_counts: List[int] = []
|
||||
for date, *counts in history['data'][1:]:
|
||||
for date, *counts in history["data"][1:]:
|
||||
if len(counts) > 0:
|
||||
# Update the last_counts variable if an update is available.
|
||||
last_counts = counts
|
||||
|
@ -126,42 +161,43 @@ def __print_burndown(iteration: Dict[str, Any], show_accepted: bool) -> None:
|
|||
# If there are no last_counts either, just skip the day
|
||||
continue
|
||||
|
||||
progress = ''
|
||||
progress = ""
|
||||
if show_accepted:
|
||||
progress += __burndown(COLOR_ACCEPTED, 'A',
|
||||
counts[0] - accepted_points)
|
||||
progress += \
|
||||
__burndown(COLOR_DELIVERED, 'D', counts[1]) + \
|
||||
__burndown(COLOR_FINISHED, 'F', counts[2]) + \
|
||||
__burndown(COLOR_STARTED, 'S', counts[3]) + \
|
||||
__burndown(COLOR_PLANNED, 'P', counts[5]) + \
|
||||
__burndown(COLOR_PLANNED, 'U', counts[6])
|
||||
progress += __burndown(COLOR_ACCEPTED, "A", counts[0] - accepted_points)
|
||||
progress += (
|
||||
__burndown(COLOR_DELIVERED, "D", counts[1])
|
||||
+ __burndown(COLOR_FINISHED, "F", counts[2])
|
||||
+ __burndown(COLOR_STARTED, "S", counts[3])
|
||||
+ __burndown(COLOR_PLANNED, "P", counts[5])
|
||||
+ __burndown(COLOR_PLANNED, "U", counts[6])
|
||||
)
|
||||
|
||||
print(f'{date}: {progress}')
|
||||
print(f"{date}: {progress}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def _stories_current(project: str, scope: str, show_accepted: bool) -> None:
|
||||
try:
|
||||
project_id = int(Config['project_aliases'][project])
|
||||
project_id = int(Config["project_aliases"][project])
|
||||
except KeyError:
|
||||
project_id = base32.decode(project)
|
||||
|
||||
iterations = api.projects.get_iterations(project_id, scope=scope)
|
||||
if not iterations:
|
||||
print(f'No stories in {scope}')
|
||||
print(f"No stories in {scope}")
|
||||
return
|
||||
|
||||
iteration = iterations[0]
|
||||
|
||||
persons = _get_persons(project_id=project_id)
|
||||
totals: DefaultDict[int, Dict[str, int]] = \
|
||||
defaultdict(lambda: dict((state, 0) for state in STATES))
|
||||
totals: DefaultDict[int, Dict[str, int]] = defaultdict(
|
||||
lambda: dict((state, 0) for state in STATES)
|
||||
)
|
||||
|
||||
__print_stories(iteration['stories'], persons, totals, show_accepted)
|
||||
__print_stories(iteration["stories"], persons, totals, show_accepted)
|
||||
__print_totals(totals, persons, show_accepted)
|
||||
if scope == 'current':
|
||||
if scope == "current":
|
||||
__print_burndown(iteration, show_accepted)
|
||||
|
||||
|
||||
|
@ -170,18 +206,32 @@ def _set_story_state(story: str, state: str) -> None:
|
|||
api.stories.put_story(story_id, current_state=state)
|
||||
|
||||
|
||||
@cli.command('stories')
|
||||
@click.argument('project', type=click.STRING)
|
||||
@click.argument('story', required=False)
|
||||
@click.argument('action', required=False)
|
||||
@click.option('--scope', default='current')
|
||||
@click.option('--show-accepted/--hide-accepted', default=True)
|
||||
@click.option('--set-state', type=click.Choice(STATES))
|
||||
def _complete_projects(
|
||||
ctx: click.Context, args: List[str], incomplete: str
|
||||
) -> List[str]:
|
||||
return [
|
||||
alias for alias in Config["project_aliases"] if alias.startswith(incomplete)
|
||||
]
|
||||
|
||||
|
||||
@cli.command("stories")
|
||||
@click.argument("project", type=click.STRING, autocompletion=_complete_projects)
|
||||
@click.argument("story", required=False)
|
||||
@click.argument("action", required=False)
|
||||
@click.option("--scope", default="current")
|
||||
@click.option("--show-accepted/--hide-accepted", default=True)
|
||||
@click.option("--set-state", type=click.Choice(STATES))
|
||||
@require_login
|
||||
def stories(project: str, story: Optional[str], action: Optional[str],
|
||||
scope: str, show_accepted: bool, set_state: str) -> None:
|
||||
def stories(
|
||||
project: str,
|
||||
story: Optional[str],
|
||||
action: Optional[str],
|
||||
scope: str,
|
||||
show_accepted: bool,
|
||||
set_state: str,
|
||||
) -> None:
|
||||
if story is not None:
|
||||
state_actions = 'start', 'finish', 'deliver', 'accept', 'reject'
|
||||
state_actions = "start", "finish", "deliver", "accept", "reject"
|
||||
if set_state is not None:
|
||||
_set_story_state(story, set_state)
|
||||
elif action is not None:
|
||||
|
|
|
@ -18,9 +18,9 @@ class Config(object, metaclass=_Config):
|
|||
|
||||
@staticmethod
|
||||
def __get_filename() -> str:
|
||||
data_dir = appdirs.user_data_dir('pivotalcli', 'sijmen')
|
||||
data_dir = appdirs.user_data_dir("pivotalcli", "sijmen")
|
||||
os.makedirs(data_dir, exist_ok=True)
|
||||
return os.path.join(data_dir, 'config.ini')
|
||||
return os.path.join(data_dir, "config.ini")
|
||||
|
||||
@classmethod
|
||||
def read(cls) -> None:
|
||||
|
@ -28,5 +28,5 @@ class Config(object, metaclass=_Config):
|
|||
|
||||
@classmethod
|
||||
def write(cls) -> None:
|
||||
with open(cls.__get_filename(), mode='w') as config_file:
|
||||
with open(cls.__get_filename(), mode="w") as config_file:
|
||||
cls.config.write(config_file)
|
||||
|
|
Loading…
Reference in a new issue