diff --git a/api/projects.py b/api/projects.py index ec9f8a5..02c3f4d 100644 --- a/api/projects.py +++ b/api/projects.py @@ -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() diff --git a/api/stories.py b/api/stories.py index ac1d289..cdc6776 100644 --- a/api/stories.py +++ b/api/stories.py @@ -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() diff --git a/commands/_stories_info.py b/commands/_stories_info.py index cd48757..c7d72d4 100644 --- a/commands/_stories_info.py +++ b/commands/_stories_info.py @@ -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) diff --git a/commands/login.py b/commands/login.py index 7ac47a5..6443f46 100644 --- a/commands/login.py +++ b/commands/login.py @@ -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() diff --git a/commands/stories.py b/commands/stories.py index 77e792b..81ea805 100644 --- a/commands/stories.py +++ b/commands/stories.py @@ -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: diff --git a/config.py b/config.py index 31298a4..6cc1fee 100644 --- a/config.py +++ b/config.py @@ -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)