bitranox-template-py-cli 2.2.0__tar.gz → 2.4.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.2.0 → bitranox_template_py_cli-2.4.0}/CHANGELOG.md +30 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/PKG-INFO +4 -4
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/docs/systemdesign/module_reference.md +5 -4
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/pyproject.toml +5 -4
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/__init__conf__.py +1 -1
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/commands/config.py +4 -4
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/commands/email/__init__.py +3 -2
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/commands/email/_common.py +62 -34
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/commands/email/send_email.py +5 -5
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/commands/email/send_notification.py +5 -5
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/config_load.py +12 -12
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/logging/setup.py +46 -22
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/memory/__init__.py +4 -4
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/memory/logging.py +7 -6
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/application/ports.py +5 -5
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/composition/__init__.py +8 -8
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/conftest.py +12 -12
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_env_file.py +1 -1
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_deploy_permissions.py +1 -1
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_logging_dotenv_isolation.py +42 -6
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_mail.py +81 -2
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_ports.py +8 -8
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/.devcontainer/devcontainer.json +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/.devcontainer/settings.json +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/.env.example +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/.gitattributes +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/.github/actions/extract-metadata/action.yml +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/.github/dependabot.yml +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/.github/workflows/codeql.yml +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/.github/workflows/default_cicd_public.yml +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/.github/workflows/default_release_public.yml +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/.gitignore +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/.qlty/qlty.toml +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/.snyk +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/CONFIG.md +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/CONTRIBUTING.md +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/DEVELOPMENT.md +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/INSTALL.md +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/LICENSE +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/Makefile +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/README.md +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/SECURITY.md +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/ai-stance.md +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/ai-transparency.md +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/codecov.yml +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/docs/adr/0001-memory-adapters-in-src.md +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/notebooks/Quickstart.ipynb +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/rename.sh +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/rename_dry.sh +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/reset_git_history.sh +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/__init__.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/__main__.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/__init__.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/__init__.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/commands/__init__.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/commands/info.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/commands/logging.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/constants.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/context.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/exit_codes.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/main.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/py.typed +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/root.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/safe_console.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/typed_click.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/__init__.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/40-layered-config.toml +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/50-mail.toml +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/90-logging.toml +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/defaultconfig.toml +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/deploy.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/display.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/loader.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/overrides.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/py.typed +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/email/__init__.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/email/config.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/email/py.typed +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/email/sender.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/email/transport.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/email/validation.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/logging/__init__.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/logging/py.typed +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/memory/config.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/memory/email.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/py.typed +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/application/__init__.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/application/py.typed +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/composition/py.typed +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/domain/__init__.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/domain/behaviors.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/domain/enums.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/domain/errors.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/domain/py.typed +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/entry.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/py.typed +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_behaviors.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_cache_effectiveness.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_attachment_refused.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_config.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_config_errors.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_core.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_email.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_email_config_errors.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_exit_codes.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_main_exit.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_overrides.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_validation.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_config_overrides.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_declared_dependencies.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_deploy_mode_safety.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_display.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_email_attachment_lists.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_email_config_translation.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_email_password_secrecy.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_email_shipped_defaults.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_enums.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_errors.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_logging.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_memory_logging.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_metadata.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_metadata_sync.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_module_entry.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_module_reference_sync.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_permission_defaults.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_property_email.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_property_overrides.py +0 -0
- {bitranox_template_py_cli-2.2.0 → bitranox_template_py_cli-2.4.0}/tests/test_safe_console.py +0 -0
|
@@ -6,6 +6,36 @@ the [Keep a Changelog](https://keepachangelog.com/) format.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.4.0] 2026-10-08 17:34:36
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **The email commands' SMTP options reach the configuration as a typed `EmailConfigOverrides`
|
|
13
|
+
model instead of a plain dict.** `EmailConfigOverrides.from_cli_options(...)` takes the Click
|
|
14
|
+
values (an option that was not given is `None`, an unset `--smtp-host` an empty tuple) and
|
|
15
|
+
`apply_validated_overrides` merges only the options that were given. Its fields carry
|
|
16
|
+
EmailConfig's own names, so a field EmailConfig renames is refused instead of slipping through,
|
|
17
|
+
and `--smtp-password` is a `SecretStr` from the moment it is read; the model is a btx_lib_mail
|
|
18
|
+
`SecretSafeModel`, so a refused password never appears in a validation error's details.
|
|
19
|
+
`filter_sentinels` is gone; the command line behaves as before.
|
|
20
|
+
- Requires `pydantic>=2.14.0` (and `virtualenv>=21.14.6` in the dev extra).
|
|
21
|
+
|
|
22
|
+
## [2.3.0] 2026-10-08 16:14:12
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- **`config --profile NAME` checks the profile's logging settings instead of restarting logging.**
|
|
26
|
+
The check now runs through lib_log_rich's `validate_config`, which refuses exactly what starting
|
|
27
|
+
logging refuses, with the same `Error:` lines and exit 78, so nothing changes for an invalid
|
|
28
|
+
setting. What changes: the running logging runtime is no longer shut down and started again, so
|
|
29
|
+
the rest of the command keeps logging with the settings logging started with (not the profile's),
|
|
30
|
+
and a `main()` call from a worker thread no longer replaces the logging every other thread uses.
|
|
31
|
+
The `RestartLogging` port is now `CheckLoggingConfig` (`AppServices.check_logging_config`; the
|
|
32
|
+
testing composition's double refuses nothing). Requires `lib_log_rich>=6.4.2`.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- **A logging refusal no single setting owns reads `lib_log_rich: <reason>`.** Such a problem (for
|
|
36
|
+
example TLS together with UDP for Graylog, which lib_log_rich 6.4.2 now refuses up front) has no
|
|
37
|
+
key, and its `Error:` line came out as `lib_log_rich.: <reason>` with a dangling dot.
|
|
38
|
+
|
|
9
39
|
## [2.2.0] 2026-10-08 13:56:49
|
|
10
40
|
|
|
11
41
|
### Removed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: bitranox_template_py_cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.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
|
|
@@ -25,9 +25,9 @@ Requires-Dist: btx-lib-mail>=4.0.0
|
|
|
25
25
|
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
|
-
Requires-Dist: lib-log-rich>=6.
|
|
28
|
+
Requires-Dist: lib-log-rich>=6.4.2
|
|
29
29
|
Requires-Dist: orjson>=3.13.0
|
|
30
|
-
Requires-Dist: pydantic>=2.
|
|
30
|
+
Requires-Dist: pydantic>=2.14.0
|
|
31
31
|
Requires-Dist: python-dotenv>=1.2.4
|
|
32
32
|
Requires-Dist: rich-click>=1.9.9
|
|
33
33
|
Provides-Extra: dev
|
|
@@ -49,7 +49,7 @@ Requires-Dist: ruff>=0.16.10; extra == 'dev'
|
|
|
49
49
|
Requires-Dist: textual>=8.2.8; extra == 'dev'
|
|
50
50
|
Requires-Dist: twine>=7.0.0; extra == 'dev'
|
|
51
51
|
Requires-Dist: urllib3>=2.8.0; extra == 'dev'
|
|
52
|
-
Requires-Dist: virtualenv>=21.14.
|
|
52
|
+
Requires-Dist: virtualenv>=21.14.6; extra == 'dev'
|
|
53
53
|
Requires-Dist: wheel>=0.48.0; extra == 'dev'
|
|
54
54
|
Description-Content-Type: text/markdown
|
|
55
55
|
|
|
@@ -26,21 +26,22 @@ 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); `check_logging_config` refuses another configuration's logging settings through lib_log_rich's `validate_config`, without touching the running runtime
|
|
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, `check_logging` checks the logging settings of a configuration reloaded by `config --profile`, refusing them 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`
|
|
40
40
|
- `commands/info.py` - info, hello, fail commands
|
|
41
41
|
- `commands/config.py` - config, config-deploy, config-generate-examples commands
|
|
42
42
|
- `commands/email/` - Email commands package:
|
|
43
|
-
- `_common.py` - Shared SMTP override options
|
|
43
|
+
- `_common.py` - Shared SMTP override options and their typed `EmailConfigOverrides`, config
|
|
44
|
+
loading and error-to-exit-code mapping
|
|
44
45
|
- `send_email.py` - send-email command
|
|
45
46
|
- `send_notification.py` - send-notification command
|
|
46
47
|
- `commands/logging.py` - logdemo command
|
|
@@ -409,7 +410,7 @@ The `adapters/memory/` package provides lightweight implementations for testing:
|
|
|
409
410
|
|---------------------|-----------------------------------------------------------------------------|
|
|
410
411
|
| `memory/config.py` | `GetConfig`, `GetDefaultConfigPath`, `DeployConfiguration`, `DisplayConfig` |
|
|
411
412
|
| `memory/email.py` | `SendEmail`, `SendNotification` |
|
|
412
|
-
| `memory/logging.py` | `InitLogging`, `
|
|
413
|
+
| `memory/logging.py` | `InitLogging`, `CheckLoggingConfig` |
|
|
413
414
|
|
|
414
415
|
`adapters/memory` exports `EmailSpy` (from `memory/email.py`): its `send_email` and
|
|
415
416
|
`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.
|
|
3
|
+
version = "2.4.0"
|
|
4
4
|
description = "Template CLI application with configuration management and structured logging"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -9,11 +9,12 @@ dependencies = [
|
|
|
9
9
|
# Imported directly (adapters/cli/main.py, commands/config.py), not only through rich-click.
|
|
10
10
|
"click>=8.5.0",
|
|
11
11
|
"lib_cli_exit_tools>=2.4.0",
|
|
12
|
-
|
|
12
|
+
# 6.4.2: validate_config, the check config --profile runs, refuses exactly what init() refuses.
|
|
13
|
+
"lib_log_rich>=6.4.2",
|
|
13
14
|
"python-dotenv>=1.2.4",
|
|
14
15
|
"lib_layered_config>=7.0.1",
|
|
15
16
|
"btx_lib_mail>=4.0.0",
|
|
16
|
-
"pydantic>=2.
|
|
17
|
+
"pydantic>=2.14.0",
|
|
17
18
|
"orjson>=3.13.0",
|
|
18
19
|
]
|
|
19
20
|
license = { text = "MIT" }
|
|
@@ -76,7 +77,7 @@ dev = [
|
|
|
76
77
|
"wheel>=0.48.0", # CVE-2025-24049
|
|
77
78
|
"pynacl>=1.6.2", # CVE-2025-69277
|
|
78
79
|
"urllib3>=2.8.0", # CVE-2025-66471, CVE-2025-66418
|
|
79
|
-
"virtualenv>=21.14.
|
|
80
|
+
"virtualenv>=21.14.6", # CVE-2025-22702
|
|
80
81
|
]
|
|
81
82
|
|
|
82
83
|
[project.scripts]
|
|
@@ -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.4.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.
|
|
@@ -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 check_logging, load_config, report_load_failure, require_config
|
|
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,8 +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, and
|
|
114
|
-
|
|
113
|
+
overrides stored in the CLI context, and checks its logging settings, which
|
|
114
|
+
are refused the way the root refuses its own; logging itself keeps running unchanged.
|
|
115
115
|
|
|
116
116
|
Args:
|
|
117
117
|
ctx: The running command's click context, exited with 78 when loading failed.
|
|
@@ -128,7 +128,7 @@ def _resolve_config(ctx: click.Context, cli_ctx: CLIContext, profile: str | None
|
|
|
128
128
|
cli_ctx.services, profile=profile, env_file=cli_ctx.env_file, set_overrides=cli_ctx.set_overrides
|
|
129
129
|
)
|
|
130
130
|
if error is None:
|
|
131
|
-
error =
|
|
131
|
+
error = check_logging(cli_ctx.services, config)
|
|
132
132
|
if error is not None:
|
|
133
133
|
report_load_failure(error, show_traceback=cli_ctx.traceback)
|
|
134
134
|
ctx.exit(ExitCode.CONFIG_ERROR)
|
|
@@ -5,12 +5,13 @@ Provides commands for sending emails and notifications via SMTP.
|
|
|
5
5
|
Contents:
|
|
6
6
|
* :func:`.send_email.cli_send_email` - Send email with optional HTML and attachments.
|
|
7
7
|
* :func:`.send_notification.cli_send_notification` - Send simple plain-text notification.
|
|
8
|
+
* :class:`._common.EmailConfigOverrides` - The SMTP options a command line gave, typed.
|
|
8
9
|
"""
|
|
9
10
|
|
|
10
11
|
from __future__ import annotations
|
|
11
12
|
|
|
12
|
-
from ._common import
|
|
13
|
+
from ._common import EmailConfigOverrides
|
|
13
14
|
from .send_email import cli_send_email
|
|
14
15
|
from .send_notification import cli_send_notification
|
|
15
16
|
|
|
16
|
-
__all__ = ["
|
|
17
|
+
__all__ = ["EmailConfigOverrides", "cli_send_email", "cli_send_notification"]
|
|
@@ -9,10 +9,10 @@ from __future__ import annotations
|
|
|
9
9
|
import functools
|
|
10
10
|
import logging
|
|
11
11
|
import os
|
|
12
|
-
from typing import TYPE_CHECKING, Any, NoReturn
|
|
12
|
+
from typing import TYPE_CHECKING, Any, NoReturn
|
|
13
13
|
|
|
14
|
-
from btx_lib_mail import AttachmentSecurityError
|
|
15
|
-
from pydantic import ValidationError
|
|
14
|
+
from btx_lib_mail import AttachmentSecurityError, SecretSafeModel
|
|
15
|
+
from pydantic import ConfigDict, SecretStr, ValidationError
|
|
16
16
|
|
|
17
17
|
from bitranox_template_py_cli import __init__conf__
|
|
18
18
|
from bitranox_template_py_cli.adapters.email.config import describe_validation_error
|
|
@@ -33,51 +33,79 @@ if TYPE_CHECKING:
|
|
|
33
33
|
logger = logging.getLogger(__name__)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
"""
|
|
36
|
+
class EmailConfigOverrides(SecretSafeModel):
|
|
37
|
+
"""The EmailConfig fields the command line overrides; ``None`` means the option was not given.
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
and
|
|
39
|
+
Each field carries EmailConfig's own name, so a field EmailConfig renames is refused here
|
|
40
|
+
instead of being passed along unnoticed. The password stays a ``SecretStr`` from the moment
|
|
41
|
+
the command line hands it over, and a validation error never carries its value, not even in
|
|
42
|
+
``errors()`` or ``json()``.
|
|
43
|
+
"""
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
46
|
+
credential_fields = frozenset({"smtp_password"})
|
|
47
|
+
|
|
48
|
+
smtphosts: tuple[str, ...] | None = None
|
|
49
|
+
smtp_username: str | None = None
|
|
50
|
+
smtp_password: SecretStr | None = None
|
|
51
|
+
smtp_use_starttls: bool | None = None
|
|
52
|
+
smtp_timeout: float | None = None
|
|
53
|
+
raise_on_missing_attachments: bool | None = None
|
|
54
|
+
raise_on_invalid_recipient: bool | None = None
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_cli_options(
|
|
58
|
+
cls,
|
|
59
|
+
*,
|
|
60
|
+
smtp_hosts: tuple[str, ...] = (),
|
|
61
|
+
smtp_username: str | None = None,
|
|
62
|
+
smtp_password: str | None = None,
|
|
63
|
+
use_starttls: bool | None = None,
|
|
64
|
+
timeout: float | None = None,
|
|
65
|
+
raise_on_missing_attachments: bool | None = None,
|
|
66
|
+
raise_on_invalid_recipient: bool | None = None,
|
|
67
|
+
) -> EmailConfigOverrides:
|
|
68
|
+
"""Build the overrides from the values Click hands a command.
|
|
69
|
+
|
|
70
|
+
Click reports an option that was not given as ``None``, and a ``multiple=True`` option
|
|
71
|
+
that was not given as an empty tuple; both become ``None`` here.
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
>>> EmailConfigOverrides.from_cli_options(smtp_hosts=(), timeout=5.0).model_dump(exclude_none=True)
|
|
75
|
+
{'smtp_timeout': 5.0}
|
|
76
|
+
"""
|
|
77
|
+
return cls(
|
|
78
|
+
smtphosts=smtp_hosts or None,
|
|
79
|
+
smtp_username=smtp_username,
|
|
80
|
+
smtp_password=None if smtp_password is None else SecretStr(smtp_password),
|
|
81
|
+
smtp_use_starttls=use_starttls,
|
|
82
|
+
smtp_timeout=timeout,
|
|
83
|
+
raise_on_missing_attachments=raise_on_missing_attachments,
|
|
84
|
+
raise_on_invalid_recipient=raise_on_invalid_recipient,
|
|
85
|
+
)
|
|
45
86
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"""
|
|
49
|
-
result: dict[str, Any] = {}
|
|
50
|
-
for k, v in kwargs.items():
|
|
51
|
-
if v is None or v == ():
|
|
52
|
-
continue
|
|
53
|
-
if isinstance(v, tuple):
|
|
54
|
-
result[k] = list(cast("tuple[Any, ...]", v))
|
|
55
|
-
else:
|
|
56
|
-
result[k] = v
|
|
57
|
-
return result
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def apply_validated_overrides(base_config: EmailConfig, overrides: dict[str, Any]) -> EmailConfig:
|
|
87
|
+
|
|
88
|
+
def apply_validated_overrides(base_config: EmailConfig, overrides: EmailConfigOverrides) -> EmailConfig:
|
|
61
89
|
"""Apply overrides with full Pydantic validation.
|
|
62
90
|
|
|
63
|
-
Uses model_validate() with a merged
|
|
64
|
-
to ensure
|
|
91
|
+
Uses model_validate() with a merged mapping instead of model_copy(update=...)
|
|
92
|
+
to ensure EmailConfig's validators run on all overridden values.
|
|
65
93
|
|
|
66
94
|
Args:
|
|
67
95
|
base_config: Base EmailConfig to merge overrides into.
|
|
68
|
-
overrides:
|
|
96
|
+
overrides: The options given on the command line.
|
|
69
97
|
|
|
70
98
|
Returns:
|
|
71
|
-
|
|
99
|
+
``base_config`` itself when no option was given, else a new EmailConfig with the
|
|
100
|
+
overrides applied and validated.
|
|
72
101
|
|
|
73
102
|
Raises:
|
|
74
103
|
ValidationError: When overrides contain invalid values.
|
|
75
104
|
"""
|
|
76
|
-
|
|
105
|
+
given = overrides.model_dump(exclude_none=True)
|
|
106
|
+
if not given:
|
|
77
107
|
return base_config
|
|
78
|
-
|
|
79
|
-
merged = {**base_dict, **overrides}
|
|
80
|
-
return EmailConfig.model_validate(merged)
|
|
108
|
+
return EmailConfig.model_validate({**base_config.model_dump(), **given})
|
|
81
109
|
|
|
82
110
|
|
|
83
111
|
def smtp_config_options(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
@@ -337,9 +365,9 @@ def _handle_send_error(
|
|
|
337
365
|
|
|
338
366
|
|
|
339
367
|
__all__ = [
|
|
368
|
+
"EmailConfigOverrides",
|
|
340
369
|
"apply_validated_overrides",
|
|
341
370
|
"execute_with_email_error_handling",
|
|
342
|
-
"filter_sentinels",
|
|
343
371
|
"handle_validation_error",
|
|
344
372
|
"load_and_validate_email_config",
|
|
345
373
|
"smtp_config_options",
|
|
@@ -18,9 +18,9 @@ from ...constants import CLICK_CONTEXT_SETTINGS
|
|
|
18
18
|
from ...context import get_cli_context
|
|
19
19
|
from ...typed_click import option
|
|
20
20
|
from ._common import (
|
|
21
|
+
EmailConfigOverrides,
|
|
21
22
|
apply_validated_overrides,
|
|
22
23
|
execute_with_email_error_handling,
|
|
23
|
-
filter_sentinels,
|
|
24
24
|
handle_validation_error,
|
|
25
25
|
load_and_validate_email_config,
|
|
26
26
|
smtp_config_options,
|
|
@@ -112,12 +112,12 @@ def cli_send_email(
|
|
|
112
112
|
|
|
113
113
|
with lib_log_rich.runtime.bind(job_id="cli-send-email", extra=extra):
|
|
114
114
|
email_config = load_and_validate_email_config(config, cli_ctx.services.load_email_config_from_dict)
|
|
115
|
-
overrides =
|
|
116
|
-
|
|
115
|
+
overrides = EmailConfigOverrides.from_cli_options(
|
|
116
|
+
smtp_hosts=smtp_hosts,
|
|
117
117
|
smtp_username=smtp_username,
|
|
118
118
|
smtp_password=smtp_password,
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
use_starttls=use_starttls,
|
|
120
|
+
timeout=timeout,
|
|
121
121
|
raise_on_missing_attachments=raise_on_missing_attachments,
|
|
122
122
|
raise_on_invalid_recipient=raise_on_invalid_recipient,
|
|
123
123
|
)
|
|
@@ -17,9 +17,9 @@ from ...constants import CLICK_CONTEXT_SETTINGS
|
|
|
17
17
|
from ...context import get_cli_context
|
|
18
18
|
from ...typed_click import option
|
|
19
19
|
from ._common import (
|
|
20
|
+
EmailConfigOverrides,
|
|
20
21
|
apply_validated_overrides,
|
|
21
22
|
execute_with_email_error_handling,
|
|
22
|
-
filter_sentinels,
|
|
23
23
|
handle_validation_error,
|
|
24
24
|
load_and_validate_email_config,
|
|
25
25
|
smtp_config_options,
|
|
@@ -70,12 +70,12 @@ def cli_send_notification(
|
|
|
70
70
|
|
|
71
71
|
with lib_log_rich.runtime.bind(job_id="cli-send-notification", extra=extra):
|
|
72
72
|
email_config = load_and_validate_email_config(config, cli_ctx.services.load_email_config_from_dict)
|
|
73
|
-
overrides =
|
|
74
|
-
|
|
73
|
+
overrides = EmailConfigOverrides.from_cli_options(
|
|
74
|
+
smtp_hosts=smtp_hosts,
|
|
75
75
|
smtp_username=smtp_username,
|
|
76
76
|
smtp_password=smtp_password,
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
use_starttls=use_starttls,
|
|
78
|
+
timeout=timeout,
|
|
79
79
|
raise_on_invalid_recipient=raise_on_invalid_recipient,
|
|
80
80
|
)
|
|
81
81
|
try:
|
|
@@ -16,7 +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:`
|
|
19
|
+
* :func:`check_logging` - the same refusal for a configuration loaded after logging started.
|
|
20
20
|
* :func:`require_config` - the configuration, or exit 78 naming the failure.
|
|
21
21
|
* :func:`report_load_failure` - the one-line report, after the traceback on request.
|
|
22
22
|
* :func:`echo_load_traceback` - the loader's traceback alone, for a caller with its own line.
|
|
@@ -105,7 +105,8 @@ def start_logging(
|
|
|
105
105
|
refused setting) before it raises, and the commands that read the configuration refuse with
|
|
106
106
|
exit 78 and the refusal: ``lib_log_rich.<key>: <reason>`` for a problem the section's type
|
|
107
107
|
check finds, lib_log_rich's own message for a value only lib_log_rich refuses, whether it
|
|
108
|
-
came from the section or a ``LOG_*`` variable (``lib_log_rich:
|
|
108
|
+
came from the section or a ``LOG_*`` variable (``lib_log_rich: Invalid runtime settings:
|
|
109
|
+
console_level: Unknown log level: 'bogus'``).
|
|
109
110
|
|
|
110
111
|
Args:
|
|
111
112
|
services: The composition's services; only ``init_logging`` is used.
|
|
@@ -124,25 +125,24 @@ def start_logging(
|
|
|
124
125
|
return config, config_error
|
|
125
126
|
|
|
126
127
|
|
|
127
|
-
def
|
|
128
|
-
"""
|
|
128
|
+
def check_logging(services: AppServices, config: Config) -> Exception | None:
|
|
129
|
+
"""Judge the logging settings of a configuration loaded after logging started; a refusal is returned.
|
|
129
130
|
|
|
130
131
|
``config --profile`` reloads the configuration once logging is running, so
|
|
131
132
|
: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.
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
invalid value there would display with exit 0 while plain ``config`` exits 78. The check
|
|
134
|
+
refuses what :func:`start_logging` refuses, in the same words, and never touches the
|
|
135
|
+
running runtime: the command keeps logging with the settings logging started with.
|
|
135
136
|
|
|
136
137
|
Args:
|
|
137
|
-
services: The composition's services; only ``
|
|
138
|
+
services: The composition's services; only ``check_logging_config`` is used.
|
|
138
139
|
config: The configuration :func:`load_config` returned.
|
|
139
|
-
env_file: The ``--env-file`` path, or None; logging reads its ``LOG_*`` lines from it.
|
|
140
140
|
|
|
141
141
|
Returns:
|
|
142
|
-
None, or the refusal
|
|
142
|
+
None, or the refusal.
|
|
143
143
|
"""
|
|
144
144
|
try:
|
|
145
|
-
services.
|
|
145
|
+
services.check_logging_config(config)
|
|
146
146
|
except InvalidLoggingConfigError as exc:
|
|
147
147
|
return exc
|
|
148
148
|
return None
|
|
@@ -196,10 +196,10 @@ def require_config(ctx: click.Context, cli_ctx: CLIContext) -> Config:
|
|
|
196
196
|
|
|
197
197
|
|
|
198
198
|
__all__ = [
|
|
199
|
+
"check_logging",
|
|
199
200
|
"echo_load_traceback",
|
|
200
201
|
"load_config",
|
|
201
202
|
"report_load_failure",
|
|
202
203
|
"require_config",
|
|
203
|
-
"restart_logging",
|
|
204
204
|
"start_logging",
|
|
205
205
|
]
|
|
@@ -7,7 +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:`
|
|
10
|
+
* :func:`check_logging_config` - refuse another configuration's logging settings without touching the runtime.
|
|
11
11
|
* :class:`InvalidLoggingConfigError` - the ``[lib_log_rich]`` section or a ``LOG_*`` variable
|
|
12
12
|
cannot configure logging.
|
|
13
13
|
* :func:`_build_runtime_config` - constructs RuntimeConfig from layered sources.
|
|
@@ -100,10 +100,12 @@ class InvalidLoggingConfigError(ConfigurationError):
|
|
|
100
100
|
"""lib_log_rich refuses its settings: one ``<key>: <reason>`` in :attr:`problems` per problem.
|
|
101
101
|
|
|
102
102
|
The settings are the ``[lib_log_rich]`` section plus any ``LOG_*`` variable. A problem the
|
|
103
|
-
type check of the section finds names the key and never repeats the refused value
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
type check of the section finds names the key and never repeats the refused value; one no
|
|
104
|
+
single key owns (TLS over UDP for Graylog) reads ``lib_log_rich: <reason>``. A value only
|
|
105
|
+
lib_log_rich itself refuses, such as an unknown level in ``LOG_CONSOLE_LEVEL`` or in the
|
|
106
|
+
section's ``console_level``, is reported in lib_log_rich's own words, which name the setting
|
|
107
|
+
and repeat the value (``lib_log_rich: Invalid runtime settings: console_level: Unknown log
|
|
108
|
+
level: 'bogus'``).
|
|
107
109
|
|
|
108
110
|
Attributes:
|
|
109
111
|
problems: One line per refused setting.
|
|
@@ -128,15 +130,33 @@ _PROJECT_MARKERS = ("pyproject.toml", ".git")
|
|
|
128
130
|
|
|
129
131
|
|
|
130
132
|
def _problems(error: BaseException) -> list[str]:
|
|
131
|
-
"""One line per problem
|
|
133
|
+
"""One line per problem: key and reason from the first pydantic error in ``error``'s chain.
|
|
134
|
+
|
|
135
|
+
A pydantic problem never repeats the refused value; one no single key owns (a model-wide
|
|
136
|
+
check) has no key and reads ``lib_log_rich: <reason>``. Without a pydantic error in the chain,
|
|
137
|
+
the line is lib_log_rich's own first line, which may repeat the value
|
|
138
|
+
(``Invalid runtime settings: console_level: Unknown log level: 'bogus'``).
|
|
139
|
+
"""
|
|
132
140
|
cause: BaseException | None = error
|
|
133
141
|
while cause is not None and not isinstance(cause, ValidationError):
|
|
134
142
|
cause = cause.__cause__ or cause.__context__
|
|
135
143
|
if cause is None:
|
|
136
144
|
# Not a pydantic error (e.g. an unknown level name in a LOG_* variable): lib_log_rich's
|
|
137
|
-
# own first line
|
|
145
|
+
# own first line.
|
|
138
146
|
return [f"lib_log_rich: {str(error).splitlines()[0]}"]
|
|
139
|
-
return [f"
|
|
147
|
+
return [f"{_problem_key(item['loc'])}: {item['msg']}" for item in cause.errors()]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _problem_key(loc: tuple[int | str, ...]) -> str:
|
|
151
|
+
"""``lib_log_rich.<key>`` for a problem located at a key, bare ``lib_log_rich`` for a model-wide one.
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
>>> _problem_key(("rate_limit",))
|
|
155
|
+
'lib_log_rich.rate_limit'
|
|
156
|
+
>>> _problem_key(())
|
|
157
|
+
'lib_log_rich'
|
|
158
|
+
"""
|
|
159
|
+
return ".".join(["lib_log_rich", *(str(part) for part in loc)])
|
|
140
160
|
|
|
141
161
|
|
|
142
162
|
def _nearest_dotenv() -> Path | None:
|
|
@@ -281,35 +301,39 @@ def init_logging(config: Config, *, dotenv_path: str | None = None) -> None:
|
|
|
281
301
|
lib_log_rich.runtime.attach_std_logging()
|
|
282
302
|
|
|
283
303
|
|
|
284
|
-
def
|
|
285
|
-
"""
|
|
304
|
+
def check_logging_config(config: Config) -> None:
|
|
305
|
+
"""Refuse ``config``'s logging settings as :func:`init_logging` would, without touching the runtime.
|
|
286
306
|
|
|
287
307
|
:func:`init_logging` returns at once while a runtime is running, so a configuration loaded
|
|
288
308
|
after logging started (``config --profile``'s reload) would never have its logging settings
|
|
289
|
-
judged.
|
|
290
|
-
|
|
291
|
-
|
|
309
|
+
judged. lib_log_rich's ``validate_config`` refuses exactly what its ``init`` refuses, with the
|
|
310
|
+
same message and the same ``LOG_*`` overrides, and neither reads nor replaces the running
|
|
311
|
+
runtime, so logging keeps running with the settings it started with, on every thread.
|
|
292
312
|
|
|
293
313
|
Args:
|
|
294
314
|
config: Already-loaded layered configuration object.
|
|
295
|
-
dotenv_path: As for :func:`init_logging`.
|
|
296
315
|
|
|
297
316
|
Raises:
|
|
298
|
-
InvalidLoggingConfigError: As for :func:`init_logging
|
|
317
|
+
InvalidLoggingConfigError: As for :func:`init_logging`, with the same problem lines.
|
|
299
318
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
319
|
+
Example:
|
|
320
|
+
>>> check_logging_config(Config({"lib_log_rich": {"console_level": "info"}}, {}))
|
|
321
|
+
>>> try:
|
|
322
|
+
... check_logging_config(Config({"lib_log_rich": {"rate_limit": "100:60"}}, {}))
|
|
323
|
+
... except InvalidLoggingConfigError as exc:
|
|
324
|
+
... print(exc)
|
|
325
|
+
lib_log_rich.rate_limit: Input should be a valid tuple
|
|
303
326
|
"""
|
|
304
|
-
|
|
305
|
-
lib_log_rich.runtime.
|
|
306
|
-
|
|
327
|
+
try:
|
|
328
|
+
lib_log_rich.runtime.validate_config(_build_runtime_config(config))
|
|
329
|
+
except ValueError as exc: # pydantic's ValidationError is a ValueError
|
|
330
|
+
raise InvalidLoggingConfigError(_problems(exc)) from exc
|
|
307
331
|
|
|
308
332
|
|
|
309
333
|
__all__ = [
|
|
310
334
|
"InvalidLoggingConfigError",
|
|
311
335
|
"LoggingConfigModel",
|
|
336
|
+
"check_logging_config",
|
|
312
337
|
"init_logging",
|
|
313
338
|
"log_variables_hidden",
|
|
314
|
-
"restart_logging",
|
|
315
339
|
]
|
|
@@ -20,28 +20,28 @@ from .config import (
|
|
|
20
20
|
get_default_config_path_in_memory,
|
|
21
21
|
)
|
|
22
22
|
from .email import EmailSpy
|
|
23
|
-
from .logging import
|
|
23
|
+
from .logging import check_logging_config_in_memory, init_logging_in_memory
|
|
24
24
|
|
|
25
25
|
# Static conformance assertions
|
|
26
26
|
if TYPE_CHECKING:
|
|
27
27
|
from bitranox_template_py_cli.application.ports import (
|
|
28
|
+
CheckLoggingConfig,
|
|
28
29
|
GetConfig,
|
|
29
30
|
GetDefaultConfigPath,
|
|
30
31
|
InitLogging,
|
|
31
|
-
RestartLogging,
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
_assert_get_config: GetConfig = get_config_in_memory
|
|
35
35
|
_assert_get_default_config_path: GetDefaultConfigPath = get_default_config_path_in_memory
|
|
36
36
|
_assert_init_logging: InitLogging = init_logging_in_memory
|
|
37
|
-
|
|
37
|
+
_assert_check_logging_config: CheckLoggingConfig = check_logging_config_in_memory
|
|
38
38
|
|
|
39
39
|
__all__ = [
|
|
40
40
|
"EmailSpy",
|
|
41
|
+
"check_logging_config_in_memory",
|
|
41
42
|
"deploy_configuration_in_memory",
|
|
42
43
|
"display_config_in_memory",
|
|
43
44
|
"get_config_in_memory",
|
|
44
45
|
"get_default_config_path_in_memory",
|
|
45
46
|
"init_logging_in_memory",
|
|
46
|
-
"restart_logging_in_memory",
|
|
47
47
|
]
|
|
@@ -63,17 +63,18 @@ def init_logging_in_memory(config: Config, *, dotenv_path: str | None = None) ->
|
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def
|
|
67
|
-
"""
|
|
66
|
+
def check_logging_config_in_memory(config: Config) -> None:
|
|
67
|
+
"""Refuse nothing: the test runtime ignores the configuration, so no setting in it can break it.
|
|
68
|
+
|
|
69
|
+
:func:`init_logging_in_memory` starts a fixed runtime whatever the configuration holds.
|
|
68
70
|
|
|
69
71
|
Args:
|
|
70
|
-
config: Unused; the parameter exists to satisfy the ``
|
|
71
|
-
dotenv_path: Unused for the same reason.
|
|
72
|
+
config: Unused; the parameter exists to satisfy the ``CheckLoggingConfig`` protocol.
|
|
72
73
|
|
|
73
74
|
Example:
|
|
74
75
|
>>> from lib_layered_config import Config
|
|
75
|
-
>>>
|
|
76
|
+
>>> check_logging_config_in_memory(Config({"lib_log_rich": {"rate_limit": "100:60"}}, {}))
|
|
76
77
|
"""
|
|
77
78
|
|
|
78
79
|
|
|
79
|
-
__all__ = ["
|
|
80
|
+
__all__ = ["check_logging_config_in_memory", "init_logging_in_memory"]
|