bitranox-template-py-cli 2.1.1__tar.gz → 2.1.3__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.1 → bitranox_template_py_cli-2.1.3}/CHANGELOG.md +29 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/PKG-INFO +2 -2
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/docs/systemdesign/module_reference.md +19 -17
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/pyproject.toml +2 -2
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/__init__conf__.py +1 -1
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/commands/config.py +5 -2
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/commands/email/_common.py +28 -2
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/config_load.py +33 -1
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/exit_codes.py +3 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/50-mail.toml +2 -1
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/email/transport.py +4 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/logging/setup.py +27 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/memory/__init__.py +4 -1
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/memory/logging.py +14 -1
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/application/ports.py +11 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/composition/__init__.py +8 -5
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/conftest.py +12 -6
- bitranox_template_py_cli-2.1.3/tests/test_cli_attachment_refused.py +191 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_cli_env_file.py +1 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_deploy_permissions.py +1 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_logging_dotenv_isolation.py +84 -2
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_ports.py +14 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/.devcontainer/devcontainer.json +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/.devcontainer/settings.json +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/.env.example +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/.gitattributes +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/.github/actions/extract-metadata/action.yml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/.github/dependabot.yml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/.github/workflows/codeql.yml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/.github/workflows/default_cicd_public.yml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/.github/workflows/default_release_public.yml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/.gitignore +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/.qlty/qlty.toml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/.snyk +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/CONFIG.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/CONTRIBUTING.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/DEVELOPMENT.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/INSTALL.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/LICENSE +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/Makefile +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/README.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/SECURITY.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/ai-stance.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/ai-transparency.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/codecov.yml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/docs/adr/0001-memory-adapters-in-src.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/notebooks/Quickstart.ipynb +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/rename.sh +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/rename_dry.sh +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/reset_git_history.sh +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/__main__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/commands/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/commands/email/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/commands/email/send_email.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/commands/email/send_notification.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/commands/info.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/commands/logging.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/constants.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/context.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/main.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/root.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/safe_console.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/typed_click.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/40-layered-config.toml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/90-logging.toml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/defaultconfig.toml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/deploy.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/display.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/loader.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/overrides.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/email/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/email/config.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/email/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/email/sender.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/email/validation.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/logging/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/logging/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/memory/config.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/memory/email.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/application/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/application/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/composition/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/domain/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/domain/behaviors.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/domain/enums.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/domain/errors.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/domain/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/entry.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_behaviors.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_cache_effectiveness.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_cli_config.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_cli_config_errors.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_cli_core.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_cli_email.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_cli_email_config_errors.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_cli_exit_codes.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_cli_main_exit.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_cli_overrides.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_cli_validation.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_config_overrides.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_declared_dependencies.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_deploy_mode_safety.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_display.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_email_attachment_lists.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_email_config_translation.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_email_password_secrecy.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_email_shipped_defaults.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_enums.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_errors.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_logging.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_mail.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_memory_logging.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_metadata.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_metadata_sync.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_module_entry.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_module_reference_sync.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_permission_defaults.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_property_email.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_property_overrides.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.3}/tests/test_safe_console.py +0 -0
|
@@ -6,6 +6,35 @@ the [Keep a Changelog](https://keepachangelog.com/) format.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.1.3] 2026-10-08 11:57:40
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- **`config --profile NAME` refuses an invalid logging setting like `config` does.** The
|
|
13
|
+
subcommand's `--profile` reloads the configuration after logging has started, and that reload
|
|
14
|
+
skipped the logging settings, so a `[lib_log_rich]` value or `LOG_*` variable that makes plain
|
|
15
|
+
`config` exit 78 was displayed with exit 0. The reload now restarts logging with the reloaded
|
|
16
|
+
configuration, which refuses exactly what plain `config` refuses (a scrub pattern, a console
|
|
17
|
+
format preset or style key and a level name included) with the same `Error:` lines, also for a
|
|
18
|
+
`[lib_log_rich]` section that is invalid only in that profile's own file; the rest of the
|
|
19
|
+
command then logs with the profile's settings. New `RestartLogging` port
|
|
20
|
+
(`AppServices.restart_logging`; the testing composition's double keeps its quiet runtime).
|
|
21
|
+
|
|
22
|
+
## [2.1.2] 2026-10-08 09:43:32
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- **A refused attachment exits 77, not 1 with a traceback (exit code change).** An attachment
|
|
26
|
+
btx_lib_mail's security checks refuse (a blocked extension such as `.exe`, a file under a
|
|
27
|
+
blocked directory, a symlink, a file over the size limit) made `send-email` exit 1 with
|
|
28
|
+
`Error: Unexpected error - Attachment security violation (...)` and log the refusal with a
|
|
29
|
+
traceback, as if the program had crashed. Since btx_lib_mail 4.0.0 every blocked extension is
|
|
30
|
+
refused on every platform, so this is an ordinary outcome on Linux and macOS too. It now exits
|
|
31
|
+
77 (`ExitCode.ATTACHMENT_REFUSED`, sysexits `EX_NOPERM`: not permitted by policy) with one
|
|
32
|
+
line, `Error: Attachment refused by security policy - <the library's reason>`, logs no
|
|
33
|
+
traceback, and delivers nothing. A script that checks for 1 after a refused attachment must
|
|
34
|
+
check for 77. With `email.attachments.raise_on_security_violation = false` the attachment is
|
|
35
|
+
still skipped with a warning and the message sent, as before; a violation that names no attachment
|
|
36
|
+
left to drop is refused in warn mode too, and now exits 77 where it exited 1.
|
|
37
|
+
|
|
9
38
|
## [2.1.1] 2026-10-06 18:13:05
|
|
10
39
|
|
|
11
40
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: bitranox_template_py_cli
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.3
|
|
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
|
|
@@ -26,7 +26,7 @@ Requires-Dist: click>=8.5.0
|
|
|
26
26
|
Requires-Dist: lib-cli-exit-tools>=2.4.0
|
|
27
27
|
Requires-Dist: lib-layered-config>=7.0.1
|
|
28
28
|
Requires-Dist: lib-log-rich>=6.3.9
|
|
29
|
-
Requires-Dist: orjson>=3.
|
|
29
|
+
Requires-Dist: orjson>=3.13.0
|
|
30
30
|
Requires-Dist: pydantic>=2.13.5
|
|
31
31
|
Requires-Dist: python-dotenv>=1.2.4
|
|
32
32
|
Requires-Dist: rich-click>=1.9.9
|
|
@@ -26,14 +26,14 @@ moves or removes a module, a CLI option or a public name.
|
|
|
26
26
|
- `src/bitranox_template_py_cli/adapters/email/transport.py` - `send_email` / `send_notification` over btx_lib_mail
|
|
27
27
|
- `src/bitranox_template_py_cli/adapters/email/sender.py` - Re-exports of `config.py` and `transport.py`
|
|
28
28
|
- `src/bitranox_template_py_cli/adapters/email/validation.py` - Email recipient validation
|
|
29
|
-
- `src/bitranox_template_py_cli/adapters/logging/setup.py` - lib_log_rich initialization; takes only the `LOG_*` lines of a `.env`, and raises `InvalidLoggingConfigError` for a refused `[lib_log_rich]` value or `LOG_*` variable, after starting logging with its defaults (and without the `LOG_*` variables only when one of them is refused)
|
|
29
|
+
- `src/bitranox_template_py_cli/adapters/logging/setup.py` - lib_log_rich initialization; takes only the `LOG_*` lines of a `.env`, and raises `InvalidLoggingConfigError` for a refused `[lib_log_rich]` value or `LOG_*` variable, after starting logging with its defaults (and without the `LOG_*` variables only when one of them is refused); `restart_logging` shuts the running runtime down and starts it again with another configuration
|
|
30
30
|
- `src/bitranox_template_py_cli/adapters/cli/` - CLI adapter package:
|
|
31
31
|
- `__init__.py` - Public facade
|
|
32
32
|
- `constants.py` - Shared constants
|
|
33
33
|
- `safe_console.py` - Encode-safe terminal output; use `safe_console.echo` instead of `click.echo`
|
|
34
34
|
- `exit_codes.py` - POSIX exit codes (ExitCode IntEnum)
|
|
35
35
|
- `context.py` - Click context helpers
|
|
36
|
-
- `config_load.py` - Configuration load for the CLI; records a load failure, `require_config` refuses with exit 78
|
|
36
|
+
- `config_load.py` - Configuration load for the CLI; records a load failure, `require_config` refuses with exit 78, `restart_logging` restarts logging with a configuration reloaded by `config --profile`, refusing its logging settings as the root does
|
|
37
37
|
- `root.py` - Root command group
|
|
38
38
|
- `main.py` - Entry point
|
|
39
39
|
- `typed_click.py` - Strictly typed wrappers for rich_click's `option`, `version_option` and `get_current_context`
|
|
@@ -70,6 +70,7 @@ moves or removes a module, a CLI option or a public name.
|
|
|
70
70
|
- `tests/conftest.py` - Shared fixtures (see Test Fixtures below)
|
|
71
71
|
- `tests/test_behaviors.py` - Domain function tests
|
|
72
72
|
- `tests/test_cache_effectiveness.py` - Configuration loading returns consistent (cached) results
|
|
73
|
+
- `tests/test_cli_attachment_refused.py` - An attachment btx_lib_mail's security checks refuse exits 77 with one `Error:` line, nothing delivered
|
|
73
74
|
- `tests/test_cli_config.py` - `config`, `config-deploy`, `config-generate-examples` commands
|
|
74
75
|
- `tests/test_cli_config_errors.py` - A configuration that cannot be loaded: who reports it, and who still runs
|
|
75
76
|
- `tests/test_cli_core.py` - Traceback, main entry, help, hello, fail, info, unknown command
|
|
@@ -136,19 +137,20 @@ Run `lint-imports` to verify compliance.
|
|
|
136
137
|
|
|
137
138
|
POSIX-conventional exit codes defined in `adapters/cli/exit_codes.py`:
|
|
138
139
|
|
|
139
|
-
| Code | Name
|
|
140
|
-
|
|
141
|
-
| 0 | `SUCCESS`
|
|
142
|
-
| 1 | `GENERAL_ERROR`
|
|
143
|
-
| 2 | `FILE_NOT_FOUND`
|
|
144
|
-
| 13 | `PERMISSION_DENIED`
|
|
145
|
-
| 22 | `INVALID_ARGUMENT`
|
|
146
|
-
| 69 | `SMTP_FAILURE`
|
|
147
|
-
|
|
|
148
|
-
|
|
|
149
|
-
|
|
|
150
|
-
|
|
|
151
|
-
|
|
|
140
|
+
| Code | Name | Usage |
|
|
141
|
+
|------|----------------------|------------------------------------------------------------------------------------------------------------------------|
|
|
142
|
+
| 0 | `SUCCESS` | Command completed successfully |
|
|
143
|
+
| 1 | `GENERAL_ERROR` | Unhandled exception, general failure |
|
|
144
|
+
| 2 | `FILE_NOT_FOUND` | Attachment or file not found; also click's usage error (bad option value, malformed `--set`, invalid `--profile` name) |
|
|
145
|
+
| 13 | `PERMISSION_DENIED` | Cannot write to target directory |
|
|
146
|
+
| 22 | `INVALID_ARGUMENT` | Invalid CLI argument or section not found |
|
|
147
|
+
| 69 | `SMTP_FAILURE` | SMTP delivery failed |
|
|
148
|
+
| 77 | `ATTACHMENT_REFUSED` | Attachment refused by btx_lib_mail's security checks (blocked extension or directory, symlink, size, ...) |
|
|
149
|
+
| 78 | `CONFIG_ERROR` | Configuration missing, not loadable or invalid |
|
|
150
|
+
| 110 | `TIMEOUT` | Operation timed out |
|
|
151
|
+
| 130 | `SIGNAL_INT` | Interrupted (SIGINT/Ctrl+C) |
|
|
152
|
+
| 141 | `BROKEN_PIPE` | Output pipe closed |
|
|
153
|
+
| 143 | `SIGNAL_TERM` | Terminated (SIGTERM) |
|
|
152
154
|
|
|
153
155
|
---
|
|
154
156
|
|
|
@@ -243,7 +245,7 @@ Send email using configured SMTP settings.
|
|
|
243
245
|
| `--raise-on-missing-attachments / --no-raise-on-missing-attachments` | Override missing-attachment handling |
|
|
244
246
|
| `--raise-on-invalid-recipient / --no-raise-on-invalid-recipient` | Override invalid-recipient handling |
|
|
245
247
|
|
|
246
|
-
**Exit codes:** 0, 2 (file not found, or usage error), 22 (invalid option value), 69 (SMTP failure), 78 (no SMTP hosts, an invalid `[email]` section, or configuration not loadable)
|
|
248
|
+
**Exit codes:** 0, 2 (file not found, or usage error), 22 (invalid option value), 69 (SMTP failure), 77 (an attachment refused by btx_lib_mail's security checks), 78 (no SMTP hosts, an invalid `[email]` section, or configuration not loadable)
|
|
247
249
|
|
|
248
250
|
### send-notification
|
|
249
251
|
|
|
@@ -411,7 +413,7 @@ The `adapters/memory/` package provides lightweight implementations for testing:
|
|
|
411
413
|
|---------------------|-----------------------------------------------------------------------------|
|
|
412
414
|
| `memory/config.py` | `GetConfig`, `GetDefaultConfigPath`, `DeployConfiguration`, `DisplayConfig` |
|
|
413
415
|
| `memory/email.py` | `SendEmail`, `SendNotification` |
|
|
414
|
-
| `memory/logging.py` | `InitLogging`
|
|
416
|
+
| `memory/logging.py` | `InitLogging`, `RestartLogging` |
|
|
415
417
|
|
|
416
418
|
`adapters/memory` exports `EmailSpy` (from `memory/email.py`): its `send_email` and
|
|
417
419
|
`send_notification` methods satisfy the email ports and record each call as a `CapturedEmail` /
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "bitranox_template_py_cli"
|
|
3
|
-
version = "2.1.
|
|
3
|
+
version = "2.1.3"
|
|
4
4
|
description = "Template CLI application with configuration management and structured logging"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -14,7 +14,7 @@ dependencies = [
|
|
|
14
14
|
"lib_layered_config>=7.0.1",
|
|
15
15
|
"btx_lib_mail>=4.0.0",
|
|
16
16
|
"pydantic>=2.13.5",
|
|
17
|
-
"orjson>=3.
|
|
17
|
+
"orjson>=3.13.0",
|
|
18
18
|
]
|
|
19
19
|
license = { text = "MIT" }
|
|
20
20
|
authors = [{ name = "bitranox", email = "bitranox@gmail.com" }]
|
|
@@ -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.
|
|
43
|
+
version = "2.1.3"
|
|
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.
|
|
@@ -32,7 +32,7 @@ from bitranox_template_py_cli.adapters.config.overrides import nest_overrides
|
|
|
32
32
|
from bitranox_template_py_cli.domain.enums import DeployTarget, OutputFormat
|
|
33
33
|
|
|
34
34
|
from .. import safe_console
|
|
35
|
-
from ..config_load import load_config, report_load_failure, require_config
|
|
35
|
+
from ..config_load import load_config, report_load_failure, require_config, restart_logging
|
|
36
36
|
from ..constants import CLICK_CONTEXT_SETTINGS
|
|
37
37
|
from ..context import CLIContext, get_cli_context
|
|
38
38
|
from ..exit_codes import ExitCode
|
|
@@ -110,7 +110,8 @@ def _resolve_config(ctx: click.Context, cli_ctx: CLIContext, profile: str | None
|
|
|
110
110
|
|
|
111
111
|
When a subcommand-level profile override is specified, reloads config
|
|
112
112
|
with that profile, the root's ``--env-file`` and any root-level ``--set``
|
|
113
|
-
overrides stored in the CLI context
|
|
113
|
+
overrides stored in the CLI context, and restarts logging with it, which
|
|
114
|
+
refuses its logging settings the way the root refuses its own.
|
|
114
115
|
|
|
115
116
|
Args:
|
|
116
117
|
ctx: The running command's click context, exited with 78 when loading failed.
|
|
@@ -126,6 +127,8 @@ def _resolve_config(ctx: click.Context, cli_ctx: CLIContext, profile: str | None
|
|
|
126
127
|
config, error = load_config(
|
|
127
128
|
cli_ctx.services, profile=profile, env_file=cli_ctx.env_file, set_overrides=cli_ctx.set_overrides
|
|
128
129
|
)
|
|
130
|
+
if error is None:
|
|
131
|
+
error = restart_logging(cli_ctx.services, config, env_file=cli_ctx.env_file)
|
|
129
132
|
if error is not None:
|
|
130
133
|
report_load_failure(error, show_traceback=cli_ctx.traceback)
|
|
131
134
|
ctx.exit(ExitCode.CONFIG_ERROR)
|
|
@@ -11,6 +11,7 @@ import logging
|
|
|
11
11
|
import os
|
|
12
12
|
from typing import TYPE_CHECKING, Any, NoReturn, cast
|
|
13
13
|
|
|
14
|
+
from btx_lib_mail import AttachmentSecurityError
|
|
14
15
|
from pydantic import ValidationError
|
|
15
16
|
|
|
16
17
|
from bitranox_template_py_cli import __init__conf__
|
|
@@ -169,8 +170,10 @@ def execute_with_email_error_handling(
|
|
|
169
170
|
1. ConfigurationError -> CONFIG_ERROR (78): Missing/invalid config
|
|
170
171
|
2. ValueError -> INVALID_ARGUMENT (22): Invalid parameters or email format
|
|
171
172
|
3. FileNotFoundError -> FILE_NOT_FOUND (2): Missing attachment (if enabled)
|
|
172
|
-
4.
|
|
173
|
-
|
|
173
|
+
4. AttachmentSecurityError -> ATTACHMENT_REFUSED (77): An attachment btx_lib_mail's
|
|
174
|
+
security checks refuse (blocked extension or directory, symlink, size, ...)
|
|
175
|
+
5. DeliveryError/RuntimeError -> SMTP_FAILURE (69): SMTP transport failures
|
|
176
|
+
6. Exception (catch-all) -> GENERAL_ERROR (1): Unexpected errors with traceback
|
|
174
177
|
|
|
175
178
|
This ordering ensures specific exceptions aren't caught by broader handlers.
|
|
176
179
|
When adding new exception types, insert them before the catch-all Exception handler.
|
|
@@ -205,6 +208,8 @@ def execute_with_email_error_handling(
|
|
|
205
208
|
"Attachment file not found",
|
|
206
209
|
exit_code=ExitCode.FILE_NOT_FOUND,
|
|
207
210
|
)
|
|
211
|
+
except AttachmentSecurityError as exc:
|
|
212
|
+
_refuse_attachment(exc)
|
|
208
213
|
except (DeliveryError, RuntimeError) as exc:
|
|
209
214
|
_handle_send_error(
|
|
210
215
|
exc,
|
|
@@ -264,6 +269,27 @@ def _refuse_email_config(exc: ValidationError, heading: str, *, exit_code: ExitC
|
|
|
264
269
|
get_current_context().exit(exit_code)
|
|
265
270
|
|
|
266
271
|
|
|
272
|
+
def _refuse_attachment(exc: AttachmentSecurityError) -> NoReturn:
|
|
273
|
+
"""Report an attachment btx_lib_mail refused on security grounds, then exit 77.
|
|
274
|
+
|
|
275
|
+
The refusal is policy working as configured, not a crash, so no traceback is logged. The
|
|
276
|
+
user sees the library's reason, which names the violation and the resolved path; it never
|
|
277
|
+
carries a credential.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
exc: The refusal btx_lib_mail raised.
|
|
281
|
+
|
|
282
|
+
Raises:
|
|
283
|
+
click.exceptions.Exit: Always raised, with the ATTACHMENT_REFUSED exit code.
|
|
284
|
+
"""
|
|
285
|
+
logger.error(
|
|
286
|
+
"Attachment refused by security policy",
|
|
287
|
+
extra={"reason": exc.reason, "violation_type": exc.violation_type.value},
|
|
288
|
+
)
|
|
289
|
+
safe_console.echo(f"\nError: Attachment refused by security policy - {exc.reason}", err=True)
|
|
290
|
+
get_current_context().exit(ExitCode.ATTACHMENT_REFUSED)
|
|
291
|
+
|
|
292
|
+
|
|
267
293
|
def _handle_send_result(result: bool, recipients: list[str] | None, message_type: str) -> None:
|
|
268
294
|
"""Handle the result of a send operation.
|
|
269
295
|
|
|
@@ -16,6 +16,7 @@ Contents:
|
|
|
16
16
|
* :func:`load_config` - load with profile, ``.env`` and ``--set``, or say why not.
|
|
17
17
|
* :func:`start_logging` - start logging; an invalid ``[lib_log_rich]`` value or ``LOG_*``
|
|
18
18
|
variable is a load failure.
|
|
19
|
+
* :func:`restart_logging` - the same refusal for a configuration loaded after logging started.
|
|
19
20
|
* :func:`require_config` - the configuration, or exit 78 naming the failure.
|
|
20
21
|
* :func:`report_load_failure` - the one-line report, after the traceback on request.
|
|
21
22
|
* :func:`echo_load_traceback` - the loader's traceback alone, for a caller with its own line.
|
|
@@ -123,6 +124,30 @@ def start_logging(
|
|
|
123
124
|
return config, config_error
|
|
124
125
|
|
|
125
126
|
|
|
127
|
+
def restart_logging(services: AppServices, config: Config, *, env_file: str | None) -> Exception | None:
|
|
128
|
+
"""Restart logging with a configuration loaded after it started; a refused setting is returned.
|
|
129
|
+
|
|
130
|
+
``config --profile`` reloads the configuration once logging is running, so
|
|
131
|
+
:func:`start_logging` never sees the reloaded ``[lib_log_rich]`` section; without this an
|
|
132
|
+
invalid value there would display with exit 0 while plain ``config`` exits 78. Restarting
|
|
133
|
+
runs the very start :func:`start_logging` ran, so it refuses the same settings in the same
|
|
134
|
+
words, and the rest of the command logs with the reloaded settings.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
services: The composition's services; only ``restart_logging`` is used.
|
|
138
|
+
config: The configuration :func:`load_config` returned.
|
|
139
|
+
env_file: The ``--env-file`` path, or None; logging reads its ``LOG_*`` lines from it.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
None, or the refusal; logging then runs with its defaults.
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
services.restart_logging(config, dotenv_path=env_file)
|
|
146
|
+
except InvalidLoggingConfigError as exc:
|
|
147
|
+
return exc
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
126
151
|
def echo_load_traceback(error: Exception, *, show_traceback: bool) -> None:
|
|
127
152
|
"""Write the loader's chained traceback to stderr when ``--traceback`` was given.
|
|
128
153
|
|
|
@@ -170,4 +195,11 @@ def require_config(ctx: click.Context, cli_ctx: CLIContext) -> Config:
|
|
|
170
195
|
return cli_ctx.config
|
|
171
196
|
|
|
172
197
|
|
|
173
|
-
__all__ = [
|
|
198
|
+
__all__ = [
|
|
199
|
+
"echo_load_traceback",
|
|
200
|
+
"load_config",
|
|
201
|
+
"report_load_failure",
|
|
202
|
+
"require_config",
|
|
203
|
+
"restart_logging",
|
|
204
|
+
"start_logging",
|
|
205
|
+
]
|
|
@@ -27,6 +27,8 @@ class ExitCode(IntEnum):
|
|
|
27
27
|
* 2-13: errno-derived codes (ENOENT, EACCES)
|
|
28
28
|
* 22: EINVAL
|
|
29
29
|
* 69: EX_UNAVAILABLE (sysexits.h)
|
|
30
|
+
* 77: EX_NOPERM (sysexits.h): refused by policy, such as an email attachment
|
|
31
|
+
btx_lib_mail's security checks reject
|
|
30
32
|
* 78: EX_CONFIG (sysexits.h)
|
|
31
33
|
* 110: ETIMEDOUT
|
|
32
34
|
* 128+N: signal N (informational only)
|
|
@@ -44,6 +46,7 @@ class ExitCode(IntEnum):
|
|
|
44
46
|
PERMISSION_DENIED = 13
|
|
45
47
|
INVALID_ARGUMENT = 22
|
|
46
48
|
SMTP_FAILURE = 69
|
|
49
|
+
ATTACHMENT_REFUSED = 77
|
|
47
50
|
CONFIG_ERROR = 78
|
|
48
51
|
TIMEOUT = 110
|
|
49
52
|
SIGNAL_INT = 130
|
|
@@ -235,5 +235,6 @@ allow_symlinks = false
|
|
|
235
235
|
# Default: true (raise AttachmentSecurityError)
|
|
236
236
|
# Environment Variable: BITRANOX_TEMPLATE_PY_CLI___EMAIL__ATTACHMENTS__RAISE_ON_SECURITY_VIOLATION=true
|
|
237
237
|
# .env: EMAIL__ATTACHMENTS__RAISE_ON_SECURITY_VIOLATION=true
|
|
238
|
-
# Note: When
|
|
238
|
+
# Note: When true, send-email refuses the whole message and exits 77 (EX_NOPERM)
|
|
239
|
+
# naming the violation; when false, violations log a warning and skip the attachment
|
|
239
240
|
raise_on_security_violation = true
|
|
@@ -128,6 +128,10 @@ def send_email(
|
|
|
128
128
|
ConfigurationError: No SMTP hosts configured.
|
|
129
129
|
FileNotFoundError: Required attachment missing or unreadable and
|
|
130
130
|
config.raise_on_missing_attachments is True.
|
|
131
|
+
AttachmentSecurityError: btx_lib_mail's security checks refused an
|
|
132
|
+
attachment (blocked extension or directory, symlink, size, ...)
|
|
133
|
+
and config.attachment_raise_on_security_violation is True.
|
|
134
|
+
Raised before anything is delivered.
|
|
131
135
|
DeliveryError: All SMTP hosts failed for a recipient.
|
|
132
136
|
|
|
133
137
|
Side Effects:
|
|
@@ -7,6 +7,7 @@ eliminating duplication between module entry (__main__.py) and console script
|
|
|
7
7
|
Contents:
|
|
8
8
|
* :func:`init_logging` - idempotent logging initialization from the ``[lib_log_rich]``
|
|
9
9
|
section and the ``LOG_*`` variables (the environment, plus the ``LOG_*`` lines of a ``.env``).
|
|
10
|
+
* :func:`restart_logging` - stop the running runtime and start it again with another configuration.
|
|
10
11
|
* :class:`InvalidLoggingConfigError` - the ``[lib_log_rich]`` section or a ``LOG_*`` variable
|
|
11
12
|
cannot configure logging.
|
|
12
13
|
* :func:`_build_runtime_config` - constructs RuntimeConfig from layered sources.
|
|
@@ -280,9 +281,35 @@ def init_logging(config: Config, *, dotenv_path: str | None = None) -> None:
|
|
|
280
281
|
lib_log_rich.runtime.attach_std_logging()
|
|
281
282
|
|
|
282
283
|
|
|
284
|
+
def restart_logging(config: Config, *, dotenv_path: str | None = None) -> None:
|
|
285
|
+
"""Stop the running lib_log_rich runtime, then start it again with ``config``.
|
|
286
|
+
|
|
287
|
+
:func:`init_logging` returns at once while a runtime is running, so a configuration loaded
|
|
288
|
+
after logging started (``config --profile``'s reload) would never have its logging settings
|
|
289
|
+
judged. This runs that same start again: a setting lib_log_rich refuses raises exactly as it
|
|
290
|
+
does there, and logging is left running with the defaults. lib_log_rich has no public call
|
|
291
|
+
that checks settings without starting the runtime.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
config: Already-loaded layered configuration object.
|
|
295
|
+
dotenv_path: As for :func:`init_logging`.
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
InvalidLoggingConfigError: As for :func:`init_logging`.
|
|
299
|
+
|
|
300
|
+
Side Effects:
|
|
301
|
+
Flushes and shuts down the running runtime first (its queue, Graylog and journald
|
|
302
|
+
adapters included); the new one carries ``config``'s logging settings.
|
|
303
|
+
"""
|
|
304
|
+
if lib_log_rich.runtime.is_initialised():
|
|
305
|
+
lib_log_rich.runtime.shutdown()
|
|
306
|
+
init_logging(config, dotenv_path=dotenv_path)
|
|
307
|
+
|
|
308
|
+
|
|
283
309
|
__all__ = [
|
|
284
310
|
"InvalidLoggingConfigError",
|
|
285
311
|
"LoggingConfigModel",
|
|
286
312
|
"init_logging",
|
|
287
313
|
"log_variables_hidden",
|
|
314
|
+
"restart_logging",
|
|
288
315
|
]
|
|
@@ -20,7 +20,7 @@ from .config import (
|
|
|
20
20
|
get_default_config_path_in_memory,
|
|
21
21
|
)
|
|
22
22
|
from .email import EmailSpy
|
|
23
|
-
from .logging import init_logging_in_memory
|
|
23
|
+
from .logging import init_logging_in_memory, restart_logging_in_memory
|
|
24
24
|
|
|
25
25
|
# Static conformance assertions
|
|
26
26
|
if TYPE_CHECKING:
|
|
@@ -28,11 +28,13 @@ if TYPE_CHECKING:
|
|
|
28
28
|
GetConfig,
|
|
29
29
|
GetDefaultConfigPath,
|
|
30
30
|
InitLogging,
|
|
31
|
+
RestartLogging,
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
_assert_get_config: GetConfig = get_config_in_memory
|
|
34
35
|
_assert_get_default_config_path: GetDefaultConfigPath = get_default_config_path_in_memory
|
|
35
36
|
_assert_init_logging: InitLogging = init_logging_in_memory
|
|
37
|
+
_assert_restart_logging: RestartLogging = restart_logging_in_memory
|
|
36
38
|
|
|
37
39
|
__all__ = [
|
|
38
40
|
"EmailSpy",
|
|
@@ -41,4 +43,5 @@ __all__ = [
|
|
|
41
43
|
"get_config_in_memory",
|
|
42
44
|
"get_default_config_path_in_memory",
|
|
43
45
|
"init_logging_in_memory",
|
|
46
|
+
"restart_logging_in_memory",
|
|
44
47
|
]
|
|
@@ -63,4 +63,17 @@ def init_logging_in_memory(config: Config, *, dotenv_path: str | None = None) ->
|
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
def restart_logging_in_memory(config: Config, *, dotenv_path: str | None = None) -> None:
|
|
67
|
+
"""Keep the test runtime: :func:`init_logging_in_memory` ignores the configuration, so a restart changes nothing.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
config: Unused; the parameter exists to satisfy the ``RestartLogging`` protocol.
|
|
71
|
+
dotenv_path: Unused for the same reason.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> from lib_layered_config import Config
|
|
75
|
+
>>> restart_logging_in_memory(Config({"lib_log_rich": {"rate_limit": "100:60"}}, {}))
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = ["init_logging_in_memory", "restart_logging_in_memory"]
|
|
@@ -109,6 +109,16 @@ class InitLogging(Protocol):
|
|
|
109
109
|
def __call__(self, config: Config, *, dotenv_path: str | None = None) -> None: ...
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
class RestartLogging(Protocol):
|
|
113
|
+
"""Stop the running logging runtime and start it again with ``config``, as ``InitLogging`` does.
|
|
114
|
+
|
|
115
|
+
For a configuration loaded after logging is running; a refused setting raises
|
|
116
|
+
``InvalidLoggingConfigError`` after logging has been started with its defaults.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __call__(self, config: Config, *, dotenv_path: str | None = None) -> None: ...
|
|
120
|
+
|
|
121
|
+
|
|
112
122
|
__all__ = [
|
|
113
123
|
"DeployConfiguration",
|
|
114
124
|
"DisplayConfig",
|
|
@@ -116,6 +126,7 @@ __all__ = [
|
|
|
116
126
|
"GetDefaultConfigPath",
|
|
117
127
|
"InitLogging",
|
|
118
128
|
"LoadEmailConfigFromDict",
|
|
129
|
+
"RestartLogging",
|
|
119
130
|
"SendEmail",
|
|
120
131
|
"SendNotification",
|
|
121
132
|
]
|
|
@@ -19,7 +19,7 @@ from ..adapters.email.sender import (
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
# Logging services
|
|
22
|
-
from ..adapters.logging.setup import init_logging
|
|
22
|
+
from ..adapters.logging.setup import init_logging, restart_logging
|
|
23
23
|
|
|
24
24
|
# Static conformance assertions — pyright verifies that each adapter function
|
|
25
25
|
# structurally satisfies its corresponding Protocol at type-check time.
|
|
@@ -32,6 +32,7 @@ if TYPE_CHECKING:
|
|
|
32
32
|
GetDefaultConfigPath,
|
|
33
33
|
InitLogging,
|
|
34
34
|
LoadEmailConfigFromDict,
|
|
35
|
+
RestartLogging,
|
|
35
36
|
SendEmail,
|
|
36
37
|
SendNotification,
|
|
37
38
|
)
|
|
@@ -44,6 +45,7 @@ if TYPE_CHECKING:
|
|
|
44
45
|
_assert_send_notification: SendNotification = send_notification
|
|
45
46
|
_assert_load_email_config_from_dict: LoadEmailConfigFromDict = load_email_config_from_dict
|
|
46
47
|
_assert_init_logging: InitLogging = init_logging
|
|
48
|
+
_assert_restart_logging: RestartLogging = restart_logging
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
@dataclass(frozen=True, slots=True)
|
|
@@ -58,6 +60,7 @@ class AppServices:
|
|
|
58
60
|
send_notification: SendNotification
|
|
59
61
|
load_email_config_from_dict: LoadEmailConfigFromDict
|
|
60
62
|
init_logging: InitLogging
|
|
63
|
+
restart_logging: RestartLogging
|
|
61
64
|
|
|
62
65
|
|
|
63
66
|
def build_production() -> AppServices:
|
|
@@ -71,6 +74,7 @@ def build_production() -> AppServices:
|
|
|
71
74
|
send_notification=send_notification,
|
|
72
75
|
load_email_config_from_dict=load_email_config_from_dict,
|
|
73
76
|
init_logging=init_logging,
|
|
77
|
+
restart_logging=restart_logging,
|
|
74
78
|
)
|
|
75
79
|
|
|
76
80
|
|
|
@@ -92,6 +96,7 @@ def build_testing(*, spy: EmailSpy | None = None) -> AppServices:
|
|
|
92
96
|
get_config_in_memory,
|
|
93
97
|
get_default_config_path_in_memory,
|
|
94
98
|
init_logging_in_memory,
|
|
99
|
+
restart_logging_in_memory,
|
|
95
100
|
)
|
|
96
101
|
|
|
97
102
|
email_spy = spy if spy is not None else EmailSpy()
|
|
@@ -106,23 +111,21 @@ def build_testing(*, spy: EmailSpy | None = None) -> AppServices:
|
|
|
106
111
|
# The real translation: an in-memory one would let every CLI test skip it.
|
|
107
112
|
load_email_config_from_dict=load_email_config_from_dict,
|
|
108
113
|
init_logging=init_logging_in_memory,
|
|
114
|
+
restart_logging=restart_logging_in_memory,
|
|
109
115
|
)
|
|
110
116
|
|
|
111
117
|
|
|
112
118
|
__all__ = [
|
|
113
|
-
# Composition
|
|
114
119
|
"AppServices",
|
|
115
120
|
"build_production",
|
|
116
121
|
"build_testing",
|
|
117
122
|
"deploy_configuration",
|
|
118
123
|
"display_config",
|
|
119
|
-
# Configuration
|
|
120
124
|
"get_config",
|
|
121
125
|
"get_default_config_path",
|
|
122
|
-
# Logging
|
|
123
126
|
"init_logging",
|
|
124
127
|
"load_email_config_from_dict",
|
|
125
|
-
|
|
128
|
+
"restart_logging",
|
|
126
129
|
"send_email",
|
|
127
130
|
"send_notification",
|
|
128
131
|
]
|
|
@@ -384,7 +384,7 @@ def inject_config(
|
|
|
384
384
|
result = cli_runner.invoke(cli, ["config"], obj=factory)
|
|
385
385
|
assert "key" in result.output
|
|
386
386
|
"""
|
|
387
|
-
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory
|
|
387
|
+
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory, restart_logging_in_memory
|
|
388
388
|
from bitranox_template_py_cli.composition import AppServices, build_production
|
|
389
389
|
|
|
390
390
|
def _inject(config: Config) -> Callable[[], AppServices]:
|
|
@@ -403,6 +403,7 @@ def inject_config(
|
|
|
403
403
|
# The quiet runtime: production init_logging queues INFO lines that race into
|
|
404
404
|
# CliRunner's stderr, so a stderr assertion would depend on timing.
|
|
405
405
|
init_logging=init_logging_in_memory,
|
|
406
|
+
restart_logging=restart_logging_in_memory,
|
|
406
407
|
)
|
|
407
408
|
return lambda: test_services
|
|
408
409
|
|
|
@@ -438,7 +439,7 @@ def inject_config_with_profile_capture(
|
|
|
438
439
|
cli_runner.invoke(cli, ["--profile", "staging", "config"], obj=factory)
|
|
439
440
|
assert captured == ["staging"]
|
|
440
441
|
"""
|
|
441
|
-
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory
|
|
442
|
+
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory, restart_logging_in_memory
|
|
442
443
|
from bitranox_template_py_cli.composition import AppServices, build_production
|
|
443
444
|
|
|
444
445
|
def _inject(config: Config, captured_profiles: list[str | None]) -> Callable[[], AppServices]:
|
|
@@ -458,6 +459,7 @@ def inject_config_with_profile_capture(
|
|
|
458
459
|
# The quiet runtime: production init_logging queues INFO lines that race into
|
|
459
460
|
# CliRunner's stderr, so a stderr assertion would depend on timing.
|
|
460
461
|
init_logging=init_logging_in_memory,
|
|
462
|
+
restart_logging=restart_logging_in_memory,
|
|
461
463
|
)
|
|
462
464
|
return lambda: test_services
|
|
463
465
|
|
|
@@ -493,7 +495,7 @@ def inject_deploy_with_profile_capture(
|
|
|
493
495
|
cli_runner.invoke(cli, ["--profile", "prod", "config-deploy", ...], obj=factory)
|
|
494
496
|
assert captured == ["prod"]
|
|
495
497
|
"""
|
|
496
|
-
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory
|
|
498
|
+
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory, restart_logging_in_memory
|
|
497
499
|
from bitranox_template_py_cli.composition import AppServices, build_production
|
|
498
500
|
|
|
499
501
|
def _inject(deployed_path: Path, captured_profiles: list[str | None]) -> Callable[[], AppServices]:
|
|
@@ -522,6 +524,7 @@ def inject_deploy_with_profile_capture(
|
|
|
522
524
|
# The quiet runtime: production init_logging queues INFO lines that race into
|
|
523
525
|
# CliRunner's stderr, so a stderr assertion would depend on timing.
|
|
524
526
|
init_logging=init_logging_in_memory,
|
|
527
|
+
restart_logging=restart_logging_in_memory,
|
|
525
528
|
)
|
|
526
529
|
return lambda: test_services
|
|
527
530
|
|
|
@@ -553,7 +556,7 @@ def inject_deploy_configuration() -> Callable[[Callable[..., list[Path]]], Calla
|
|
|
553
556
|
cli_runner.invoke(cli, ["config-deploy", "--target", "user"], obj=factory)
|
|
554
557
|
assert len(calls) == 1
|
|
555
558
|
"""
|
|
556
|
-
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory
|
|
559
|
+
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory, restart_logging_in_memory
|
|
557
560
|
from bitranox_template_py_cli.composition import AppServices, build_production
|
|
558
561
|
|
|
559
562
|
def _inject(deploy_fn: Callable[..., list[Path]]) -> Callable[[], AppServices]:
|
|
@@ -569,6 +572,7 @@ def inject_deploy_configuration() -> Callable[[Callable[..., list[Path]]], Calla
|
|
|
569
572
|
# The quiet runtime: production init_logging queues INFO lines that race into
|
|
570
573
|
# CliRunner's stderr, so a stderr assertion would depend on timing.
|
|
571
574
|
init_logging=init_logging_in_memory,
|
|
575
|
+
restart_logging=restart_logging_in_memory,
|
|
572
576
|
)
|
|
573
577
|
return lambda: test_services
|
|
574
578
|
|
|
@@ -654,7 +658,7 @@ def email_cli_context(
|
|
|
654
658
|
assert ctx.spy.sent_notifications[0].subject == "Hi"
|
|
655
659
|
"""
|
|
656
660
|
from bitranox_template_py_cli.adapters.email.config import load_email_config_from_dict
|
|
657
|
-
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory
|
|
661
|
+
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory, restart_logging_in_memory
|
|
658
662
|
from bitranox_template_py_cli.adapters.memory.email import EmailSpy as EmailSpyImpl
|
|
659
663
|
from bitranox_template_py_cli.composition import AppServices, build_production
|
|
660
664
|
|
|
@@ -677,6 +681,7 @@ def email_cli_context(
|
|
|
677
681
|
# The quiet runtime: production init_logging queues INFO lines that race into
|
|
678
682
|
# CliRunner's stderr, so a stderr assertion would depend on timing.
|
|
679
683
|
init_logging=init_logging_in_memory,
|
|
684
|
+
restart_logging=restart_logging_in_memory,
|
|
680
685
|
)
|
|
681
686
|
return EmailCliContext(factory=lambda: test_services, spy=spy)
|
|
682
687
|
|
|
@@ -708,7 +713,7 @@ def config_cli_context(
|
|
|
708
713
|
result = cli_runner.invoke(cli, ["config"], obj=factory)
|
|
709
714
|
assert "key" in result.output
|
|
710
715
|
"""
|
|
711
|
-
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory
|
|
716
|
+
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory, restart_logging_in_memory
|
|
712
717
|
from bitranox_template_py_cli.composition import AppServices, build_production
|
|
713
718
|
|
|
714
719
|
def _create(config_data: dict[str, Any]) -> Callable[[], AppServices]:
|
|
@@ -729,6 +734,7 @@ def config_cli_context(
|
|
|
729
734
|
# The quiet runtime: production init_logging queues INFO lines that race into
|
|
730
735
|
# CliRunner's stderr, so a stderr assertion would depend on timing.
|
|
731
736
|
init_logging=init_logging_in_memory,
|
|
737
|
+
restart_logging=restart_logging_in_memory,
|
|
732
738
|
)
|
|
733
739
|
return lambda: test_services
|
|
734
740
|
|