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.
46 lines
1.7 KiB
Python
46 lines
1.7 KiB
Python
import csv
|
||
import io
|
||
|
||
from ..types import ReportRow
|
||
from .base import Formatter
|
||
|
||
|
||
class CSVFormatter(Formatter):
|
||
"""Форматтер для экспорта в CSV.
|
||
|
||
Использует полные значения project/version (а не display-значения с пустыми
|
||
ячейками для групп). Каждая строка CSV самодостаточна — это корректно для
|
||
табличного формата (#31). Файл сохраняется в UTF-8 с BOM (utf-8-sig) для
|
||
корректного отображения кириллицы в Microsoft Excel (#26).
|
||
"""
|
||
|
||
def __init__(self, no_time: bool = False, **_kwargs):
|
||
super().__init__()
|
||
self.no_time = no_time
|
||
|
||
def format(self, rows: list[ReportRow]) -> str:
|
||
output = io.StringIO()
|
||
writer = csv.writer(output, dialect="excel")
|
||
headers = ["Project", "Version", "Issue ID", "Subject", "Status"]
|
||
if not self.no_time:
|
||
headers.append("Spent Time")
|
||
writer.writerow(headers)
|
||
for r in rows:
|
||
time_text = r["time_text"].replace("\n", " / ")
|
||
data = [
|
||
r["project"],
|
||
r["version"],
|
||
r["issue_id"],
|
||
r["subject"],
|
||
r["status_ru"],
|
||
]
|
||
if not self.no_time:
|
||
data.append(time_text)
|
||
writer.writerow(data)
|
||
return output.getvalue()
|
||
|
||
def save(self, rows: list[ReportRow], output_path: str) -> None:
|
||
content = self.format(rows)
|
||
with open(output_path, "w", encoding="utf-8-sig", newline="") as f:
|
||
f.write(content)
|