#43 — Имя файла и пути по умолчанию: - resolve_output_path() в yaml_config.py: резолвит --output с учётом output.dir, filename_template, default_format из YAML-конфига - Bare format (xlsx/odt/...) → путь по шаблону - Без расширения → автодописывается default_format (.xlsx) - Config.get_output_dir/filename/default_format() #47 — datetime precision и дедупликация: - period.last_used.from/to в YAML-конфиге и AppConfig - period.precision: date|datetime - _compute_dedup_cutoff() в cli.py: при precision=datetime вычисляет cutoff из period.last_used.to - _parse_datetime() + AND-логика дедупликации в client.py: запись исключается если created_on И updated_on < cutoff - Пропуск issues без часов после дедупликации Closes #43 Closes #47
This commit is contained in:
@@ -3,7 +3,7 @@ import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
@@ -14,7 +14,7 @@ from .client import RedmineAPIError, fetch_issues_with_spent_time
|
||||
from .config import Config
|
||||
from .formatters.factory import get_console_formatter, get_formatter_by_extension
|
||||
from .report_builder import build_grouped_report, calculate_summary
|
||||
from .yaml_config import ensure_config_dir
|
||||
from .yaml_config import ensure_config_dir, resolve_output_path
|
||||
|
||||
|
||||
def parse_date_range(date_arg: str) -> tuple[str, str]:
|
||||
@@ -99,6 +99,29 @@ def _run_init_config(config_path: str, force: bool) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _compute_dedup_cutoff() -> Optional[datetime]:
|
||||
"""Compute deduplication cutoff from config.
|
||||
|
||||
If period.precision == 'datetime' and period.last_used.to is set,
|
||||
return the datetime to filter out entries already accounted for.
|
||||
Otherwise return None.
|
||||
"""
|
||||
if Config.get_period_precision() != "datetime":
|
||||
return None
|
||||
|
||||
last_to = Config.get_last_used_to()
|
||||
if not last_to:
|
||||
return None
|
||||
|
||||
try:
|
||||
dt = datetime.fromisoformat(last_to.replace("Z", "+00:00"))
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def main(argv: Optional[List[str]] = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="redmine-reporter",
|
||||
@@ -243,6 +266,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
to_date,
|
||||
user_id=args.user_id or args.user_login or args.user_name,
|
||||
by_activity=args.by_activity,
|
||||
dedup_before=_compute_dedup_cutoff(),
|
||||
)
|
||||
except RedmineAPIError as e:
|
||||
print(f"❌ {e.message}", file=sys.stderr)
|
||||
@@ -284,7 +308,24 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
print(f" [{activity}]: {value}h", file=sys.stderr)
|
||||
|
||||
if args.output:
|
||||
output_ext = os.path.splitext(args.output)[1].lower()
|
||||
output_arg = resolve_output_path(
|
||||
args.output,
|
||||
output_dir=Config.get_output_dir(),
|
||||
filename_template=Config.get_output_filename(),
|
||||
default_format=Config.get_output_default_format(),
|
||||
author=Config.get_author(args.author),
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
)
|
||||
|
||||
if output_arg is None:
|
||||
print(
|
||||
"❌ Файл без расширения. Укажите расширение: .odt, .csv, .md, .html, .json или .xlsx",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
output_ext = os.path.splitext(output_arg)[1].lower()
|
||||
|
||||
if not output_ext:
|
||||
print(
|
||||
@@ -317,8 +358,8 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
return 1
|
||||
|
||||
try:
|
||||
formatter.save(rows, args.output)
|
||||
print(f"✅ Report saved to {args.output}")
|
||||
formatter.save(rows, output_arg)
|
||||
print(f"✅ Report saved to {output_arg}")
|
||||
except Exception as e:
|
||||
fmt = output_ext.lstrip(".").upper()
|
||||
print(f"❌ {fmt} export error: {e}", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user