From 06cd57e2c4b37f7308b522776f9fcf734dbe72f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D0=BA=D0=BE=D1=81=20=D0=90=D1=80=D1=82=D0=B5?= =?UTF-8?q?=D0=BC=20=D0=9D=D0=B8=D0=BA=D0=BE=D0=BB=D0=B0=D0=B5=D0=B2=D0=B8?= =?UTF-8?q?=D1=87?= Date: Thu, 5 Feb 2026 15:31:31 +0700 Subject: [PATCH] Blacked --- redmine_reporter/cli.py | 24 ++++++++-------------- redmine_reporter/client.py | 22 +++++++++----------- redmine_reporter/config.py | 1 - redmine_reporter/formatters/console.py | 19 +++++++++-------- redmine_reporter/formatters/csv.py | 18 +++++++++-------- redmine_reporter/formatters/factory.py | 1 - redmine_reporter/formatters/markdown.py | 2 +- redmine_reporter/formatters/odt.py | 20 ++++++++++++++---- redmine_reporter/report_builder.py | 27 ++++++++++++------------- redmine_reporter/utils.py | 17 +++++++++++++--- tests/test_cli.py | 9 ++++----- tests/test_config.py | 9 ++++----- tests/test_formatters.py | 19 +++++++++-------- tests/test_utils.py | 1 + 14 files changed, 102 insertions(+), 87 deletions(-) diff --git a/redmine_reporter/cli.py b/redmine_reporter/cli.py index a81a1b4..823f553 100644 --- a/redmine_reporter/cli.py +++ b/redmine_reporter/cli.py @@ -22,32 +22,26 @@ def parse_date_range(date_arg: str) -> tuple[str, str]: def main(argv: Optional[List[str]] = None) -> int: parser = argparse.ArgumentParser( prog="redmine-reporter", - description="Generate Redmine issue report based on your time entries." + description="Generate Redmine issue report based on your time entries.", ) parser.add_argument( "--date", default=Config.get_default_date_range(), # help="Date range in format YYYY-MM-DD--YYYY-MM-DD (default: %(default)s)" - help="Date range in format YYYY-MM-DD--YYYY-MM-DD (default from .env or %(default)s)" + help="Date range in format YYYY-MM-DD--YYYY-MM-DD (default from .env or %(default)s)", ) parser.add_argument( - "--compact", - action="store_true", - help="Use compact plain-text output instead of table" + "--compact", action="store_true", help="Use compact plain-text output instead of table" ) parser.add_argument( "--output", - help="Path to output .odt file (e.g., report.odt). If omitted, prints to stdout." + help="Path to output .odt file (e.g., report.odt). If omitted, prints to stdout.", ) parser.add_argument( - "--author", - default="", - help="Override author name from .env (REDMINE_AUTHOR)" + "--author", default="", help="Override author name from .env (REDMINE_AUTHOR)" ) parser.add_argument( - "--no-time", - action="store_true", - help="Do not include spent time into table" + "--no-time", action="store_true", help="Do not include spent time into table" ) args = parser.parse_args(argv) @@ -80,10 +74,8 @@ def main(argv: Optional[List[str]] = None) -> int: if args.output: output_ext = os.path.splitext(args.output)[1].lower() - formatter = get_formatter_by_extension(output_ext, - author=Config.get_author(args.author), - from_date=from_date, - to_date=to_date + formatter = get_formatter_by_extension( + output_ext, author=Config.get_author(args.author), from_date=from_date, to_date=to_date ) if not formatter: diff --git a/redmine_reporter/client.py b/redmine_reporter/client.py index 9c3465f..1fd476d 100644 --- a/redmine_reporter/client.py +++ b/redmine_reporter/client.py @@ -5,7 +5,9 @@ from .config import Config from .utils import get_version -def fetch_issues_with_spent_time(from_date: str, to_date: str) -> Optional[List[Tuple[Issue, float]]]: +def fetch_issues_with_spent_time( + from_date: str, to_date: str +) -> Optional[List[Tuple[Issue, float]]]: """ Fetch unique issues linked to time entries of the current user in given date range, along with total spent hours per issue. @@ -16,21 +18,19 @@ def fetch_issues_with_spent_time(from_date: str, to_date: str) -> Optional[List[ Config.REDMINE_URL, username=Config.REDMINE_USER, password=Config.REDMINE_PASSWORD, - requests={'verify': '/etc/ssl/certs/ca-certificates.crt'} + requests={"verify": "/etc/ssl/certs/ca-certificates.crt"}, ) - current_user = redmine.user.get('current') + current_user = redmine.user.get("current") time_entries = redmine.time_entry.filter( - user_id=current_user.id, - from_date=from_date, - to_date=to_date + user_id=current_user.id, from_date=from_date, to_date=to_date ) # Агрегируем часы по issue.id spent_time: Dict[int, float] = {} issue_ids = set() for entry in time_entries: - if hasattr(entry, 'issue') and entry.issue and hasattr(entry, 'hours'): + if hasattr(entry, "issue") and entry.issue and hasattr(entry, "hours"): iid = entry.issue.id issue_ids.add(iid) spent_time[iid] = spent_time.get(iid, 0.0) + float(entry.hours) @@ -39,12 +39,8 @@ def fetch_issues_with_spent_time(from_date: str, to_date: str) -> Optional[List[ return None # Загружаем полные объекты задач - issue_list_str = ','.join(str(i) for i in issue_ids) - issues = redmine.issue.filter( - issue_id=issue_list_str, - status_id='*', - sort='project:asc' - ) + issue_list_str = ",".join(str(i) for i in issue_ids) + issues = redmine.issue.filter(issue_id=issue_list_str, status_id="*", sort="project:asc") # Сопоставляем задачи с суммарным временем result = [] diff --git a/redmine_reporter/config.py b/redmine_reporter/config.py index 28872fe..90ef923 100644 --- a/redmine_reporter/config.py +++ b/redmine_reporter/config.py @@ -1,7 +1,6 @@ import os from dotenv import load_dotenv - load_dotenv() diff --git a/redmine_reporter/formatters/console.py b/redmine_reporter/formatters/console.py index 649c1da..072ac3b 100644 --- a/redmine_reporter/formatters/console.py +++ b/redmine_reporter/formatters/console.py @@ -8,15 +8,17 @@ class TableFormatter(Formatter): """Форматтер для вывода красивой таблицы в консоль.""" def format(self, rows: List[ReportRow]) -> str: - table_rows = [['Проект', 'Версия', 'Задача', 'Статус', 'Затрачено']] + table_rows = [["Проект", "Версия", "Задача", "Статус", "Затрачено"]] for r in rows: - table_rows.append([ - r['display_project'], - r['display_version'], - f"{r['issue_id']}. {r['subject']}", - r['status_ru'], - r['time_text'] - ]) + table_rows.append( + [ + r["display_project"], + r["display_version"], + f"{r['issue_id']}. {r['subject']}", + r["status_ru"], + r["time_text"], + ] + ) return tabulate(table_rows, headers="firstrow", tablefmt="fancy_grid") def save(self, rows: List[ReportRow], output_path: str) -> None: @@ -24,6 +26,7 @@ class TableFormatter(Formatter): # Это делается в CLI. raise NotImplementedError("TableFormatter не поддерживает сохранение в файл.") + class CompactFormatter(Formatter): """Форматтер для компактного вывода в консоль.""" diff --git a/redmine_reporter/formatters/csv.py b/redmine_reporter/formatters/csv.py index 4019c6a..26a0ee5 100644 --- a/redmine_reporter/formatters/csv.py +++ b/redmine_reporter/formatters/csv.py @@ -16,14 +16,16 @@ class CSVFormatter(Formatter): writer = csv.writer(output, dialect="excel") writer.writerow(["Project", "Version", "Issue ID", "Subject", "Status", "Spent Time"]) for r in rows: - writer.writerow([ - r["project"], - r["version"], - r["issue_id"], - r["subject"], - r["status_ru"], - r["time_text"] - ]) + writer.writerow( + [ + r["project"], + r["version"], + r["issue_id"], + r["subject"], + r["status_ru"], + r["time_text"], + ] + ) return output.getvalue() def save(self, rows: List[ReportRow], output_path: str) -> None: diff --git a/redmine_reporter/formatters/factory.py b/redmine_reporter/formatters/factory.py index 3142502..b477a04 100644 --- a/redmine_reporter/formatters/factory.py +++ b/redmine_reporter/formatters/factory.py @@ -6,7 +6,6 @@ from .markdown import MarkdownFormatter from .odt import ODTFormatter from .html import HTMLFormatter - # Словарь для сопоставления расширений файлов с классами форматтеров FORMATTER_MAP: Dict[str, Type[Formatter]] = { ".odt": ODTFormatter, diff --git a/redmine_reporter/formatters/markdown.py b/redmine_reporter/formatters/markdown.py index 714302f..5538436 100644 --- a/redmine_reporter/formatters/markdown.py +++ b/redmine_reporter/formatters/markdown.py @@ -12,7 +12,7 @@ class MarkdownFormatter(Formatter): def format(self, rows: List[ReportRow]) -> str: lines = [ "| Проект | Версия | Задача | Статус | Затрачено |", - "|--------|--------|--------|--------|-----------|" + "|--------|--------|--------|--------|-----------|", ] for r in rows: task_cell = f"{r['issue_id']}. {r['subject']}" diff --git a/redmine_reporter/formatters/odt.py b/redmine_reporter/formatters/odt.py index 83d6df0..32bcc30 100644 --- a/redmine_reporter/formatters/odt.py +++ b/redmine_reporter/formatters/odt.py @@ -26,7 +26,11 @@ class ODTFormatter(Formatter): Форматирует данные в объект OpenDocument. """ - with resources.files("redmine_reporter").joinpath("templates", "template.odt").open("rb") as f: + with ( + resources.files("redmine_reporter") + .joinpath("templates", "template.odt") + .open("rb") as f + ): doc = load(f) para_style_name = "Standard" @@ -56,7 +60,13 @@ class ODTFormatter(Formatter): # Заголовки header_row = TableRow() - for text in ["Наименование Проекта", "Номер версии*", "Задача", "Статус Готовность*", "Затрачено за отчетный период"]: + for text in [ + "Наименование Проекта", + "Номер версии*", + "Задача", + "Статус Готовность*", + "Затрачено за отчетный период", + ]: cell = TableCell(stylename=cell_style_name) cell.addElement(P(stylename=para_style_name, text=text)) header_row.addElement(cell) @@ -77,7 +87,9 @@ class ODTFormatter(Formatter): # Данные с двухуровневой группировкой и объединением ячеек for project, versions in projects.items(): - total_project_rows = sum(len(rows_for_version) for rows_for_version in versions.values()) + total_project_rows = sum( + len(rows_for_version) for rows_for_version in versions.values() + ) first_version_in_project = True for version, rows_for_version in versions.items(): @@ -137,7 +149,7 @@ class ODTFormatter(Formatter): "«Задача» - Номер по Redmine и формулировка.", "«Статус» - Актуальное состояние задачи на момент отчета. Статусы: закрыто, в работе, ожидание, решена.", "«Готовность» – Опциональное поле в процентах.", - "«Затрачено за отчетный период» - в днях или часах." + "«Затрачено за отчетный период» - в днях или часах.", ]: doc.text.addElement(P(stylename=para_style_name, text=line)) diff --git a/redmine_reporter/report_builder.py b/redmine_reporter/report_builder.py index e1f0c42..1ba655f 100644 --- a/redmine_reporter/report_builder.py +++ b/redmine_reporter/report_builder.py @@ -3,21 +3,20 @@ from redminelib.resources import Issue from .types import ReportRow from .utils import get_version, hours_to_human - STATUS_TRANSLATION = { - 'New': 'В работе', - 'In Progress': 'В работе', - 'Feedback': 'В работе', - 'Re-opened': 'В работе', - 'Code Review': 'Решена', - 'Wait Release': 'Закрыто', - 'Pending': 'Ожидание', - 'Resolved': 'Решена', - 'Testing': 'Решена', - 'Confirming': 'Ожидание', - 'Closed': 'Закрыто', - 'Rejected': 'Закрыто', - 'Frozen': 'Ожидание', + "New": "В работе", + "In Progress": "В работе", + "Feedback": "В работе", + "Re-opened": "В работе", + "Code Review": "Решена", + "Wait Release": "Закрыто", + "Pending": "Ожидание", + "Resolved": "Решена", + "Testing": "Решена", + "Confirming": "Ожидание", + "Closed": "Закрыто", + "Rejected": "Закрыто", + "Frozen": "Ожидание", } diff --git a/redmine_reporter/utils.py b/redmine_reporter/utils.py index 789dcae..d3e9b36 100644 --- a/redmine_reporter/utils.py +++ b/redmine_reporter/utils.py @@ -10,8 +10,19 @@ def get_month_name_from_range(from_date: str, to_date: str) -> str: return "Январь" months = [ - "", "Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", - "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь" + "", + "Январь", + "Февраль", + "Март", + "Апрель", + "Май", + "Июнь", + "Июль", + "Август", + "Сентябрь", + "Октябрь", + "Ноябрь", + "Декабрь", ] return months[end.month] @@ -19,7 +30,7 @@ def get_month_name_from_range(from_date: str, to_date: str) -> str: def get_version(issue) -> str: """Возвращает версию задачи или '', если не задана.""" - return str(getattr(issue, 'fixed_version', '')) + return str(getattr(issue, "fixed_version", "")) def hours_to_human(hours: float) -> str: diff --git a/tests/test_cli.py b/tests/test_cli.py index b8e3150..07cd0cf 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,11 +4,10 @@ from unittest import mock from redmine_reporter.cli import main -@mock.patch.dict("os.environ", { - "REDMINE_URL": "https://red.eltex.loc/", - "REDMINE_USER": "x", - "REDMINE_PASSWORD": "y" -}) +@mock.patch.dict( + "os.environ", + {"REDMINE_URL": "https://red.eltex.loc/", "REDMINE_USER": "x", "REDMINE_PASSWORD": "y"}, +) @mock.patch("redmine_reporter.client.fetch_issues_with_spent_time") def test_cli_smoke(mock_fetch): mock_fetch.return_value = [] diff --git a/tests/test_config.py b/tests/test_config.py index 8ca4f1c..f79b3e1 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,11 +4,10 @@ from unittest import mock from redmine_reporter.config import Config -@mock.patch.dict(os.environ, { - "REDMINE_URL": "https://red.eltex.loc/", - "REDMINE_USER": "test", - "REDMINE_PASSWORD": "secret" -}) +@mock.patch.dict( + os.environ, + {"REDMINE_URL": "https://red.eltex.loc/", "REDMINE_USER": "test", "REDMINE_PASSWORD": "secret"}, +) def test_config_valid(): Config.validate() # не должно быть исключения diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 3c42d39..c6f46aa 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -27,7 +27,7 @@ def make_fake_report_rows() -> List[ReportRow]: "issue_id": 101, "subject": "Реализовать фичу X", "status_ru": "В работе", - "time_text": "4ч 30м" + "time_text": "4ч 30м", }, { "project": "Проект A", @@ -37,7 +37,7 @@ def make_fake_report_rows() -> List[ReportRow]: "issue_id": 102, "subject": "Исправить баг Y", "status_ru": "Решена", - "time_text": "2ч" + "time_text": "2ч", }, # Проект A, v2.0 { @@ -48,7 +48,7 @@ def make_fake_report_rows() -> List[ReportRow]: "issue_id": 103, "subject": "Документация Z", "status_ru": "Ожидание", - "time_text": "1ч" + "time_text": "1ч", }, # Проект B, без версии { @@ -59,7 +59,7 @@ def make_fake_report_rows() -> List[ReportRow]: "issue_id": 201, "subject": "Обновить README", "status_ru": "Закрыто", - "time_text": "0ч" + "time_text": "0ч", }, # Проект C, v1.0 { @@ -70,7 +70,7 @@ def make_fake_report_rows() -> List[ReportRow]: "issue_id": 301, "subject": "Настроить CI", "status_ru": "В работе", - "time_text": "3ч 15м" + "time_text": "3ч 15м", }, # Проект C, v1.1 { @@ -81,7 +81,7 @@ def make_fake_report_rows() -> List[ReportRow]: "issue_id": 302, "subject": "Добавить тесты", "status_ru": "В работе", - "time_text": "5ч" + "time_text": "5ч", }, { "project": "Проект C", @@ -91,7 +91,7 @@ def make_fake_report_rows() -> List[ReportRow]: "issue_id": 303, "subject": "Рефакторинг", "status_ru": "Решена", - "time_text": "6ч 45м" + "time_text": "6ч 45м", }, ] @@ -117,7 +117,10 @@ FORMATTER_FACTORIES = [ ("compact", lambda: CompactFormatter()), ("csv", lambda: CSVFormatter()), ("markdown", lambda: MarkdownFormatter()), - ("odt", lambda: ODTFormatter(author="Тест Автор", from_date="2026-01-01", to_date="2026-01-31")), + ( + "odt", + lambda: ODTFormatter(author="Тест Автор", from_date="2026-01-01", to_date="2026-01-31"), + ), ] diff --git a/tests/test_utils.py b/tests/test_utils.py index 1b30673..4769679 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -19,6 +19,7 @@ def test_get_month_name_from_range(): def test_get_version(): class MockIssue: pass + issue_with = MockIssue() issue_with.fixed_version = "v2.5.0" assert get_version(issue_with) == "v2.5.0"