From 2a39de467f2dd51c3844af2b6197833f49488a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D0=BA=D0=BE=D1=81=20=D0=90=D1=80=D1=82=D0=B5?= =?UTF-8?q?=D0=BC=20=D0=9D=D0=B8=D0=BA=D0=BE=D0=BB=D0=B0=D0=B5=D0=B2=D0=B8?= =?UTF-8?q?=D1=87?= Date: Thu, 22 Jan 2026 16:42:39 +0700 Subject: [PATCH] Add CSV format support Closes #5 --- .gitignore | 3 ++- redmine_reporter/cli.py | 39 +++++++++++++++++++----------- redmine_reporter/formatter_csv.py | 40 +++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 15 deletions(-) create mode 100644 redmine_reporter/formatter_csv.py diff --git a/.gitignore b/.gitignore index 1bcc252..a65102e 100644 --- a/.gitignore +++ b/.gitignore @@ -87,5 +87,6 @@ secrets.json *.bak # Just in case +.~* report.odt -.~lock.*.odt# +report.csv diff --git a/redmine_reporter/cli.py b/redmine_reporter/cli.py index 538f813..aee2f97 100644 --- a/redmine_reporter/cli.py +++ b/redmine_reporter/cli.py @@ -7,6 +7,7 @@ from .config import Config from .client import fetch_issues_with_spent_time from .formatter import format_compact, format_table from .formatter_odt import format_odt +from .formatter_csv import format_csv def parse_date_range(date_arg: str) -> tuple[str, str]: @@ -75,25 +76,35 @@ def main(argv: Optional[List[str]] = None) -> int: print(f"✅ Total issues: {len(issue_hours)} [{args.date}]") if args.output: - if not args.output.endswith(".odt"): - print("❌ Output file must end with .odt", file=sys.stderr) + if not (args.output.endswith(".odt") or args.output.endswith(".csv")): + print("❌ Output file must end with .odt or .csv", file=sys.stderr) return 1 - try: - doc = format_odt( - issue_hours, - author=Config.get_author(args.author), - from_date=from_date, - to_date=to_date, - fill_time=not args.no_time - ) - doc.save(args.output) + try: + if args.output.endswith(".odt"): + doc = format_odt( + issue_hours, + author=Config.get_author(args.author), + from_date=from_date, + to_date=to_date, + fill_time=not args.no_time + ) + doc.save(args.output) + elif args.output.endswith(".csv"): + csv_content = format_csv(issue_hours, fill_time=not args.no_time) + with open(args.output, "w", encoding="utf-8", newline="") as f: + f.write(csv_content) + print(f"✅ Report saved to {args.output}") - except ImportError: - print("❌ odfpy is not installed. Install with: pip install odfpy", file=sys.stderr) + except ImportError as e: + if args.output.endswith(".odt"): + print("❌ odfpy is not installed. Install with: pip install odfpy", file=sys.stderr) + else: + print(f"❌ Import error: {e}", file=sys.stderr) return 1 except Exception as e: - print(f"❌ ODT export error: {e}", file=sys.stderr) + fmt = "ODT" if args.output.endswith(".odt") else "CSV" + print(f"❌ {fmt} export error: {e}", file=sys.stderr) return 1 else: try: diff --git a/redmine_reporter/formatter_csv.py b/redmine_reporter/formatter_csv.py new file mode 100644 index 0000000..ec68724 --- /dev/null +++ b/redmine_reporter/formatter_csv.py @@ -0,0 +1,40 @@ +import csv +import io +from typing import List, Tuple +from redminelib.resources import Issue +from .formatter import get_version, hours_to_human, STATUS_TRANSLATION + + +def format_csv( + issue_hours: List[Tuple[Issue, float]], + fill_time: bool = True, + dialect: str = "excel" +) -> str: + """ + Formats the list of issues with spent time into CSV. + Returns a string containing the CSV content. + """ + + output = io.StringIO() + writer = csv.writer(output, dialect=dialect) + + # Header + writer.writerow(["Project", "Version", "Issue ID", "Subject", "Status", "Spent Time"]) + + for issue, hours in issue_hours: + project = str(issue.project) + version = get_version(issue) + status_en = str(issue.status) + status_ru = STATUS_TRANSLATION.get(status_en, status_en) + time_text = hours_to_human(hours) if fill_time else "" + + writer.writerow([ + project, + version, + issue.id, + issue.subject, + status_ru, + time_text + ]) + + return output.getvalue()