Automatically use token from config and more cleanups
This commit is contained in:
parent
18508fd549
commit
f61cb1632f
8 changed files with 100 additions and 92 deletions
|
@ -0,0 +1,15 @@
|
||||||
|
from typing import Any, Callable, Dict
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
_BASE_URL = 'https://www.pivotaltracker.com/services/v5'
|
||||||
|
|
||||||
|
|
||||||
|
def _with_token(function: Callable) -> Callable:
|
||||||
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||||
|
return function(token=Config['user']['api_token'],
|
||||||
|
*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def _headers(token: str, **other: str) -> Dict[str, Any]:
|
||||||
|
return {'X-TrackerToken': token, **other}
|
|
@ -3,44 +3,46 @@ from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from . import _BASE_URL, _headers, _with_token
|
||||||
|
|
||||||
|
|
||||||
|
@_with_token
|
||||||
def get(token: str) -> List[Dict[str, Any]]:
|
def get(token: str) -> List[Dict[str, Any]]:
|
||||||
r = requests.get(
|
r = requests.get(f'{_BASE_URL}/projects', headers=_headers(token))
|
||||||
f'https://www.pivotaltracker.com/services/v5/projects',
|
|
||||||
headers={'X-TrackerToken': token})
|
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
def get_project(token: str, project_id: int) -> Dict[str, Any]:
|
@_with_token
|
||||||
r = requests.get(
|
def get_project(project_id: int, token: str) -> Dict[str, Any]:
|
||||||
f'https://www.pivotaltracker.com/services/v5/projects/{project_id}',
|
r = requests.get(f'{_BASE_URL}/projects/{project_id}',
|
||||||
headers={'X-TrackerToken': token})
|
headers=_headers(token))
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
def get_stories(token: str, project_id: int) -> List[Dict[str, Any]]:
|
@_with_token
|
||||||
r = requests.get(
|
def get_stories(project_id: int, token: str) -> List[Dict[str, Any]]:
|
||||||
f'https://www.pivotaltracker.com/services/v5/projects/{project_id}'
|
r = requests.get(f'{_BASE_URL}/projects/{project_id}/stories',
|
||||||
'/stories', headers={'X-TrackerToken': token})
|
headers=_headers(token))
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
def get_memberships(token: str, project_id: int) -> List[Dict[str, Any]]:
|
@_with_token
|
||||||
r = requests.get(
|
def get_memberships(project_id: int, token: str) -> List[Dict[str, Any]]:
|
||||||
f'https://www.pivotaltracker.com/services/v5/projects/{project_id}'
|
r = requests.get(f'{_BASE_URL}/projects/{project_id}/memberships',
|
||||||
'/memberships', headers={'X-TrackerToken': token})
|
headers=_headers(token))
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
def get_iterations(token: str, project_id: int, scope: str) \
|
@_with_token
|
||||||
|
def get_iterations(project_id: int, scope: str, token: str) \
|
||||||
-> List[Dict[str, Any]]:
|
-> List[Dict[str, Any]]:
|
||||||
r = requests.get(
|
r = requests.get(f'{_BASE_URL}/projects/{project_id}/iterations',
|
||||||
f'https://www.pivotaltracker.com/services/v5/projects/{project_id}'
|
headers=_headers(token), params={'scope': scope})
|
||||||
f'/iterations?scope={scope}', headers={'X-TrackerToken': token})
|
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
def get_story_transitions(token: str, project_id: int,
|
@_with_token
|
||||||
|
def get_story_transitions(str, project_id: int, token: str,
|
||||||
after: Optional[datetime] = None,
|
after: Optional[datetime] = None,
|
||||||
before: Optional[datetime] = None) \
|
before: Optional[datetime] = None) \
|
||||||
-> List[Dict[str, Any]]:
|
-> List[Dict[str, Any]]:
|
||||||
|
@ -50,19 +52,15 @@ def get_story_transitions(token: str, project_id: int,
|
||||||
if before:
|
if before:
|
||||||
parameters['occurred_before'] = before.isoformat()
|
parameters['occurred_before'] = before.isoformat()
|
||||||
|
|
||||||
r = requests.get(
|
r = requests.get(f'{_BASE_URL}/projects/{project_id}/story_transitions',
|
||||||
f'https://www.pivotaltracker.com/services/v5/projects/{project_id}'
|
headers=_headers(token), params=parameters)
|
||||||
f'/story_transitions',
|
|
||||||
headers={'X-TrackerToken': token},
|
|
||||||
params=parameters)
|
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
def get_history_days(token: str, project_id: int, start: datetime) \
|
@_with_token
|
||||||
|
def get_history_days(project_id: int, start: datetime, token: str) \
|
||||||
-> Dict[str, Any]:
|
-> Dict[str, Any]:
|
||||||
parameters = {'start_date': start.isoformat()}
|
r = requests.get(f'{_BASE_URL}/projects/{project_id}/history/days',
|
||||||
|
headers=_headers(token),
|
||||||
r = requests.get(
|
params={'start_date': start.isoformat()})
|
||||||
f'https://www.pivotaltracker.com/services/v5/projects/{project_id}'
|
|
||||||
f'/history/days', headers={'X-TrackerToken': token}, params=parameters)
|
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
|
@ -1,40 +1,42 @@
|
||||||
|
from typing import Any, Dict, List
|
||||||
import requests
|
import requests
|
||||||
from typing import Dict, Any, List
|
|
||||||
|
from . import _BASE_URL, _headers, _with_token
|
||||||
|
|
||||||
|
|
||||||
|
@_with_token
|
||||||
def get(token: str, story_id: int = None) -> Dict[str, Any]:
|
def get(token: str, story_id: int = None) -> Dict[str, Any]:
|
||||||
r = requests.get(
|
r = requests.get(f'{_BASE_URL}/stories/{story_id}',
|
||||||
f'https://www.pivotaltracker.com/services/v5/stories/{story_id}',
|
headers=_headers(token))
|
||||||
headers={'X-TrackerToken': token})
|
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
def put_story(token: str, story_id: int, **kwargs) -> Dict[str, Any]:
|
@_with_token
|
||||||
r = requests.put(
|
def put_story(story_id: int, token: str, **kwargs: Any) -> Dict[str, Any]:
|
||||||
f'https://www.pivotaltracker.com/services/v5/stories/{story_id}',
|
r = requests.put(f'{_BASE_URL}/stories/{story_id}',
|
||||||
headers={'X-TrackerToken': token}, json=kwargs)
|
headers=_headers(token), json=kwargs)
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
def get_tasks(token: str, project_id: int, story_id: int) \
|
@_with_token
|
||||||
|
def get_tasks(project_id: int, story_id: int, token: str) \
|
||||||
-> List[Dict[str, Any]]:
|
-> List[Dict[str, Any]]:
|
||||||
r = requests.get(
|
url = f'{_BASE_URL}/projects/{project_id}/stories/{story_id}/tasks'
|
||||||
f'https://www.pivotaltracker.com/services/v5/projects/{project_id}'
|
r = requests.get(url, headers=_headers(token))
|
||||||
f'/stories/{story_id}/tasks', headers={'X-TrackerToken': token})
|
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
def get_comments(token: str, project_id: int, story_id: int) \
|
@_with_token
|
||||||
|
def get_comments(project_id: int, story_id: int, token: str) \
|
||||||
-> List[Dict[str, Any]]:
|
-> List[Dict[str, Any]]:
|
||||||
r = requests.get(
|
url = f'{_BASE_URL}/projects/{project_id}/stories/{story_id}/comments'
|
||||||
f'https://www.pivotaltracker.com/services/v5/projects/{project_id}'
|
r = requests.get(url, headers=_headers(token))
|
||||||
f'/stories/{story_id}/comments', headers={'X-TrackerToken': token})
|
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
def get_blockers(token: str, project_id: int, story_id: int) \
|
@_with_token
|
||||||
|
def get_blockers(project_id: int, story_id: int, token: str) \
|
||||||
-> List[Dict[str, Any]]:
|
-> List[Dict[str, Any]]:
|
||||||
r = requests.get(
|
url = f'{_BASE_URL}/projects/{project_id}/stories/{story_id}/blockers'
|
||||||
f'https://www.pivotaltracker.com/services/v5/projects/{project_id}'
|
r = requests.get(url, headers=_headers(token))
|
||||||
f'/stories/{story_id}/blockers', headers={'X-TrackerToken': token})
|
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import api.projects
|
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
from color import Color
|
from color import Color
|
||||||
|
import api.projects
|
||||||
|
|
||||||
COLOR_TITLE = Color(Color.YELLOW)
|
COLOR_TITLE = Color(Color.YELLOW)
|
||||||
COLOR_HEADER = Color(Color.CYAN)
|
COLOR_HEADER = Color(Color.CYAN)
|
||||||
|
@ -28,8 +29,8 @@ def _format_state(state: str, header: Optional[bool] = False) -> str:
|
||||||
return STATES[state]
|
return STATES[state]
|
||||||
|
|
||||||
|
|
||||||
def _get_persons(token: str, project_id: int) -> Dict[int, Dict[str, Any]]:
|
def _get_persons(project_id: int) -> Dict[int, Dict[str, Any]]:
|
||||||
memberships = api.projects.get_memberships(token, project_id)
|
memberships = api.projects.get_memberships(project_id)
|
||||||
|
|
||||||
persons: Dict[int, Dict[str, Any]] = {}
|
persons: Dict[int, Dict[str, Any]] = {}
|
||||||
for membership in memberships:
|
for membership in memberships:
|
||||||
|
|
|
@ -8,6 +8,7 @@ import base32_crockford as base32
|
||||||
import api.stories
|
import api.stories
|
||||||
from config import Config
|
from config import Config
|
||||||
from util import print_wrap
|
from util import print_wrap
|
||||||
|
|
||||||
from . import (COLOR_HEADER, COLOR_TITLE, COLOR_WHITE, _format_state,
|
from . import (COLOR_HEADER, COLOR_TITLE, COLOR_WHITE, _format_state,
|
||||||
_get_persons)
|
_get_persons)
|
||||||
|
|
||||||
|
@ -77,9 +78,9 @@ def __print_labels(story: Dict[str, Any]) -> None:
|
||||||
print(f' {labels}', end='\n\n')
|
print(f' {labels}', end='\n\n')
|
||||||
|
|
||||||
|
|
||||||
def __print_tasks(token: str, project_id: int, story_id: int) -> None:
|
def __print_tasks(project_id: int, story_id: int) -> None:
|
||||||
"""Prints the tasks of the story, if available."""
|
"""Prints the tasks of the story, if available."""
|
||||||
tasks = api.stories.get_tasks(token, project_id, story_id)
|
tasks = api.stories.get_tasks(project_id, story_id)
|
||||||
|
|
||||||
if tasks:
|
if tasks:
|
||||||
COLOR_HEADER.print('Tasks:')
|
COLOR_HEADER.print('Tasks:')
|
||||||
|
@ -93,10 +94,10 @@ def __print_tasks(token: str, project_id: int, story_id: int) -> None:
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
def __print_comments(token: str, project_id: int, story_id: int,
|
def __print_comments(project_id: int, story_id: int,
|
||||||
persons: Dict[int, Dict[str, Any]]) -> None:
|
persons: Dict[int, Dict[str, Any]]) -> None:
|
||||||
"""Prints the comments on the story, if available."""
|
"""Prints the comments on the story, if available."""
|
||||||
comments = api.stories.get_comments(token, project_id, story_id)
|
comments = api.stories.get_comments(project_id, story_id)
|
||||||
|
|
||||||
if comments:
|
if comments:
|
||||||
COLOR_HEADER.print('Comments:')
|
COLOR_HEADER.print('Comments:')
|
||||||
|
@ -114,9 +115,9 @@ def __print_comments(token: str, project_id: int, story_id: int,
|
||||||
print(date_str, end='\n\n')
|
print(date_str, end='\n\n')
|
||||||
|
|
||||||
|
|
||||||
def __print_blockers(token: str, project_id: int, story_id: int) -> None:
|
def __print_blockers(project_id: int, story_id: int) -> None:
|
||||||
"""Prints the stories that block this story, if available."""
|
"""Prints the stories that block this story, if available."""
|
||||||
blockers = api.stories.get_blockers(token, project_id, story_id)
|
blockers = api.stories.get_blockers(project_id, story_id)
|
||||||
|
|
||||||
if blockers:
|
if blockers:
|
||||||
COLOR_HEADER.print('Blockers:')
|
COLOR_HEADER.print('Blockers:')
|
||||||
|
@ -134,21 +135,16 @@ def __print_blockers(token: str, project_id: int, story_id: int) -> None:
|
||||||
|
|
||||||
|
|
||||||
def stories_info(story_b32: str) -> None:
|
def stories_info(story_b32: str) -> None:
|
||||||
try:
|
|
||||||
token = Config['user']['api_token']
|
|
||||||
except KeyError:
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
story_id = base32.decode(story_b32)
|
story_id = base32.decode(story_b32)
|
||||||
story = api.stories.get(token, story_id)
|
story = api.stories.get(story_id)
|
||||||
|
|
||||||
project_id = story['project_id']
|
project_id = story['project_id']
|
||||||
persons = _get_persons(token, project_id)
|
persons = _get_persons(project_id)
|
||||||
|
|
||||||
__print_story(story)
|
__print_story(story)
|
||||||
__print_owners(story, persons)
|
__print_owners(story, persons)
|
||||||
__print_description(story)
|
__print_description(story)
|
||||||
__print_labels(story)
|
__print_labels(story)
|
||||||
__print_tasks(token, project_id, story_id)
|
__print_tasks(project_id, story_id)
|
||||||
__print_comments(token, project_id, story_id, persons)
|
__print_comments(project_id, story_id, persons)
|
||||||
__print_blockers(token, project_id, story_id)
|
__print_blockers(project_id, story_id)
|
||||||
|
|
|
@ -2,5 +2,5 @@ import click
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def cli():
|
def cli() -> None:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -8,6 +8,7 @@ import tabulate
|
||||||
import api.projects
|
import api.projects
|
||||||
from config import Config
|
from config import Config
|
||||||
from util import require_login
|
from util import require_login
|
||||||
|
|
||||||
from .cli import cli
|
from .cli import cli
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@ def projects(context: click.Context) -> None:
|
||||||
# invoked as well. In this case, do nothing.
|
# invoked as well. In this case, do nothing.
|
||||||
return
|
return
|
||||||
|
|
||||||
projects_ = api.projects.get(Config['user']['api_token'])
|
projects_ = api.projects.get()
|
||||||
projects_.sort(key=lambda project_: project_['name'])
|
projects_.sort(key=lambda project_: project_['name'])
|
||||||
|
|
||||||
aliases: Dict[int, str] = {}
|
aliases: Dict[int, str] = {}
|
||||||
|
@ -61,11 +62,10 @@ def alias_rm(name: str) -> None:
|
||||||
@click.argument('name')
|
@click.argument('name')
|
||||||
def info(name: str) -> None:
|
def info(name: str) -> None:
|
||||||
try:
|
try:
|
||||||
token = Config['user']['api_token']
|
|
||||||
project_id = int(Config['project_aliases'][name])
|
project_id = int(Config['project_aliases'][name])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
print(f'unknown alias {name}')
|
print(f'unknown alias {name}')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
project_info = api.projects.get_project(token, project_id)
|
project_info = api.projects.get_project(project_id)
|
||||||
print(tabulate.tabulate(project_info.items()))
|
print(tabulate.tabulate(project_info.items()))
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import click
|
|
||||||
import math
|
import math
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, DefaultDict, Dict, List, Sequence, Tuple, Optional
|
from typing import Any, DefaultDict, Dict, List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
import base32_crockford as base32
|
import base32_crockford as base32
|
||||||
|
import click
|
||||||
import tabulate
|
import tabulate
|
||||||
|
|
||||||
import api.projects
|
import api.projects
|
||||||
|
@ -12,9 +12,9 @@ import api.stories
|
||||||
from config import Config
|
from config import Config
|
||||||
from util import require_login
|
from util import require_login
|
||||||
|
|
||||||
from . import COLOR_HEADER, COLOR_PLANNED, COLOR_STARTED, COLOR_FINISHED, \
|
from . import (COLOR_ACCEPTED, COLOR_DELIVERED, COLOR_FINISHED, COLOR_HEADER,
|
||||||
COLOR_DELIVERED, COLOR_ACCEPTED, COLOR_REJECTED, _format_state, \
|
COLOR_PLANNED, COLOR_REJECTED, COLOR_STARTED, Color,
|
||||||
_get_persons
|
_format_state, _get_persons)
|
||||||
from ._stories_info import stories_info
|
from ._stories_info import stories_info
|
||||||
from .cli import cli
|
from .cli import cli
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ Persons = Dict[int, Dict[str, Any]]
|
||||||
Totals = DefaultDict[int, Dict[str, int]]
|
Totals = DefaultDict[int, Dict[str, int]]
|
||||||
|
|
||||||
|
|
||||||
def __burndown(color, letter, points):
|
def __burndown(color: Color, letter: str, points: float) -> str:
|
||||||
return color.format(letter * int(math.ceil(points)))
|
return color.format(letter * int(math.ceil(points)))
|
||||||
|
|
||||||
|
|
||||||
|
@ -105,13 +105,12 @@ def __print_totals(totals: Totals, persons: Persons, show_accepted: bool) \
|
||||||
print(tabulate.tabulate(data, headers), end='\n\n')
|
print(tabulate.tabulate(data, headers), end='\n\n')
|
||||||
|
|
||||||
|
|
||||||
def __print_burndown(token: str, iteration: Dict[str, Any],
|
def __print_burndown(iteration: Dict[str, Any], show_accepted: bool) -> None:
|
||||||
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(
|
history = api.projects.get_history_days(
|
||||||
token, 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]
|
||||||
|
|
||||||
|
@ -149,29 +148,26 @@ def _stories_current(project: str, scope: str, show_accepted: bool) -> None:
|
||||||
except KeyError:
|
except KeyError:
|
||||||
project_id = base32.decode(project)
|
project_id = base32.decode(project)
|
||||||
|
|
||||||
token = Config['user']['api_token']
|
iterations = api.projects.get_iterations(project_id, scope=scope)
|
||||||
|
|
||||||
iterations = api.projects.get_iterations(
|
|
||||||
token, project_id, scope=scope)
|
|
||||||
if not iterations:
|
if not iterations:
|
||||||
print('No stories in', scope)
|
print(f'No stories in {scope}')
|
||||||
return
|
return
|
||||||
|
|
||||||
iteration = iterations[0]
|
iteration = iterations[0]
|
||||||
|
|
||||||
persons = _get_persons(token, project_id=project_id)
|
persons = _get_persons(project_id=project_id)
|
||||||
totals: DefaultDict[int, Dict[str, int]] = \
|
totals: DefaultDict[int, Dict[str, int]] = \
|
||||||
defaultdict(lambda: dict((state, 0) for state in STATES))
|
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)
|
__print_totals(totals, persons, show_accepted)
|
||||||
if scope == 'current':
|
if scope == 'current':
|
||||||
__print_burndown(token, iteration, show_accepted)
|
__print_burndown(iteration, show_accepted)
|
||||||
|
|
||||||
|
|
||||||
def _set_story_state(story: str, state: str) -> None:
|
def _set_story_state(story: str, state: str) -> None:
|
||||||
token = Config['user']['api_token']
|
|
||||||
story_id = base32.decode(story)
|
story_id = base32.decode(story)
|
||||||
api.stories.put_story(token, story_id, current_state=state)
|
api.stories.put_story(story_id, current_state=state)
|
||||||
|
|
||||||
|
|
||||||
@cli.command('stories')
|
@cli.command('stories')
|
||||||
|
|
Loading…
Reference in a new issue