From f61cb1632f475680be83618b769a18535c8c228a Mon Sep 17 00:00:00 2001 From: Sijmen Schoon Date: Tue, 5 Feb 2019 16:10:31 +0100 Subject: [PATCH] Automatically use token from config and more cleanups --- api/__init__.py | 15 ++++++++++ api/projects.py | 60 +++++++++++++++++++-------------------- api/stories.py | 42 ++++++++++++++------------- commands/__init__.py | 7 +++-- commands/_stories_info.py | 28 ++++++++---------- commands/cli.py | 2 +- commands/projects.py | 6 ++-- commands/stories.py | 32 +++++++++------------ 8 files changed, 100 insertions(+), 92 deletions(-) diff --git a/api/__init__.py b/api/__init__.py index e69de29..7f99c93 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -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} diff --git a/api/projects.py b/api/projects.py index 92c1c64..ec9f8a5 100644 --- a/api/projects.py +++ b/api/projects.py @@ -3,44 +3,46 @@ from typing import Any, Dict, List, Optional import requests +from . import _BASE_URL, _headers, _with_token + +@_with_token def get(token: str) -> List[Dict[str, Any]]: - r = requests.get( - f'https://www.pivotaltracker.com/services/v5/projects', - headers={'X-TrackerToken': token}) + r = requests.get(f'{_BASE_URL}/projects', headers=_headers(token)) return r.json() -def get_project(token: str, project_id: int) -> Dict[str, Any]: - r = requests.get( - f'https://www.pivotaltracker.com/services/v5/projects/{project_id}', - headers={'X-TrackerToken': token}) +@_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)) return r.json() -def get_stories(token: str, project_id: int) -> List[Dict[str, Any]]: - r = requests.get( - f'https://www.pivotaltracker.com/services/v5/projects/{project_id}' - '/stories', headers={'X-TrackerToken': token}) +@_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)) return r.json() -def get_memberships(token: str, project_id: int) -> List[Dict[str, Any]]: - r = requests.get( - f'https://www.pivotaltracker.com/services/v5/projects/{project_id}' - '/memberships', headers={'X-TrackerToken': token}) +@_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)) 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]]: - r = requests.get( - f'https://www.pivotaltracker.com/services/v5/projects/{project_id}' - f'/iterations?scope={scope}', headers={'X-TrackerToken': token}) + r = requests.get(f'{_BASE_URL}/projects/{project_id}/iterations', + headers=_headers(token), params={'scope': scope}) 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, before: Optional[datetime] = None) \ -> List[Dict[str, Any]]: @@ -50,19 +52,15 @@ def get_story_transitions(token: str, project_id: int, if before: parameters['occurred_before'] = before.isoformat() - r = requests.get( - f'https://www.pivotaltracker.com/services/v5/projects/{project_id}' - f'/story_transitions', - headers={'X-TrackerToken': token}, - params=parameters) + r = requests.get(f'{_BASE_URL}/projects/{project_id}/story_transitions', + headers=_headers(token), params=parameters) 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]: - parameters = {'start_date': start.isoformat()} - - r = requests.get( - f'https://www.pivotaltracker.com/services/v5/projects/{project_id}' - f'/history/days', headers={'X-TrackerToken': token}, params=parameters) + r = requests.get(f'{_BASE_URL}/projects/{project_id}/history/days', + headers=_headers(token), + params={'start_date': start.isoformat()}) return r.json() diff --git a/api/stories.py b/api/stories.py index 755c77f..ac1d289 100644 --- a/api/stories.py +++ b/api/stories.py @@ -1,40 +1,42 @@ +from typing import Any, Dict, List 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]: - r = requests.get( - f'https://www.pivotaltracker.com/services/v5/stories/{story_id}', - headers={'X-TrackerToken': token}) + r = requests.get(f'{_BASE_URL}/stories/{story_id}', + headers=_headers(token)) return r.json() -def put_story(token: str, story_id: int, **kwargs) -> Dict[str, Any]: - r = requests.put( - f'https://www.pivotaltracker.com/services/v5/stories/{story_id}', - headers={'X-TrackerToken': token}, json=kwargs) +@_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) 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]]: - r = requests.get( - f'https://www.pivotaltracker.com/services/v5/projects/{project_id}' - f'/stories/{story_id}/tasks', headers={'X-TrackerToken': token}) + url = f'{_BASE_URL}/projects/{project_id}/stories/{story_id}/tasks' + r = requests.get(url, headers=_headers(token)) 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]]: - r = requests.get( - f'https://www.pivotaltracker.com/services/v5/projects/{project_id}' - f'/stories/{story_id}/comments', headers={'X-TrackerToken': token}) + url = f'{_BASE_URL}/projects/{project_id}/stories/{story_id}/comments' + r = requests.get(url, headers=_headers(token)) 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]]: - r = requests.get( - f'https://www.pivotaltracker.com/services/v5/projects/{project_id}' - f'/stories/{story_id}/blockers', headers={'X-TrackerToken': token}) + url = f'{_BASE_URL}/projects/{project_id}/stories/{story_id}/blockers' + r = requests.get(url, headers=_headers(token)) return r.json() diff --git a/commands/__init__.py b/commands/__init__.py index 57bb165..cfecae3 100644 --- a/commands/__init__.py +++ b/commands/__init__.py @@ -1,6 +1,7 @@ -import api.projects from typing import Dict, Any, Optional + from color import Color +import api.projects COLOR_TITLE = Color(Color.YELLOW) COLOR_HEADER = Color(Color.CYAN) @@ -28,8 +29,8 @@ def _format_state(state: str, header: Optional[bool] = False) -> str: return STATES[state] -def _get_persons(token: str, project_id: int) -> Dict[int, Dict[str, Any]]: - memberships = api.projects.get_memberships(token, project_id) +def _get_persons(project_id: int) -> Dict[int, Dict[str, Any]]: + memberships = api.projects.get_memberships(project_id) persons: Dict[int, Dict[str, Any]] = {} for membership in memberships: diff --git a/commands/_stories_info.py b/commands/_stories_info.py index 0c4f2e9..cd48757 100644 --- a/commands/_stories_info.py +++ b/commands/_stories_info.py @@ -8,6 +8,7 @@ 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) @@ -77,9 +78,9 @@ def __print_labels(story: Dict[str, Any]) -> None: 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.""" - tasks = api.stories.get_tasks(token, project_id, story_id) + tasks = api.stories.get_tasks(project_id, story_id) if tasks: COLOR_HEADER.print('Tasks:') @@ -93,10 +94,10 @@ def __print_tasks(token: str, project_id: int, story_id: int) -> None: 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: """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: 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') -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.""" - blockers = api.stories.get_blockers(token, project_id, story_id) + blockers = api.stories.get_blockers(project_id, story_id) if 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: - try: - token = Config['user']['api_token'] - except KeyError: - sys.exit(1) - story_id = base32.decode(story_b32) - story = api.stories.get(token, story_id) + story = api.stories.get(story_id) project_id = story['project_id'] - persons = _get_persons(token, project_id) + persons = _get_persons(project_id) __print_story(story) __print_owners(story, persons) __print_description(story) __print_labels(story) - __print_tasks(token, project_id, story_id) - __print_comments(token, project_id, story_id, persons) - __print_blockers(token, project_id, story_id) + __print_tasks(project_id, story_id) + __print_comments(project_id, story_id, persons) + __print_blockers(project_id, story_id) diff --git a/commands/cli.py b/commands/cli.py index 21966a2..5e43602 100644 --- a/commands/cli.py +++ b/commands/cli.py @@ -2,5 +2,5 @@ import click @click.group() -def cli(): +def cli() -> None: pass diff --git a/commands/projects.py b/commands/projects.py index 1a1f54e..4c4f77a 100644 --- a/commands/projects.py +++ b/commands/projects.py @@ -8,6 +8,7 @@ import tabulate import api.projects from config import Config from util import require_login + from .cli import cli @@ -20,7 +21,7 @@ def projects(context: click.Context) -> None: # invoked as well. In this case, do nothing. return - projects_ = api.projects.get(Config['user']['api_token']) + projects_ = api.projects.get() projects_.sort(key=lambda project_: project_['name']) aliases: Dict[int, str] = {} @@ -61,11 +62,10 @@ def alias_rm(name: str) -> None: @click.argument('name') def info(name: str) -> None: try: - token = Config['user']['api_token'] project_id = int(Config['project_aliases'][name]) except KeyError: print(f'unknown alias {name}') 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())) diff --git a/commands/stories.py b/commands/stories.py index 1bc454e..77e792b 100644 --- a/commands/stories.py +++ b/commands/stories.py @@ -1,10 +1,10 @@ -import click import math from collections import defaultdict 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 click import tabulate import api.projects @@ -12,9 +12,9 @@ import api.stories from config import Config from util import require_login -from . import COLOR_HEADER, COLOR_PLANNED, COLOR_STARTED, COLOR_FINISHED, \ - COLOR_DELIVERED, COLOR_ACCEPTED, COLOR_REJECTED, _format_state, \ - _get_persons +from . import (COLOR_ACCEPTED, COLOR_DELIVERED, COLOR_FINISHED, COLOR_HEADER, + COLOR_PLANNED, COLOR_REJECTED, COLOR_STARTED, Color, + _format_state, _get_persons) from ._stories_info import stories_info from .cli import cli @@ -26,7 +26,7 @@ Persons = Dict[int, Dict[str, Any]] 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))) @@ -105,13 +105,12 @@ def __print_totals(totals: Totals, persons: Persons, show_accepted: bool) \ print(tabulate.tabulate(data, headers), end='\n\n') -def __print_burndown(token: str, iteration: Dict[str, Any], - show_accepted: bool) -> None: +def __print_burndown(iteration: Dict[str, Any], show_accepted: bool) -> 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)) + iteration['project_id'], start - timedelta(days=1)) accepted_points = history['data'][0][1] @@ -149,29 +148,26 @@ def _stories_current(project: str, scope: str, show_accepted: bool) -> None: except KeyError: project_id = base32.decode(project) - token = Config['user']['api_token'] - - iterations = api.projects.get_iterations( - token, project_id, scope=scope) + iterations = api.projects.get_iterations(project_id, scope=scope) if not iterations: - print('No stories in', scope) + print(f'No stories in {scope}') return + iteration = iterations[0] - persons = _get_persons(token, project_id=project_id) + persons = _get_persons(project_id=project_id) totals: DefaultDict[int, Dict[str, int]] = \ defaultdict(lambda: dict((state, 0) for state in STATES)) __print_stories(iteration['stories'], persons, totals, show_accepted) __print_totals(totals, persons, show_accepted) if scope == 'current': - __print_burndown(token, iteration, show_accepted) + __print_burndown(iteration, show_accepted) def _set_story_state(story: str, state: str) -> None: - token = Config['user']['api_token'] 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')