feat: add --my-tasks mode for open assigned issues (#71)
List open Redmine issues assigned to the current (or overridden) user, sorted by priority rank (P1..P5) then version due date with a stable id tie-break. Renders an OSC 8 hyperlinked console table on stdout and a counts/hours summary line on stderr; version due dates are fetched once per unique version, unavailable versions degrade to an empty due date. Also migrates the codebase to ruff 0.17.3 defaults (PEP 585/604 annotations, targeted noqa for intentional blind excepts and CLI root logging). Behavior-neutral: 408 tests pass with -W error.
This commit is contained in:
@@ -5,17 +5,18 @@ import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
import yaml
|
||||
from dotenv import find_dotenv, load_dotenv
|
||||
|
||||
from . import __version__
|
||||
from .client import RedmineAPIError, fetch_issues_with_spent_time
|
||||
from .client import RedmineAPIError, fetch_issues_with_spent_time, fetch_open_issues
|
||||
from .config import Config
|
||||
from .formatters.factory import get_console_formatter, get_formatter_by_extension
|
||||
from .formatters.task_table import TaskTableFormatter
|
||||
from .mailer import send_report
|
||||
from .report_builder import build_grouped_report, calculate_summary
|
||||
from .task_list import build_task_rows, calculate_task_summary, format_summary_line
|
||||
from .yaml_config import ensure_config_dir, resolve_output_path, save_period_to_config
|
||||
|
||||
# Маскируем API-ключ Redmine в тексте ошибок (#55):
|
||||
@@ -208,10 +209,9 @@ def _save_and_maybe_send(
|
||||
file=sys.stderr,
|
||||
)
|
||||
else:
|
||||
known_exts = ", ".join([".odt", ".csv", ".md", ".html", ".json", ".xlsx"])
|
||||
print(
|
||||
f"❌ Неизвестный формат файла: {output_ext!r}. "
|
||||
f"Поддерживаются: {known_exts}",
|
||||
"Поддерживаются: .odt, .csv, .md, .html, .json, .xlsx",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1, False
|
||||
@@ -219,7 +219,7 @@ def _save_and_maybe_send(
|
||||
try:
|
||||
formatter.save(rows, output_arg)
|
||||
print(f"✅ Report saved to {output_arg}")
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: BLE001 -- форматёры (odfpy/openpyxl/csv) бросают произвольные типы, CLI обязан вернуть код 1 на любой сбой
|
||||
fmt = output_ext.lstrip(".").upper()
|
||||
print(f"❌ {fmt} export error: {e}", file=sys.stderr)
|
||||
return 1, False
|
||||
@@ -264,7 +264,47 @@ def _save_and_maybe_send(
|
||||
return 0, False
|
||||
|
||||
|
||||
def main(argv: Optional[List[str]] = None) -> int:
|
||||
def _run_my_tasks(args: argparse.Namespace) -> int:
|
||||
"""Режим --my-tasks: таблица открытых задач пользователя в stdout (#71).
|
||||
|
||||
Саммари печатается в stderr, а не в stdout: при перенаправлении вывода
|
||||
в файл остаётся только таблица без служебных строк.
|
||||
"""
|
||||
try:
|
||||
items = fetch_open_issues(
|
||||
user_id=args.user_id or args.user_login or args.user_name
|
||||
)
|
||||
except RedmineAPIError as e:
|
||||
print(f"❌ {_sanitize_error_text(e.message)}", file=sys.stderr)
|
||||
if (args.verbose or args.debug) and e.original is not None:
|
||||
# Точка входа CLI: root-логгер осознанно настроен через basicConfig
|
||||
# в main(), module-логгер здесь дал бы второй набор хендлеров.
|
||||
logging.exception("Original Redmine API error") # noqa: LOG015
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error: {_sanitize_error_text(str(e))}", file=sys.stderr)
|
||||
if args.verbose or args.debug:
|
||||
logging.exception("Unexpected error") # noqa: LOG015
|
||||
return 1
|
||||
|
||||
if not items:
|
||||
print("ℹ️ Открытых задач нет.", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
rows = build_task_rows(items, Config.get_status_translation())
|
||||
summary = calculate_task_summary(items)
|
||||
print(format_summary_line(summary), file=sys.stderr)
|
||||
print(file=sys.stderr)
|
||||
try:
|
||||
table = TaskTableFormatter(base_url=Config.get_redmine_url()).format(rows)
|
||||
except Exception as e: # noqa: BLE001 -- сбой рендера таблицы не должен ронять CLI трейсбеком: печатаем ошибку и возвращаем код 1
|
||||
print(f"❌ Formatting error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
print(table)
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="redmine-reporter",
|
||||
description="Generate Redmine issue report based on your time entries.",
|
||||
@@ -349,6 +389,11 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
action="store_true",
|
||||
help="Send generated report via email after saving (requires email section in config); on success advance period start to its end date",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--my-tasks",
|
||||
action="store_true",
|
||||
help="List open issues assigned to you, sorted by urgency, and exit",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
# --init-config: обработка до всего остального
|
||||
@@ -374,6 +419,25 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
return 1
|
||||
return _run_init_config(args.config_path, args.force)
|
||||
|
||||
# --my-tasks: отдельный режим, отчётное окно и файлы к нему неприменимы
|
||||
if args.my_tasks:
|
||||
report_flags = [
|
||||
args.date is not None,
|
||||
args.output,
|
||||
args.compact,
|
||||
args.summary,
|
||||
args.no_time,
|
||||
args.by_activity,
|
||||
args.commit,
|
||||
args.send,
|
||||
]
|
||||
if any(report_flags):
|
||||
print(
|
||||
"❌ --my-tasks cannot be used with report-generating flags.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
# Валидация взаимоисключающих флагов пользователя
|
||||
user_args = [args.user_id, args.user_login, args.user_name]
|
||||
if sum(bool(a) for a in user_args) > 1:
|
||||
@@ -407,6 +471,10 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
print(f"❌ Configuration error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Ветка --my-tasks: даты отчётного окна режиму не нужны.
|
||||
if args.my_tasks:
|
||||
return _run_my_tasks(args)
|
||||
|
||||
# Если --date не указан, используем дефолтный диапазон
|
||||
date_arg = args.date if args.date is not None else Config.get_default_date_range()
|
||||
try:
|
||||
@@ -425,12 +493,14 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
except RedmineAPIError as e:
|
||||
print(f"❌ {_sanitize_error_text(e.message)}", file=sys.stderr)
|
||||
if (args.verbose or args.debug) and e.original is not None:
|
||||
logging.exception("Original Redmine API error")
|
||||
# Точка входа CLI: root-логгер осознанно настроен через basicConfig
|
||||
# в main(), module-логгер здесь дал бы второй набор хендлеров.
|
||||
logging.exception("Original Redmine API error") # noqa: LOG015
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error: {_sanitize_error_text(str(e))}", file=sys.stderr)
|
||||
if args.verbose or args.debug:
|
||||
logging.exception("Unexpected error")
|
||||
logging.exception("Unexpected error") # noqa: LOG015
|
||||
return 1
|
||||
|
||||
if not issue_hours:
|
||||
@@ -554,7 +624,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
try:
|
||||
output = formatter.format(rows)
|
||||
print(output)
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: BLE001 -- консольный форматтер не должен ронять CLI: печатаем ошибку и возвращаем код 1
|
||||
print(f"❌ Formatting error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
@@ -569,7 +639,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
|
||||
try:
|
||||
save_period_to_config(args.config_path, from_str=next_from)
|
||||
except Exception as e:
|
||||
except Exception as e: # noqa: BLE001 -- сбой записи конфига не должен ронять CLI трейсбеком: сообщаем и возвращаем код 1
|
||||
print(
|
||||
f"❌ Не удалось сохранить период в конфиг: {e}",
|
||||
file=sys.stderr,
|
||||
|
||||
Reference in New Issue
Block a user