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.
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/CHANGELOG.md +13 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/PKG-INFO +1 -1
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/docs/systemdesign/module_reference.md +23 -26
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/pyproject.toml +1 -1
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/__init__conf__.py +1 -1
- {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
- {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
- {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
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/conftest.py +12 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_config.py +22 -1
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_email.py +39 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_email_password_secrecy.py +2 -5
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_permission_defaults.py +1 -3
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.devcontainer/devcontainer.json +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.devcontainer/settings.json +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.env.example +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.gitattributes +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.github/actions/extract-metadata/action.yml +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.github/dependabot.yml +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.github/workflows/codeql.yml +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.github/workflows/default_cicd_public.yml +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.github/workflows/default_release_public.yml +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.gitignore +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.qlty/qlty.toml +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.snyk +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/CONFIG.md +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/CONTRIBUTING.md +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/DEVELOPMENT.md +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/INSTALL.md +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/LICENSE +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/Makefile +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/README.md +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/SECURITY.md +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/ai-stance.md +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/ai-transparency.md +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/codecov.yml +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/docs/adr/0001-memory-adapters-in-src.md +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/notebooks/Quickstart.ipynb +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/rename.sh +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/rename_dry.sh +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/reset_git_history.sh +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/__main__.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/__init__.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/adapters/py.typed +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/application/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/application/ports.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/application/py.typed +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/composition/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/composition/py.typed +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/domain/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/domain/behaviors.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/domain/enums.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/domain/errors.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/domain/py.typed +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/entry.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/src/bitranox_template_py_cli/py.typed +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_behaviors.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cache_effectiveness.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_attachment_refused.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_config_errors.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_core.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_email_config_errors.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_env_file.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_exit_codes.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_main_exit.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_overrides.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_validation.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_config_overrides.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_declared_dependencies.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_deploy_mode_safety.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_deploy_permissions.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_display.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_email_attachment_lists.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_email_config_translation.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_email_shipped_defaults.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_enums.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_errors.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_logging.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_logging_dotenv_isolation.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_mail.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_memory_logging.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_metadata.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_metadata_sync.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_module_entry.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_module_reference_sync.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_ports.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_property_email.py +0 -0
- {bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_property_overrides.py +0 -0
- {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.
|
|
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
|
|
255
|
-
|
|
256
|
-
| `--to ADDRESS`
|
|
257
|
-
| `--subject TEXT`
|
|
258
|
-
| `--message TEXT`
|
|
259
|
-
| `--from ADDRESS`
|
|
260
|
-
| `--smtp-host HOST:PORT`
|
|
261
|
-
| `--smtp-username USER`
|
|
262
|
-
| `--smtp-password PASS`
|
|
263
|
-
| `--use-starttls / --no-use-starttls`
|
|
264
|
-
| `--timeout SECONDS`
|
|
265
|
-
| `--raise-on-
|
|
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-
|
|
437
|
+
**Last Updated:** 2026-10-08 (`send-notification` options, test fixtures)
|
|
@@ -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.
|
|
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,
|
|
88
|
-
``
|
|
89
|
-
|
|
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,
|
|
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
|
-
|
|
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)
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_permission_defaults.py
RENAMED
|
@@ -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(
|
|
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
|
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.devcontainer/devcontainer.json
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.devcontainer/settings.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/.github/workflows/codeql.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/notebooks/Quickstart.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cache_effectiveness.py
RENAMED
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_config_errors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_env_file.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_exit_codes.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_main_exit.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_overrides.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_cli_validation.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_config_overrides.py
RENAMED
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_deploy_mode_safety.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_deploy_permissions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_memory_logging.py
RENAMED
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_metadata_sync.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_module_entry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_property_email.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_property_overrides.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.3 → bitranox_template_py_cli-2.2.0}/tests/test_safe_console.py
RENAMED
|
File without changes
|