bitranox-template-py-cli 2.1.2__tar.gz → 2.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/CHANGELOG.md +13 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/PKG-INFO +1 -1
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/docs/systemdesign/module_reference.md +3 -3
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/pyproject.toml +1 -1
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/__init__conf__.py +1 -1
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/commands/config.py +5 -2
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/config_load.py +33 -1
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/logging/setup.py +27 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/memory/__init__.py +4 -1
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/memory/logging.py +14 -1
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/application/ports.py +11 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/composition/__init__.py +8 -5
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/conftest.py +12 -6
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cli_env_file.py +1 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_deploy_permissions.py +1 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_logging_dotenv_isolation.py +84 -2
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_ports.py +14 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/.devcontainer/devcontainer.json +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/.devcontainer/settings.json +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/.env.example +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/.gitattributes +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/.github/actions/extract-metadata/action.yml +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/.github/dependabot.yml +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/.github/workflows/codeql.yml +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/.github/workflows/default_cicd_public.yml +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/.github/workflows/default_release_public.yml +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/.gitignore +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/.qlty/qlty.toml +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/.snyk +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/CONFIG.md +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/CONTRIBUTING.md +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/DEVELOPMENT.md +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/INSTALL.md +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/LICENSE +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/Makefile +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/README.md +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/SECURITY.md +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/ai-stance.md +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/ai-transparency.md +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/codecov.yml +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/docs/adr/0001-memory-adapters-in-src.md +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/notebooks/Quickstart.ipynb +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/rename.sh +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/rename_dry.sh +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/reset_git_history.sh +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/__main__.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/commands/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/commands/email/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/commands/email/_common.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/commands/email/send_email.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/commands/email/send_notification.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/commands/info.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/commands/logging.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/constants.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/context.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/exit_codes.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/main.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/py.typed +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/root.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/safe_console.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/cli/typed_click.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/40-layered-config.toml +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/50-mail.toml +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/90-logging.toml +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/defaultconfig.toml +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/deploy.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/display.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/loader.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/overrides.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/config/py.typed +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/email/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/email/config.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/email/py.typed +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/email/sender.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/email/transport.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/email/validation.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/logging/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/logging/py.typed +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/memory/config.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/memory/email.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/adapters/py.typed +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/application/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/application/py.typed +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/composition/py.typed +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/domain/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/domain/behaviors.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/domain/enums.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/domain/errors.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/domain/py.typed +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/entry.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/src/bitranox_template_py_cli/py.typed +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_behaviors.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cache_effectiveness.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cli_attachment_refused.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cli_config.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cli_config_errors.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cli_core.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cli_email.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cli_email_config_errors.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cli_exit_codes.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cli_main_exit.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cli_overrides.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cli_validation.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_config_overrides.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_declared_dependencies.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_deploy_mode_safety.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_display.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_email_attachment_lists.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_email_config_translation.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_email_password_secrecy.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_email_shipped_defaults.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_enums.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_errors.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_logging.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_mail.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_memory_logging.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_metadata.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_metadata_sync.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_module_entry.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_module_reference_sync.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_permission_defaults.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_property_email.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_property_overrides.py +0 -0
- {bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_safe_console.py +0 -0
|
@@ -6,6 +6,19 @@ the [Keep a Changelog](https://keepachangelog.com/) format.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.1.3] 2026-10-08 11:57:40
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- **`config --profile NAME` refuses an invalid logging setting like `config` does.** The
|
|
13
|
+
subcommand's `--profile` reloads the configuration after logging has started, and that reload
|
|
14
|
+
skipped the logging settings, so a `[lib_log_rich]` value or `LOG_*` variable that makes plain
|
|
15
|
+
`config` exit 78 was displayed with exit 0. The reload now restarts logging with the reloaded
|
|
16
|
+
configuration, which refuses exactly what plain `config` refuses (a scrub pattern, a console
|
|
17
|
+
format preset or style key and a level name included) with the same `Error:` lines, also for a
|
|
18
|
+
`[lib_log_rich]` section that is invalid only in that profile's own file; the rest of the
|
|
19
|
+
command then logs with the profile's settings. New `RestartLogging` port
|
|
20
|
+
(`AppServices.restart_logging`; the testing composition's double keeps its quiet runtime).
|
|
21
|
+
|
|
9
22
|
## [2.1.2] 2026-10-08 09:43:32
|
|
10
23
|
|
|
11
24
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: bitranox_template_py_cli
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.3
|
|
4
4
|
Summary: Template CLI application with configuration management and structured logging
|
|
5
5
|
Project-URL: Homepage, https://github.com/bitranox/bitranox_template_py_cli
|
|
6
6
|
Project-URL: Repository, https://github.com/bitranox/bitranox_template_py_cli.git
|
|
@@ -26,14 +26,14 @@ moves or removes a module, a CLI option or a public name.
|
|
|
26
26
|
- `src/bitranox_template_py_cli/adapters/email/transport.py` - `send_email` / `send_notification` over btx_lib_mail
|
|
27
27
|
- `src/bitranox_template_py_cli/adapters/email/sender.py` - Re-exports of `config.py` and `transport.py`
|
|
28
28
|
- `src/bitranox_template_py_cli/adapters/email/validation.py` - Email recipient validation
|
|
29
|
-
- `src/bitranox_template_py_cli/adapters/logging/setup.py` - lib_log_rich initialization; takes only the `LOG_*` lines of a `.env`, and raises `InvalidLoggingConfigError` for a refused `[lib_log_rich]` value or `LOG_*` variable, after starting logging with its defaults (and without the `LOG_*` variables only when one of them is refused)
|
|
29
|
+
- `src/bitranox_template_py_cli/adapters/logging/setup.py` - lib_log_rich initialization; takes only the `LOG_*` lines of a `.env`, and raises `InvalidLoggingConfigError` for a refused `[lib_log_rich]` value or `LOG_*` variable, after starting logging with its defaults (and without the `LOG_*` variables only when one of them is refused); `restart_logging` shuts the running runtime down and starts it again with another configuration
|
|
30
30
|
- `src/bitranox_template_py_cli/adapters/cli/` - CLI adapter package:
|
|
31
31
|
- `__init__.py` - Public facade
|
|
32
32
|
- `constants.py` - Shared constants
|
|
33
33
|
- `safe_console.py` - Encode-safe terminal output; use `safe_console.echo` instead of `click.echo`
|
|
34
34
|
- `exit_codes.py` - POSIX exit codes (ExitCode IntEnum)
|
|
35
35
|
- `context.py` - Click context helpers
|
|
36
|
-
- `config_load.py` - Configuration load for the CLI; records a load failure, `require_config` refuses with exit 78
|
|
36
|
+
- `config_load.py` - Configuration load for the CLI; records a load failure, `require_config` refuses with exit 78, `restart_logging` restarts logging with a configuration reloaded by `config --profile`, refusing its logging settings as the root does
|
|
37
37
|
- `root.py` - Root command group
|
|
38
38
|
- `main.py` - Entry point
|
|
39
39
|
- `typed_click.py` - Strictly typed wrappers for rich_click's `option`, `version_option` and `get_current_context`
|
|
@@ -413,7 +413,7 @@ The `adapters/memory/` package provides lightweight implementations for testing:
|
|
|
413
413
|
|---------------------|-----------------------------------------------------------------------------|
|
|
414
414
|
| `memory/config.py` | `GetConfig`, `GetDefaultConfigPath`, `DeployConfiguration`, `DisplayConfig` |
|
|
415
415
|
| `memory/email.py` | `SendEmail`, `SendNotification` |
|
|
416
|
-
| `memory/logging.py` | `InitLogging`
|
|
416
|
+
| `memory/logging.py` | `InitLogging`, `RestartLogging` |
|
|
417
417
|
|
|
418
418
|
`adapters/memory` exports `EmailSpy` (from `memory/email.py`): its `send_email` and
|
|
419
419
|
`send_notification` methods satisfy the email ports and record each call as a `CapturedEmail` /
|
|
@@ -40,7 +40,7 @@ name = "bitranox_template_py_cli"
|
|
|
40
40
|
#: Human-readable summary shown in CLI help output.
|
|
41
41
|
title = "Template CLI application with configuration management and structured logging"
|
|
42
42
|
#: Current release version pulled from ``pyproject.toml`` by automation.
|
|
43
|
-
version = "2.1.
|
|
43
|
+
version = "2.1.3"
|
|
44
44
|
#: Repository homepage presented to users.
|
|
45
45
|
homepage = "https://github.com/bitranox/bitranox_template_py_cli"
|
|
46
46
|
#: Author attribution surfaced in CLI output.
|
|
@@ -32,7 +32,7 @@ from bitranox_template_py_cli.adapters.config.overrides import nest_overrides
|
|
|
32
32
|
from bitranox_template_py_cli.domain.enums import DeployTarget, OutputFormat
|
|
33
33
|
|
|
34
34
|
from .. import safe_console
|
|
35
|
-
from ..config_load import load_config, report_load_failure, require_config
|
|
35
|
+
from ..config_load import load_config, report_load_failure, require_config, restart_logging
|
|
36
36
|
from ..constants import CLICK_CONTEXT_SETTINGS
|
|
37
37
|
from ..context import CLIContext, get_cli_context
|
|
38
38
|
from ..exit_codes import ExitCode
|
|
@@ -110,7 +110,8 @@ def _resolve_config(ctx: click.Context, cli_ctx: CLIContext, profile: str | None
|
|
|
110
110
|
|
|
111
111
|
When a subcommand-level profile override is specified, reloads config
|
|
112
112
|
with that profile, the root's ``--env-file`` and any root-level ``--set``
|
|
113
|
-
overrides stored in the CLI context
|
|
113
|
+
overrides stored in the CLI context, and restarts logging with it, which
|
|
114
|
+
refuses its logging settings the way the root refuses its own.
|
|
114
115
|
|
|
115
116
|
Args:
|
|
116
117
|
ctx: The running command's click context, exited with 78 when loading failed.
|
|
@@ -126,6 +127,8 @@ def _resolve_config(ctx: click.Context, cli_ctx: CLIContext, profile: str | None
|
|
|
126
127
|
config, error = load_config(
|
|
127
128
|
cli_ctx.services, profile=profile, env_file=cli_ctx.env_file, set_overrides=cli_ctx.set_overrides
|
|
128
129
|
)
|
|
130
|
+
if error is None:
|
|
131
|
+
error = restart_logging(cli_ctx.services, config, env_file=cli_ctx.env_file)
|
|
129
132
|
if error is not None:
|
|
130
133
|
report_load_failure(error, show_traceback=cli_ctx.traceback)
|
|
131
134
|
ctx.exit(ExitCode.CONFIG_ERROR)
|
|
@@ -16,6 +16,7 @@ Contents:
|
|
|
16
16
|
* :func:`load_config` - load with profile, ``.env`` and ``--set``, or say why not.
|
|
17
17
|
* :func:`start_logging` - start logging; an invalid ``[lib_log_rich]`` value or ``LOG_*``
|
|
18
18
|
variable is a load failure.
|
|
19
|
+
* :func:`restart_logging` - the same refusal for a configuration loaded after logging started.
|
|
19
20
|
* :func:`require_config` - the configuration, or exit 78 naming the failure.
|
|
20
21
|
* :func:`report_load_failure` - the one-line report, after the traceback on request.
|
|
21
22
|
* :func:`echo_load_traceback` - the loader's traceback alone, for a caller with its own line.
|
|
@@ -123,6 +124,30 @@ def start_logging(
|
|
|
123
124
|
return config, config_error
|
|
124
125
|
|
|
125
126
|
|
|
127
|
+
def restart_logging(services: AppServices, config: Config, *, env_file: str | None) -> Exception | None:
|
|
128
|
+
"""Restart logging with a configuration loaded after it started; a refused setting is returned.
|
|
129
|
+
|
|
130
|
+
``config --profile`` reloads the configuration once logging is running, so
|
|
131
|
+
:func:`start_logging` never sees the reloaded ``[lib_log_rich]`` section; without this an
|
|
132
|
+
invalid value there would display with exit 0 while plain ``config`` exits 78. Restarting
|
|
133
|
+
runs the very start :func:`start_logging` ran, so it refuses the same settings in the same
|
|
134
|
+
words, and the rest of the command logs with the reloaded settings.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
services: The composition's services; only ``restart_logging`` is used.
|
|
138
|
+
config: The configuration :func:`load_config` returned.
|
|
139
|
+
env_file: The ``--env-file`` path, or None; logging reads its ``LOG_*`` lines from it.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
None, or the refusal; logging then runs with its defaults.
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
services.restart_logging(config, dotenv_path=env_file)
|
|
146
|
+
except InvalidLoggingConfigError as exc:
|
|
147
|
+
return exc
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
126
151
|
def echo_load_traceback(error: Exception, *, show_traceback: bool) -> None:
|
|
127
152
|
"""Write the loader's chained traceback to stderr when ``--traceback`` was given.
|
|
128
153
|
|
|
@@ -170,4 +195,11 @@ def require_config(ctx: click.Context, cli_ctx: CLIContext) -> Config:
|
|
|
170
195
|
return cli_ctx.config
|
|
171
196
|
|
|
172
197
|
|
|
173
|
-
__all__ = [
|
|
198
|
+
__all__ = [
|
|
199
|
+
"echo_load_traceback",
|
|
200
|
+
"load_config",
|
|
201
|
+
"report_load_failure",
|
|
202
|
+
"require_config",
|
|
203
|
+
"restart_logging",
|
|
204
|
+
"start_logging",
|
|
205
|
+
]
|
|
@@ -7,6 +7,7 @@ eliminating duplication between module entry (__main__.py) and console script
|
|
|
7
7
|
Contents:
|
|
8
8
|
* :func:`init_logging` - idempotent logging initialization from the ``[lib_log_rich]``
|
|
9
9
|
section and the ``LOG_*`` variables (the environment, plus the ``LOG_*`` lines of a ``.env``).
|
|
10
|
+
* :func:`restart_logging` - stop the running runtime and start it again with another configuration.
|
|
10
11
|
* :class:`InvalidLoggingConfigError` - the ``[lib_log_rich]`` section or a ``LOG_*`` variable
|
|
11
12
|
cannot configure logging.
|
|
12
13
|
* :func:`_build_runtime_config` - constructs RuntimeConfig from layered sources.
|
|
@@ -280,9 +281,35 @@ def init_logging(config: Config, *, dotenv_path: str | None = None) -> None:
|
|
|
280
281
|
lib_log_rich.runtime.attach_std_logging()
|
|
281
282
|
|
|
282
283
|
|
|
284
|
+
def restart_logging(config: Config, *, dotenv_path: str | None = None) -> None:
|
|
285
|
+
"""Stop the running lib_log_rich runtime, then start it again with ``config``.
|
|
286
|
+
|
|
287
|
+
:func:`init_logging` returns at once while a runtime is running, so a configuration loaded
|
|
288
|
+
after logging started (``config --profile``'s reload) would never have its logging settings
|
|
289
|
+
judged. This runs that same start again: a setting lib_log_rich refuses raises exactly as it
|
|
290
|
+
does there, and logging is left running with the defaults. lib_log_rich has no public call
|
|
291
|
+
that checks settings without starting the runtime.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
config: Already-loaded layered configuration object.
|
|
295
|
+
dotenv_path: As for :func:`init_logging`.
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
InvalidLoggingConfigError: As for :func:`init_logging`.
|
|
299
|
+
|
|
300
|
+
Side Effects:
|
|
301
|
+
Flushes and shuts down the running runtime first (its queue, Graylog and journald
|
|
302
|
+
adapters included); the new one carries ``config``'s logging settings.
|
|
303
|
+
"""
|
|
304
|
+
if lib_log_rich.runtime.is_initialised():
|
|
305
|
+
lib_log_rich.runtime.shutdown()
|
|
306
|
+
init_logging(config, dotenv_path=dotenv_path)
|
|
307
|
+
|
|
308
|
+
|
|
283
309
|
__all__ = [
|
|
284
310
|
"InvalidLoggingConfigError",
|
|
285
311
|
"LoggingConfigModel",
|
|
286
312
|
"init_logging",
|
|
287
313
|
"log_variables_hidden",
|
|
314
|
+
"restart_logging",
|
|
288
315
|
]
|
|
@@ -20,7 +20,7 @@ from .config import (
|
|
|
20
20
|
get_default_config_path_in_memory,
|
|
21
21
|
)
|
|
22
22
|
from .email import EmailSpy
|
|
23
|
-
from .logging import init_logging_in_memory
|
|
23
|
+
from .logging import init_logging_in_memory, restart_logging_in_memory
|
|
24
24
|
|
|
25
25
|
# Static conformance assertions
|
|
26
26
|
if TYPE_CHECKING:
|
|
@@ -28,11 +28,13 @@ if TYPE_CHECKING:
|
|
|
28
28
|
GetConfig,
|
|
29
29
|
GetDefaultConfigPath,
|
|
30
30
|
InitLogging,
|
|
31
|
+
RestartLogging,
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
_assert_get_config: GetConfig = get_config_in_memory
|
|
34
35
|
_assert_get_default_config_path: GetDefaultConfigPath = get_default_config_path_in_memory
|
|
35
36
|
_assert_init_logging: InitLogging = init_logging_in_memory
|
|
37
|
+
_assert_restart_logging: RestartLogging = restart_logging_in_memory
|
|
36
38
|
|
|
37
39
|
__all__ = [
|
|
38
40
|
"EmailSpy",
|
|
@@ -41,4 +43,5 @@ __all__ = [
|
|
|
41
43
|
"get_config_in_memory",
|
|
42
44
|
"get_default_config_path_in_memory",
|
|
43
45
|
"init_logging_in_memory",
|
|
46
|
+
"restart_logging_in_memory",
|
|
44
47
|
]
|
|
@@ -63,4 +63,17 @@ def init_logging_in_memory(config: Config, *, dotenv_path: str | None = None) ->
|
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
def restart_logging_in_memory(config: Config, *, dotenv_path: str | None = None) -> None:
|
|
67
|
+
"""Keep the test runtime: :func:`init_logging_in_memory` ignores the configuration, so a restart changes nothing.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
config: Unused; the parameter exists to satisfy the ``RestartLogging`` protocol.
|
|
71
|
+
dotenv_path: Unused for the same reason.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> from lib_layered_config import Config
|
|
75
|
+
>>> restart_logging_in_memory(Config({"lib_log_rich": {"rate_limit": "100:60"}}, {}))
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = ["init_logging_in_memory", "restart_logging_in_memory"]
|
|
@@ -109,6 +109,16 @@ class InitLogging(Protocol):
|
|
|
109
109
|
def __call__(self, config: Config, *, dotenv_path: str | None = None) -> None: ...
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
class RestartLogging(Protocol):
|
|
113
|
+
"""Stop the running logging runtime and start it again with ``config``, as ``InitLogging`` does.
|
|
114
|
+
|
|
115
|
+
For a configuration loaded after logging is running; a refused setting raises
|
|
116
|
+
``InvalidLoggingConfigError`` after logging has been started with its defaults.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __call__(self, config: Config, *, dotenv_path: str | None = None) -> None: ...
|
|
120
|
+
|
|
121
|
+
|
|
112
122
|
__all__ = [
|
|
113
123
|
"DeployConfiguration",
|
|
114
124
|
"DisplayConfig",
|
|
@@ -116,6 +126,7 @@ __all__ = [
|
|
|
116
126
|
"GetDefaultConfigPath",
|
|
117
127
|
"InitLogging",
|
|
118
128
|
"LoadEmailConfigFromDict",
|
|
129
|
+
"RestartLogging",
|
|
119
130
|
"SendEmail",
|
|
120
131
|
"SendNotification",
|
|
121
132
|
]
|
|
@@ -19,7 +19,7 @@ from ..adapters.email.sender import (
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
# Logging services
|
|
22
|
-
from ..adapters.logging.setup import init_logging
|
|
22
|
+
from ..adapters.logging.setup import init_logging, restart_logging
|
|
23
23
|
|
|
24
24
|
# Static conformance assertions — pyright verifies that each adapter function
|
|
25
25
|
# structurally satisfies its corresponding Protocol at type-check time.
|
|
@@ -32,6 +32,7 @@ if TYPE_CHECKING:
|
|
|
32
32
|
GetDefaultConfigPath,
|
|
33
33
|
InitLogging,
|
|
34
34
|
LoadEmailConfigFromDict,
|
|
35
|
+
RestartLogging,
|
|
35
36
|
SendEmail,
|
|
36
37
|
SendNotification,
|
|
37
38
|
)
|
|
@@ -44,6 +45,7 @@ if TYPE_CHECKING:
|
|
|
44
45
|
_assert_send_notification: SendNotification = send_notification
|
|
45
46
|
_assert_load_email_config_from_dict: LoadEmailConfigFromDict = load_email_config_from_dict
|
|
46
47
|
_assert_init_logging: InitLogging = init_logging
|
|
48
|
+
_assert_restart_logging: RestartLogging = restart_logging
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
@dataclass(frozen=True, slots=True)
|
|
@@ -58,6 +60,7 @@ class AppServices:
|
|
|
58
60
|
send_notification: SendNotification
|
|
59
61
|
load_email_config_from_dict: LoadEmailConfigFromDict
|
|
60
62
|
init_logging: InitLogging
|
|
63
|
+
restart_logging: RestartLogging
|
|
61
64
|
|
|
62
65
|
|
|
63
66
|
def build_production() -> AppServices:
|
|
@@ -71,6 +74,7 @@ def build_production() -> AppServices:
|
|
|
71
74
|
send_notification=send_notification,
|
|
72
75
|
load_email_config_from_dict=load_email_config_from_dict,
|
|
73
76
|
init_logging=init_logging,
|
|
77
|
+
restart_logging=restart_logging,
|
|
74
78
|
)
|
|
75
79
|
|
|
76
80
|
|
|
@@ -92,6 +96,7 @@ def build_testing(*, spy: EmailSpy | None = None) -> AppServices:
|
|
|
92
96
|
get_config_in_memory,
|
|
93
97
|
get_default_config_path_in_memory,
|
|
94
98
|
init_logging_in_memory,
|
|
99
|
+
restart_logging_in_memory,
|
|
95
100
|
)
|
|
96
101
|
|
|
97
102
|
email_spy = spy if spy is not None else EmailSpy()
|
|
@@ -106,23 +111,21 @@ def build_testing(*, spy: EmailSpy | None = None) -> AppServices:
|
|
|
106
111
|
# The real translation: an in-memory one would let every CLI test skip it.
|
|
107
112
|
load_email_config_from_dict=load_email_config_from_dict,
|
|
108
113
|
init_logging=init_logging_in_memory,
|
|
114
|
+
restart_logging=restart_logging_in_memory,
|
|
109
115
|
)
|
|
110
116
|
|
|
111
117
|
|
|
112
118
|
__all__ = [
|
|
113
|
-
# Composition
|
|
114
119
|
"AppServices",
|
|
115
120
|
"build_production",
|
|
116
121
|
"build_testing",
|
|
117
122
|
"deploy_configuration",
|
|
118
123
|
"display_config",
|
|
119
|
-
# Configuration
|
|
120
124
|
"get_config",
|
|
121
125
|
"get_default_config_path",
|
|
122
|
-
# Logging
|
|
123
126
|
"init_logging",
|
|
124
127
|
"load_email_config_from_dict",
|
|
125
|
-
|
|
128
|
+
"restart_logging",
|
|
126
129
|
"send_email",
|
|
127
130
|
"send_notification",
|
|
128
131
|
]
|
|
@@ -384,7 +384,7 @@ def inject_config(
|
|
|
384
384
|
result = cli_runner.invoke(cli, ["config"], obj=factory)
|
|
385
385
|
assert "key" in result.output
|
|
386
386
|
"""
|
|
387
|
-
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory
|
|
387
|
+
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory, restart_logging_in_memory
|
|
388
388
|
from bitranox_template_py_cli.composition import AppServices, build_production
|
|
389
389
|
|
|
390
390
|
def _inject(config: Config) -> Callable[[], AppServices]:
|
|
@@ -403,6 +403,7 @@ def inject_config(
|
|
|
403
403
|
# The quiet runtime: production init_logging queues INFO lines that race into
|
|
404
404
|
# CliRunner's stderr, so a stderr assertion would depend on timing.
|
|
405
405
|
init_logging=init_logging_in_memory,
|
|
406
|
+
restart_logging=restart_logging_in_memory,
|
|
406
407
|
)
|
|
407
408
|
return lambda: test_services
|
|
408
409
|
|
|
@@ -438,7 +439,7 @@ def inject_config_with_profile_capture(
|
|
|
438
439
|
cli_runner.invoke(cli, ["--profile", "staging", "config"], obj=factory)
|
|
439
440
|
assert captured == ["staging"]
|
|
440
441
|
"""
|
|
441
|
-
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory
|
|
442
|
+
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory, restart_logging_in_memory
|
|
442
443
|
from bitranox_template_py_cli.composition import AppServices, build_production
|
|
443
444
|
|
|
444
445
|
def _inject(config: Config, captured_profiles: list[str | None]) -> Callable[[], AppServices]:
|
|
@@ -458,6 +459,7 @@ def inject_config_with_profile_capture(
|
|
|
458
459
|
# The quiet runtime: production init_logging queues INFO lines that race into
|
|
459
460
|
# CliRunner's stderr, so a stderr assertion would depend on timing.
|
|
460
461
|
init_logging=init_logging_in_memory,
|
|
462
|
+
restart_logging=restart_logging_in_memory,
|
|
461
463
|
)
|
|
462
464
|
return lambda: test_services
|
|
463
465
|
|
|
@@ -493,7 +495,7 @@ def inject_deploy_with_profile_capture(
|
|
|
493
495
|
cli_runner.invoke(cli, ["--profile", "prod", "config-deploy", ...], obj=factory)
|
|
494
496
|
assert captured == ["prod"]
|
|
495
497
|
"""
|
|
496
|
-
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory
|
|
498
|
+
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory, restart_logging_in_memory
|
|
497
499
|
from bitranox_template_py_cli.composition import AppServices, build_production
|
|
498
500
|
|
|
499
501
|
def _inject(deployed_path: Path, captured_profiles: list[str | None]) -> Callable[[], AppServices]:
|
|
@@ -522,6 +524,7 @@ def inject_deploy_with_profile_capture(
|
|
|
522
524
|
# The quiet runtime: production init_logging queues INFO lines that race into
|
|
523
525
|
# CliRunner's stderr, so a stderr assertion would depend on timing.
|
|
524
526
|
init_logging=init_logging_in_memory,
|
|
527
|
+
restart_logging=restart_logging_in_memory,
|
|
525
528
|
)
|
|
526
529
|
return lambda: test_services
|
|
527
530
|
|
|
@@ -553,7 +556,7 @@ def inject_deploy_configuration() -> Callable[[Callable[..., list[Path]]], Calla
|
|
|
553
556
|
cli_runner.invoke(cli, ["config-deploy", "--target", "user"], obj=factory)
|
|
554
557
|
assert len(calls) == 1
|
|
555
558
|
"""
|
|
556
|
-
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory
|
|
559
|
+
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory, restart_logging_in_memory
|
|
557
560
|
from bitranox_template_py_cli.composition import AppServices, build_production
|
|
558
561
|
|
|
559
562
|
def _inject(deploy_fn: Callable[..., list[Path]]) -> Callable[[], AppServices]:
|
|
@@ -569,6 +572,7 @@ def inject_deploy_configuration() -> Callable[[Callable[..., list[Path]]], Calla
|
|
|
569
572
|
# The quiet runtime: production init_logging queues INFO lines that race into
|
|
570
573
|
# CliRunner's stderr, so a stderr assertion would depend on timing.
|
|
571
574
|
init_logging=init_logging_in_memory,
|
|
575
|
+
restart_logging=restart_logging_in_memory,
|
|
572
576
|
)
|
|
573
577
|
return lambda: test_services
|
|
574
578
|
|
|
@@ -654,7 +658,7 @@ def email_cli_context(
|
|
|
654
658
|
assert ctx.spy.sent_notifications[0].subject == "Hi"
|
|
655
659
|
"""
|
|
656
660
|
from bitranox_template_py_cli.adapters.email.config import load_email_config_from_dict
|
|
657
|
-
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory
|
|
661
|
+
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory, restart_logging_in_memory
|
|
658
662
|
from bitranox_template_py_cli.adapters.memory.email import EmailSpy as EmailSpyImpl
|
|
659
663
|
from bitranox_template_py_cli.composition import AppServices, build_production
|
|
660
664
|
|
|
@@ -677,6 +681,7 @@ def email_cli_context(
|
|
|
677
681
|
# The quiet runtime: production init_logging queues INFO lines that race into
|
|
678
682
|
# CliRunner's stderr, so a stderr assertion would depend on timing.
|
|
679
683
|
init_logging=init_logging_in_memory,
|
|
684
|
+
restart_logging=restart_logging_in_memory,
|
|
680
685
|
)
|
|
681
686
|
return EmailCliContext(factory=lambda: test_services, spy=spy)
|
|
682
687
|
|
|
@@ -708,7 +713,7 @@ def config_cli_context(
|
|
|
708
713
|
result = cli_runner.invoke(cli, ["config"], obj=factory)
|
|
709
714
|
assert "key" in result.output
|
|
710
715
|
"""
|
|
711
|
-
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory
|
|
716
|
+
from bitranox_template_py_cli.adapters.memory import init_logging_in_memory, restart_logging_in_memory
|
|
712
717
|
from bitranox_template_py_cli.composition import AppServices, build_production
|
|
713
718
|
|
|
714
719
|
def _create(config_data: dict[str, Any]) -> Callable[[], AppServices]:
|
|
@@ -729,6 +734,7 @@ def config_cli_context(
|
|
|
729
734
|
# The quiet runtime: production init_logging queues INFO lines that race into
|
|
730
735
|
# CliRunner's stderr, so a stderr assertion would depend on timing.
|
|
731
736
|
init_logging=init_logging_in_memory,
|
|
737
|
+
restart_logging=restart_logging_in_memory,
|
|
732
738
|
)
|
|
733
739
|
return lambda: test_services
|
|
734
740
|
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cli_env_file.py
RENAMED
|
@@ -52,6 +52,7 @@ def inject_config_with_dotenv_capture(
|
|
|
52
52
|
send_notification=prod.send_notification,
|
|
53
53
|
load_email_config_from_dict=prod.load_email_config_from_dict,
|
|
54
54
|
init_logging=prod.init_logging,
|
|
55
|
+
restart_logging=prod.restart_logging,
|
|
55
56
|
)
|
|
56
57
|
return lambda: test_services
|
|
57
58
|
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_deploy_permissions.py
RENAMED
|
@@ -77,6 +77,7 @@ def inject_deploy_with_permission_capture(
|
|
|
77
77
|
send_notification=prod.send_notification,
|
|
78
78
|
load_email_config_from_dict=prod.load_email_config_from_dict,
|
|
79
79
|
init_logging=prod.init_logging,
|
|
80
|
+
restart_logging=prod.restart_logging,
|
|
80
81
|
)
|
|
81
82
|
return lambda: test_services
|
|
82
83
|
|
|
@@ -15,9 +15,11 @@ The end-to-end tests run the real CLI in a subprocess with every configuration l
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
+
import logging
|
|
18
19
|
import os
|
|
19
20
|
import subprocess
|
|
20
21
|
import sys
|
|
22
|
+
from pathlib import Path
|
|
21
23
|
from typing import TYPE_CHECKING
|
|
22
24
|
|
|
23
25
|
import lib_log_rich.runtime
|
|
@@ -25,11 +27,14 @@ import pytest
|
|
|
25
27
|
from lib_layered_config import Config
|
|
26
28
|
|
|
27
29
|
from bitranox_template_py_cli import __init__conf__
|
|
28
|
-
from bitranox_template_py_cli.adapters.logging.setup import
|
|
30
|
+
from bitranox_template_py_cli.adapters.logging.setup import (
|
|
31
|
+
InvalidLoggingConfigError,
|
|
32
|
+
init_logging,
|
|
33
|
+
restart_logging,
|
|
34
|
+
)
|
|
29
35
|
|
|
30
36
|
if TYPE_CHECKING:
|
|
31
37
|
from collections.abc import Iterator
|
|
32
|
-
from pathlib import Path
|
|
33
38
|
|
|
34
39
|
PREFIX = __init__conf__.LAYEREDCONF_SLUG.upper().replace("-", "_")
|
|
35
40
|
_HOMES = ("HOME", "USERPROFILE", "XDG_CONFIG_HOME", "APPDATA", "LOCALAPPDATA")
|
|
@@ -206,6 +211,83 @@ def test_an_invalid_logging_section_does_not_block_the_deploy_that_replaces_it(t
|
|
|
206
211
|
assert result.returncode == 0, result.stderr
|
|
207
212
|
|
|
208
213
|
|
|
214
|
+
# ------------------------------------------------------------------ config --profile reloads, and checks logging too
|
|
215
|
+
|
|
216
|
+
#: One refused setting of each kind lib_log_rich has: the section's type check, its own range
|
|
217
|
+
#: check, a ``LOG_*`` value its settings resolution refuses, and the values only starting the
|
|
218
|
+
#: runtime refuses - a level name (in the section or a ``LOG_*`` variable), a scrub pattern, a
|
|
219
|
+
#: console format preset and a console style key.
|
|
220
|
+
_REFUSED_LOGGING = {
|
|
221
|
+
"type": {f"{PREFIX}___LIB_LOG_RICH__RATE_LIMIT": "100:60"},
|
|
222
|
+
"range": {f"{PREFIX}___LIB_LOG_RICH__QUEUE_MAXSIZE": "0"},
|
|
223
|
+
"settings-variable": {"LOG_RATE_LIMIT": "bogus"},
|
|
224
|
+
"section-level": {f"{PREFIX}___LIB_LOG_RICH__CONSOLE_LEVEL": "bogus"},
|
|
225
|
+
"log-variable": {"LOG_CONSOLE_LEVEL": "bogus"},
|
|
226
|
+
"scrub-pattern": {"LOG_SCRUB_PATTERNS": "token=("},
|
|
227
|
+
"console-preset": {"LOG_CONSOLE_FORMAT_PRESET": "bogus"},
|
|
228
|
+
"console-style": {"LOG_CONSOLE_STYLES": "bogus=red"},
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _error_lines(result: subprocess.CompletedProcess[str]) -> list[str]:
|
|
233
|
+
return [line for line in result.stderr.splitlines() if line.startswith("Error:")]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@pytest.mark.os_agnostic
|
|
237
|
+
@pytest.mark.parametrize("env", _REFUSED_LOGGING.values(), ids=_REFUSED_LOGGING.keys())
|
|
238
|
+
def test_config_with_a_profile_refuses_a_logging_setting_like_config_without_one(
|
|
239
|
+
tmp_path: Path, env: dict[str, str]
|
|
240
|
+
) -> None:
|
|
241
|
+
plain = _run(tmp_path, "config", env=env)
|
|
242
|
+
reloaded = _run(tmp_path, "config", "--profile", "production", env=env)
|
|
243
|
+
|
|
244
|
+
# Premise: plain config refuses this setting with one line per problem.
|
|
245
|
+
assert plain.returncode == 78, plain.stderr
|
|
246
|
+
assert _error_lines(plain), plain.stderr
|
|
247
|
+
assert reloaded.returncode == 78, reloaded.stderr
|
|
248
|
+
assert _error_lines(reloaded) == _error_lines(plain)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@pytest.mark.os_agnostic
|
|
252
|
+
def test_a_logging_section_refused_only_in_the_profile_file_refuses_config_with_that_profile(tmp_path: Path) -> None:
|
|
253
|
+
# The user layer lives in a different place on each OS; the deploy writes it there and says where.
|
|
254
|
+
deployed = _run(tmp_path, "config-deploy", "--target", "user", "--profile", "broken")
|
|
255
|
+
assert deployed.returncode == 0, deployed.stderr
|
|
256
|
+
written = [line.strip()[2:] for line in deployed.stdout.splitlines() if line.strip().startswith("+ ")]
|
|
257
|
+
profile_files = [Path(path) for path in written if Path(path).name == "config.toml"]
|
|
258
|
+
assert len(profile_files) == 1, deployed.stdout
|
|
259
|
+
# config.d merges after config.toml, and the deployed 90-logging.toml sets rate_limit itself.
|
|
260
|
+
(profile_files[0].parent / "config.d" / "99-refused.toml").write_text(
|
|
261
|
+
'[lib_log_rich]\nrate_limit = "100:60"\n', encoding="utf-8"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
plain = _run(tmp_path, "config")
|
|
265
|
+
reloaded = _run(tmp_path, "config", "--profile", "broken")
|
|
266
|
+
|
|
267
|
+
assert plain.returncode == 0, plain.stderr
|
|
268
|
+
assert reloaded.returncode == 78, reloaded.stderr
|
|
269
|
+
errors = _error_lines(reloaded)
|
|
270
|
+
assert len(errors) == 1, reloaded.stderr
|
|
271
|
+
assert "lib_log_rich.rate_limit" in errors[0]
|
|
272
|
+
assert "100:60" not in reloaded.stderr
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@pytest.mark.os_agnostic
|
|
276
|
+
def test_restarting_logging_refuses_a_setting_although_logging_already_runs(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
277
|
+
"""``init_logging`` returns at once on a running runtime; a restart must judge the new settings."""
|
|
278
|
+
init_logging(Config({}, {}))
|
|
279
|
+
# Premise: logging runs, so a plain init_logging would no longer check anything.
|
|
280
|
+
assert lib_log_rich.runtime.is_initialised()
|
|
281
|
+
monkeypatch.setenv("LOG_SCRUB_PATTERNS", "token=(")
|
|
282
|
+
|
|
283
|
+
with pytest.raises(InvalidLoggingConfigError, match="Invalid scrub pattern"):
|
|
284
|
+
restart_logging(Config({}, {}))
|
|
285
|
+
|
|
286
|
+
assert lib_log_rich.runtime.is_initialised()
|
|
287
|
+
handlers = [h for h in logging.getLogger().handlers if isinstance(h, lib_log_rich.runtime.StdlibLoggingHandler)]
|
|
288
|
+
assert len(handlers) == 1
|
|
289
|
+
|
|
290
|
+
|
|
209
291
|
# ------------------------------------------------------------------ a refused setting still leaves logging running
|
|
210
292
|
|
|
211
293
|
|
|
@@ -19,6 +19,7 @@ from bitranox_template_py_cli.adapters.memory import (
|
|
|
19
19
|
get_config_in_memory,
|
|
20
20
|
get_default_config_path_in_memory,
|
|
21
21
|
init_logging_in_memory,
|
|
22
|
+
restart_logging_in_memory,
|
|
22
23
|
)
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING:
|
|
@@ -27,6 +28,7 @@ if TYPE_CHECKING:
|
|
|
27
28
|
GetDefaultConfigPath,
|
|
28
29
|
InitLogging,
|
|
29
30
|
LoadEmailConfigFromDict,
|
|
31
|
+
RestartLogging,
|
|
30
32
|
SendEmail,
|
|
31
33
|
SendNotification,
|
|
32
34
|
)
|
|
@@ -73,6 +75,12 @@ def init_logging_impl() -> InitLogging:
|
|
|
73
75
|
return init_logging_in_memory
|
|
74
76
|
|
|
75
77
|
|
|
78
|
+
@pytest.fixture
|
|
79
|
+
def restart_logging_impl() -> RestartLogging:
|
|
80
|
+
"""Provide in-memory RestartLogging implementation."""
|
|
81
|
+
return restart_logging_in_memory
|
|
82
|
+
|
|
83
|
+
|
|
76
84
|
@pytest.mark.os_agnostic
|
|
77
85
|
def test_get_config_returns_config_with_dict(get_config_impl: GetConfig) -> None:
|
|
78
86
|
"""GetConfig must return a Config whose as_dict() yields a dict."""
|
|
@@ -143,6 +151,12 @@ def test_init_logging_does_not_raise(init_logging_impl: InitLogging) -> None:
|
|
|
143
151
|
init_logging_impl(config)
|
|
144
152
|
|
|
145
153
|
|
|
154
|
+
@pytest.mark.os_agnostic
|
|
155
|
+
def test_restart_logging_in_memory_refuses_nothing(restart_logging_impl: RestartLogging) -> None:
|
|
156
|
+
"""The test runtime ignores the configuration, so restarting it refuses no logging section either."""
|
|
157
|
+
restart_logging_impl(Config({"lib_log_rich": {"rate_limit": "100:60"}}, {}))
|
|
158
|
+
|
|
159
|
+
|
|
146
160
|
# ======================== Composition Wiring Tests ========================
|
|
147
161
|
|
|
148
162
|
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/.devcontainer/devcontainer.json
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/.devcontainer/settings.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/.github/workflows/codeql.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/notebooks/Quickstart.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cache_effectiveness.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cli_config_errors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cli_exit_codes.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cli_main_exit.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cli_overrides.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_cli_validation.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_config_overrides.py
RENAMED
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_deploy_mode_safety.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_memory_logging.py
RENAMED
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_metadata_sync.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_module_entry.py
RENAMED
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_permission_defaults.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_property_email.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_property_overrides.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.2 → bitranox_template_py_cli-2.1.3}/tests/test_safe_console.py
RENAMED
|
File without changes
|