bitranox-template-py-cli 1.5.0__tar.gz → 1.5.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-1.5.0 → bitranox_template_py_cli-1.5.2}/.github/workflows/default_release_public.yml +1 -1
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/CHANGELOG.md +21 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/PKG-INFO +2 -2
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/pyproject.toml +2 -2
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/__init__conf__.py +1 -1
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/cli/commands/config.py +1 -1
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/config/permissions.py +55 -25
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/memory/email.py +44 -21
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/conftest.py +1 -1
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_cli_email.py +15 -15
- bitranox_template_py_cli-1.5.2/tests/test_cli_env_file.py +177 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_deploy_permissions.py +13 -13
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/.devcontainer/devcontainer.json +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/.devcontainer/settings.json +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/.env.example +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/.github/actions/extract-metadata/action.yml +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/.github/dependabot.yml +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/.github/workflows/codeql.yml +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/.github/workflows/default_cicd_public.yml +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/.gitignore +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/.qlty/qlty.toml +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/.snyk +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/CONFIG.md +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/CONTRIBUTING.md +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/DEVELOPMENT.md +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/INSTALL.md +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/LICENSE +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/Makefile +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/README.md +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/SECURITY.md +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/codecov.yml +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/docs/adr/0001-memory-adapters-in-src.md +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/docs/systemdesign/module_reference.md +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/notebooks/Quickstart.ipynb +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/rename.sh +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/rename_dry.sh +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/reset_git_history.sh +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/__init__.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/__main__.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/__init__.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/cli/__init__.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/cli/commands/__init__.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/cli/commands/email/__init__.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/cli/commands/email/_common.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/cli/commands/email/send_email.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/cli/commands/email/send_notification.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/cli/commands/info.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/cli/commands/logging.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/cli/constants.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/cli/context.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/cli/exit_codes.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/cli/main.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/cli/py.typed +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/cli/root.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/config/__init__.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/40-layered-config.toml +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/50-mail.toml +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/config/defaultconfig.d/90-logging.toml +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/config/defaultconfig.toml +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/config/deploy.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/config/display.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/config/loader.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/config/overrides.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/config/py.typed +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/email/__init__.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/email/config.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/email/py.typed +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/email/sender.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/email/transport.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/email/validation.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/logging/__init__.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/logging/py.typed +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/logging/setup.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/memory/__init__.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/memory/config.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/memory/logging.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/adapters/py.typed +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/application/__init__.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/application/ports.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/application/py.typed +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/composition/__init__.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/composition/py.typed +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/domain/__init__.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/domain/behaviors.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/domain/enums.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/domain/errors.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/domain/py.typed +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/entry.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/src/bitranox_template_py_cli/py.typed +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_behaviors.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_cache_effectiveness.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_cli_config.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_cli_core.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_cli_exit_codes.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_cli_overrides.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_cli_validation.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_config_overrides.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_display.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_enums.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_errors.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_logging.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_mail.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_metadata.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_metadata_sync.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_module_entry.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_ports.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_property_email.py +0 -0
- {bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_property_overrides.py +0 -0
|
@@ -6,6 +6,27 @@ the [Keep a Changelog](https://keepachangelog.com/) format.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.5.2] - 2026-03-05
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Re-release as 1.5.2: PyPI rejected 1.5.1 due to filename reuse after prior upload deletion
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- `get_permission_defaults()` now returns a `PermissionDefaults` Pydantic model instead of a raw dict, with typed field access and `dir_mode_for()`/`file_mode_for()` methods
|
|
16
|
+
- `EmailSpy` now stores captured calls as `CapturedEmail` and `CapturedNotification` frozen dataclasses instead of `list[dict[str, Any]]`
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Tests for `--env-file` CLI option (argument passing, validation, end-to-end integration)
|
|
20
|
+
|
|
21
|
+
## [1.5.1] - 2026-03-05 [YANKED]
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- `get_permission_defaults()` now returns a `PermissionDefaults` Pydantic model instead of a raw dict, with typed field access and `dir_mode_for()`/`file_mode_for()` methods
|
|
25
|
+
- `EmailSpy` now stores captured calls as `CapturedEmail` and `CapturedNotification` frozen dataclasses instead of `list[dict[str, Any]]`
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Tests for `--env-file` CLI option (argument passing, validation, end-to-end integration)
|
|
29
|
+
|
|
9
30
|
## [1.5.0] - 2026-03-02
|
|
10
31
|
|
|
11
32
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bitranox_template_py_cli
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.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
|
|
@@ -44,7 +44,7 @@ Requires-Dist: pytest>=9.0.2; extra == 'dev'
|
|
|
44
44
|
Requires-Dist: python-multipart>=0.0.22; extra == 'dev'
|
|
45
45
|
Requires-Dist: rtoml>=0.13.0; extra == 'dev'
|
|
46
46
|
Requires-Dist: ruff>=0.15.4; extra == 'dev'
|
|
47
|
-
Requires-Dist: textual>=8.0.
|
|
47
|
+
Requires-Dist: textual>=8.0.2; extra == 'dev'
|
|
48
48
|
Requires-Dist: twine>=6.2.0; extra == 'dev'
|
|
49
49
|
Requires-Dist: urllib3>=2.6.3; extra == 'dev'
|
|
50
50
|
Requires-Dist: virtualenv>=21.1.0; extra == 'dev'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "bitranox_template_py_cli"
|
|
3
|
-
version = "1.5.
|
|
3
|
+
version = "1.5.2"
|
|
4
4
|
description = "Template CLI application with configuration management and structured logging"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -47,7 +47,7 @@ dev = [
|
|
|
47
47
|
"twine>=6.2.0",
|
|
48
48
|
"codecov-cli>=11.2.6",
|
|
49
49
|
"pip-audit>=2.10.0",
|
|
50
|
-
"textual>=8.0.
|
|
50
|
+
"textual>=8.0.2",
|
|
51
51
|
"import-linter>=2.10",
|
|
52
52
|
"rtoml>=0.13.0",
|
|
53
53
|
"httpx>=0.28.1",
|
|
@@ -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 = "1.5.
|
|
43
|
+
version = "1.5.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.
|
|
@@ -247,7 +247,7 @@ def _execute_deploy(
|
|
|
247
247
|
perm_defaults = get_permission_defaults(cli_ctx.config)
|
|
248
248
|
|
|
249
249
|
# CLI --permissions/--no-permissions overrides config enabled setting
|
|
250
|
-
effective_set_permissions = set_permissions if set_permissions is not None else
|
|
250
|
+
effective_set_permissions = set_permissions if set_permissions is not None else perm_defaults.enabled
|
|
251
251
|
|
|
252
252
|
try:
|
|
253
253
|
deployed_paths = cli_ctx.services.deploy_configuration(
|
|
@@ -14,6 +14,7 @@ from lib_layered_config import (
|
|
|
14
14
|
DEFAULT_USER_DIR_MODE,
|
|
15
15
|
DEFAULT_USER_FILE_MODE,
|
|
16
16
|
)
|
|
17
|
+
from pydantic import BaseModel, ConfigDict
|
|
17
18
|
|
|
18
19
|
from bitranox_template_py_cli.domain.enums import DeployTarget
|
|
19
20
|
|
|
@@ -21,6 +22,37 @@ if TYPE_CHECKING:
|
|
|
21
22
|
from lib_layered_config import Config
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
class PermissionDefaults(BaseModel):
|
|
26
|
+
"""Validated, immutable permission defaults for deployment layers.
|
|
27
|
+
|
|
28
|
+
Parsed at the boundary from ``[lib_layered_config.default_permissions]``
|
|
29
|
+
config section. All fields have library-level fallback defaults.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> defaults = PermissionDefaults()
|
|
33
|
+
>>> defaults.user_directory == 0o700
|
|
34
|
+
True
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(frozen=True)
|
|
38
|
+
|
|
39
|
+
app_directory: int = DEFAULT_APP_DIR_MODE
|
|
40
|
+
app_file: int = DEFAULT_APP_FILE_MODE
|
|
41
|
+
host_directory: int = DEFAULT_APP_DIR_MODE
|
|
42
|
+
host_file: int = DEFAULT_APP_FILE_MODE
|
|
43
|
+
user_directory: int = DEFAULT_USER_DIR_MODE
|
|
44
|
+
user_file: int = DEFAULT_USER_FILE_MODE
|
|
45
|
+
enabled: bool = True
|
|
46
|
+
|
|
47
|
+
def dir_mode_for(self, layer: str) -> int:
|
|
48
|
+
"""Return directory mode for the given layer name."""
|
|
49
|
+
return getattr(self, f"{layer}_directory")
|
|
50
|
+
|
|
51
|
+
def file_mode_for(self, layer: str) -> int:
|
|
52
|
+
"""Return file mode for the given layer name."""
|
|
53
|
+
return getattr(self, f"{layer}_file")
|
|
54
|
+
|
|
55
|
+
|
|
24
56
|
def parse_mode(value: int | str, default: int) -> int:
|
|
25
57
|
"""Parse a permission mode value from config.
|
|
26
58
|
|
|
@@ -53,15 +85,20 @@ def parse_mode(value: int | str, default: int) -> int:
|
|
|
53
85
|
return default
|
|
54
86
|
|
|
55
87
|
|
|
56
|
-
def
|
|
57
|
-
"""
|
|
88
|
+
def _parse_mode_from_section(section: dict[str, int | str | bool], key: str, default: int) -> int:
|
|
89
|
+
"""Extract and parse a mode value from a raw config section dict.
|
|
90
|
+
|
|
91
|
+
Used only at the boundary when parsing the raw config dict into
|
|
92
|
+
PermissionDefaults. The raw section comes from lib_layered_config's
|
|
93
|
+
Config.get() which returns untyped dicts.
|
|
94
|
+
"""
|
|
58
95
|
raw = section.get(key, default)
|
|
59
96
|
if isinstance(raw, bool):
|
|
60
97
|
return default
|
|
61
98
|
return parse_mode(raw, default)
|
|
62
99
|
|
|
63
100
|
|
|
64
|
-
def get_permission_defaults(config: Config) ->
|
|
101
|
+
def get_permission_defaults(config: Config) -> PermissionDefaults:
|
|
65
102
|
"""Load permission defaults from [lib_layered_config.default_permissions].
|
|
66
103
|
|
|
67
104
|
Reads configurable permission defaults for each deployment layer.
|
|
@@ -71,35 +108,29 @@ def get_permission_defaults(config: Config) -> dict[str, int | bool]:
|
|
|
71
108
|
config: Configuration object with merged settings.
|
|
72
109
|
|
|
73
110
|
Returns:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
- app_file: File mode for app layer (default 0o644)
|
|
77
|
-
- host_directory: Directory mode for host layer (default 0o755)
|
|
78
|
-
- host_file: File mode for host layer (default 0o644)
|
|
79
|
-
- user_directory: Directory mode for user layer (default 0o700)
|
|
80
|
-
- user_file: File mode for user layer (default 0o600)
|
|
81
|
-
- enabled: Whether permission setting is enabled (default True)
|
|
111
|
+
PermissionDefaults model with typed fields for each layer's
|
|
112
|
+
directory and file modes, plus an enabled flag.
|
|
82
113
|
|
|
83
114
|
Example:
|
|
84
115
|
>>> from lib_layered_config import Config
|
|
85
116
|
>>> config = Config({}, {}) # Empty config
|
|
86
117
|
>>> defaults = get_permission_defaults(config)
|
|
87
|
-
>>> defaults
|
|
118
|
+
>>> defaults.user_directory == 0o700
|
|
88
119
|
True
|
|
89
120
|
"""
|
|
90
121
|
section = config.get("lib_layered_config", {}).get("default_permissions", {})
|
|
91
122
|
# NOTE: lib_layered_config does not define separate HOST_* constants.
|
|
92
123
|
# Host layer shares defaults with app layer (both world-readable: 755/644).
|
|
93
124
|
# This is intentional per CLAUDE.md "Deployment Permissions" documentation.
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
125
|
+
return PermissionDefaults(
|
|
126
|
+
app_directory=_parse_mode_from_section(section, "app_directory", DEFAULT_APP_DIR_MODE),
|
|
127
|
+
app_file=_parse_mode_from_section(section, "app_file", DEFAULT_APP_FILE_MODE),
|
|
128
|
+
host_directory=_parse_mode_from_section(section, "host_directory", DEFAULT_APP_DIR_MODE),
|
|
129
|
+
host_file=_parse_mode_from_section(section, "host_file", DEFAULT_APP_FILE_MODE),
|
|
130
|
+
user_directory=_parse_mode_from_section(section, "user_directory", DEFAULT_USER_DIR_MODE),
|
|
131
|
+
user_file=_parse_mode_from_section(section, "user_file", DEFAULT_USER_FILE_MODE),
|
|
132
|
+
enabled=section.get("enabled", True),
|
|
133
|
+
)
|
|
103
134
|
|
|
104
135
|
|
|
105
136
|
def get_modes_for_target(
|
|
@@ -140,15 +171,14 @@ def get_modes_for_target(
|
|
|
140
171
|
defaults = get_permission_defaults(config)
|
|
141
172
|
layer = target.value # "app", "host", or "user"
|
|
142
173
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
dir_mode: int = dir_mode_override if dir_mode_override is not None else int(defaults[f"{layer}_directory"])
|
|
146
|
-
file_mode: int = file_mode_override if file_mode_override is not None else int(defaults[f"{layer}_file"])
|
|
174
|
+
dir_mode: int = dir_mode_override if dir_mode_override is not None else defaults.dir_mode_for(layer)
|
|
175
|
+
file_mode: int = file_mode_override if file_mode_override is not None else defaults.file_mode_for(layer)
|
|
147
176
|
|
|
148
177
|
return dir_mode, file_mode
|
|
149
178
|
|
|
150
179
|
|
|
151
180
|
__all__ = [
|
|
181
|
+
"PermissionDefaults",
|
|
152
182
|
"get_modes_for_target",
|
|
153
183
|
"get_permission_defaults",
|
|
154
184
|
"parse_mode",
|
|
@@ -4,6 +4,8 @@ Provides email functions that satisfy the same Protocols as production
|
|
|
4
4
|
adapters but perform no SMTP operations.
|
|
5
5
|
|
|
6
6
|
Contents:
|
|
7
|
+
* :class:`CapturedEmail` - Typed record for captured send_email calls.
|
|
8
|
+
* :class:`CapturedNotification` - Typed record for captured send_notification calls.
|
|
7
9
|
* :class:`EmailSpy` - Captures email calls for test assertions.
|
|
8
10
|
* :func:`load_email_config_from_dict_in_memory` - In-memory config loader.
|
|
9
11
|
"""
|
|
@@ -19,9 +21,28 @@ from ..email.sender import EmailConfig
|
|
|
19
21
|
from ..email.validation import validate_recipients
|
|
20
22
|
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class CapturedEmail:
|
|
26
|
+
"""Typed record of a captured send_email call."""
|
|
27
|
+
|
|
28
|
+
config: EmailConfig
|
|
29
|
+
recipients: str | Sequence[str] | None
|
|
30
|
+
subject: str
|
|
31
|
+
body: str
|
|
32
|
+
body_html: str
|
|
33
|
+
from_address: str | None
|
|
34
|
+
attachments: list[Path] | None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class CapturedNotification:
|
|
39
|
+
"""Typed record of a captured send_notification call."""
|
|
40
|
+
|
|
41
|
+
config: EmailConfig
|
|
42
|
+
recipients: str | Sequence[str] | None
|
|
43
|
+
subject: str
|
|
44
|
+
message: str
|
|
45
|
+
from_address: str | None
|
|
25
46
|
|
|
26
47
|
|
|
27
48
|
@dataclass
|
|
@@ -46,8 +67,8 @@ class EmailSpy:
|
|
|
46
67
|
1
|
|
47
68
|
"""
|
|
48
69
|
|
|
49
|
-
sent_emails: list[
|
|
50
|
-
sent_notifications: list[
|
|
70
|
+
sent_emails: list[CapturedEmail] = field(default_factory=lambda: [])
|
|
71
|
+
sent_notifications: list[CapturedNotification] = field(default_factory=lambda: [])
|
|
51
72
|
should_fail: bool = False
|
|
52
73
|
raise_exception: Exception | None = None
|
|
53
74
|
|
|
@@ -88,15 +109,15 @@ class EmailSpy:
|
|
|
88
109
|
"""
|
|
89
110
|
validate_recipients(recipients)
|
|
90
111
|
self.sent_emails.append(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
CapturedEmail(
|
|
113
|
+
config=config,
|
|
114
|
+
recipients=recipients,
|
|
115
|
+
subject=subject,
|
|
116
|
+
body=body,
|
|
117
|
+
body_html=body_html,
|
|
118
|
+
from_address=from_address,
|
|
119
|
+
attachments=list(attachments) if attachments else None,
|
|
120
|
+
)
|
|
100
121
|
)
|
|
101
122
|
if self.raise_exception is not None:
|
|
102
123
|
raise self.raise_exception
|
|
@@ -129,13 +150,13 @@ class EmailSpy:
|
|
|
129
150
|
"""
|
|
130
151
|
validate_recipients(recipients)
|
|
131
152
|
self.sent_notifications.append(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
153
|
+
CapturedNotification(
|
|
154
|
+
config=config,
|
|
155
|
+
recipients=recipients,
|
|
156
|
+
subject=subject,
|
|
157
|
+
message=message,
|
|
158
|
+
from_address=from_address,
|
|
159
|
+
)
|
|
139
160
|
)
|
|
140
161
|
if self.raise_exception is not None:
|
|
141
162
|
raise self.raise_exception
|
|
@@ -151,6 +172,8 @@ def load_email_config_from_dict_in_memory(
|
|
|
151
172
|
|
|
152
173
|
|
|
153
174
|
__all__ = [
|
|
175
|
+
"CapturedEmail",
|
|
176
|
+
"CapturedNotification",
|
|
154
177
|
"EmailSpy",
|
|
155
178
|
"load_email_config_from_dict_in_memory",
|
|
156
179
|
]
|
|
@@ -566,7 +566,7 @@ def email_cli_context(
|
|
|
566
566
|
obj=ctx.factory,
|
|
567
567
|
)
|
|
568
568
|
assert result.exit_code == 0
|
|
569
|
-
assert ctx.spy.sent_notifications[0]
|
|
569
|
+
assert ctx.spy.sent_notifications[0].subject == "Hi"
|
|
570
570
|
"""
|
|
571
571
|
from bitranox_template_py_cli.adapters.memory import load_email_config_from_dict_in_memory
|
|
572
572
|
from bitranox_template_py_cli.adapters.memory.email import EmailSpy as EmailSpyImpl
|
|
@@ -72,8 +72,8 @@ def test_when_send_email_is_invoked_with_valid_config_it_sends(
|
|
|
72
72
|
assert result.exit_code == 0
|
|
73
73
|
assert "Email sent successfully" in result.output
|
|
74
74
|
assert len(ctx.spy.sent_emails) == 1
|
|
75
|
-
assert ctx.spy.sent_emails[0]
|
|
76
|
-
assert ctx.spy.sent_emails[0]
|
|
75
|
+
assert ctx.spy.sent_emails[0].subject == "Test Subject"
|
|
76
|
+
assert ctx.spy.sent_emails[0].recipients == ["recipient@test.com"]
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
@pytest.mark.os_agnostic
|
|
@@ -106,7 +106,7 @@ def test_when_send_email_receives_multiple_recipients_it_accepts_them(
|
|
|
106
106
|
)
|
|
107
107
|
|
|
108
108
|
assert result.exit_code == 0
|
|
109
|
-
assert ctx.spy.sent_emails[0]
|
|
109
|
+
assert ctx.spy.sent_emails[0].recipients == ["user1@test.com", "user2@test.com"]
|
|
110
110
|
|
|
111
111
|
|
|
112
112
|
@pytest.mark.os_agnostic
|
|
@@ -139,7 +139,7 @@ def test_when_send_email_includes_html_body_it_sends(
|
|
|
139
139
|
)
|
|
140
140
|
|
|
141
141
|
assert result.exit_code == 0
|
|
142
|
-
assert ctx.spy.sent_emails[0]
|
|
142
|
+
assert ctx.spy.sent_emails[0].body_html == "<h1>HTML</h1>"
|
|
143
143
|
|
|
144
144
|
|
|
145
145
|
@pytest.mark.os_agnostic
|
|
@@ -178,7 +178,7 @@ def test_when_send_email_has_attachments_it_sends(
|
|
|
178
178
|
)
|
|
179
179
|
|
|
180
180
|
assert result.exit_code == 0
|
|
181
|
-
assert ctx.spy.sent_emails[0]
|
|
181
|
+
assert ctx.spy.sent_emails[0].attachments == [Path(attachment)]
|
|
182
182
|
|
|
183
183
|
|
|
184
184
|
@pytest.mark.os_agnostic
|
|
@@ -269,8 +269,8 @@ def test_when_send_notification_is_invoked_with_valid_config_it_sends(
|
|
|
269
269
|
assert result.exit_code == 0
|
|
270
270
|
assert "Notification sent successfully" in result.output
|
|
271
271
|
assert len(ctx.spy.sent_notifications) == 1
|
|
272
|
-
assert ctx.spy.sent_notifications[0]
|
|
273
|
-
assert ctx.spy.sent_notifications[0]
|
|
272
|
+
assert ctx.spy.sent_notifications[0].subject == "Alert"
|
|
273
|
+
assert ctx.spy.sent_notifications[0].message == "System notification"
|
|
274
274
|
|
|
275
275
|
|
|
276
276
|
@pytest.mark.os_agnostic
|
|
@@ -303,7 +303,7 @@ def test_when_send_notification_receives_multiple_recipients_it_accepts_them(
|
|
|
303
303
|
)
|
|
304
304
|
|
|
305
305
|
assert result.exit_code == 0
|
|
306
|
-
assert ctx.spy.sent_notifications[0]
|
|
306
|
+
assert ctx.spy.sent_notifications[0].recipients == ["admin1@test.com", "admin2@test.com"]
|
|
307
307
|
|
|
308
308
|
|
|
309
309
|
@pytest.mark.os_agnostic
|
|
@@ -371,7 +371,7 @@ def test_when_send_email_receives_smtp_host_override_it_uses_it(
|
|
|
371
371
|
)
|
|
372
372
|
|
|
373
373
|
assert result.exit_code == 0
|
|
374
|
-
assert ctx.spy.sent_emails[0]
|
|
374
|
+
assert ctx.spy.sent_emails[0].config.smtp_hosts == ["smtp.override.com:465"]
|
|
375
375
|
|
|
376
376
|
|
|
377
377
|
@pytest.mark.os_agnostic
|
|
@@ -404,7 +404,7 @@ def test_when_send_email_receives_timeout_override_it_uses_it(
|
|
|
404
404
|
)
|
|
405
405
|
|
|
406
406
|
assert result.exit_code == 0
|
|
407
|
-
assert ctx.spy.sent_emails[0]
|
|
407
|
+
assert ctx.spy.sent_emails[0].config.timeout == 60.0
|
|
408
408
|
|
|
409
409
|
|
|
410
410
|
@pytest.mark.os_agnostic
|
|
@@ -436,7 +436,7 @@ def test_when_send_email_receives_no_use_starttls_override_it_applies_it(
|
|
|
436
436
|
)
|
|
437
437
|
|
|
438
438
|
assert result.exit_code == 0
|
|
439
|
-
assert ctx.spy.sent_emails[0]
|
|
439
|
+
assert ctx.spy.sent_emails[0].config.use_starttls is False
|
|
440
440
|
|
|
441
441
|
|
|
442
442
|
@pytest.mark.os_agnostic
|
|
@@ -471,8 +471,8 @@ def test_when_send_email_receives_credential_overrides_it_uses_them(
|
|
|
471
471
|
)
|
|
472
472
|
|
|
473
473
|
assert result.exit_code == 0
|
|
474
|
-
assert ctx.spy.sent_emails[0]
|
|
475
|
-
assert ctx.spy.sent_emails[0]
|
|
474
|
+
assert ctx.spy.sent_emails[0].config.smtp_username == "myuser"
|
|
475
|
+
assert ctx.spy.sent_emails[0].config.smtp_password == "mypass"
|
|
476
476
|
|
|
477
477
|
|
|
478
478
|
@pytest.mark.os_agnostic
|
|
@@ -505,7 +505,7 @@ def test_when_send_notification_receives_from_override_it_uses_it(
|
|
|
505
505
|
)
|
|
506
506
|
|
|
507
507
|
assert result.exit_code == 0
|
|
508
|
-
assert ctx.spy.sent_notifications[0]
|
|
508
|
+
assert ctx.spy.sent_notifications[0].from_address == "override@test.com"
|
|
509
509
|
|
|
510
510
|
|
|
511
511
|
@pytest.mark.os_agnostic
|
|
@@ -538,7 +538,7 @@ def test_when_send_notification_receives_smtp_host_override_it_uses_it(
|
|
|
538
538
|
)
|
|
539
539
|
|
|
540
540
|
assert result.exit_code == 0
|
|
541
|
-
assert ctx.spy.sent_notifications[0]
|
|
541
|
+
assert ctx.spy.sent_notifications[0].config.smtp_hosts == ["smtp.override.com:465"]
|
|
542
542
|
|
|
543
543
|
|
|
544
544
|
# ======================== Attachment path validation ========================
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""CLI --env-file option: path passing, validation, and value override."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
from click.testing import CliRunner, Result
|
|
12
|
+
from lib_layered_config import Config
|
|
13
|
+
|
|
14
|
+
from bitranox_template_py_cli.adapters import cli as cli_mod
|
|
15
|
+
from bitranox_template_py_cli.composition import AppServices, build_production
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CapturedGetConfigArgs:
|
|
20
|
+
"""Container for captured get_config keyword arguments."""
|
|
21
|
+
|
|
22
|
+
profile: str | None
|
|
23
|
+
dotenv_path: str | None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def inject_config_with_dotenv_capture(
|
|
28
|
+
clear_config_cache: None,
|
|
29
|
+
) -> Callable[[Config, list[CapturedGetConfigArgs]], Callable[[], AppServices]]:
|
|
30
|
+
"""Return a factory that captures dotenv_path passed to get_config."""
|
|
31
|
+
|
|
32
|
+
def _inject(config: Config, captured: list[CapturedGetConfigArgs]) -> Callable[[], AppServices]:
|
|
33
|
+
def _capturing_get_config(
|
|
34
|
+
*,
|
|
35
|
+
profile: str | None = None,
|
|
36
|
+
start_dir: str | None = None,
|
|
37
|
+
dotenv_path: str | None = None,
|
|
38
|
+
) -> Config:
|
|
39
|
+
captured.append(CapturedGetConfigArgs(profile=profile, dotenv_path=dotenv_path))
|
|
40
|
+
return config
|
|
41
|
+
|
|
42
|
+
prod = build_production()
|
|
43
|
+
test_services = AppServices(
|
|
44
|
+
get_config=_capturing_get_config,
|
|
45
|
+
get_default_config_path=prod.get_default_config_path,
|
|
46
|
+
deploy_configuration=prod.deploy_configuration,
|
|
47
|
+
display_config=prod.display_config,
|
|
48
|
+
send_email=prod.send_email,
|
|
49
|
+
send_notification=prod.send_notification,
|
|
50
|
+
load_email_config_from_dict=prod.load_email_config_from_dict,
|
|
51
|
+
init_logging=prod.init_logging,
|
|
52
|
+
)
|
|
53
|
+
return lambda: test_services
|
|
54
|
+
|
|
55
|
+
return _inject
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ======================== --env-file argument passing ========================
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.os_agnostic
|
|
62
|
+
def test_env_file_passes_dotenv_path_to_get_config(
|
|
63
|
+
cli_runner: CliRunner,
|
|
64
|
+
tmp_path: Path,
|
|
65
|
+
config_factory: Callable[[dict[str, Any]], Config],
|
|
66
|
+
inject_config_with_dotenv_capture: Callable[[Config, list[CapturedGetConfigArgs]], Callable[[], AppServices]],
|
|
67
|
+
) -> None:
|
|
68
|
+
"""--env-file passes the file path as dotenv_path to get_config."""
|
|
69
|
+
env_file = tmp_path / ".env"
|
|
70
|
+
env_file.write_text("# empty\n")
|
|
71
|
+
config = config_factory({})
|
|
72
|
+
captured: list[CapturedGetConfigArgs] = []
|
|
73
|
+
|
|
74
|
+
factory = inject_config_with_dotenv_capture(config, captured)
|
|
75
|
+
|
|
76
|
+
result: Result = cli_runner.invoke(cli_mod.cli, ["--env-file", str(env_file), "hello"], obj=factory)
|
|
77
|
+
|
|
78
|
+
assert result.exit_code == 0
|
|
79
|
+
assert len(captured) == 1
|
|
80
|
+
assert captured[0].dotenv_path == str(env_file)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@pytest.mark.os_agnostic
|
|
84
|
+
def test_env_file_not_specified_passes_none_dotenv_path(
|
|
85
|
+
cli_runner: CliRunner,
|
|
86
|
+
config_factory: Callable[[dict[str, Any]], Config],
|
|
87
|
+
inject_config_with_dotenv_capture: Callable[[Config, list[CapturedGetConfigArgs]], Callable[[], AppServices]],
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Without --env-file, dotenv_path is None (upward search used)."""
|
|
90
|
+
config = config_factory({})
|
|
91
|
+
captured: list[CapturedGetConfigArgs] = []
|
|
92
|
+
|
|
93
|
+
factory = inject_config_with_dotenv_capture(config, captured)
|
|
94
|
+
|
|
95
|
+
result: Result = cli_runner.invoke(cli_mod.cli, ["hello"], obj=factory)
|
|
96
|
+
|
|
97
|
+
assert result.exit_code == 0
|
|
98
|
+
assert len(captured) == 1
|
|
99
|
+
assert captured[0].dotenv_path is None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@pytest.mark.os_agnostic
|
|
103
|
+
def test_env_file_combined_with_profile(
|
|
104
|
+
cli_runner: CliRunner,
|
|
105
|
+
tmp_path: Path,
|
|
106
|
+
config_factory: Callable[[dict[str, Any]], Config],
|
|
107
|
+
inject_config_with_dotenv_capture: Callable[[Config, list[CapturedGetConfigArgs]], Callable[[], AppServices]],
|
|
108
|
+
) -> None:
|
|
109
|
+
"""--env-file and --profile can be used together."""
|
|
110
|
+
env_file = tmp_path / ".env"
|
|
111
|
+
env_file.write_text("# empty\n")
|
|
112
|
+
config = config_factory({})
|
|
113
|
+
captured: list[CapturedGetConfigArgs] = []
|
|
114
|
+
|
|
115
|
+
factory = inject_config_with_dotenv_capture(config, captured)
|
|
116
|
+
|
|
117
|
+
result: Result = cli_runner.invoke(
|
|
118
|
+
cli_mod.cli, ["--env-file", str(env_file), "--profile", "staging", "hello"], obj=factory
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
assert result.exit_code == 0
|
|
122
|
+
assert len(captured) == 1
|
|
123
|
+
assert captured[0].dotenv_path == str(env_file)
|
|
124
|
+
assert captured[0].profile == "staging"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ======================== --env-file validation ========================
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@pytest.mark.os_agnostic
|
|
131
|
+
def test_env_file_nonexistent_path_rejected(
|
|
132
|
+
cli_runner: CliRunner,
|
|
133
|
+
production_factory: Callable[[], AppServices],
|
|
134
|
+
) -> None:
|
|
135
|
+
"""--env-file with nonexistent path is rejected by Click."""
|
|
136
|
+
result: Result = cli_runner.invoke(
|
|
137
|
+
cli_mod.cli, ["--env-file", "/tmp/nonexistent_env_file_xyz.env", "hello"], obj=production_factory
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
assert result.exit_code != 0
|
|
141
|
+
assert "does not exist" in result.output
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@pytest.mark.os_agnostic
|
|
145
|
+
def test_env_file_directory_rejected(
|
|
146
|
+
cli_runner: CliRunner,
|
|
147
|
+
tmp_path: Path,
|
|
148
|
+
production_factory: Callable[[], AppServices],
|
|
149
|
+
) -> None:
|
|
150
|
+
"""--env-file with a directory path is rejected by Click."""
|
|
151
|
+
result: Result = cli_runner.invoke(cli_mod.cli, ["--env-file", str(tmp_path), "hello"], obj=production_factory)
|
|
152
|
+
|
|
153
|
+
assert result.exit_code != 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ======================== --env-file end-to-end integration ========================
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@pytest.mark.os_agnostic
|
|
160
|
+
def test_env_file_values_appear_in_config(
|
|
161
|
+
cli_runner: CliRunner,
|
|
162
|
+
tmp_path: Path,
|
|
163
|
+
clear_config_cache: None,
|
|
164
|
+
production_factory: Callable[[], AppServices],
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Values from an --env-file are visible in the merged config output."""
|
|
167
|
+
env_file = tmp_path / ".env"
|
|
168
|
+
env_file.write_text("LIB_LOG_RICH__ENVIRONMENT=from-env-file\n")
|
|
169
|
+
|
|
170
|
+
result: Result = cli_runner.invoke(
|
|
171
|
+
cli_mod.cli,
|
|
172
|
+
["--env-file", str(env_file), "config", "--format", "json", "--section", "lib_log_rich"],
|
|
173
|
+
obj=production_factory,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
assert result.exit_code == 0
|
|
177
|
+
assert "from-env-file" in result.stdout
|
{bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_deploy_permissions.py
RENAMED
|
@@ -36,13 +36,13 @@ def test_get_permission_defaults_returns_library_defaults_when_not_configured(
|
|
|
36
36
|
|
|
37
37
|
defaults = get_permission_defaults(config)
|
|
38
38
|
|
|
39
|
-
assert defaults
|
|
40
|
-
assert defaults
|
|
41
|
-
assert defaults
|
|
42
|
-
assert defaults
|
|
43
|
-
assert defaults
|
|
44
|
-
assert defaults
|
|
45
|
-
assert defaults
|
|
39
|
+
assert defaults.app_directory == 0o755
|
|
40
|
+
assert defaults.app_file == 0o644
|
|
41
|
+
assert defaults.host_directory == 0o755
|
|
42
|
+
assert defaults.host_file == 0o644
|
|
43
|
+
assert defaults.user_directory == 0o700
|
|
44
|
+
assert defaults.user_file == 0o600
|
|
45
|
+
assert defaults.enabled is True
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
@pytest.mark.os_agnostic
|
|
@@ -64,11 +64,11 @@ def test_get_permission_defaults_reads_from_config(
|
|
|
64
64
|
|
|
65
65
|
defaults = get_permission_defaults(config)
|
|
66
66
|
|
|
67
|
-
assert defaults
|
|
68
|
-
assert defaults
|
|
69
|
-
assert defaults
|
|
67
|
+
assert defaults.user_directory == 0o750
|
|
68
|
+
assert defaults.user_file == 0o640
|
|
69
|
+
assert defaults.enabled is False
|
|
70
70
|
# Non-overridden values use library defaults
|
|
71
|
-
assert defaults
|
|
71
|
+
assert defaults.app_directory == 0o755
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
@pytest.mark.os_agnostic
|
|
@@ -185,8 +185,8 @@ def test_get_permission_defaults_accepts_octal_strings(
|
|
|
185
185
|
|
|
186
186
|
defaults = get_permission_defaults(config)
|
|
187
187
|
|
|
188
|
-
assert defaults
|
|
189
|
-
assert defaults
|
|
188
|
+
assert defaults.user_directory == 0o750
|
|
189
|
+
assert defaults.user_file == 0o640
|
|
190
190
|
|
|
191
191
|
|
|
192
192
|
# ======================== CLI Option Parsing Tests ========================
|
{bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/.devcontainer/devcontainer.json
RENAMED
|
File without changes
|
{bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/.devcontainer/settings.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.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
|
{bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.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
|
{bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_cache_effectiveness.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_cli_exit_codes.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_cli_overrides.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_cli_validation.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_config_overrides.py
RENAMED
|
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-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_metadata_sync.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_module_entry.py
RENAMED
|
File without changes
|
|
File without changes
|
{bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_property_email.py
RENAMED
|
File without changes
|
{bitranox_template_py_cli-1.5.0 → bitranox_template_py_cli-1.5.2}/tests/test_property_overrides.py
RENAMED
|
File without changes
|