bitranox-template-py-cli 2.1.1__tar.gz → 2.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/CHANGELOG.md +16 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/PKG-INFO +2 -2
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/docs/systemdesign/module_reference.md +16 -14
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/pyproject.toml +2 -2
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/__init__conf__.py +1 -1
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/commands/email/_common.py +28 -2
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/exit_codes.py +3 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/50-mail.toml +2 -1
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/email/transport.py +4 -0
- bitranox_template_py_cli-2.1.2/tests/test_cli_attachment_refused.py +191 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/.devcontainer/devcontainer.json +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/.devcontainer/settings.json +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/.env.example +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/.gitattributes +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/.github/actions/extract-metadata/action.yml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/.github/dependabot.yml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/.github/workflows/codeql.yml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/.github/workflows/default_cicd_public.yml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/.github/workflows/default_release_public.yml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/.gitignore +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/.qlty/qlty.toml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/.snyk +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/CONFIG.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/CONTRIBUTING.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/DEVELOPMENT.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/INSTALL.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/LICENSE +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/Makefile +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/README.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/SECURITY.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/ai-stance.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/ai-transparency.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/codecov.yml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/docs/adr/0001-memory-adapters-in-src.md +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/notebooks/Quickstart.ipynb +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/rename.sh +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/rename_dry.sh +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/reset_git_history.sh +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/__main__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/commands/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/commands/config.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/commands/email/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/commands/email/send_email.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/commands/email/send_notification.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/commands/info.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/commands/logging.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/config_load.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/constants.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/context.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/main.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/root.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/safe_console.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/cli/typed_click.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/40-layered-config.toml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/90-logging.toml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/defaultconfig.toml +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/deploy.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/display.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/loader.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/overrides.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/config/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/email/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/email/config.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/email/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/email/sender.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/email/validation.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/logging/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/logging/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/logging/setup.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/memory/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/memory/config.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/memory/email.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/memory/logging.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/adapters/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/application/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/application/ports.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/application/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/composition/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/composition/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/domain/__init__.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/domain/behaviors.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/domain/enums.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/domain/errors.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/domain/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/entry.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/src/bitranox_template_py_cli/py.typed +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/conftest.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_behaviors.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_cache_effectiveness.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_cli_config.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_cli_config_errors.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_cli_core.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_cli_email.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_cli_email_config_errors.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_cli_env_file.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_cli_exit_codes.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_cli_main_exit.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_cli_overrides.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_cli_validation.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_config_overrides.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_declared_dependencies.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_deploy_mode_safety.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_deploy_permissions.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_display.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_email_attachment_lists.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_email_config_translation.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_email_password_secrecy.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_email_shipped_defaults.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_enums.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_errors.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_logging.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_logging_dotenv_isolation.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_mail.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_memory_logging.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_metadata.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_metadata_sync.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_module_entry.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_module_reference_sync.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_permission_defaults.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_ports.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_property_email.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_property_overrides.py +0 -0
- {bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_safe_console.py +0 -0
|
@@ -6,6 +6,22 @@ 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
|
+
|
|
9
25
|
## [2.1.1] 2026-10-06 18:13:05
|
|
10
26
|
|
|
11
27
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: bitranox_template_py_cli
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: Template CLI application with configuration management and structured logging
|
|
5
5
|
Project-URL: Homepage, https://github.com/bitranox/bitranox_template_py_cli
|
|
6
6
|
Project-URL: Repository, https://github.com/bitranox/bitranox_template_py_cli.git
|
|
@@ -26,7 +26,7 @@ Requires-Dist: click>=8.5.0
|
|
|
26
26
|
Requires-Dist: lib-cli-exit-tools>=2.4.0
|
|
27
27
|
Requires-Dist: lib-layered-config>=7.0.1
|
|
28
28
|
Requires-Dist: lib-log-rich>=6.3.9
|
|
29
|
-
Requires-Dist: orjson>=3.
|
|
29
|
+
Requires-Dist: orjson>=3.13.0
|
|
30
30
|
Requires-Dist: pydantic>=2.13.5
|
|
31
31
|
Requires-Dist: python-dotenv>=1.2.4
|
|
32
32
|
Requires-Dist: rich-click>=1.9.9
|
|
@@ -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
|
|
@@ -136,19 +137,20 @@ Run `lint-imports` to verify compliance.
|
|
|
136
137
|
|
|
137
138
|
POSIX-conventional exit codes defined in `adapters/cli/exit_codes.py`:
|
|
138
139
|
|
|
139
|
-
| Code | Name
|
|
140
|
-
|
|
141
|
-
| 0 | `SUCCESS`
|
|
142
|
-
| 1 | `GENERAL_ERROR`
|
|
143
|
-
| 2 | `FILE_NOT_FOUND`
|
|
144
|
-
| 13 | `PERMISSION_DENIED`
|
|
145
|
-
| 22 | `INVALID_ARGUMENT`
|
|
146
|
-
| 69 | `SMTP_FAILURE`
|
|
147
|
-
|
|
|
148
|
-
|
|
|
149
|
-
|
|
|
150
|
-
|
|
|
151
|
-
|
|
|
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) |
|
|
152
154
|
|
|
153
155
|
---
|
|
154
156
|
|
|
@@ -243,7 +245,7 @@ Send email using configured SMTP settings.
|
|
|
243
245
|
| `--raise-on-missing-attachments / --no-raise-on-missing-attachments` | Override missing-attachment handling |
|
|
244
246
|
| `--raise-on-invalid-recipient / --no-raise-on-invalid-recipient` | Override invalid-recipient handling |
|
|
245
247
|
|
|
246
|
-
**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)
|
|
247
249
|
|
|
248
250
|
### send-notification
|
|
249
251
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "bitranox_template_py_cli"
|
|
3
|
-
version = "2.1.
|
|
3
|
+
version = "2.1.2"
|
|
4
4
|
description = "Template CLI application with configuration management and structured logging"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -14,7 +14,7 @@ dependencies = [
|
|
|
14
14
|
"lib_layered_config>=7.0.1",
|
|
15
15
|
"btx_lib_mail>=4.0.0",
|
|
16
16
|
"pydantic>=2.13.5",
|
|
17
|
-
"orjson>=3.
|
|
17
|
+
"orjson>=3.13.0",
|
|
18
18
|
]
|
|
19
19
|
license = { text = "MIT" }
|
|
20
20
|
authors = [{ name = "bitranox", email = "bitranox@gmail.com" }]
|
|
@@ -40,7 +40,7 @@ name = "bitranox_template_py_cli"
|
|
|
40
40
|
#: Human-readable summary shown in CLI help output.
|
|
41
41
|
title = "Template CLI application with configuration management and structured logging"
|
|
42
42
|
#: Current release version pulled from ``pyproject.toml`` by automation.
|
|
43
|
-
version = "2.1.
|
|
43
|
+
version = "2.1.2"
|
|
44
44
|
#: Repository homepage presented to users.
|
|
45
45
|
homepage = "https://github.com/bitranox/bitranox_template_py_cli"
|
|
46
46
|
#: Author attribution surfaced in CLI output.
|
|
@@ -11,6 +11,7 @@ import logging
|
|
|
11
11
|
import os
|
|
12
12
|
from typing import TYPE_CHECKING, Any, NoReturn, cast
|
|
13
13
|
|
|
14
|
+
from btx_lib_mail import AttachmentSecurityError
|
|
14
15
|
from pydantic import ValidationError
|
|
15
16
|
|
|
16
17
|
from bitranox_template_py_cli import __init__conf__
|
|
@@ -169,8 +170,10 @@ def execute_with_email_error_handling(
|
|
|
169
170
|
1. ConfigurationError -> CONFIG_ERROR (78): Missing/invalid config
|
|
170
171
|
2. ValueError -> INVALID_ARGUMENT (22): Invalid parameters or email format
|
|
171
172
|
3. FileNotFoundError -> FILE_NOT_FOUND (2): Missing attachment (if enabled)
|
|
172
|
-
4.
|
|
173
|
-
|
|
173
|
+
4. AttachmentSecurityError -> ATTACHMENT_REFUSED (77): An attachment btx_lib_mail's
|
|
174
|
+
security checks refuse (blocked extension or directory, symlink, size, ...)
|
|
175
|
+
5. DeliveryError/RuntimeError -> SMTP_FAILURE (69): SMTP transport failures
|
|
176
|
+
6. Exception (catch-all) -> GENERAL_ERROR (1): Unexpected errors with traceback
|
|
174
177
|
|
|
175
178
|
This ordering ensures specific exceptions aren't caught by broader handlers.
|
|
176
179
|
When adding new exception types, insert them before the catch-all Exception handler.
|
|
@@ -205,6 +208,8 @@ def execute_with_email_error_handling(
|
|
|
205
208
|
"Attachment file not found",
|
|
206
209
|
exit_code=ExitCode.FILE_NOT_FOUND,
|
|
207
210
|
)
|
|
211
|
+
except AttachmentSecurityError as exc:
|
|
212
|
+
_refuse_attachment(exc)
|
|
208
213
|
except (DeliveryError, RuntimeError) as exc:
|
|
209
214
|
_handle_send_error(
|
|
210
215
|
exc,
|
|
@@ -264,6 +269,27 @@ def _refuse_email_config(exc: ValidationError, heading: str, *, exit_code: ExitC
|
|
|
264
269
|
get_current_context().exit(exit_code)
|
|
265
270
|
|
|
266
271
|
|
|
272
|
+
def _refuse_attachment(exc: AttachmentSecurityError) -> NoReturn:
|
|
273
|
+
"""Report an attachment btx_lib_mail refused on security grounds, then exit 77.
|
|
274
|
+
|
|
275
|
+
The refusal is policy working as configured, not a crash, so no traceback is logged. The
|
|
276
|
+
user sees the library's reason, which names the violation and the resolved path; it never
|
|
277
|
+
carries a credential.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
exc: The refusal btx_lib_mail raised.
|
|
281
|
+
|
|
282
|
+
Raises:
|
|
283
|
+
click.exceptions.Exit: Always raised, with the ATTACHMENT_REFUSED exit code.
|
|
284
|
+
"""
|
|
285
|
+
logger.error(
|
|
286
|
+
"Attachment refused by security policy",
|
|
287
|
+
extra={"reason": exc.reason, "violation_type": exc.violation_type.value},
|
|
288
|
+
)
|
|
289
|
+
safe_console.echo(f"\nError: Attachment refused by security policy - {exc.reason}", err=True)
|
|
290
|
+
get_current_context().exit(ExitCode.ATTACHMENT_REFUSED)
|
|
291
|
+
|
|
292
|
+
|
|
267
293
|
def _handle_send_result(result: bool, recipients: list[str] | None, message_type: str) -> None:
|
|
268
294
|
"""Handle the result of a send operation.
|
|
269
295
|
|
|
@@ -27,6 +27,8 @@ class ExitCode(IntEnum):
|
|
|
27
27
|
* 2-13: errno-derived codes (ENOENT, EACCES)
|
|
28
28
|
* 22: EINVAL
|
|
29
29
|
* 69: EX_UNAVAILABLE (sysexits.h)
|
|
30
|
+
* 77: EX_NOPERM (sysexits.h): refused by policy, such as an email attachment
|
|
31
|
+
btx_lib_mail's security checks reject
|
|
30
32
|
* 78: EX_CONFIG (sysexits.h)
|
|
31
33
|
* 110: ETIMEDOUT
|
|
32
34
|
* 128+N: signal N (informational only)
|
|
@@ -44,6 +46,7 @@ class ExitCode(IntEnum):
|
|
|
44
46
|
PERMISSION_DENIED = 13
|
|
45
47
|
INVALID_ARGUMENT = 22
|
|
46
48
|
SMTP_FAILURE = 69
|
|
49
|
+
ATTACHMENT_REFUSED = 77
|
|
47
50
|
CONFIG_ERROR = 78
|
|
48
51
|
TIMEOUT = 110
|
|
49
52
|
SIGNAL_INT = 130
|
|
@@ -235,5 +235,6 @@ allow_symlinks = false
|
|
|
235
235
|
# Default: true (raise AttachmentSecurityError)
|
|
236
236
|
# Environment Variable: BITRANOX_TEMPLATE_PY_CLI___EMAIL__ATTACHMENTS__RAISE_ON_SECURITY_VIOLATION=true
|
|
237
237
|
# .env: EMAIL__ATTACHMENTS__RAISE_ON_SECURITY_VIOLATION=true
|
|
238
|
-
# Note: When
|
|
238
|
+
# Note: When true, send-email refuses the whole message and exits 77 (EX_NOPERM)
|
|
239
|
+
# naming the violation; when false, violations log a warning and skip the attachment
|
|
239
240
|
raise_on_security_violation = true
|
|
@@ -128,6 +128,10 @@ def send_email(
|
|
|
128
128
|
ConfigurationError: No SMTP hosts configured.
|
|
129
129
|
FileNotFoundError: Required attachment missing or unreadable and
|
|
130
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.
|
|
131
135
|
DeliveryError: All SMTP hosts failed for a recipient.
|
|
132
136
|
|
|
133
137
|
Side Effects:
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""An attachment btx_lib_mail refuses on security grounds exits 77, through the real ``main()``.
|
|
2
|
+
|
|
3
|
+
The refusal is btx_lib_mail's own check, run by the real ``send_email`` adapter: only the SMTP
|
|
4
|
+
transport is replaced, by a recorder, so a test proves both the exit code and that nothing was
|
|
5
|
+
delivered. ``AttachmentSecurityError`` is neither a ``ValueError`` nor a ``RuntimeError``, so
|
|
6
|
+
without its own branch it fell into the catch-all as exit 1, "Unexpected error".
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import dataclasses
|
|
12
|
+
import shutil
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import IO, TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
from btx_lib_mail import ConfMail
|
|
19
|
+
from lib_layered_config import Config
|
|
20
|
+
|
|
21
|
+
from bitranox_template_py_cli.adapters.cli.exit_codes import ExitCode
|
|
22
|
+
from bitranox_template_py_cli.adapters.cli.main import main
|
|
23
|
+
from bitranox_template_py_cli.adapters.email.transport import send_email
|
|
24
|
+
from bitranox_template_py_cli.composition import AppServices, build_testing
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
28
|
+
|
|
29
|
+
from btx_lib_mail.lib_mail import DeliveryOptions
|
|
30
|
+
|
|
31
|
+
from bitranox_template_py_cli.adapters.email.config import EmailConfig
|
|
32
|
+
|
|
33
|
+
EMAIL = {"email": {"smtp_hosts": ["smtp.test.com:587"], "from_address": "sender@test.com"}}
|
|
34
|
+
SEND_EMAIL = ["send-email", "--to", "recipient@test.com", "--subject", "s", "--body", "b"]
|
|
35
|
+
REFUSED = "Error: Attachment refused by security policy - "
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _Recording:
|
|
39
|
+
"""A transport that records each recipient it is handed and sends nothing."""
|
|
40
|
+
|
|
41
|
+
def __init__(self) -> None:
|
|
42
|
+
self.recipients: list[str] = []
|
|
43
|
+
|
|
44
|
+
def deliver(self, *, host: str, sender: str, recipient: str, message: IO[bytes], delivery: DeliveryOptions) -> None:
|
|
45
|
+
self.recipients.append(recipient)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _real_send_email(transport: _Recording) -> Callable[..., bool]:
|
|
49
|
+
"""The production ``send_email`` adapter with only its SMTP transport replaced."""
|
|
50
|
+
|
|
51
|
+
def send(
|
|
52
|
+
*,
|
|
53
|
+
config: EmailConfig,
|
|
54
|
+
recipients: str | Sequence[str] | None = None,
|
|
55
|
+
subject: str,
|
|
56
|
+
body: str = "",
|
|
57
|
+
body_html: str = "",
|
|
58
|
+
from_address: str | None = None,
|
|
59
|
+
attachments: Sequence[Path] | None = None,
|
|
60
|
+
) -> bool:
|
|
61
|
+
return send_email(
|
|
62
|
+
config=config,
|
|
63
|
+
recipients=recipients,
|
|
64
|
+
subject=subject,
|
|
65
|
+
body=body,
|
|
66
|
+
body_html=body_html,
|
|
67
|
+
from_address=from_address,
|
|
68
|
+
attachments=attachments,
|
|
69
|
+
transport=transport,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return send
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _services(transport: _Recording, data: dict[str, Any]) -> Callable[[], AppServices]:
|
|
76
|
+
config = Config(data, {})
|
|
77
|
+
|
|
78
|
+
def get_config(**_kwargs: Any) -> Config:
|
|
79
|
+
return config
|
|
80
|
+
|
|
81
|
+
def build() -> AppServices:
|
|
82
|
+
return dataclasses.replace(build_testing(), get_config=get_config, send_email=_real_send_email(transport))
|
|
83
|
+
|
|
84
|
+
return build
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _send_with(attachment: Path, transport: _Recording, data: dict[str, Any] = EMAIL) -> int:
|
|
88
|
+
return main([*SEND_EMAIL, "--attachment", str(attachment)], services_factory=_services(transport, data))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _error_lines(err: str) -> list[str]:
|
|
92
|
+
return [line for line in err.splitlines() if line.startswith("Error:")]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _is_blocked(directory: Path) -> bool:
|
|
96
|
+
resolved = directory.resolve()
|
|
97
|
+
return any(resolved.is_relative_to(blocked.resolve()) for blocked in ConfMail().attachment_blocked_directories)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _a_file_in_a_blocked_directory() -> Path:
|
|
101
|
+
for directory in sorted(ConfMail().attachment_blocked_directories):
|
|
102
|
+
if not directory.is_dir():
|
|
103
|
+
continue
|
|
104
|
+
for entry in sorted(directory.iterdir()):
|
|
105
|
+
if entry.is_file() and not entry.is_symlink():
|
|
106
|
+
return entry
|
|
107
|
+
raise AssertionError("no regular file directly inside any default blocked directory on this OS")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@pytest.fixture
|
|
111
|
+
def unblocked_dir(tmp_path: Path) -> Iterator[Path]:
|
|
112
|
+
"""A fresh directory under no default blocked directory, so only the file itself is judged.
|
|
113
|
+
|
|
114
|
+
pytest's tmp_path is under /var on macOS (/private/var/folders), which the defaults block.
|
|
115
|
+
"""
|
|
116
|
+
base = next((base for base in (tmp_path, Path.home(), Path.cwd()) if not _is_blocked(base)), None)
|
|
117
|
+
assert base is not None, "no candidate directory is outside the default blocked directories"
|
|
118
|
+
directory = Path(tempfile.mkdtemp(prefix="attachment-refused-", dir=base))
|
|
119
|
+
yield directory
|
|
120
|
+
shutil.rmtree(directory, ignore_errors=True)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@pytest.mark.os_agnostic
|
|
124
|
+
def test_the_attachment_refused_code_is_sysexits_ex_noperm() -> None:
|
|
125
|
+
assert ExitCode.ATTACHMENT_REFUSED == 77
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@pytest.mark.os_agnostic
|
|
129
|
+
def test_a_blocked_extension_exits_77_with_one_error_line_and_delivers_nothing(
|
|
130
|
+
capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture, unblocked_dir: Path
|
|
131
|
+
) -> None:
|
|
132
|
+
attachment = unblocked_dir / "x.exe"
|
|
133
|
+
attachment.write_bytes(b"MZ")
|
|
134
|
+
transport = _Recording()
|
|
135
|
+
|
|
136
|
+
exit_code = _send_with(attachment, transport)
|
|
137
|
+
|
|
138
|
+
err = capsys.readouterr().err
|
|
139
|
+
assert exit_code == 77
|
|
140
|
+
assert _error_lines(err) == [f'{REFUSED}extension ".exe" is blocked: "{attachment.resolve()}"']
|
|
141
|
+
assert "Unexpected error" not in err
|
|
142
|
+
assert [record.getMessage() for record in caplog.records if record.exc_info] == []
|
|
143
|
+
assert transport.recipients == []
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@pytest.mark.os_agnostic
|
|
147
|
+
def test_a_file_in_a_blocked_directory_exits_77_with_one_error_line_and_delivers_nothing(
|
|
148
|
+
capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture
|
|
149
|
+
) -> None:
|
|
150
|
+
attachment = _a_file_in_a_blocked_directory()
|
|
151
|
+
transport = _Recording()
|
|
152
|
+
|
|
153
|
+
exit_code = _send_with(attachment, transport)
|
|
154
|
+
|
|
155
|
+
err = capsys.readouterr().err
|
|
156
|
+
lines = _error_lines(err)
|
|
157
|
+
assert exit_code == 77
|
|
158
|
+
assert len(lines) == 1
|
|
159
|
+
assert lines[0].startswith(f"{REFUSED}path under blocked directory ")
|
|
160
|
+
assert "Unexpected error" not in err
|
|
161
|
+
assert [record.getMessage() for record in caplog.records if record.exc_info] == []
|
|
162
|
+
assert transport.recipients == []
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@pytest.mark.os_agnostic
|
|
166
|
+
def test_an_ordinary_attachment_is_delivered(capsys: pytest.CaptureFixture[str], unblocked_dir: Path) -> None:
|
|
167
|
+
"""The control: the same wiring delivers a harmless file, so the 77 above is the refusal."""
|
|
168
|
+
attachment = unblocked_dir / "report.txt"
|
|
169
|
+
attachment.write_text("ok", encoding="utf-8")
|
|
170
|
+
transport = _Recording()
|
|
171
|
+
|
|
172
|
+
exit_code = _send_with(attachment, transport)
|
|
173
|
+
|
|
174
|
+
assert exit_code == 0, capsys.readouterr().err
|
|
175
|
+
assert transport.recipients == ["recipient@test.com"]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@pytest.mark.os_agnostic
|
|
179
|
+
def test_with_security_violations_set_to_warn_the_blocked_file_is_skipped_and_the_message_sent(
|
|
180
|
+
capsys: pytest.CaptureFixture[str], unblocked_dir: Path
|
|
181
|
+
) -> None:
|
|
182
|
+
"""77 belongs to the strict setting: in warn mode btx_lib_mail drops the file and sends the rest."""
|
|
183
|
+
attachment = unblocked_dir / "x.exe"
|
|
184
|
+
attachment.write_bytes(b"MZ")
|
|
185
|
+
transport = _Recording()
|
|
186
|
+
warn = {"email": {**EMAIL["email"], "attachments": {"raise_on_security_violation": False}}}
|
|
187
|
+
|
|
188
|
+
exit_code = _send_with(attachment, transport, warn)
|
|
189
|
+
|
|
190
|
+
assert exit_code == 0, capsys.readouterr().err
|
|
191
|
+
assert transport.recipients == ["recipient@test.com"]
|
{bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/.devcontainer/devcontainer.json
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/.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.1 → bitranox_template_py_cli-2.1.2}/.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.1 → bitranox_template_py_cli-2.1.2}/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.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_cache_effectiveness.py
RENAMED
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/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.1 → bitranox_template_py_cli-2.1.2}/tests/test_cli_env_file.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_cli_exit_codes.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_cli_main_exit.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_cli_overrides.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_cli_validation.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_config_overrides.py
RENAMED
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_deploy_mode_safety.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/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
|
|
File without changes
|
{bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_memory_logging.py
RENAMED
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_metadata_sync.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_module_entry.py
RENAMED
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_permission_defaults.py
RENAMED
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_property_email.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_property_overrides.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-2.1.1 → bitranox_template_py_cli-2.1.2}/tests/test_safe_console.py
RENAMED
|
File without changes
|