Redmine/requests exceptions may embed the request URL containing the
API key (?key=SECRET) or the X-Redmine-API-Key header value. The CLI
printed the raw exception text to stderr, leaking the key into
terminals and logs.
Add _sanitize_error_text() which masks 'key=<value>' query params and
the X-Redmine-API-Key header as 'key=***' / 'X-Redmine-API-Key: ***',
and apply it to every exception-derived message printed to stderr
(fetch errors in main() and SMTP errors in _save_and_maybe_send()).
Full traceback with original exception details is now available under
both --verbose and --debug (previously only --debug, only for
RedmineAPIError). Exit codes and error-handling structure unchanged.
Closes#55
Dedup with precision=datetime crashed with TypeError: redminelib returns
naive created_on/updated_on while the cutoff from _compute_dedup_cutoff
is aware UTC. Normalize at a single point: _parse_datetime now always
returns an aware datetime (naive treated as UTC), matching its docstring.
The dedup cutoff is normalized the same way, and any residual comparison
TypeError is wrapped in RedmineAPIError instead of leaking raw.
Also fix --commit saving last_used.to as naive local time; it now stores
aware UTC (datetime.now(timezone.utc)) so the next run computes a correct
aware cutoff.
Closes#58
With period.dynamic: true and period.precision: datetime, running
without --date crashed: compute_next_period() returns a datetime range
(YYYY-MM-DDTHH:MM:SS), which parse_date_range() rejected with a
fullmatch against YYYY-MM-DD only.
parse_date_range now accepts YYYY-MM-DDTHH:MM:SS--YYYY-MM-DDTHH:MM:SS
in addition to plain date ranges, returning datetime strings unchanged
(pass-through, matching compute_next_period output). Mixed-precision
ranges, invalid times and reversed ranges are still rejected with the
existing error messages; date-range behavior is unchanged.
Closes#59
- tests/test_cli.py: fix 26 fetch_issues_with_spent_time mocks to return
3-element tuples (issue, hours, activities) matching the real signature
- pyproject.toml: add [tool.pytest.ini_options] with testpaths and
pythonpath so bare pytest works
- tests/test_client.py: add pagination test (>100 time entries arriving
in pages, hours aggregated across all pages)
- tests/test_formatters.py: add structural tests — XLSX full header row
and grand total row via openpyxl, HTML thead with all columns and
tbody row count
- add strict xfail trap-tests for known bugs: #58 (naive created_on vs
aware dedup cutoff TypeError) and #59 (parse_date_range rejects
datetime range from dynamic+datetime period)
Closes#67
Add email.html config flag (default false). When enabled, --send
includes an HTML version of the report body generated via HTMLFormatter
alongside the plain-text part in a multipart/alternative message.
- EmailConfig gains html: bool field
- mailer.build_message/send_report accept rows for HTML generation
- CLI passes rows to send_report
- --init-config generates email.html: false
- README.md and docs/CONFIG.md updated
Bump version to 1.10.0.
Closes#48
Add YAML section `report.no_time` that controls `--no-time` behavior
in automatic modes (`--commit`, `--send`). CLI flag `--no-time`
always wins. Manual `--output` ignores the YAML setting.
- Config.get_report_no_time() reads the YAML value (defaults to false)
- cli._resolve_no_time() encapsulates CLI > YAML priority
- --init-config now generates the report section
- docs updated in README.md and docs/CONFIG.md
Closes#49
- cli.py: wrap save_period_to_config in try/except to avoid
unhandled traceback on disk full / permission denial (I2)
- test_cli.py: remove duplicate _MockIssue class definition (I3)
Closes#45
- New redmine_reporter/mailer.py: SMTP email sending with
{author}/{period} template substitution, MIME attachment
with correct content-type per file extension
- Config.get_email_config(): returns EmailConfig from YAML
or None when not configured
- CLI --send flag: sends report after generation, works with
--output, --commit, or standalone (saves to template path)
- 31 new tests (22 mailer + 6 CLI + 3 config)
- 249/249 tests passing, ruff clean, mypy clean
- --commit: после генерации отчёта сохраняет период в
period.last_used.from/to YAML-конфига
- При period.dynamic=true следующий запуск вычисляет период от last_used:
полный месяц → следующий месяц, произвольный диапазон → та же длина
- При period.dynamic=false перезаписывает default_from/default_to
- При period.precision=datetime сохраняет timestamps с точностью до секунд
- --commit без --output сохраняет отчёт по шаблону из конфига
- compute_next_period() в config.py
- save_period_to_config() в yaml_config.py
- 23 новых теста (7 CLI, 6 config, 6 yaml, 4 date_range)
Closes#44
#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#43Closes#47
- Add user_id parameter to fetch_issues_with_spent_time().
- Support numeric ID, login, or full name resolution.
- Reject ambiguous names and unknown users with clear messages.
- Add CLI flags: --user-id, --user-login, --user-name.
- Only allow one user flag at a time.
- Add ResourceNotFoundError handling.
- Update README with usage examples.
- Add tests for user resolution and CLI flags.
Closes#40
- Introduce RedmineAPIError with human-friendly messages.
- Distinguish AuthError, ForbiddenError, HTTP status codes, timeouts
and connection errors in client.py.
- Update CLI to print the readable message instead of generic
"Redmine API error: ...".
- Log original exception with traceback when --debug is enabled.
- Add tests for all error paths and CLI output.
Closes#39
Bump version from 1.5.0 to 1.6.0 in __init__.py and pyproject.toml.
Update test_cli_version_flag to assert against the current package
version instead of a hardcoded string.
- Treat empty list from fetch_issues_with_spent_time as 'no entries' and exit 0
instead of crashing later in the pipeline.
- Update CLI output-extension tests to use non-empty mock data so they actually
reach the extension validation code path.
- Add openpyxl to mypy ignore_missing_imports overrides.
Closes#36
- #23: add JSONFormatter and XLSXFormatter
- add openpyxl dependency for .xlsx export
- #22: add --summary flag and calculate_summary() in report_builder
- ReportRow now carries raw hours for summary calculations
- update CLI help and README with .json/.xlsx formats and --summary
- add tests for new formatters and summary computation
Closes#22, closes#23
- #17: default date range falls back to current month when .env dates are missing
- #18: add --version, --verbose, --debug flags
- #20: add --url, --api-key, --config CLI overrides
- Config supports CLI overrides for URL/API key and explicit config file loading
- Update README with new CLI options
- Add tests for new flags and config overrides
Closes#17, closes#18, closes#20
- #13 (critical): Add CoveredTableCell elements to ODT for valid row spans (ODF 1.2)
- #28: Move "Total issues" info message from stdout to stderr (clean pipe output)
- #27: Wrap HTML export in full document with DOCTYPE and meta charset utf-8
- #26: Save CSV with utf-8-sig encoding (UTF-8 BOM for Excel compatibility)
- #31: Document CSV uses full project/version values (not display_* like console/MD)
- #30: Fix ODT header formatting when author is empty (no leading dot/space)
- #36: Remove test_cli_smoke_empty testing unreachable code path (return [])
- #37: Remove unused mock_path variable in ODT test fixture
- #34: Remove unreachable len(parts) != 2 check in parse_date_range
Closes#13, #28, #27, #26, #31, #30, #36, #37, #34
Add missing __init__.py to formatters/ so setuptools.find_packages
includes the subpackage in wheel/sdist builds (#16).
Move ODTFormatter import from top-level to lazy import inside
get_formatter_by_extension() so missing odfpy no longer crashes module
load before main() runs. Remove dead except ImportError handler in
cli.py save() block; surface a clear 'odfpy is not installed' message
when the formatter factory returns None for .odt (#25).
Closes#16, closes#25