bitranox-template-py-cli 2.1.3__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/CHANGELOG.md +13 -0
  2. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/PKG-INFO +1 -1
  3. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/docs/systemdesign/module_reference.md +23 -26
  4. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/pyproject.toml +1 -1
  5. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/__init__conf__.py +1 -1
  6. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/commands/email/_common.py +4 -8
  7. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/commands/email/send_email.py +5 -0
  8. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/commands/email/send_notification.py +0 -2
  9. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/conftest.py +12 -0
  10. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_config.py +22 -1
  11. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_email.py +39 -0
  12. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_email_password_secrecy.py +2 -5
  13. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_permission_defaults.py +1 -3
  14. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.devcontainer/devcontainer.json +0 -0
  15. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.devcontainer/settings.json +0 -0
  16. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.env.example +0 -0
  17. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.gitattributes +0 -0
  18. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.github/actions/extract-metadata/action.yml +0 -0
  19. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.github/dependabot.yml +0 -0
  20. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.github/workflows/codeql.yml +0 -0
  21. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.github/workflows/default_cicd_public.yml +0 -0
  22. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.github/workflows/default_release_public.yml +0 -0
  23. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.gitignore +0 -0
  24. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.qlty/qlty.toml +0 -0
  25. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.snyk +0 -0
  26. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/CONFIG.md +0 -0
  27. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/CONTRIBUTING.md +0 -0
  28. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/DEVELOPMENT.md +0 -0
  29. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/INSTALL.md +0 -0
  30. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/LICENSE +0 -0
  31. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/Makefile +0 -0
  32. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/README.md +0 -0
  33. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/SECURITY.md +0 -0
  34. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/ai-stance.md +0 -0
  35. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/ai-transparency.md +0 -0
  36. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/codecov.yml +0 -0
  37. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/docs/adr/0001-memory-adapters-in-src.md +0 -0
  38. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/notebooks/Quickstart.ipynb +0 -0
  39. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/rename.sh +0 -0
  40. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/rename_dry.sh +0 -0
  41. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/reset_git_history.sh +0 -0
  42. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/__init__.py +0 -0
  43. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/__main__.py +0 -0
  44. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/__init__.py +0 -0
  45. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/__init__.py +0 -0
  46. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/commands/__init__.py +0 -0
  47. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/commands/config.py +0 -0
  48. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/commands/email/__init__.py +0 -0
  49. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/commands/info.py +0 -0
  50. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/commands/logging.py +0 -0
  51. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/config_load.py +0 -0
  52. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/constants.py +0 -0
  53. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/context.py +0 -0
  54. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/exit_codes.py +0 -0
  55. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/main.py +0 -0
  56. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/py.typed +0 -0
  57. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/root.py +0 -0
  58. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/safe_console.py +0 -0
  59. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/cli/typed_click.py +0 -0
  60. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/config/__init__.py +0 -0
  61. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/40-layered-config.toml +0 -0
  62. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/50-mail.toml +0 -0
  63. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/90-logging.toml +0 -0
  64. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/config/defaultconfig.toml +0 -0
  65. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/config/deploy.py +0 -0
  66. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/config/display.py +0 -0
  67. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/config/loader.py +0 -0
  68. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/config/overrides.py +0 -0
  69. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/config/py.typed +0 -0
  70. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/email/__init__.py +0 -0
  71. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/email/config.py +0 -0
  72. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/email/py.typed +0 -0
  73. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/email/sender.py +0 -0
  74. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/email/transport.py +0 -0
  75. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/email/validation.py +0 -0
  76. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/logging/__init__.py +0 -0
  77. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/logging/py.typed +0 -0
  78. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/logging/setup.py +0 -0
  79. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/memory/__init__.py +0 -0
  80. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/memory/config.py +0 -0
  81. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/memory/email.py +0 -0
  82. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/memory/logging.py +0 -0
  83. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/py.typed +0 -0
  84. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/application/__init__.py +0 -0
  85. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/application/ports.py +0 -0
  86. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/application/py.typed +0 -0
  87. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/composition/__init__.py +0 -0
  88. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/composition/py.typed +0 -0
  89. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/domain/__init__.py +0 -0
  90. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/domain/behaviors.py +0 -0
  91. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/domain/enums.py +0 -0
  92. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/domain/errors.py +0 -0
  93. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/domain/py.typed +0 -0
  94. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/entry.py +0 -0
  95. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/py.typed +0 -0
  96. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_behaviors.py +0 -0
  97. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cache_effectiveness.py +0 -0
  98. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_attachment_refused.py +0 -0
  99. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_config_errors.py +0 -0
  100. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_core.py +0 -0
  101. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_email_config_errors.py +0 -0
  102. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_env_file.py +0 -0
  103. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_exit_codes.py +0 -0
  104. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_main_exit.py +0 -0
  105. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_overrides.py +0 -0
  106. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_validation.py +0 -0
  107. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_config_overrides.py +0 -0
  108. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_declared_dependencies.py +0 -0
  109. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_deploy_mode_safety.py +0 -0
  110. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_deploy_permissions.py +0 -0
  111. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_display.py +0 -0
  112. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_email_attachment_lists.py +0 -0
  113. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_email_config_translation.py +0 -0
  114. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_email_shipped_defaults.py +0 -0
  115. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_enums.py +0 -0
  116. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_errors.py +0 -0
  117. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_logging.py +0 -0
  118. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_logging_dotenv_isolation.py +0 -0
  119. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_mail.py +0 -0
  120. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_memory_logging.py +0 -0
  121. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_metadata.py +0 -0
  122. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_metadata_sync.py +0 -0
  123. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_module_entry.py +0 -0
  124. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_module_reference_sync.py +0 -0
  125. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_ports.py +0 -0
  126. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_property_email.py +0 -0
  127. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_property_overrides.py +0 -0
  128. {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_safe_console.py +0 -0
@@ -6,6 +6,19 @@ the [Keep a Changelog](https://keepachangelog.com/) format.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.2.0] 2026-10-08 13:56:49
10
+
11
+ ### Removed
12
+ - **`send-notification --raise-on-missing-attachments` (usage change).** A notification takes no
13
+ attachments, so the switch did nothing there. `send-notification` now refuses it as an unknown
14
+ option (exit 2); `send-email` keeps it. Drop the flag from any script that passes it to
15
+ `send-notification`.
16
+
17
+ ### Tests
18
+ - The `config-deploy --force` report for a deploy in which every target file is already identical
19
+ is covered end to end. New shared fixture `user_layer_in_tmp_path` points every OS's user-layer
20
+ location at `tmp_path`; it replaces two hand-written copies of the same loop.
21
+
9
22
  ## [2.1.3] 2026-10-08 11:57:40
10
23
 
11
24
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: bitranox_template_py_cli
3
- Version: 2.1.3
3
+ Version: 2.2.0
4
4
  Summary: Template CLI application with configuration management and structured logging
5
5
  Project-URL: Homepage, https://github.com/bitranox/bitranox_template_py_cli
6
6
  Project-URL: Repository, https://github.com/bitranox/bitranox_template_py_cli.git
@@ -251,22 +251,18 @@ Send email using configured SMTP settings.
251
251
 
252
252
  Send simple plain-text notification email.
253
253
 
254
- | Option | Description |
255
- |----------------------------------------------------------------------|--------------------------------------|
256
- | `--to ADDRESS` | Recipient (repeatable) |
257
- | `--subject TEXT` | Subject - required |
258
- | `--message TEXT` | Message - required |
259
- | `--from ADDRESS` | Override sender |
260
- | `--smtp-host HOST:PORT` | Override SMTP host (repeatable) |
261
- | `--smtp-username USER` | Override username |
262
- | `--smtp-password PASS` | Override password |
263
- | `--use-starttls / --no-use-starttls` | Override STARTTLS |
264
- | `--timeout SECONDS` | Override timeout |
265
- | `--raise-on-missing-attachments / --no-raise-on-missing-attachments` | Override missing-attachment handling |
266
- | `--raise-on-invalid-recipient / --no-raise-on-invalid-recipient` | Override invalid-recipient handling |
267
-
268
- `--raise-on-missing-attachments` is accepted for symmetry with `send-email`, but a notification
269
- carries no attachments, so it has no effect.
254
+ | Option | Description |
255
+ |------------------------------------------------------------------|-------------------------------------|
256
+ | `--to ADDRESS` | Recipient (repeatable) |
257
+ | `--subject TEXT` | Subject - required |
258
+ | `--message TEXT` | Message - required |
259
+ | `--from ADDRESS` | Override sender |
260
+ | `--smtp-host HOST:PORT` | Override SMTP host (repeatable) |
261
+ | `--smtp-username USER` | Override username |
262
+ | `--smtp-password PASS` | Override password |
263
+ | `--use-starttls / --no-use-starttls` | Override STARTTLS |
264
+ | `--timeout SECONDS` | Override timeout |
265
+ | `--raise-on-invalid-recipient / --no-raise-on-invalid-recipient` | Override invalid-recipient handling |
270
266
 
271
267
  **Exit codes:** 0, 2 (usage error), 22 (invalid option value), 69 (SMTP failure), 78 (no SMTP hosts, an invalid `[email]` section, or configuration not loadable)
272
268
 
@@ -425,16 +421,17 @@ Use `composition.build_testing()` to wire all in-memory adapters. It wires the r
425
421
 
426
422
  ### Test Fixtures (conftest.py)
427
423
 
428
- | Fixture | Purpose |
429
- |---------------------------|-----------------------------------------------------------------------------------------------------------------------|
430
- | `config_factory` | Creates real `Config` instances from test data |
431
- | `inject_config` | Injects config into CLI path |
432
- | `cli_runner` | Fresh `CliRunner` per test |
433
- | `strip_ansi` | Strips ANSI escape codes from output |
434
- | `clear_config_cache` | Clears LRU cache before tests |
435
- | `managed_traceback_state` | Resets/restores traceback configuration |
436
- | `isolated_logging_state` | Autouse: after every test, shuts the lib_log_rich runtime down and restores the root logger; yields that restore step |
424
+ | Fixture | Purpose |
425
+ |---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
426
+ | `config_factory` | Creates real `Config` instances from test data |
427
+ | `inject_config` | Injects config into CLI path |
428
+ | `cli_runner` | Fresh `CliRunner` per test |
429
+ | `strip_ansi` | Strips ANSI escape codes from output |
430
+ | `clear_config_cache` | Clears LRU cache before tests |
431
+ | `managed_traceback_state` | Resets/restores traceback configuration |
432
+ | `isolated_logging_state` | Autouse: after every test, shuts the lib_log_rich runtime down and restores the root logger; yields that restore step |
433
+ | `user_layer_in_tmp_path` | Points every OS's user-layer location (`HOME`, `USERPROFILE`, `XDG_CONFIG_HOME`, `APPDATA`, `LOCALAPPDATA`) at `tmp_path` and returns it, so a real deploy writes there |
437
434
 
438
435
  ---
439
436
 
440
- **Last Updated:** 2026-10-06 (file index, CLI options and public names brought in line with the source tree)
437
+ **Last Updated:** 2026-10-08 (`send-notification` options, test fixtures)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bitranox_template_py_cli"
3
- version = "2.1.3"
3
+ version = "2.2.0"
4
4
  description = "Template CLI application with configuration management and structured logging"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -40,7 +40,7 @@ name = "bitranox_template_py_cli"
40
40
  #: Human-readable summary shown in CLI help output.
41
41
  title = "Template CLI application with configuration management and structured logging"
42
42
  #: Current release version pulled from ``pyproject.toml`` by automation.
43
- version = "2.1.3"
43
+ version = "2.2.0"
44
44
  #: Repository homepage presented to users.
45
45
  homepage = "https://github.com/bitranox/bitranox_template_py_cli"
46
46
  #: Author attribution surfaced in CLI output.
@@ -84,9 +84,10 @@ def smtp_config_options(func: Callable[..., Any]) -> Callable[..., Any]:
84
84
  """Apply shared SMTP configuration override options to a Click command.
85
85
 
86
86
  Adds CLI flags for the SMTP connection and delivery settings (hosts, credentials,
87
- STARTTLS, timeout, the two raise_on_* switches). ``starttls_verify``,
88
- ``local_hostname``, ``delivery_deadline``, ``recipient_max_count`` and the attachment
89
- settings have no flag; set them with ``--set email.<key>=...``.
87
+ STARTTLS, timeout, ``raise_on_invalid_recipient``). ``send-email`` adds its own
88
+ ``--raise-on-missing-attachments`` switch, since only it takes attachments.
89
+ ``starttls_verify``, ``local_hostname``, ``delivery_deadline``, ``recipient_max_count``
90
+ and the attachment settings have no flag; set them with ``--set email.<key>=...``.
90
91
  """
91
92
  options = [
92
93
  option(
@@ -100,11 +101,6 @@ def smtp_config_options(func: Callable[..., Any]) -> Callable[..., Any]:
100
101
  option("--smtp-password", default=None, help="Override SMTP authentication password"),
101
102
  option("--use-starttls/--no-use-starttls", default=None, help="Override STARTTLS setting"),
102
103
  option("--timeout", "timeout", type=float, default=None, help="Override socket timeout in seconds"),
103
- option(
104
- "--raise-on-missing-attachments/--no-raise-on-missing-attachments",
105
- default=None,
106
- help="Override missing attachment handling",
107
- ),
108
104
  option(
109
105
  "--raise-on-invalid-recipient/--no-raise-on-invalid-recipient",
110
106
  default=None,
@@ -73,6 +73,11 @@ def _log_send_email_start(
73
73
  type=click.Path(path_type=str),
74
74
  help="File to attach (can specify multiple)",
75
75
  )
76
+ @option(
77
+ "--raise-on-missing-attachments/--no-raise-on-missing-attachments",
78
+ default=None,
79
+ help="Override missing attachment handling",
80
+ )
76
81
  @smtp_config_options
77
82
  @click.pass_context
78
83
  def cli_send_email(
@@ -53,7 +53,6 @@ def cli_send_notification(
53
53
  smtp_password: str | None,
54
54
  use_starttls: bool | None,
55
55
  timeout: float | None,
56
- raise_on_missing_attachments: bool | None,
57
56
  raise_on_invalid_recipient: bool | None,
58
57
  ) -> None:
59
58
  """Send a simple plain-text notification email.
@@ -77,7 +76,6 @@ def cli_send_notification(
77
76
  smtp_password=smtp_password,
78
77
  smtp_use_starttls=use_starttls,
79
78
  smtp_timeout=timeout,
80
- raise_on_missing_attachments=raise_on_missing_attachments,
81
79
  raise_on_invalid_recipient=raise_on_invalid_recipient,
82
80
  )
83
81
  try:
@@ -212,6 +212,18 @@ def production_factory() -> Callable[[], AppServices]:
212
212
  return build_production
213
213
 
214
214
 
215
+ @pytest.fixture
216
+ def user_layer_in_tmp_path(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
217
+ """Point every OS's user-layer location at ``tmp_path`` and return it.
218
+
219
+ A deploy then writes under ``tmp_path`` rather than into the real user configuration
220
+ directory, and the developer's own user-layer files cannot decide a result.
221
+ """
222
+ for name in ("HOME", "USERPROFILE", "XDG_CONFIG_HOME", "APPDATA", "LOCALAPPDATA"):
223
+ monkeypatch.setenv(name, str(tmp_path))
224
+ return tmp_path
225
+
226
+
215
227
  @pytest.fixture
216
228
  def strip_ansi() -> Callable[[str], str]:
217
229
  """Return a helper that strips ANSI escape sequences from a string.
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from pathlib import Path
5
6
  from typing import TYPE_CHECKING, Any
6
7
 
7
8
  import pytest
@@ -11,7 +12,6 @@ from bitranox_template_py_cli.adapters.config import loader as config_mod
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from collections.abc import Callable, Mapping
14
- from pathlib import Path
15
15
 
16
16
  from click.testing import CliRunner, Result
17
17
  from lib_layered_config import Config
@@ -257,6 +257,27 @@ def test_when_config_deploy_finds_no_files_to_create_it_informs_user(
257
257
  assert "--force" in result.output
258
258
 
259
259
 
260
+ @pytest.mark.os_agnostic
261
+ def test_when_a_forced_config_deploy_finds_every_file_identical_it_says_nothing_was_written(
262
+ cli_runner: CliRunner,
263
+ production_factory: Callable[[], Any],
264
+ user_layer_in_tmp_path: Path,
265
+ ) -> None:
266
+ """A second forced deploy writes nothing, and does not suggest the --force it was given."""
267
+ command = ["config-deploy", "--target", "user", "--force"]
268
+ first: Result = cli_runner.invoke(cli_mod.cli, command, obj=production_factory)
269
+ written = [Path(line.removeprefix(" + ")) for line in first.output.splitlines() if line.startswith(" + ")]
270
+ assert first.exit_code == 0
271
+ assert written # liveness: the first deploy writes the files the second one finds identical
272
+ assert all(path.resolve().is_relative_to(user_layer_in_tmp_path.resolve()) for path in written)
273
+
274
+ second: Result = cli_runner.invoke(cli_mod.cli, command, obj=production_factory)
275
+
276
+ assert second.exit_code == 0
277
+ assert "No files were written: every target file is already identical" in second.output
278
+ assert "--force" not in second.output
279
+
280
+
260
281
  @pytest.mark.os_agnostic
261
282
  def test_when_config_deploy_encounters_permission_error_it_handles_gracefully(
262
283
  cli_runner: CliRunner,
@@ -548,6 +548,45 @@ def test_when_send_notification_receives_smtp_host_override_it_uses_it(
548
548
  assert ctx.spy.sent_notifications[0].config.smtphosts == ["smtp.override.com:465"]
549
549
 
550
550
 
551
+ @pytest.mark.os_agnostic
552
+ @pytest.mark.parametrize("flag", ["--raise-on-missing-attachments", "--no-raise-on-missing-attachments"])
553
+ def test_when_send_notification_receives_a_missing_attachments_flag_it_refuses_the_option(
554
+ cli_runner: CliRunner,
555
+ email_cli_context: Callable[[dict[str, Any]], EmailCliContext],
556
+ flag: str,
557
+ ) -> None:
558
+ """send-notification takes no attachments, so it offers no missing-attachment switch."""
559
+ ctx = email_cli_context({"smtp_hosts": ["smtp.test.com:587"], "from_address": "alerts@test.com"})
560
+
561
+ result: Result = cli_runner.invoke(
562
+ cli_mod.cli,
563
+ ["send-notification", "--to", "admin@test.com", "--subject", "Alert", "--message", "Hi", flag],
564
+ obj=ctx.factory,
565
+ )
566
+
567
+ assert result.exit_code == 2
568
+ assert "No such option" in result.output
569
+ assert ctx.spy.sent_notifications == []
570
+
571
+
572
+ @pytest.mark.os_agnostic
573
+ def test_when_send_email_receives_no_raise_on_missing_attachments_it_overrides_the_config(
574
+ cli_runner: CliRunner,
575
+ email_cli_context: Callable[[dict[str, Any]], EmailCliContext],
576
+ ) -> None:
577
+ """send-email keeps the switch: --no-raise-on-missing-attachments reaches the delivery config."""
578
+ ctx = email_cli_context({"smtp_hosts": ["smtp.test.com:587"], "from_address": "sender@test.com"})
579
+
580
+ result: Result = cli_runner.invoke(
581
+ cli_mod.cli,
582
+ ["send-email", "--to", "r@test.com", "--subject", "S", "--body", "B", "--no-raise-on-missing-attachments"],
583
+ obj=ctx.factory,
584
+ )
585
+
586
+ assert result.exit_code == 0
587
+ assert ctx.spy.sent_emails[0].config.raise_on_missing_attachments is False
588
+
589
+
551
590
  # ======================== Attachment path validation ========================
552
591
 
553
592
 
@@ -92,15 +92,12 @@ def test_an_all_digit_password_is_read_as_its_digits() -> None:
92
92
 
93
93
  @pytest.mark.os_agnostic
94
94
  def test_an_all_digit_password_from_the_environment_loads(
95
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path, clear_config_cache: None
95
+ monkeypatch: pytest.MonkeyPatch, user_layer_in_tmp_path: Path, clear_config_cache: None
96
96
  ) -> None:
97
97
  """Through the real loader: the environment layer turns the digits into an int."""
98
98
  prefix = __init__conf__.LAYEREDCONF_SLUG.upper().replace("-", "_")
99
99
  monkeypatch.setenv(f"{prefix}___EMAIL__SMTP_PASSWORD", DIGITS)
100
- # Every OS's user-layer location, so a developer's own config cannot decide the result.
101
- for name in ("HOME", "USERPROFILE", "XDG_CONFIG_HOME", "APPDATA", "LOCALAPPDATA"):
102
- monkeypatch.setenv(name, str(tmp_path))
103
- config = build_production().get_config(dotenv_path=str(tmp_path / "absent.env"))
100
+ config = build_production().get_config(dotenv_path=str(user_layer_in_tmp_path / "absent.env"))
104
101
 
105
102
  assert config.get("email", {}).get("smtp_password") == int(DIGITS)
106
103
  assert load_email_config_from_dict(config.as_dict()).smtp_password == SecretStr(DIGITS)
@@ -192,14 +192,12 @@ def test_no_permissions_with_a_mode_is_a_usage_error(
192
192
 
193
193
 
194
194
  @pytest.fixture
195
- def real_deploy(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Callable[[], AppServices]:
195
+ def real_deploy(user_layer_in_tmp_path: Path) -> Callable[[], AppServices]:
196
196
  """The testing composition with the production deploy, every user-layer location under ``tmp_path``.
197
197
 
198
198
  The refusals below happen before anything is written; should one ever be accepted, the
199
199
  deploy lands in ``tmp_path`` rather than in the real user configuration directory.
200
200
  """
201
- for name in ("HOME", "USERPROFILE", "XDG_CONFIG_HOME", "APPDATA", "LOCALAPPDATA"):
202
- monkeypatch.setenv(name, str(tmp_path))
203
201
  return lambda: dataclasses.replace(build_testing(), deploy_configuration=deploy_configuration)
204
202
 
205
203