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:
Кокос Артем Николаевич
2026-10-28 15:26:41 +07:00
parent 3ce4f408ed
commit 5d1cf4ab74
27 changed files with 1255 additions and 137 deletions

View File

@@ -1,4 +1,4 @@
from typing import Dict, List, Optional, Tuple, cast
from typing import cast
from redminelib.resources import Issue
@@ -22,7 +22,7 @@ STATUS_TRANSLATION = {
}
def _format_activities(activities: Dict[str, float]) -> str:
def _format_activities(activities: dict[str, float]) -> str:
"""Форматирует разбивку по активностям в многострочный текст."""
if not activities:
return ""
@@ -32,11 +32,11 @@ def _format_activities(activities: Dict[str, float]) -> str:
def build_grouped_report(
issue_hours: List[Tuple[Issue, float, Optional[Dict[str, float]]]],
issue_hours: list[tuple[Issue, float, dict[str, float] | None]],
fill_time: bool = True,
by_activity: bool = False,
status_translation: Optional[Dict[str, str]] = None,
) -> List[ReportRow]:
status_translation: dict[str, str] | None = None,
) -> list[ReportRow]:
"""
Преобразует список задач с затраченным временем в плоский список строк отчёта,
с учётом группировки по проекту и версии (пустые ячейки для повторяющихся значений).
@@ -59,12 +59,12 @@ def build_grouped_report(
status_translation if status_translation is not None else STATUS_TRANSLATION
)
rows: List[ReportRow] = []
rows: list[ReportRow] = []
prev_project: str = ""
prev_version: str = ""
for issue, hours, *rest in issue_hours:
activities: Optional[Dict[str, float]] = rest[0] if rest else None
activities: dict[str, float] | None = rest[0] if rest else None
project = str(issue.project)
version = get_version(issue)
status_en = str(issue.status)
@@ -108,14 +108,14 @@ def build_grouped_report(
def calculate_summary(
rows: List[ReportRow],
rows: list[ReportRow],
by_activity: bool = False,
) -> Dict[str, float]:
) -> dict[str, float]:
"""Возвращает сводку: общее время, время по проектам, версиям и активностям."""
total = 0.0
by_project: Dict[str, float] = {}
by_project_version: Dict[str, float] = {}
by_activity_name: Dict[str, float] = {}
by_project: dict[str, float] = {}
by_project_version: dict[str, float] = {}
by_activity_name: dict[str, float] = {}
for r in rows:
hours = r.get("hours", 0.0)
@@ -130,7 +130,7 @@ def calculate_summary(
for name, act_hours in activities.items():
by_activity_name[name] = by_activity_name.get(name, 0.0) + act_hours
result: Dict[str, float] = {
result: dict[str, float] = {
"total": round(total, 2),
**{f"project:{k}": round(v, 2) for k, v in by_project.items()},
**{f"version:{k}": round(v, 2) for k, v in by_project_version.items()},
@@ -144,8 +144,8 @@ def calculate_summary(
def group_rows_by_project_and_version(
rows: List[ReportRow],
) -> Dict[str, Dict[str, List[ReportRow]]]:
rows: list[ReportRow],
) -> dict[str, dict[str, list[ReportRow]]]:
"""
Группирует плоский список строк отчёта в иерархию project → version → [rows].
@@ -153,7 +153,7 @@ def group_rows_by_project_and_version(
Возвращает обычный dict, сохраняющий порядок вставки (Python 3.7+).
Используется форматтерами HTML и ODT для объединения ячеек.
"""
projects: Dict[str, Dict[str, List[ReportRow]]] = {}
projects: dict[str, dict[str, list[ReportRow]]] = {}
for r in rows:
project = r["project"]
version = r["version"]