bitranox-template-py-cli 2.3.0__tar.gz → 2.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/CHANGELOG.md +13 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/PKG-INFO +3 -3
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/docs/systemdesign/module_reference.md +2 -1
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/pyproject.toml +3 -3
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/__init__conf__.py +1 -1
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/commands/email/__init__.py +3 -2
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/commands/email/_common.py +62 -34
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/commands/email/send_email.py +5 -5
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/commands/email/send_notification.py +5 -5
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_mail.py +81 -2
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/.devcontainer/devcontainer.json +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/.devcontainer/settings.json +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/.env.example +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/.gitattributes +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/.github/actions/extract-metadata/action.yml +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/.github/dependabot.yml +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/.github/workflows/codeql.yml +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/.github/workflows/default_cicd_public.yml +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/.github/workflows/default_release_public.yml +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/.gitignore +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/.qlty/qlty.toml +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/.snyk +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/CONFIG.md +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/CONTRIBUTING.md +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/DEVELOPMENT.md +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/INSTALL.md +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/LICENSE +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/Makefile +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/README.md +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/SECURITY.md +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/ai-stance.md +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/ai-transparency.md +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/codecov.yml +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/docs/adr/0001-memory-adapters-in-src.md +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/notebooks/Quickstart.ipynb +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/rename.sh +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/rename_dry.sh +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/reset_git_history.sh +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/__init__.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/__main__.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/__init__.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/__init__.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/commands/__init__.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/commands/config.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/commands/info.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/commands/logging.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/config_load.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/constants.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/context.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/exit_codes.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/main.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/py.typed +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/root.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/safe_console.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/cli/typed_click.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/__init__.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/40-layered-config.toml +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/50-mail.toml +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/90-logging.toml +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/defaultconfig.toml +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/deploy.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/display.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/loader.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/overrides.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/config/py.typed +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/email/__init__.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/email/config.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/email/py.typed +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/email/sender.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/email/transport.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/email/validation.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/logging/__init__.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/logging/py.typed +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/logging/setup.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/memory/__init__.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/memory/config.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/memory/email.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/memory/logging.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/adapters/py.typed +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/application/__init__.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/application/ports.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/application/py.typed +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/composition/__init__.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/composition/py.typed +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/domain/__init__.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/domain/behaviors.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/domain/enums.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/domain/errors.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/domain/py.typed +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/entry.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/src/bitranox_template_py_cli/py.typed +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/conftest.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_behaviors.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cache_effectiveness.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_attachment_refused.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_config.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_config_errors.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_core.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_email.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_email_config_errors.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_env_file.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_exit_codes.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_main_exit.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_overrides.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_validation.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_config_overrides.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_declared_dependencies.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_deploy_mode_safety.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_deploy_permissions.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_display.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_email_attachment_lists.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_email_config_translation.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_email_password_secrecy.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_email_shipped_defaults.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_enums.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_errors.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_logging.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_logging_dotenv_isolation.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_memory_logging.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_metadata.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_metadata_sync.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_module_entry.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_module_reference_sync.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_permission_defaults.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_ports.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_property_email.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_property_overrides.py +0 -0
- {bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/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.4.0] 2026-10-08 17:34:36
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **The email commands' SMTP options reach the configuration as a typed `EmailConfigOverrides`
|
|
13
|
+
model instead of a plain dict.** `EmailConfigOverrides.from_cli_options(...)` takes the Click
|
|
14
|
+
values (an option that was not given is `None`, an unset `--smtp-host` an empty tuple) and
|
|
15
|
+
`apply_validated_overrides` merges only the options that were given. Its fields carry
|
|
16
|
+
EmailConfig's own names, so a field EmailConfig renames is refused instead of slipping through,
|
|
17
|
+
and `--smtp-password` is a `SecretStr` from the moment it is read; the model is a btx_lib_mail
|
|
18
|
+
`SecretSafeModel`, so a refused password never appears in a validation error's details.
|
|
19
|
+
`filter_sentinels` is gone; the command line behaves as before.
|
|
20
|
+
- Requires `pydantic>=2.14.0` (and `virtualenv>=21.14.6` in the dev extra).
|
|
21
|
+
|
|
9
22
|
## [2.3.0] 2026-10-08 16:14:12
|
|
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.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Template CLI application with configuration management and structured logging
|
|
5
5
|
Project-URL: Homepage, https://github.com/bitranox/bitranox_template_py_cli
|
|
6
6
|
Project-URL: Repository, https://github.com/bitranox/bitranox_template_py_cli.git
|
|
@@ -27,7 +27,7 @@ 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.4.2
|
|
29
29
|
Requires-Dist: orjson>=3.13.0
|
|
30
|
-
Requires-Dist: pydantic>=2.
|
|
30
|
+
Requires-Dist: pydantic>=2.14.0
|
|
31
31
|
Requires-Dist: python-dotenv>=1.2.4
|
|
32
32
|
Requires-Dist: rich-click>=1.9.9
|
|
33
33
|
Provides-Extra: dev
|
|
@@ -49,7 +49,7 @@ Requires-Dist: ruff>=0.16.10; extra == 'dev'
|
|
|
49
49
|
Requires-Dist: textual>=8.2.8; extra == 'dev'
|
|
50
50
|
Requires-Dist: twine>=7.0.0; extra == 'dev'
|
|
51
51
|
Requires-Dist: urllib3>=2.8.0; extra == 'dev'
|
|
52
|
-
Requires-Dist: virtualenv>=21.14.
|
|
52
|
+
Requires-Dist: virtualenv>=21.14.6; extra == 'dev'
|
|
53
53
|
Requires-Dist: wheel>=0.48.0; extra == 'dev'
|
|
54
54
|
Description-Content-Type: text/markdown
|
|
55
55
|
|
|
@@ -40,7 +40,8 @@ moves or removes a module, a CLI option or a public name.
|
|
|
40
40
|
- `commands/info.py` - info, hello, fail commands
|
|
41
41
|
- `commands/config.py` - config, config-deploy, config-generate-examples commands
|
|
42
42
|
- `commands/email/` - Email commands package:
|
|
43
|
-
- `_common.py` - Shared SMTP override options
|
|
43
|
+
- `_common.py` - Shared SMTP override options and their typed `EmailConfigOverrides`, config
|
|
44
|
+
loading and error-to-exit-code mapping
|
|
44
45
|
- `send_email.py` - send-email command
|
|
45
46
|
- `send_notification.py` - send-notification command
|
|
46
47
|
- `commands/logging.py` - logdemo command
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "bitranox_template_py_cli"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.4.0"
|
|
4
4
|
description = "Template CLI application with configuration management and structured logging"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -14,7 +14,7 @@ dependencies = [
|
|
|
14
14
|
"python-dotenv>=1.2.4",
|
|
15
15
|
"lib_layered_config>=7.0.1",
|
|
16
16
|
"btx_lib_mail>=4.0.0",
|
|
17
|
-
"pydantic>=2.
|
|
17
|
+
"pydantic>=2.14.0",
|
|
18
18
|
"orjson>=3.13.0",
|
|
19
19
|
]
|
|
20
20
|
license = { text = "MIT" }
|
|
@@ -77,7 +77,7 @@ dev = [
|
|
|
77
77
|
"wheel>=0.48.0", # CVE-2025-24049
|
|
78
78
|
"pynacl>=1.6.2", # CVE-2025-69277
|
|
79
79
|
"urllib3>=2.8.0", # CVE-2025-66471, CVE-2025-66418
|
|
80
|
-
"virtualenv>=21.14.
|
|
80
|
+
"virtualenv>=21.14.6", # CVE-2025-22702
|
|
81
81
|
]
|
|
82
82
|
|
|
83
83
|
[project.scripts]
|
|
@@ -40,7 +40,7 @@ name = "bitranox_template_py_cli"
|
|
|
40
40
|
#: Human-readable summary shown in CLI help output.
|
|
41
41
|
title = "Template CLI application with configuration management and structured logging"
|
|
42
42
|
#: Current release version pulled from ``pyproject.toml`` by automation.
|
|
43
|
-
version = "2.
|
|
43
|
+
version = "2.4.0"
|
|
44
44
|
#: Repository homepage presented to users.
|
|
45
45
|
homepage = "https://github.com/bitranox/bitranox_template_py_cli"
|
|
46
46
|
#: Author attribution surfaced in CLI output.
|
|
@@ -5,12 +5,13 @@ Provides commands for sending emails and notifications via SMTP.
|
|
|
5
5
|
Contents:
|
|
6
6
|
* :func:`.send_email.cli_send_email` - Send email with optional HTML and attachments.
|
|
7
7
|
* :func:`.send_notification.cli_send_notification` - Send simple plain-text notification.
|
|
8
|
+
* :class:`._common.EmailConfigOverrides` - The SMTP options a command line gave, typed.
|
|
8
9
|
"""
|
|
9
10
|
|
|
10
11
|
from __future__ import annotations
|
|
11
12
|
|
|
12
|
-
from ._common import
|
|
13
|
+
from ._common import EmailConfigOverrides
|
|
13
14
|
from .send_email import cli_send_email
|
|
14
15
|
from .send_notification import cli_send_notification
|
|
15
16
|
|
|
16
|
-
__all__ = ["
|
|
17
|
+
__all__ = ["EmailConfigOverrides", "cli_send_email", "cli_send_notification"]
|
|
@@ -9,10 +9,10 @@ from __future__ import annotations
|
|
|
9
9
|
import functools
|
|
10
10
|
import logging
|
|
11
11
|
import os
|
|
12
|
-
from typing import TYPE_CHECKING, Any, NoReturn
|
|
12
|
+
from typing import TYPE_CHECKING, Any, NoReturn
|
|
13
13
|
|
|
14
|
-
from btx_lib_mail import AttachmentSecurityError
|
|
15
|
-
from pydantic import ValidationError
|
|
14
|
+
from btx_lib_mail import AttachmentSecurityError, SecretSafeModel
|
|
15
|
+
from pydantic import ConfigDict, SecretStr, ValidationError
|
|
16
16
|
|
|
17
17
|
from bitranox_template_py_cli import __init__conf__
|
|
18
18
|
from bitranox_template_py_cli.adapters.email.config import describe_validation_error
|
|
@@ -33,51 +33,79 @@ if TYPE_CHECKING:
|
|
|
33
33
|
logger = logging.getLogger(__name__)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
"""
|
|
36
|
+
class EmailConfigOverrides(SecretSafeModel):
|
|
37
|
+
"""The EmailConfig fields the command line overrides; ``None`` means the option was not given.
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
and
|
|
39
|
+
Each field carries EmailConfig's own name, so a field EmailConfig renames is refused here
|
|
40
|
+
instead of being passed along unnoticed. The password stays a ``SecretStr`` from the moment
|
|
41
|
+
the command line hands it over, and a validation error never carries its value, not even in
|
|
42
|
+
``errors()`` or ``json()``.
|
|
43
|
+
"""
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
46
|
+
credential_fields = frozenset({"smtp_password"})
|
|
47
|
+
|
|
48
|
+
smtphosts: tuple[str, ...] | None = None
|
|
49
|
+
smtp_username: str | None = None
|
|
50
|
+
smtp_password: SecretStr | None = None
|
|
51
|
+
smtp_use_starttls: bool | None = None
|
|
52
|
+
smtp_timeout: float | None = None
|
|
53
|
+
raise_on_missing_attachments: bool | None = None
|
|
54
|
+
raise_on_invalid_recipient: bool | None = None
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_cli_options(
|
|
58
|
+
cls,
|
|
59
|
+
*,
|
|
60
|
+
smtp_hosts: tuple[str, ...] = (),
|
|
61
|
+
smtp_username: str | None = None,
|
|
62
|
+
smtp_password: str | None = None,
|
|
63
|
+
use_starttls: bool | None = None,
|
|
64
|
+
timeout: float | None = None,
|
|
65
|
+
raise_on_missing_attachments: bool | None = None,
|
|
66
|
+
raise_on_invalid_recipient: bool | None = None,
|
|
67
|
+
) -> EmailConfigOverrides:
|
|
68
|
+
"""Build the overrides from the values Click hands a command.
|
|
69
|
+
|
|
70
|
+
Click reports an option that was not given as ``None``, and a ``multiple=True`` option
|
|
71
|
+
that was not given as an empty tuple; both become ``None`` here.
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
>>> EmailConfigOverrides.from_cli_options(smtp_hosts=(), timeout=5.0).model_dump(exclude_none=True)
|
|
75
|
+
{'smtp_timeout': 5.0}
|
|
76
|
+
"""
|
|
77
|
+
return cls(
|
|
78
|
+
smtphosts=smtp_hosts or None,
|
|
79
|
+
smtp_username=smtp_username,
|
|
80
|
+
smtp_password=None if smtp_password is None else SecretStr(smtp_password),
|
|
81
|
+
smtp_use_starttls=use_starttls,
|
|
82
|
+
smtp_timeout=timeout,
|
|
83
|
+
raise_on_missing_attachments=raise_on_missing_attachments,
|
|
84
|
+
raise_on_invalid_recipient=raise_on_invalid_recipient,
|
|
85
|
+
)
|
|
45
86
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"""
|
|
49
|
-
result: dict[str, Any] = {}
|
|
50
|
-
for k, v in kwargs.items():
|
|
51
|
-
if v is None or v == ():
|
|
52
|
-
continue
|
|
53
|
-
if isinstance(v, tuple):
|
|
54
|
-
result[k] = list(cast("tuple[Any, ...]", v))
|
|
55
|
-
else:
|
|
56
|
-
result[k] = v
|
|
57
|
-
return result
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def apply_validated_overrides(base_config: EmailConfig, overrides: dict[str, Any]) -> EmailConfig:
|
|
87
|
+
|
|
88
|
+
def apply_validated_overrides(base_config: EmailConfig, overrides: EmailConfigOverrides) -> EmailConfig:
|
|
61
89
|
"""Apply overrides with full Pydantic validation.
|
|
62
90
|
|
|
63
|
-
Uses model_validate() with a merged
|
|
64
|
-
to ensure
|
|
91
|
+
Uses model_validate() with a merged mapping instead of model_copy(update=...)
|
|
92
|
+
to ensure EmailConfig's validators run on all overridden values.
|
|
65
93
|
|
|
66
94
|
Args:
|
|
67
95
|
base_config: Base EmailConfig to merge overrides into.
|
|
68
|
-
overrides:
|
|
96
|
+
overrides: The options given on the command line.
|
|
69
97
|
|
|
70
98
|
Returns:
|
|
71
|
-
|
|
99
|
+
``base_config`` itself when no option was given, else a new EmailConfig with the
|
|
100
|
+
overrides applied and validated.
|
|
72
101
|
|
|
73
102
|
Raises:
|
|
74
103
|
ValidationError: When overrides contain invalid values.
|
|
75
104
|
"""
|
|
76
|
-
|
|
105
|
+
given = overrides.model_dump(exclude_none=True)
|
|
106
|
+
if not given:
|
|
77
107
|
return base_config
|
|
78
|
-
|
|
79
|
-
merged = {**base_dict, **overrides}
|
|
80
|
-
return EmailConfig.model_validate(merged)
|
|
108
|
+
return EmailConfig.model_validate({**base_config.model_dump(), **given})
|
|
81
109
|
|
|
82
110
|
|
|
83
111
|
def smtp_config_options(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
@@ -337,9 +365,9 @@ def _handle_send_error(
|
|
|
337
365
|
|
|
338
366
|
|
|
339
367
|
__all__ = [
|
|
368
|
+
"EmailConfigOverrides",
|
|
340
369
|
"apply_validated_overrides",
|
|
341
370
|
"execute_with_email_error_handling",
|
|
342
|
-
"filter_sentinels",
|
|
343
371
|
"handle_validation_error",
|
|
344
372
|
"load_and_validate_email_config",
|
|
345
373
|
"smtp_config_options",
|
|
@@ -18,9 +18,9 @@ from ...constants import CLICK_CONTEXT_SETTINGS
|
|
|
18
18
|
from ...context import get_cli_context
|
|
19
19
|
from ...typed_click import option
|
|
20
20
|
from ._common import (
|
|
21
|
+
EmailConfigOverrides,
|
|
21
22
|
apply_validated_overrides,
|
|
22
23
|
execute_with_email_error_handling,
|
|
23
|
-
filter_sentinels,
|
|
24
24
|
handle_validation_error,
|
|
25
25
|
load_and_validate_email_config,
|
|
26
26
|
smtp_config_options,
|
|
@@ -112,12 +112,12 @@ def cli_send_email(
|
|
|
112
112
|
|
|
113
113
|
with lib_log_rich.runtime.bind(job_id="cli-send-email", extra=extra):
|
|
114
114
|
email_config = load_and_validate_email_config(config, cli_ctx.services.load_email_config_from_dict)
|
|
115
|
-
overrides =
|
|
116
|
-
|
|
115
|
+
overrides = EmailConfigOverrides.from_cli_options(
|
|
116
|
+
smtp_hosts=smtp_hosts,
|
|
117
117
|
smtp_username=smtp_username,
|
|
118
118
|
smtp_password=smtp_password,
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
use_starttls=use_starttls,
|
|
120
|
+
timeout=timeout,
|
|
121
121
|
raise_on_missing_attachments=raise_on_missing_attachments,
|
|
122
122
|
raise_on_invalid_recipient=raise_on_invalid_recipient,
|
|
123
123
|
)
|
|
@@ -17,9 +17,9 @@ from ...constants import CLICK_CONTEXT_SETTINGS
|
|
|
17
17
|
from ...context import get_cli_context
|
|
18
18
|
from ...typed_click import option
|
|
19
19
|
from ._common import (
|
|
20
|
+
EmailConfigOverrides,
|
|
20
21
|
apply_validated_overrides,
|
|
21
22
|
execute_with_email_error_handling,
|
|
22
|
-
filter_sentinels,
|
|
23
23
|
handle_validation_error,
|
|
24
24
|
load_and_validate_email_config,
|
|
25
25
|
smtp_config_options,
|
|
@@ -70,12 +70,12 @@ def cli_send_notification(
|
|
|
70
70
|
|
|
71
71
|
with lib_log_rich.runtime.bind(job_id="cli-send-notification", extra=extra):
|
|
72
72
|
email_config = load_and_validate_email_config(config, cli_ctx.services.load_email_config_from_dict)
|
|
73
|
-
overrides =
|
|
74
|
-
|
|
73
|
+
overrides = EmailConfigOverrides.from_cli_options(
|
|
74
|
+
smtp_hosts=smtp_hosts,
|
|
75
75
|
smtp_username=smtp_username,
|
|
76
76
|
smtp_password=smtp_password,
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
use_starttls=use_starttls,
|
|
78
|
+
timeout=timeout,
|
|
79
79
|
raise_on_invalid_recipient=raise_on_invalid_recipient,
|
|
80
80
|
)
|
|
81
81
|
try:
|
|
@@ -19,7 +19,7 @@ from btx_lib_mail import ConfMail
|
|
|
19
19
|
from pydantic import SecretStr
|
|
20
20
|
from pydantic import ValidationError as PydanticValidationError
|
|
21
21
|
|
|
22
|
-
from bitranox_template_py_cli.adapters.cli.commands.email._common import apply_validated_overrides
|
|
22
|
+
from bitranox_template_py_cli.adapters.cli.commands.email._common import EmailConfigOverrides, apply_validated_overrides
|
|
23
23
|
from bitranox_template_py_cli.adapters.email.sender import (
|
|
24
24
|
EmailConfig,
|
|
25
25
|
load_email_config_from_dict,
|
|
@@ -368,10 +368,89 @@ def test_no_rendering_of_the_email_config_shows_the_password(render: Callable[[E
|
|
|
368
368
|
def test_an_override_keeps_the_configured_password() -> None:
|
|
369
369
|
config = EmailConfig(smtphosts=["smtp.example.com:587"], smtp_username="user", smtp_password=SecretStr(_PASSWORD))
|
|
370
370
|
|
|
371
|
-
overridden = apply_validated_overrides(config,
|
|
371
|
+
overridden = apply_validated_overrides(config, EmailConfigOverrides.from_cli_options(timeout=5.0))
|
|
372
372
|
|
|
373
373
|
assert overridden.smtp_password is not None
|
|
374
374
|
assert overridden.smtp_password.get_secret_value() == _PASSWORD
|
|
375
|
+
assert overridden.smtp_timeout == 5.0
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@pytest.mark.os_agnostic
|
|
379
|
+
def test_no_override_given_returns_the_configuration_itself() -> None:
|
|
380
|
+
config = EmailConfig(smtphosts=["smtp.example.com:587"])
|
|
381
|
+
|
|
382
|
+
assert apply_validated_overrides(config, EmailConfigOverrides.from_cli_options()) is config
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@pytest.mark.os_agnostic
|
|
386
|
+
def test_an_unset_host_option_keeps_the_hosts_and_a_set_one_replaces_them() -> None:
|
|
387
|
+
config = EmailConfig(smtphosts=["smtp.example.com:587"])
|
|
388
|
+
|
|
389
|
+
unset = apply_validated_overrides(config, EmailConfigOverrides.from_cli_options(smtp_hosts=()))
|
|
390
|
+
replaced = apply_validated_overrides(
|
|
391
|
+
config, EmailConfigOverrides.from_cli_options(smtp_hosts=("other.example.com:25", "third.example.com:25"))
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
assert unset.smtphosts == ["smtp.example.com:587"]
|
|
395
|
+
assert replaced.smtphosts == ["other.example.com:25", "third.example.com:25"]
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@pytest.mark.os_agnostic
|
|
399
|
+
def test_a_password_given_on_the_command_line_never_renders_from_the_overrides() -> None:
|
|
400
|
+
overrides = EmailConfigOverrides.from_cli_options(smtp_password=_PASSWORD)
|
|
401
|
+
|
|
402
|
+
assert _PASSWORD not in repr(overrides)
|
|
403
|
+
assert _PASSWORD not in str(overrides.model_dump())
|
|
404
|
+
assert overrides.smtp_password is not None
|
|
405
|
+
assert overrides.smtp_password.get_secret_value() == _PASSWORD
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@pytest.mark.os_agnostic
|
|
409
|
+
def test_an_override_refuses_a_name_that_is_not_an_email_config_field() -> None:
|
|
410
|
+
"""The overrides carry EmailConfig's own field names, so a renamed field cannot pass unnoticed."""
|
|
411
|
+
with pytest.raises(PydanticValidationError, match="smtp_hosts"):
|
|
412
|
+
EmailConfigOverrides.model_validate({"smtp_hosts": ["smtp.example.com:587"]})
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
@pytest.mark.os_agnostic
|
|
416
|
+
def test_each_command_line_option_lands_in_its_own_field() -> None:
|
|
417
|
+
"""Every option is distinct from its neighbours, so a swapped parameter shows as a mismatch."""
|
|
418
|
+
overrides = EmailConfigOverrides.from_cli_options(
|
|
419
|
+
smtp_hosts=("smtp.example.com:587",),
|
|
420
|
+
smtp_username="user",
|
|
421
|
+
smtp_password=_PASSWORD,
|
|
422
|
+
use_starttls=False,
|
|
423
|
+
timeout=7.5,
|
|
424
|
+
raise_on_missing_attachments=True,
|
|
425
|
+
raise_on_invalid_recipient=False,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
dumped = overrides.model_dump()
|
|
429
|
+
secret = dumped.pop("smtp_password")
|
|
430
|
+
|
|
431
|
+
assert secret.get_secret_value() == _PASSWORD
|
|
432
|
+
assert dumped == {
|
|
433
|
+
"smtphosts": ("smtp.example.com:587",),
|
|
434
|
+
"smtp_username": "user",
|
|
435
|
+
"smtp_use_starttls": False,
|
|
436
|
+
"smtp_timeout": 7.5,
|
|
437
|
+
"raise_on_missing_attachments": True,
|
|
438
|
+
"raise_on_invalid_recipient": False,
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@pytest.mark.os_agnostic
|
|
443
|
+
def test_a_refused_password_value_stays_out_of_the_error_details() -> None:
|
|
444
|
+
with pytest.raises(PydanticValidationError) as caught:
|
|
445
|
+
EmailConfigOverrides.model_validate({"smtp_password": [_PASSWORD]})
|
|
446
|
+
|
|
447
|
+
assert _PASSWORD not in str(caught.value.errors())
|
|
448
|
+
assert _PASSWORD not in caught.value.json()
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@pytest.mark.os_agnostic
|
|
452
|
+
def test_every_override_names_an_email_config_field() -> None:
|
|
453
|
+
assert set(EmailConfigOverrides.model_fields) <= set(EmailConfig.model_fields)
|
|
375
454
|
|
|
376
455
|
|
|
377
456
|
# ======================== load_email_config_from_dict ========================
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/.devcontainer/devcontainer.json
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/.devcontainer/settings.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/.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.3.0 → bitranox_template_py_cli-2.4.0}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cache_effectiveness.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_config_errors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_env_file.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_exit_codes.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_main_exit.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_overrides.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_cli_validation.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_config_overrides.py
RENAMED
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_deploy_mode_safety.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_deploy_permissions.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.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_memory_logging.py
RENAMED
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_metadata_sync.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_module_entry.py
RENAMED
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_permission_defaults.py
RENAMED
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_property_email.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_property_overrides.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.3.0 → bitranox_template_py_cli-2.4.0}/tests/test_safe_console.py
RENAMED
|
File without changes
|