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

@@ -4,7 +4,6 @@ import sys
from dataclasses import dataclass, field
from datetime import date
from pathlib import Path
from typing import Dict, Union
import yaml
from dotenv import load_dotenv
@@ -13,7 +12,7 @@ from .yaml_config import check_file_permissions, resolve_env_vars
logger = logging.getLogger(__name__)
DEFAULT_REDMINE_VERIFY: Union[bool, str] = True
DEFAULT_REDMINE_VERIFY: bool | str = True
FALSE_VALUES = {"0", "false", "no", "off"}
TRUE_VALUES = {"1", "true", "yes", "on"}
@@ -45,17 +44,17 @@ class AppConfig:
redmine_url: str = ""
redmine_api_key: str = ""
redmine_author: str = ""
redmine_verify: Union[bool, str] = DEFAULT_REDMINE_VERIFY
redmine_verify: bool | str = DEFAULT_REDMINE_VERIFY
period_from: str = ""
output_dir: str = ""
output_filename: str = "{author}_{from}_{to}.{ext}"
output_default_format: str = "xlsx"
report_no_time: bool = False
report_status_translation: Dict[str, str] = field(default_factory=dict)
report_status_translation: dict[str, str] = field(default_factory=dict)
email: EmailConfig = field(default_factory=EmailConfig)
@classmethod
def from_yaml(cls, path: Union[str, Path]) -> "AppConfig":
def from_yaml(cls, path: str | Path) -> "AppConfig":
"""Загружает настройки из YAML-файла.
Если файл не существует — возвращает конфиг со значениями по умолчанию.
@@ -118,7 +117,7 @@ class AppConfig:
return str(value)
@staticmethod
def _resolve_str_dict(raw: dict, section: str, key: str) -> Dict[str, str]:
def _resolve_str_dict(raw: dict, section: str, key: str) -> dict[str, str]:
"""Читает вложенный mapping строк из YAML-секции.
Не-mapping значение логируется с warning и игнорируется.
@@ -140,7 +139,7 @@ class AppConfig:
type(value).__name__,
)
return {}
result: Dict[str, str] = {}
result: dict[str, str] = {}
for k, v in value.items():
if v is None or isinstance(v, (dict, list)):
logger.warning(
@@ -168,7 +167,7 @@ class AppConfig:
return False
@classmethod
def _resolve_verify(cls, raw: dict) -> Union[bool, str]:
def _resolve_verify(cls, raw: dict) -> bool | str:
"""Семантика verify_ssl, единая с REDMINE_VERIFY из env:
- true (bool или строка) → True — стандартная проверка TLS (requests);
@@ -314,7 +313,7 @@ class Config:
return os.getenv("REDMINE_PASSWORD", "").strip()
@classmethod
def get_redmine_verify(cls) -> Union[bool, str]:
def get_redmine_verify(cls) -> bool | str:
value = os.getenv("REDMINE_VERIFY", "").strip()
if value:
normalized = value.lower()
@@ -364,7 +363,7 @@ class Config:
return False
@classmethod
def get_status_translation(cls) -> Dict[str, str]:
def get_status_translation(cls) -> dict[str, str]:
"""Полный словарь перевода статусов: встроенный + переопределения из YAML.
Возвращает новый словарь — модульный STATUS_TRANSLATION не мутируется.
@@ -413,8 +412,7 @@ class Config:
# Будущая start инвертирует диапазон и ломает фильтр Redmine,
# поэтому сжимаем окно до сегодняшнего дня.
if start > today:
start = today
start = min(start, today)
return f"{start.isoformat()}--{today.isoformat()}"
@classmethod