Commit Graph

49 Commits

Author SHA1 Message Date
Кокос Артем Николаевич
1683b0f893 fix: default report period to current month
Без --date, env и YAML отчёт строится за текущий месяц: начало периода —
1-е число текущего месяца, конец — сегодня (date.today()), согласовано с
fallback default_to→today из #50. Приоритеты источников
(CLI > env > .env > YAML > дефолт) и precision=datetime не затронуты.

Сам fallback уже существовал в коде (хардкод устаревшего периода был
убран ранее); коммит фиксирует поведение детерминированными тестами с
моком date.today (RED проверен регрессионным зондом: при возврате
хардкода 2025-12-19--2026-01-31 тесты падают) и обновляет README и
docs/CONFIG.md: дефолт явно описан как «текущий месяц».

Closes #56
2026-07-17 13:38:14 +07:00
Кокос Артем Николаевич
1df1194f58 refactor: remove import-time side effects
Импорт модулей больше не мутирует процесс:

- config.py: module-level load_dotenv(override=False) мутировал os.environ
  при любом импорте. Вызов перенесён в Config.load_yaml() — точку явной
  загрузки конфигурации, семантика override=False и путь по умолчанию
  сохранены. --config PATH (load_dotenv(path, override=True)) не затронут.
- mailer.py: глобальная регистрация email.charset.add_charset('utf-8', ...)
  меняла кодировку для всего процесса. Заменена на точечное применение
  8bit UTF-8 к телу письма в _utf8_text_part() — payload в UTF-8
  (surrogateescape) + Content-Transfer-Encoding: 8bit на конкретной части.
  Проверено: сериализация в байты (BytesGenerator, как в send_message)
  побайтово совпадает со старым поведением.

Тест test_env_var_takes_priority_over_dotenv адаптирован к новой точке
вызова load_dotenv; test_body_substitution переведён с as_string() на
декодированный payload (str-сериализация не-ASCII части без глобальной
мутации реестра теперь даёт base64; wire-формат 8bit сохранён и
зафиксирован test_wire_format_is_8bit_utf8).

Closes #64
2026-07-17 13:35:02 +07:00
Кокос Артем Николаевич
829f1b73fd fix: unify verify_ssl semantics across config sources
YAML verify_ssl: true раньше подставлял захардкоженный путь
/etc/ssl/certs/ca-certificates.crt, а REDMINE_VERIFY=true — bool True.
Путь отсутствует на части дистрибутивов, семантика источников различалась.

Теперь едино для YAML и env:
- true (bool/строка) → True: стандартная проверка TLS средствами requests;
- false (bool/строка) → False (+ сохраняется warning из #57);
- иная строка → путь к CA-bundle как есть.
- дефолт (значение не задано) → True вместо захардкоженного пути.

Существующие тесты на путь-от-true переписаны под новую семантику
(изменение поведения): test_verify_ssl_true_returns_default_ca_path →
test_verify_ssl_true_returns_true.

Closes #62
2026-07-17 13:28:32 +07:00
Кокос Артем Николаевич
debdede97a Merge branch 'fix/55-cli-sanitize' 2026-07-17 12:57:43 +07:00
Кокос Артем Николаевич
fb1ca1d9b8 fix: sanitize Redmine exception output in CLI
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
2026-07-17 12:51:23 +07:00
Кокос Артем Николаевич
b624a8b8c2 fix: warn when TLS verification is disabled
REDMINE_VERIFY=false / verify_ssl: false silently disabled TLS
certificate verification, exposing the connection to MITM attacks.
Config.validate() now prints a warning to stderr when verification
is disabled (exactly False; True or a CA-bundle path stays quiet).
validate() runs exactly once at CLI startup, so the warning is
emitted once per run and is visible in every scenario.

Closes #57
2026-07-17 12:50:40 +07:00
Кокос Артем Николаевич
a1febd6999 fix: require HTTPS for Redmine URL
Config.validate() now rejects REDMINE_URL values whose scheme is not
HTTPS (http://, missing scheme, or any other scheme). The API key is
sent in request headers, so a plain-HTTP endpoint would expose it.
Scheme comparison is case-insensitive per RFC 3986.

Closes #54
2026-07-17 12:48:45 +07:00
Кокос Артем Николаевич
594db90227 fix: warn on silent data loss in report
Issues not returned by issue.filter (no access / deleted) and failures
of the time entry activities lookup were silently swallowed: the report
was built from partial data without telling the user.

Now both cases emit a warning to stderr (project ⚠️ style, #28):
- skipped issue IDs and total lost hours after issue matching;
- activities lookup failure (still returns {} and falls back to
  activity names from time entries).

These are warnings, not errors: the report is still built from
available data and the exit code is unchanged.

Closes #61
2026-07-17 12:27:50 +07:00
Кокос Артем Николаевич
598f2d35a1 fix: resolve --user-login by exact match
Redmine user.filter(login=...) performs an inexact substring search, but
_resolve_user_id took users[0].id unconditionally, so a report could
silently be built for the wrong user (e.g. 'ivanov' matching 'ivanov2').

Now only exact (case-sensitive) login matches are considered: exactly one
match resolves to its id, multiple matches raise an ambiguity error,
no match falls through to the name lookup and then to a 'not found'
error — symmetric with the --user-name resolution.

Closes #60
2026-07-17 12:21:47 +07:00
Кокос Артем Николаевич
46674ba926 fix: normalize datetimes to aware UTC in dedup
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
2026-07-17 12:11:35 +07:00
Кокос Артем Николаевич
c4ec23048a fix: accept datetime ranges in parse_date_range
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
2026-07-17 11:44:13 +07:00
Кокос Артем Николаевич
d135408f5e test: strengthen #58 dedup trap to assert filtering
The trap mocked issue.filter to return only issue 2, so result == [2]
would pass even with a wrong fix that suppresses TypeError but keeps all
entries. Now the API mock returns both issues; only correct filtering
of the pre-cutoff time entry keeps the result at [2].

Refs #67, #58
2026-07-17 11:22:10 +07:00
Кокос Артем Николаевич
3a3a7bb39c test: fix cli mocks, add pytest config and coverage gaps
- 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
2026-07-17 10:59:47 +07:00
Кокос Артем Николаевич
608afe08e3 fix: default missing period.default_to to today 2026-07-10 16:33:28 +07:00
Кокос Артем Николаевич
e1862462af feat: add HTML email body for --send (#48)
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
2026-07-10 15:24:27 +07:00
Кокос Артем Николаевич
863ad50cc3 feat: add report.no_time config option for automatic modes
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
2026-07-10 14:38:07 +07:00
Кокос Артем Николаевич
0968560090 style: apply ruff format to all source files 2026-07-10 12:39:24 +07:00
Кокос Артем Николаевич
25425901b1 fix: handle save_period_to_config errors, remove duplicate _MockIssue
- 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)
2026-07-10 12:37:46 +07:00
Кокос Артем Николаевич
b0e353c565 feat: auto-email sending via SMTP (--send flag)
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
2026-07-10 12:37:02 +07:00
Кокос Артем Николаевич
9b78d66769 feat: --commit flag (#44)
- --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
2026-07-07 10:47:08 +07:00
Кокос Артем Николаевич
47152f8f04 feat: output path defaults (#43) + datetime precision & dedup (#47)
#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
2026-07-07 10:16:46 +07:00
Кокос Артем Николаевич
67350bfcd6 fix: verify_ssl: true in YAML now uses DEFAULT_REDMINE_VERIFY path
Previously boolean True was passed directly to python-redmine
which used system CA bundle instead of the project's default CA file.
Now 'true' in YAML maps to DEFAULT_REDMINE_VERIFY path.
2026-07-03 18:24:11 +07:00
Кокос Артем Николаевич
0fa4e271a7 fix: auto-load YAML config on CLI startup
CLI now reads ~/.config/redmine-reporter/config.yml automatically.
Previously Config.load_yaml() existed but was never called.
2026-07-03 18:19:30 +07:00
Кокос Артем Николаевич
b1a565bc9e feat: filename template expansion with {date} placeholder
- Add expand_filename_template() to yaml_config.py
- Supports {author}, {from}, {to}, {date}, {ext} placeholders
- {date} formats as DD_MM_YYYY (e.g. 31_03_2026)
- Spaces in author replaced with underscores for safe filenames
- Unknown placeholders left as-is
- Add comprehensive CONFIG.md documentation covering YAML setup, migration, and template syntax
2026-07-03 18:13:55 +07:00
Кокос Артем Николаевич
c962a93f30 feat: YAML config support (~/.config/redmine-reporter/config.yml)
- Add AppConfig/SmtpConfig/EmailConfig dataclasses with from_yaml()/from_env()
- Add yaml_config.py: ${VAR} resolver, 0700/0600 permission helpers
- Config.get_*() methods gain YAML fallback in priority chain
- Priority: CLI > env > .env > YAML > code defaults
- CLI --init-config generates YAML from current environment
- --force flag allows overwriting existing config
- Secrets default to ${VAR} references, plaintext allowed
- Full backward compatibility with existing .env setups

Closes #46
2026-07-03 18:00:28 +07:00
Кокос Артем Николаевич
59af7ce464 feat(activity): add --by-activity flag to break down spent time by activity type
- Load time entry activity names from Redmine enumeration.
- Aggregate hours per issue and per activity in fetch_issues_with_spent_time.
- Add --by-activity CLI flag and propagate it through report builder,
  summary, and all formatters (console, CSV, HTML, JSON, ODT).
- Keep backward-compatible 2-tuple input in build_grouped_report.
- Bump version to 1.8.0.

Closes #41
2026-06-29 16:44:52 +07:00
Кокос Артем Николаевич
f6861382e6 feat(user): report on another user's time entries
- 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
2026-06-29 15:15:17 +07:00
Кокос Артем Николаевич
f80f3a8b52 feat(errors): provide readable Redmine API error messages
- 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
2026-06-29 14:58:42 +07:00
Кокос Артем Николаевич
222d31730e feat(xlsx): make Excel export production-grade
- Add merge cells for project/version groups.
- Add numeric Hours column and human-readable Spent Time.
- Add version/project/grand totals.
- Apply auto-width, freeze panes, auto-filter and styling.
- Respect --no-time: keep columns empty, skip totals.
- Pass no_time flag through formatter factory to all file formatters.
- Add tests for XLSX features and --no-time behavior.

Closes #38
2026-06-29 14:41:37 +07:00
Кокос Артем Николаевич
86f083aa79 chore(release): bump version to 1.6.0
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.
2026-06-29 12:38:54 +07:00
Кокос Артем Николаевич
a82be05b83 fix: handle empty time entries and add missing mypy override for openpyxl
- 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
2026-06-29 12:21:44 +07:00
Кокос Артем Николаевич
ca89832d74 feat: JSON, Excel export and time summary
- #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
2026-06-29 12:09:58 +07:00
Кокос Артем Николаевич
f6afc4096d feat(cli): dynamic default range, --version, --verbose, --debug, --url, --api-key, --config
- #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
2026-06-29 12:02:51 +07:00
Кокос Артем Николаевич
738d9d543e fix: mount retry adapter on Redmine engine session
python-redmine exposes  as a context manager, while the
actual requests.Session lives in . Mounting the
retry adapter on the wrong object caused:

    'function' object has no attribute 'mount'

on startup. Update the related test to mock the real session.
2026-06-29 11:53:09 +07:00
Артём Кокос
58fa5a7ab4 Add retry/backoff, timeout, and issue_id chunking to Redmine client
- #24: Mount HTTPAdapter with Retry(total=3, backoff_factor=0.5) for
  HTTP 429/500/502/503/504 on the Redmine session, set 30s timeout
- #21: Split large issue_id lists into chunks of 100 to avoid exceeding
  URL length limits on reverse proxies

Closes #24, #21
2026-06-27 13:06:41 +07:00
Артём Кокос
14219564dd Fix 9 bugs: ODT covered-cells, CSV BOM, HTML charset, stderr, dead code
- #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
2026-06-27 13:01:32 +07:00
Артём Кокос
da069993b9 Extract grouping logic into shared function (#19)
Add group_rows_by_project_and_version() to report_builder.py and replace
the duplicated inline grouping in both html.py and odt.py with a single
call to the shared function. This ensures consistent grouping behavior
across formatters and removes a maintenance burden — any change to
grouping logic now happens in one place.

Closes #19
2026-06-26 00:21:58 +07:00
Артём Кокос
dbc4cf960a Fix version display, deterministic sort, remove duplicate sort
get_version() now returns version.name instead of str(version), which
returned the numeric ID from redminelib.resources.Version. Falls back
to str() when .name is absent (#14).

Add issue.id as tertiary sort key in build_grouped_report so tasks
within the same project+version group always appear in the same order
regardless of API response ordering (#33).

Remove redundant sort from client.fetch_issues_with_spent_time — the
sort already runs in report_builder.build_grouped_report, so doing it
twice wastes CPU. Also remove the now-unused get_version import (#32).

Closes #14, closes #32, closes #33
2026-06-25 23:32:14 +07:00
Артём Кокос
3a6d1b7ba7 Fix .env override priority and password whitespace stripping
Change load_dotenv() to load_dotenv(override=False) so real environment
variables always take priority over stale .env files. This prevents a
hardcoded or outdated REDMINE_API_KEY in .env from silently overriding
a fresh token exported in the shell (#15).

Add .strip() to get_redmine_password() for consistency with all other
config getters — trailing whitespace in .env no longer causes silent
auth failures (#35).

Closes #15, closes #35
2026-06-25 21:06:19 +07:00
Артём Кокос
3956decd4e Fix package structure and lazy-import ODT formatter
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
2026-06-25 20:42:13 +07:00
Кокос Артем Николаевич
2db0ab1f0b Tighten configuration and export handling 2026-05-22 17:41:56 +07:00
Кокос Артем Николаевич
8bc8181ce3 Add Redmine API token authentication 2026-05-22 17:19:00 +07:00
Artem Kokos
7bc6e024c0 Quick fixes & tests 2026-03-28 23:55:46 +07:00
Кокос Артем Николаевич
06cd57e2c4 Blacked 2026-02-05 15:31:31 +07:00
Кокос Артем Николаевич
355849e004 Fix issue mock for tests 2026-01-26 12:37:57 +07:00
Артём Кокос
d839be8776 Add unit-tests 2026-01-25 12:40:23 +07:00
Артём Кокос
1f77088c21 Add tests for all formatters 2026-01-25 11:19:04 +07:00
Artem Kokos
bca24189c7 ODT table support 2026-01-21 14:22:18 +07:00
Кокос Артем Николаевич
e412bb7446 Initial commit 2026-01-20 09:56:25 +07:00