Automatically use token from config and more cleanups

This commit is contained in:
Sijmen 2019-02-05 16:10:31 +01:00
parent 18508fd549
commit f61cb1632f
8 changed files with 100 additions and 92 deletions

View File

@ -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}

View File

@ -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()

View File

@ -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()

View File

@ -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:

View File

@ -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)

View File

@ -2,5 +2,5 @@ import click
@click.group() @click.group()
def cli(): def cli() -> None:
pass pass

View File

@ -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()))

View File

@ -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')