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.
Files changed (128) hide show
  1. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/CHANGELOG.md +37 -0
  2. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/PKG-INFO +2 -2
  3. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/docs/systemdesign/module_reference.md +23 -20
  4. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/pyproject.toml +2 -2
  5. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/__init__conf__.py +1 -1
  6. {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
  7. {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
  8. {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
  9. {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
  10. {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
  11. {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
  12. {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
  13. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/application/ports.py +2 -0
  14. bitranox_template_py_cli-2.1.2/tests/test_cli_attachment_refused.py +191 -0
  15. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_email_config_translation.py +20 -3
  16. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_logging_dotenv_isolation.py +129 -2
  17. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_mail.py +67 -0
  18. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_memory_logging.py +25 -0
  19. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.devcontainer/devcontainer.json +0 -0
  20. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.devcontainer/settings.json +0 -0
  21. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.env.example +0 -0
  22. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.gitattributes +0 -0
  23. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.github/actions/extract-metadata/action.yml +0 -0
  24. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.github/dependabot.yml +0 -0
  25. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.github/workflows/codeql.yml +0 -0
  26. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.github/workflows/default_cicd_public.yml +0 -0
  27. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.github/workflows/default_release_public.yml +0 -0
  28. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.gitignore +0 -0
  29. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.qlty/qlty.toml +0 -0
  30. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/.snyk +0 -0
  31. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/CONFIG.md +0 -0
  32. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/CONTRIBUTING.md +0 -0
  33. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/DEVELOPMENT.md +0 -0
  34. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/INSTALL.md +0 -0
  35. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/LICENSE +0 -0
  36. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/Makefile +0 -0
  37. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/README.md +0 -0
  38. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/SECURITY.md +0 -0
  39. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/ai-stance.md +0 -0
  40. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/ai-transparency.md +0 -0
  41. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/codecov.yml +0 -0
  42. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/docs/adr/0001-memory-adapters-in-src.md +0 -0
  43. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/notebooks/Quickstart.ipynb +0 -0
  44. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/rename.sh +0 -0
  45. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/rename_dry.sh +0 -0
  46. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/reset_git_history.sh +0 -0
  47. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/__init__.py +0 -0
  48. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/__main__.py +0 -0
  49. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/__init__.py +0 -0
  50. {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
  51. {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
  52. {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
  53. {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
  54. {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
  55. {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
  56. {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
  57. {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
  58. {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
  59. {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
  60. {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
  61. {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
  62. {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
  63. {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
  64. {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
  65. {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
  66. {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
  67. {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
  68. {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
  69. {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
  70. {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
  71. {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
  72. {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
  73. {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
  74. {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
  75. {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
  76. {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
  77. {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
  78. {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
  79. {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
  80. {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
  81. {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
  82. {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
  83. {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
  84. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/py.typed +0 -0
  85. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/application/__init__.py +0 -0
  86. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/application/py.typed +0 -0
  87. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/composition/__init__.py +0 -0
  88. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/composition/py.typed +0 -0
  89. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/domain/__init__.py +0 -0
  90. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/domain/behaviors.py +0 -0
  91. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/domain/enums.py +0 -0
  92. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/domain/errors.py +0 -0
  93. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/domain/py.typed +0 -0
  94. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/entry.py +0 -0
  95. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/py.typed +0 -0
  96. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/conftest.py +0 -0
  97. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_behaviors.py +0 -0
  98. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cache_effectiveness.py +0 -0
  99. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_config.py +0 -0
  100. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_config_errors.py +0 -0
  101. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_core.py +0 -0
  102. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_email.py +0 -0
  103. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_email_config_errors.py +0 -0
  104. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_env_file.py +0 -0
  105. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_exit_codes.py +0 -0
  106. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_main_exit.py +0 -0
  107. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_overrides.py +0 -0
  108. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_cli_validation.py +0 -0
  109. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_config_overrides.py +0 -0
  110. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_declared_dependencies.py +0 -0
  111. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_deploy_mode_safety.py +0 -0
  112. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_deploy_permissions.py +0 -0
  113. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_display.py +0 -0
  114. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_email_attachment_lists.py +0 -0
  115. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_email_password_secrecy.py +0 -0
  116. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_email_shipped_defaults.py +0 -0
  117. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_enums.py +0 -0
  118. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_errors.py +0 -0
  119. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_logging.py +0 -0
  120. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_metadata.py +0 -0
  121. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_metadata_sync.py +0 -0
  122. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_module_entry.py +0 -0
  123. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_module_reference_sync.py +0 -0
  124. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_permission_defaults.py +0 -0
  125. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_ports.py +0 -0
  126. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_property_email.py +0 -0
  127. {bitranox_template_py_cli-2.1.0 → bitranox_template_py_cli-2.1.2}/tests/test_property_overrides.py +0 -0
  128. {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.0
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.12.0
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 | Usage |
139
- |------|---------------------|------------------------------------------------------------------------------------------------------------------------|
140
- | 0 | `SUCCESS` | Command completed successfully |
141
- | 1 | `GENERAL_ERROR` | Unhandled exception, general failure |
142
- | 2 | `FILE_NOT_FOUND` | Attachment or file not found; also click's usage error (bad option value, malformed `--set`, invalid `--profile` name) |
143
- | 13 | `PERMISSION_DENIED` | Cannot write to target directory |
144
- | 22 | `INVALID_ARGUMENT` | Invalid CLI argument or section not found |
145
- | 69 | `SMTP_FAILURE` | SMTP delivery failed |
146
- | 78 | `CONFIG_ERROR` | Configuration missing, not loadable or invalid |
147
- | 110 | `TIMEOUT` | Operation timed out |
148
- | 130 | `SIGNAL_INT` | Interrupted (SIGINT/Ctrl+C) |
149
- | 141 | `BROKEN_PIPE` | Output pipe closed |
150
- | 143 | `SIGNAL_TERM` | Terminated (SIGTERM) |
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`, and checks every SMTP host's syntax (port range, IPv6 brackets) when it loads. In
326
- Python the fields use the library's names; the configuration file keeps its own keys (third
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 third column that differ from the field names are mapped;
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 five keys whose names differ |
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.0"
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.12.0",
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.0"
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 settings have no flag; set them with
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. DeliveryError/RuntimeError -> SMTP_FAILURE (69): SMTP transport failures
173
- 5. Exception (catch-all) -> GENERAL_ERROR (1): Unexpected errors with traceback
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]`` is a load failure.
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 command, ``config-deploy``
101
- (which replaces the file holding it) included. Logging then starts with its defaults, and
102
- the commands that read the configuration refuse with exit 78 naming the key.
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 false, violations log a warning and skip the attachment
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 config.raise_on_missing_attachments
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 with layered config.
9
- * :class:`InvalidLoggingConfigError` - the ``[lib_log_rich]`` section cannot configure logging.
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 lib_layered_config import Config
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 names
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
- ... init_logging(Config({"lib_log_rich": {"rate_limit": "100:60"}}, {}))
107
- ... except InvalidLoggingConfigError as exc:
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): its own first line, which names
130
- # the setting.
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 holds a value lib_log_rich
203
- refuses; logging is not started.
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 (ValidationError, ValueError) as exc:
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, so a test run does not pick up the developer's own environment.
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
- lib_log_rich.runtime.init(
49
- lib_log_rich.runtime.RuntimeConfig(
50
- service=f"{__init__conf__.name}-test",
51
- environment="test",
52
- console_level=LogLevel.ERROR,
53
- enable_journald=False,
54
- enable_eventlog=False,
55
- enable_graylog=False,
56
- queue_enabled=False,
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: ...