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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user