bitranox-template-py-cli 2.1.0__tar.gz → 2.1.2__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.0 → bitranox_template_py_cli-2.1.2}/CHANGELOG.md +37 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/PKG-INFO +2 -2
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/docs/systemdesign/module_reference.md +23 -20
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/pyproject.toml +2 -2
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/__init__conf__.py +1 -1
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/commands/email/_common.py +30 -4
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/config_load.py +9 -5
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/exit_codes.py +3 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/50-mail.toml +2 -1
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/email/transport.py +9 -3
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/logging/setup.py +62 -14
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/memory/logging.py +16 -11
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/application/ports.py +2 -0
- bitranox_template_py_cli-2.1.2/tests/test_cli_attachment_refused.py +191 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_email_config_translation.py +20 -3
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_logging_dotenv_isolation.py +129 -2
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_mail.py +67 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_memory_logging.py +25 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.devcontainer/devcontainer.json +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.devcontainer/settings.json +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.env.example +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.gitattributes +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.github/actions/extract-metadata/action.yml +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.github/dependabot.yml +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.github/workflows/codeql.yml +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.github/workflows/default_cicd_public.yml +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.github/workflows/default_release_public.yml +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.gitignore +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.qlty/qlty.toml +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.snyk +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/CONFIG.md +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/CONTRIBUTING.md +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/DEVELOPMENT.md +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/INSTALL.md +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/LICENSE +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/Makefile +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/README.md +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/SECURITY.md +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/ai-stance.md +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/ai-transparency.md +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/codecov.yml +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/docs/adr/0001-memory-adapters-in-src.md +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/notebooks/Quickstart.ipynb +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/rename.sh +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/rename_dry.sh +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/reset_git_history.sh +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/__main__.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/commands/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/commands/config.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/commands/email/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/commands/email/send_email.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/commands/email/send_notification.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/commands/info.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/commands/logging.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/constants.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/context.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/main.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/py.typed +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/root.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/safe_console.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/typed_click.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/40-layered-config.toml +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/90-logging.toml +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/defaultconfig.toml +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/deploy.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/display.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/loader.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/overrides.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/py.typed +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/email/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/email/config.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/email/py.typed +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/email/sender.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/email/validation.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/logging/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/logging/py.typed +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/memory/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/memory/config.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/memory/email.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/py.typed +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/application/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/application/py.typed +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/composition/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/composition/py.typed +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/domain/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/domain/behaviors.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/domain/enums.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/domain/errors.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/domain/py.typed +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/entry.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/py.typed +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/conftest.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_behaviors.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cache_effectiveness.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_config.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_config_errors.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_core.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_email.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_email_config_errors.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_env_file.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_exit_codes.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_main_exit.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_overrides.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_validation.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_config_overrides.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_declared_dependencies.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_deploy_mode_safety.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_deploy_permissions.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_display.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_email_attachment_lists.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_email_password_secrecy.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_email_shipped_defaults.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_enums.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_errors.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_logging.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_metadata.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_metadata_sync.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_module_entry.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_module_reference_sync.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_permission_defaults.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_ports.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_property_email.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_property_overrides.py +0 -0
- {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_safe_console.py +0 -0
|
@@ -6,6 +6,43 @@ the [Keep a Changelog](https://keepachangelog.com/) format.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.1.2] 2026-10-08 09:43:32
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **A refused attachment exits 77, not 1 with a traceback (exit code change).** An attachment
|
|
13
|
+
btx_lib_mail's security checks refuse (a blocked extension such as `.exe`, a file under a
|
|
14
|
+
blocked directory, a symlink, a file over the size limit) made `send-email` exit 1 with
|
|
15
|
+
`Error: Unexpected error - Attachment security violation (...)` and log the refusal with a
|
|
16
|
+
traceback, as if the program had crashed. Since btx_lib_mail 4.0.0 every blocked extension is
|
|
17
|
+
refused on every platform, so this is an ordinary outcome on Linux and macOS too. It now exits
|
|
18
|
+
77 (`ExitCode.ATTACHMENT_REFUSED`, sysexits `EX_NOPERM`: not permitted by policy) with one
|
|
19
|
+
line, `Error: Attachment refused by security policy - <the library's reason>`, logs no
|
|
20
|
+
traceback, and delivers nothing. A script that checks for 1 after a refused attachment must
|
|
21
|
+
check for 77. With `email.attachments.raise_on_security_violation = false` the attachment is
|
|
22
|
+
still skipped with a warning and the message sent, as before; a violation that names no attachment
|
|
23
|
+
left to drop is refused in warn mode too, and now exits 77 where it exited 1.
|
|
24
|
+
|
|
25
|
+
## [2.1.1] 2026-10-06 18:13:05
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **A refused `LOG_*` variable no longer disables every command (exit code change).** A value
|
|
29
|
+
lib_log_rich refuses in a `LOG_*` variable, set in the environment or in the `.env` logging
|
|
30
|
+
reads (`LOG_CONSOLE_LEVEL=bogus` in the `--env-file`), made every command, `info` and
|
|
31
|
+
`config-deploy` included, exit 1 with `InvalidLoggingConfigError: lib_log_rich: Unknown log
|
|
32
|
+
level: 'bogus'`: the fallback restarted logging with an empty configuration, but lib_log_rich
|
|
33
|
+
reads the `LOG_*` variables on every start and refused the same variable again. Logging now
|
|
34
|
+
falls back to its defaults with every `LOG_*` variable hidden for that start (and put back
|
|
35
|
+
afterwards), so only the commands that read the configuration (`config`, `send-email`,
|
|
36
|
+
`send-notification`) exit 78, and the others run with exit 0. The 78 carries lib_log_rich's
|
|
37
|
+
own message, `Error: lib_log_rich: Unknown log level: 'bogus'`, which may name neither the
|
|
38
|
+
variable nor where it was set. A refused `[lib_log_rich]` value still leaves every valid
|
|
39
|
+
`LOG_*` variable in force for the fallback, as in 2.1.0: only a refused variable hides them.
|
|
40
|
+
- **The testing composition ignores the developer's `LOG_*` variables.** `build_testing()`'s
|
|
41
|
+
logging runtime (`init_logging_in_memory`) now starts with every `LOG_*` variable hidden and
|
|
42
|
+
puts them back afterwards. A `LOG_CONSOLE_LEVEL=bogus` in the shell running the tests made
|
|
43
|
+
every command under `build_testing()` fail with `ValueError: Unknown log level: 'bogus'`, and a
|
|
44
|
+
valid one changed the quiet test runtime. Production logging is unaffected.
|
|
45
|
+
|
|
9
46
|
## [2.1.0] 2026-10-06 15:37:27
|
|
10
47
|
|
|
11
48
|
### Added
|
|
@@ -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.2
|
|
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,7 +26,7 @@ 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
|
|
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)
|
|
30
30
|
- `src/bitranox_template_py_cli/adapters/cli/` - CLI adapter package:
|
|
31
31
|
- `__init__.py` - Public facade
|
|
32
32
|
- `constants.py` - Shared constants
|
|
@@ -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
|
|
@@ -92,6 +93,7 @@ moves or removes a module, a CLI option or a public name.
|
|
|
92
93
|
- `tests/test_enums.py` - Domain enum tests
|
|
93
94
|
- `tests/test_errors.py` - Domain error types
|
|
94
95
|
- `tests/test_logging.py` - Logging configuration model
|
|
96
|
+
- `tests/test_logging_dotenv_isolation.py` - Only `LOG_*` lines of a `.env` reach the environment; an invalid `[lib_log_rich]` section or `LOG_*` variable is a configuration failure that leaves logging running
|
|
95
97
|
- `tests/test_mail.py` - Email configuration and sending tests (the `integration` ones send real mail)
|
|
96
98
|
- `tests/test_memory_logging.py` - Testing-composition logging runtime and the per-test logging reset
|
|
97
99
|
- `tests/test_metadata.py` - Package metadata and PEP 561 marker tests
|
|
@@ -135,19 +137,20 @@ Run `lint-imports` to verify compliance.
|
|
|
135
137
|
|
|
136
138
|
POSIX-conventional exit codes defined in `adapters/cli/exit_codes.py`:
|
|
137
139
|
|
|
138
|
-
| Code | Name
|
|
139
|
-
|
|
140
|
-
| 0 | `SUCCESS`
|
|
141
|
-
| 1 | `GENERAL_ERROR`
|
|
142
|
-
| 2 | `FILE_NOT_FOUND`
|
|
143
|
-
| 13 | `PERMISSION_DENIED`
|
|
144
|
-
| 22 | `INVALID_ARGUMENT`
|
|
145
|
-
| 69 | `SMTP_FAILURE`
|
|
146
|
-
|
|
|
147
|
-
|
|
|
148
|
-
|
|
|
149
|
-
|
|
|
150
|
-
|
|
|
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) |
|
|
151
154
|
|
|
152
155
|
---
|
|
153
156
|
|
|
@@ -242,7 +245,7 @@ Send email using configured SMTP settings.
|
|
|
242
245
|
| `--raise-on-missing-attachments / --no-raise-on-missing-attachments` | Override missing-attachment handling |
|
|
243
246
|
| `--raise-on-invalid-recipient / --no-raise-on-invalid-recipient` | Override invalid-recipient handling |
|
|
244
247
|
|
|
245
|
-
**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)
|
|
246
249
|
|
|
247
250
|
### send-notification
|
|
248
251
|
|
|
@@ -322,9 +325,9 @@ Raises `ValueError` with descriptive message on invalid input.
|
|
|
322
325
|
`EmailConfig` (`adapters/email/config.py`, re-exported by `adapters/email/sender.py`) subclasses
|
|
323
326
|
btx_lib_mail's `ConfMail`: frozen, a name that is not a field is refused, the password is a
|
|
324
327
|
`SecretStr`, and a validation error never shows the password or a host. It adds `from_address` and
|
|
325
|
-
`recipients
|
|
326
|
-
|
|
327
|
-
column).
|
|
328
|
+
`recipients`. ConfMail checks every SMTP host's syntax (port range, IPv6 brackets, host name
|
|
329
|
+
labels) when it loads, so `EmailConfig` has no host check of its own. In Python the fields use
|
|
330
|
+
the library's names; the configuration file keeps its own keys (fourth column).
|
|
328
331
|
|
|
329
332
|
| Field | Type | Default | File key (`[email]`) | Description |
|
|
330
333
|
|--------------------------------|---------------------|---------|--------------------------------|------------------------------------------------------------------|
|
|
@@ -362,7 +365,7 @@ lib_layered_config is the only reader of configuration: it merges every layer (t
|
|
|
362
365
|
defaults, the app, host and user files, `.env`, the environment, `--set`) into one mapping.
|
|
363
366
|
`load_email_config_from_dict()` turns that mapping's `[email]` section into an `EmailConfig`:
|
|
364
367
|
|
|
365
|
-
- the six keys in the
|
|
368
|
+
- the six keys in the fourth column that differ from the field names are mapped;
|
|
366
369
|
- `[email.attachments]` keys become `attachment_<key>`;
|
|
367
370
|
- a key that is not listed is refused (exit 78, `email.<key>: unknown key`), whatever layer it came from;
|
|
368
371
|
- blank text means "not configured"; a single host or address string is a one-entry list;
|
|
@@ -379,7 +382,7 @@ problem, never with the refused value.
|
|
|
379
382
|
|---------------------|------------------------------------------------------------------------------|
|
|
380
383
|
| `SECTION_KEYS` | Every key `[email]` accepts (the `attachments` table included) |
|
|
381
384
|
| `ATTACHMENT_KEYS` | Every key `[email.attachments]` accepts; each names field `attachment_<key>` |
|
|
382
|
-
| `FILE_KEY_TO_FIELD` | File key -> `EmailConfig` field, for the
|
|
385
|
+
| `FILE_KEY_TO_FIELD` | File key -> `EmailConfig` field, for the six keys whose names differ |
|
|
383
386
|
|
|
384
387
|
---
|
|
385
388
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "bitranox_template_py_cli"
|
|
3
|
-
version = "2.1.
|
|
3
|
+
version = "2.1.2"
|
|
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.2"
|
|
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.
|
|
@@ -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__
|
|
@@ -84,8 +85,8 @@ def smtp_config_options(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
84
85
|
|
|
85
86
|
Adds CLI flags for the SMTP connection and delivery settings (hosts, credentials,
|
|
86
87
|
STARTTLS, timeout, the two raise_on_* switches). ``starttls_verify``,
|
|
87
|
-
``local_hostname`` and the attachment
|
|
88
|
-
``--set email.<key>=...``.
|
|
88
|
+
``local_hostname``, ``delivery_deadline``, ``recipient_max_count`` and the attachment
|
|
89
|
+
settings have no flag; set them with ``--set email.<key>=...``.
|
|
89
90
|
"""
|
|
90
91
|
options = [
|
|
91
92
|
option(
|
|
@@ -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
|
|
|
@@ -14,7 +14,8 @@ configuration.
|
|
|
14
14
|
|
|
15
15
|
Contents:
|
|
16
16
|
* :func:`load_config` - load with profile, ``.env`` and ``--set``, or say why not.
|
|
17
|
-
* :func:`start_logging` - start logging; an invalid ``[lib_log_rich]``
|
|
17
|
+
* :func:`start_logging` - start logging; an invalid ``[lib_log_rich]`` value or ``LOG_*``
|
|
18
|
+
variable is a load failure.
|
|
18
19
|
* :func:`require_config` - the configuration, or exit 78 naming the failure.
|
|
19
20
|
* :func:`report_load_failure` - the one-line report, after the traceback on request.
|
|
20
21
|
* :func:`echo_load_traceback` - the loader's traceback alone, for a caller with its own line.
|
|
@@ -97,9 +98,13 @@ def start_logging(
|
|
|
97
98
|
) -> tuple[Config, Exception | None]:
|
|
98
99
|
"""Start logging with ``config``; a logging section it refuses is recorded like a load failure.
|
|
99
100
|
|
|
100
|
-
An invalid ``[lib_log_rich]`` value would otherwise stop every
|
|
101
|
-
(which replaces the file holding it) included.
|
|
102
|
-
|
|
101
|
+
An invalid ``[lib_log_rich]`` value or ``LOG_*`` variable would otherwise stop every
|
|
102
|
+
command, ``config-deploy`` (which replaces the file holding it) included. ``init_logging``
|
|
103
|
+
then starts logging with its defaults (and no ``LOG_*`` variable, if one of them is the
|
|
104
|
+
refused setting) before it raises, and the commands that read the configuration refuse with
|
|
105
|
+
exit 78 and the refusal: ``lib_log_rich.<key>: <reason>`` for a problem the section's type
|
|
106
|
+
check finds, lib_log_rich's own message for a value only lib_log_rich refuses, whether it
|
|
107
|
+
came from the section or a ``LOG_*`` variable (``lib_log_rich: Unknown log level: 'bogus'``).
|
|
103
108
|
|
|
104
109
|
Args:
|
|
105
110
|
services: The composition's services; only ``init_logging`` is used.
|
|
@@ -114,7 +119,6 @@ def start_logging(
|
|
|
114
119
|
try:
|
|
115
120
|
services.init_logging(config, dotenv_path=env_file)
|
|
116
121
|
except InvalidLoggingConfigError as exc:
|
|
117
|
-
services.init_logging(Config({}, {}), dotenv_path=env_file)
|
|
118
122
|
return Config({}, {}), config_error or exc
|
|
119
123
|
return config, config_error
|
|
120
124
|
|
|
@@ -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
|
|
@@ -122,10 +122,16 @@ def send_email(
|
|
|
122
122
|
|
|
123
123
|
Raises:
|
|
124
124
|
ValueError: No from_address configured and no override provided,
|
|
125
|
-
or no recipients configured and no override provided
|
|
125
|
+
or no recipients configured and no override provided, or more
|
|
126
|
+
recipients or attachments than config.recipient_max_count or
|
|
127
|
+
config.attachment_max_count allow (refused before any delivery).
|
|
126
128
|
ConfigurationError: No SMTP hosts configured.
|
|
127
|
-
FileNotFoundError: Required attachment missing and
|
|
128
|
-
is True.
|
|
129
|
+
FileNotFoundError: Required attachment missing or unreadable and
|
|
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.
|
|
129
135
|
DeliveryError: All SMTP hosts failed for a recipient.
|
|
130
136
|
|
|
131
137
|
Side Effects:
|
|
@@ -5,8 +5,10 @@ eliminating duplication between module entry (__main__.py) and console script
|
|
|
5
5
|
(cli.py) while ensuring initialization happens exactly once.
|
|
6
6
|
|
|
7
7
|
Contents:
|
|
8
|
-
* :func:`init_logging` - idempotent logging initialization
|
|
9
|
-
|
|
8
|
+
* :func:`init_logging` - idempotent logging initialization from the ``[lib_log_rich]``
|
|
9
|
+
section and the ``LOG_*`` variables (the environment, plus the ``LOG_*`` lines of a ``.env``).
|
|
10
|
+
* :class:`InvalidLoggingConfigError` - the ``[lib_log_rich]`` section or a ``LOG_*`` variable
|
|
11
|
+
cannot configure logging.
|
|
10
12
|
* :func:`_build_runtime_config` - constructs RuntimeConfig from layered sources.
|
|
11
13
|
|
|
12
14
|
System Role:
|
|
@@ -18,18 +20,20 @@ System Role:
|
|
|
18
20
|
from __future__ import annotations
|
|
19
21
|
|
|
20
22
|
import os
|
|
23
|
+
from contextlib import contextmanager
|
|
21
24
|
from pathlib import Path
|
|
22
25
|
from typing import TYPE_CHECKING, cast
|
|
23
26
|
|
|
24
27
|
import lib_log_rich.runtime
|
|
25
28
|
from dotenv import dotenv_values
|
|
29
|
+
from lib_layered_config import Config
|
|
26
30
|
from pydantic import BaseModel, ConfigDict, ValidationError
|
|
27
31
|
|
|
28
32
|
from bitranox_template_py_cli import __init__conf__
|
|
29
33
|
from bitranox_template_py_cli.domain.errors import ConfigurationError
|
|
30
34
|
|
|
31
35
|
if TYPE_CHECKING:
|
|
32
|
-
from
|
|
36
|
+
from collections.abc import Generator
|
|
33
37
|
|
|
34
38
|
|
|
35
39
|
class LoggingConfigModel(BaseModel):
|
|
@@ -94,18 +98,20 @@ def _build_runtime_config(config: Config) -> lib_log_rich.runtime.RuntimeConfig:
|
|
|
94
98
|
class InvalidLoggingConfigError(ConfigurationError):
|
|
95
99
|
"""lib_log_rich refuses its settings: one ``<key>: <reason>`` in :attr:`problems` per problem.
|
|
96
100
|
|
|
97
|
-
The settings are the ``[lib_log_rich]`` section plus any ``LOG_*`` variable. A problem
|
|
98
|
-
the key and never repeats the refused value.
|
|
101
|
+
The settings are the ``[lib_log_rich]`` section plus any ``LOG_*`` variable. A problem the
|
|
102
|
+
type check of the section finds names the key and never repeats the refused value. A value
|
|
103
|
+
only lib_log_rich itself refuses, such as an unknown level in ``LOG_CONSOLE_LEVEL`` or in the
|
|
104
|
+
section's ``console_level``, is reported in lib_log_rich's own words (``lib_log_rich: Unknown
|
|
105
|
+
log level: 'bogus'``), which may name neither the setting nor where it was set.
|
|
99
106
|
|
|
100
107
|
Attributes:
|
|
101
108
|
problems: One line per refused setting.
|
|
102
109
|
|
|
103
110
|
Example:
|
|
104
|
-
>>> from lib_layered_config import Config
|
|
105
111
|
>>> try:
|
|
106
|
-
...
|
|
107
|
-
... except
|
|
108
|
-
... print(exc)
|
|
112
|
+
... _build_runtime_config(Config({"lib_log_rich": {"rate_limit": "100:60"}}, {}))
|
|
113
|
+
... except ValidationError as exc:
|
|
114
|
+
... print(InvalidLoggingConfigError(_problems(exc)))
|
|
109
115
|
lib_log_rich.rate_limit: Input should be a valid tuple
|
|
110
116
|
"""
|
|
111
117
|
|
|
@@ -126,8 +132,8 @@ def _problems(error: BaseException) -> list[str]:
|
|
|
126
132
|
while cause is not None and not isinstance(cause, ValidationError):
|
|
127
133
|
cause = cause.__cause__ or cause.__context__
|
|
128
134
|
if cause is None:
|
|
129
|
-
# Not a pydantic error (e.g. an unknown level name
|
|
130
|
-
# the
|
|
135
|
+
# Not a pydantic error (e.g. an unknown level name in a LOG_* variable): lib_log_rich's
|
|
136
|
+
# own first line, which may name neither the variable nor where it was set.
|
|
131
137
|
return [f"lib_log_rich: {str(error).splitlines()[0]}"]
|
|
132
138
|
return [f"lib_log_rich.{'.'.join(str(part) for part in item['loc'])}: {item['msg']}" for item in cause.errors()]
|
|
133
139
|
|
|
@@ -182,6 +188,41 @@ def _load_log_variables(dotenv_path: str | None) -> None:
|
|
|
182
188
|
os.environ.setdefault(name, value)
|
|
183
189
|
|
|
184
190
|
|
|
191
|
+
@contextmanager
|
|
192
|
+
def log_variables_hidden() -> Generator[None]:
|
|
193
|
+
"""Remove every ``LOG_*`` variable from the environment for the block, then put each back.
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
>>> os.environ["LOG_HIDDEN_PROBE"] = "x"
|
|
197
|
+
>>> with log_variables_hidden():
|
|
198
|
+
... "LOG_HIDDEN_PROBE" in os.environ
|
|
199
|
+
False
|
|
200
|
+
>>> os.environ.pop("LOG_HIDDEN_PROBE")
|
|
201
|
+
'x'
|
|
202
|
+
"""
|
|
203
|
+
names = [name for name in os.environ if name.startswith(_LOG_VARIABLE_PREFIX)]
|
|
204
|
+
hidden = {name: os.environ.pop(name) for name in names}
|
|
205
|
+
try:
|
|
206
|
+
yield
|
|
207
|
+
finally:
|
|
208
|
+
os.environ.update(hidden)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _start_default_logging() -> None:
|
|
212
|
+
"""Start lib_log_rich with the package defaults; without the ``LOG_*`` variables only if needed.
|
|
213
|
+
|
|
214
|
+
lib_log_rich reads the ``LOG_*`` variables from the environment on every init. When the
|
|
215
|
+
refused setting came from the ``[lib_log_rich]`` section, the defaults start with them and
|
|
216
|
+
a valid ``LOG_CONSOLE_LEVEL`` still applies. When one of them is the refused setting, that
|
|
217
|
+
start is refused again, and only then do the defaults start with every ``LOG_*`` hidden.
|
|
218
|
+
"""
|
|
219
|
+
try:
|
|
220
|
+
lib_log_rich.runtime.init(_build_runtime_config(Config({}, {})))
|
|
221
|
+
except ValueError: # pydantic's ValidationError is a ValueError
|
|
222
|
+
with log_variables_hidden():
|
|
223
|
+
lib_log_rich.runtime.init(_build_runtime_config(Config({}, {})))
|
|
224
|
+
|
|
225
|
+
|
|
185
226
|
def init_logging(config: Config, *, dotenv_path: str | None = None) -> None:
|
|
186
227
|
"""Initialize lib_log_rich runtime with the provided configuration.
|
|
187
228
|
|
|
@@ -199,8 +240,12 @@ def init_logging(config: Config, *, dotenv_path: str | None = None) -> None:
|
|
|
199
240
|
to use the nearest ``.env`` from the working directory up to the project root.
|
|
200
241
|
|
|
201
242
|
Raises:
|
|
202
|
-
InvalidLoggingConfigError: The ``[lib_log_rich]`` section
|
|
203
|
-
refuses
|
|
243
|
+
InvalidLoggingConfigError: The ``[lib_log_rich]`` section or a ``LOG_*`` variable holds
|
|
244
|
+
a value lib_log_rich refuses. Logging is started anyway, with the package defaults
|
|
245
|
+
and the ``LOG_*`` variables (every one of them ignored if the defaults are refused
|
|
246
|
+
with them too), so the caller can record the failure and run the commands that do
|
|
247
|
+
not read the configuration. Only the first call can raise: logging is running after
|
|
248
|
+
it, so a later call returns at once, also with the same refused configuration.
|
|
204
249
|
|
|
205
250
|
Side Effects:
|
|
206
251
|
Copies the ``LOG_*`` lines of that ``.env`` into the process environment on first
|
|
@@ -226,9 +271,11 @@ def init_logging(config: Config, *, dotenv_path: str | None = None) -> None:
|
|
|
226
271
|
_load_log_variables(dotenv_path)
|
|
227
272
|
try:
|
|
228
273
|
lib_log_rich.runtime.init(_build_runtime_config(config))
|
|
229
|
-
except
|
|
274
|
+
except ValueError as exc: # pydantic's ValidationError is a ValueError
|
|
230
275
|
# The type check of the section and lib_log_rich's own range checks (which also see the
|
|
231
276
|
# LOG_* variables) both refuse here; neither has started the runtime.
|
|
277
|
+
_start_default_logging()
|
|
278
|
+
lib_log_rich.runtime.attach_std_logging()
|
|
232
279
|
raise InvalidLoggingConfigError(_problems(exc)) from exc
|
|
233
280
|
lib_log_rich.runtime.attach_std_logging()
|
|
234
281
|
|
|
@@ -237,4 +284,5 @@ __all__ = [
|
|
|
237
284
|
"InvalidLoggingConfigError",
|
|
238
285
|
"LoggingConfigModel",
|
|
239
286
|
"init_logging",
|
|
287
|
+
"log_variables_hidden",
|
|
240
288
|
]
|
|
@@ -12,6 +12,7 @@ import lib_log_rich.runtime
|
|
|
12
12
|
from lib_log_rich.domain import LogLevel
|
|
13
13
|
|
|
14
14
|
from bitranox_template_py_cli import __init__conf__
|
|
15
|
+
from bitranox_template_py_cli.adapters.logging.setup import log_variables_hidden
|
|
15
16
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
17
18
|
from lib_layered_config import Config
|
|
@@ -29,7 +30,10 @@ def init_logging_in_memory(config: Config, *, dotenv_path: str | None = None) ->
|
|
|
29
30
|
double has no business writing to a system log, and a queue is a background thread the
|
|
30
31
|
test run would have to reap. The console level is ERROR so ordinary INFO logging cannot
|
|
31
32
|
land in the output a test asserts on. Unlike the production initializer this never loads
|
|
32
|
-
a ``.env`` file,
|
|
33
|
+
a ``.env`` file, and it starts with every ``LOG_*`` variable hidden (each is put back
|
|
34
|
+
afterwards): lib_log_rich reads them on every init, so a developer's own
|
|
35
|
+
``LOG_CONSOLE_LEVEL`` would otherwise change the test runtime, and a value lib_log_rich
|
|
36
|
+
refuses would fail every command run under ``build_testing()``.
|
|
33
37
|
|
|
34
38
|
Args:
|
|
35
39
|
config: Layered configuration object. Unused: the test runtime is fixed, and the
|
|
@@ -45,17 +49,18 @@ def init_logging_in_memory(config: Config, *, dotenv_path: str | None = None) ->
|
|
|
45
49
|
"""
|
|
46
50
|
if lib_log_rich.runtime.is_initialised():
|
|
47
51
|
return
|
|
48
|
-
|
|
49
|
-
lib_log_rich.runtime.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
with log_variables_hidden():
|
|
53
|
+
lib_log_rich.runtime.init(
|
|
54
|
+
lib_log_rich.runtime.RuntimeConfig(
|
|
55
|
+
service=f"{__init__conf__.name}-test",
|
|
56
|
+
environment="test",
|
|
57
|
+
console_level=LogLevel.ERROR,
|
|
58
|
+
enable_journald=False,
|
|
59
|
+
enable_eventlog=False,
|
|
60
|
+
enable_graylog=False,
|
|
61
|
+
queue_enabled=False,
|
|
62
|
+
)
|
|
57
63
|
)
|
|
58
|
-
)
|
|
59
64
|
|
|
60
65
|
|
|
61
66
|
__all__ = ["init_logging_in_memory"]
|
|
@@ -102,6 +102,8 @@ class InitLogging(Protocol):
|
|
|
102
102
|
"""Initialize lib_log_rich runtime with the provided configuration.
|
|
103
103
|
|
|
104
104
|
``dotenv_path`` is the ``.env`` the configuration was loaded with, or None for the nearest one.
|
|
105
|
+
A refused logging setting raises ``InvalidLoggingConfigError`` only after logging has been
|
|
106
|
+
started with its defaults, so the caller never has to start it a second time.
|
|
105
107
|
"""
|
|
106
108
|
|
|
107
109
|
def __call__(self, config: Config, *, dotenv_path: str | None = None) -> None: ...
|