base-cli 0.4.1__tar.gz → 0.4.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.
- {base_cli-0.4.1 → base_cli-0.4.2}/CHANGELOG.md +28 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/PKG-INFO +2 -2
- {base_cli-0.4.1 → base_cli-0.4.2}/README.md +1 -1
- base_cli-0.4.2/VERSION +1 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/index.md +2 -1
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/_lifecycle.py +0 -2
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/app.py +1 -5
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/command_protocol.py +7 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/context.py +4 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/history.py +14 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/logging.py +6 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli.egg-info/PKG-INFO +2 -2
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_app_run_metadata.py +2 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_invocation_parity.py +58 -1
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_public_api.py +11 -0
- base_cli-0.4.1/VERSION +0 -1
- {base_cli-0.4.1 → base_cli-0.4.2}/.github/ISSUE_TEMPLATE/support.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/.github/base-project.yml +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/.github/pull_request_template.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/.github/workflows/compatibility.yml +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/.github/workflows/docs.yml +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/.github/workflows/examples.yml +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/.github/workflows/issue-branch-policy.yml +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/.github/workflows/package.yml +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/.github/workflows/project-intake.yml +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/.github/workflows/tests.yml +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/CONTRIBUTING.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/LICENSE +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/MANIFEST.in +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/SECURITY.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/base_manifest.yaml +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/compatibility/README.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/atlas_click/README.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/atlas_click/pyproject.toml +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/atlas_click/src/atlas_click/__init__.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/atlas_click/src/atlas_click/cli.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/atlas_click/tests/test_consumer.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/beacon_typer/README.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/beacon_typer/pyproject.toml +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/beacon_typer/src/beacon_typer/__init__.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/beacon_typer/src/beacon_typer/cli.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/beacon_typer/tests/test_consumer.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/cinder_automation/README.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/cinder_automation/pyproject.toml +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/cinder_automation/src/cinder_automation/__init__.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/cinder_automation/src/cinder_automation/cli.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/cinder_automation/tests/test_consumer.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/manifest.json +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/adopter-readiness.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/api-stability.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/cache-ownership-and-layout.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/consumer-profiles.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/extensions.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/integrations.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/json-contracts.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/local-config.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/migrations.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/output-contracts.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/performance.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/platform-support.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/releasing.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/security-review.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/security-threat-model.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/typer-adapter.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/docs/user-config-typing.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/README.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/automation_observability_app/README.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/automation_observability_app/pyproject.toml +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/automation_observability_app/src/automation_observability_app/__init__.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/automation_observability_app/src/automation_observability_app/cli.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/automation_observability_app/tests/test_cli.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/minimal_cli/README.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/minimal_cli/pyproject.toml +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/minimal_cli/src/minimal_cli/__init__.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/minimal_cli/src/minimal_cli/cli.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/minimal_cli/tests/test_cli.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/nested_click_app/README.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/nested_click_app/pyproject.toml +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/nested_click_app/src/nested_click_app/__init__.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/nested_click_app/src/nested_click_app/cli.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/nested_click_app/src/nested_click_app/plugin.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/nested_click_app/tests/test_cli.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/typed_consumer.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/typer_app/README.md +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/typer_app/pyproject.toml +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/typer_app/src/typer_app/__init__.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/typer_app/src/typer_app/cli.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/typer_app/tests/test_cli.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/examples/typer_consumer.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/__init__.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/_cleanup.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/_click_compat.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/_dependencies.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/_private_files.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/_runtime.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/attachment.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/command_filters.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/config.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/deprecations.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/errors.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/exit_codes.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/experimental.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/extensions.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/inspection.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/integrations.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/json_contracts.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/lifecycle_options.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/output.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/paths.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/profile.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/py.typed +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/redaction.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/runtime.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/testing.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli/typer.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli.egg-info/SOURCES.txt +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli.egg-info/dependency_links.txt +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli.egg-info/requires.txt +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/lib/python/base_cli.egg-info/top_level.txt +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/pyproject.toml +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/scripts/benchmark_runtime.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/scripts/validate_consumers.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/scripts/validate_docs.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/scripts/validate_examples.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/scripts/validate_installed_package.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/scripts/validate_package_artifact.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/setup.cfg +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/conftest.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_adversarial_regressions.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_api_stability.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_app_dry_run.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_app_lifecycle.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_app_log_retention.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_app_quiet.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_app_registration.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_app_run.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_app_runtime_boundary.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_app_runtime_errors.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_app_security_boundaries.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_app_startup_transaction.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_app_subcommands.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_batteries_included_config.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_benchmark_runtime.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_cleanup_security.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_click_tree_attachment.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_command_filters.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_command_protocol.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_context_workspace.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_explicit_config_validation.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_extensions.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_generic_core.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_history.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_inspection.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_integrations.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_json_contracts.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_lifecycle_options.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_logging.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_output.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_paths.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_profile.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_redaction_security.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_run_bundle_retention.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_security_documentation.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_testing.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_typed_contracts.py +0 -0
- {base_cli-0.4.1 → base_cli-0.4.2}/tests/test_typer_adapter.py +0 -0
|
@@ -11,6 +11,34 @@ and versions are tracked in the repo-root `VERSION` file.
|
|
|
11
11
|
|
|
12
12
|
- Continue compatibility hardening and adoption work for the next release.
|
|
13
13
|
|
|
14
|
+
## [0.4.2] - 2026-08-08
|
|
15
|
+
|
|
16
|
+
This is a compatible pre-1.0 patch release. It contains correctness fixes,
|
|
17
|
+
security hardening, documentation improvements, and adoption polish after
|
|
18
|
+
0.4.1; it does not introduce a new API or JSON-contract compatibility
|
|
19
|
+
boundary.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Apply the profile-resolved display command to ambient production invocation
|
|
24
|
+
metadata so logs and history match the documented wrapper-label contract.
|
|
25
|
+
|
|
26
|
+
### Security
|
|
27
|
+
|
|
28
|
+
- Compact the launcher path in retained invocation logs so diagnostic bundles
|
|
29
|
+
do not disclose the local user's home directory or username.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Fix the published documentation link to the reference applications.
|
|
34
|
+
- Classify a command that returns exit code `130` as `nonzero_return`; the
|
|
35
|
+
`interrupted` outcome remains reserved for actual interrupt exceptions.
|
|
36
|
+
|
|
37
|
+
### Documentation
|
|
38
|
+
|
|
39
|
+
- Document the exported history, command-protocol, and context APIs and enforce
|
|
40
|
+
public-symbol docstrings in the API tests.
|
|
41
|
+
|
|
14
42
|
## [0.4.1] - 2026-08-07
|
|
15
43
|
|
|
16
44
|
This is a compatible pre-1.0 patch release. It contains adoption polish,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: base-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: A small, consumer-neutral Python CLI framework
|
|
5
5
|
Author: Base Foundry
|
|
6
6
|
License: Apache-2.0
|
|
@@ -58,7 +58,7 @@ Requires-Dist: mkdocs-material<10,>=9.5; extra == "docs"
|
|
|
58
58
|
|
|
59
59
|
| Version | License | Install | Release notes |
|
|
60
60
|
| --- | --- | --- | --- |
|
|
61
|
-
| `0.4.
|
|
61
|
+
| `0.4.2` | [Apache-2.0](LICENSE) | `python -m pip install base-cli` | [v0.4.2](https://github.com/basefoundry/base-cli/releases/tag/v0.4.2) |
|
|
62
62
|
|
|
63
63
|
`base-cli` is the PyPI distribution; import it in Python as `base_cli`.
|
|
64
64
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
| Version | License | Install | Release notes |
|
|
9
9
|
| --- | --- | --- | --- |
|
|
10
|
-
| `0.4.
|
|
10
|
+
| `0.4.2` | [Apache-2.0](LICENSE) | `python -m pip install base-cli` | [v0.4.2](https://github.com/basefoundry/base-cli/releases/tag/v0.4.2) |
|
|
11
11
|
|
|
12
12
|
`base-cli` is the PyPI distribution; import it in Python as `base_cli`.
|
|
13
13
|
|
base_cli-0.4.2/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.4.2
|
|
@@ -71,6 +71,7 @@ Continue with the framework guides:
|
|
|
71
71
|
|
|
72
72
|
- [API overview](api-stability.md) for the supported public surface and
|
|
73
73
|
compatibility policy.
|
|
74
|
-
- [Reference applications](
|
|
74
|
+
- [Reference applications](https://github.com/basefoundry/base-cli/tree/main/examples)
|
|
75
|
+
for complete consumer patterns.
|
|
75
76
|
- [Installation and packaging](adopter-readiness.md) for dependency extras,
|
|
76
77
|
distribution checks, and release guidance.
|
|
@@ -116,8 +116,6 @@ def outcome_from_exit_code(exit_code: int) -> InvocationOutcome:
|
|
|
116
116
|
return InvocationOutcome("success", "ok", exit_code)
|
|
117
117
|
if exit_code == ExitCode.USAGE_ERROR:
|
|
118
118
|
return InvocationOutcome("usage_error", "error", exit_code)
|
|
119
|
-
if exit_code == ExitCode.INTERRUPTED:
|
|
120
|
-
return InvocationOutcome("interrupted", "error", exit_code)
|
|
121
119
|
return InvocationOutcome("nonzero_return", "error", exit_code)
|
|
122
120
|
|
|
123
121
|
|
|
@@ -2740,7 +2740,6 @@ def run_app(
|
|
|
2740
2740
|
print(f"ERROR: {exc}", file=sys.stderr)
|
|
2741
2741
|
return ExitCode.FAILURE
|
|
2742
2742
|
|
|
2743
|
-
explicit_argv = argv is not None
|
|
2744
2743
|
args = list(sys.argv[1:] if argv is None else argv)
|
|
2745
2744
|
leading_debug, leading_quiet = _leading_output_flags(
|
|
2746
2745
|
args,
|
|
@@ -2760,7 +2759,7 @@ def run_app(
|
|
|
2760
2759
|
try:
|
|
2761
2760
|
try:
|
|
2762
2761
|
display_command = app.profile.display_command()
|
|
2763
|
-
invocation_argv = _effective_invocation_argv(app, args,
|
|
2762
|
+
invocation_argv = _effective_invocation_argv(app, args, display_command)
|
|
2764
2763
|
command = app.click_command
|
|
2765
2764
|
click = dialect_for_command(command)
|
|
2766
2765
|
invocation_token = _INVOCATION_ARGV.set(invocation_argv)
|
|
@@ -3038,11 +3037,8 @@ def _leading_output_flags(
|
|
|
3038
3037
|
def _effective_invocation_argv(
|
|
3039
3038
|
app: App,
|
|
3040
3039
|
args: list[str],
|
|
3041
|
-
explicit_argv: bool,
|
|
3042
3040
|
display_command: str | None,
|
|
3043
3041
|
) -> list[str]:
|
|
3044
|
-
if not explicit_argv:
|
|
3045
|
-
return list(sys.argv)
|
|
3046
3042
|
return [display_command or app.name, *args]
|
|
3047
3043
|
|
|
3048
3044
|
|
|
@@ -26,11 +26,15 @@ MAX_RECORD_COUNT = 1_000_000
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class CommandProtocolError(ValueError):
|
|
29
|
+
"""Raised when a command-protocol schema or payload violates its contract."""
|
|
30
|
+
|
|
29
31
|
pass
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
@dataclass(frozen=True)
|
|
33
35
|
class FieldSpec:
|
|
36
|
+
"""Describe the wire type and nullability of one command-record field."""
|
|
37
|
+
|
|
34
38
|
value_type: str
|
|
35
39
|
nullable: bool = False
|
|
36
40
|
|
|
@@ -138,6 +142,7 @@ def dumps_record(
|
|
|
138
142
|
protocol_header: str = PROTOCOL_HEADER,
|
|
139
143
|
registry: CommandSchemaRegistry | None = None,
|
|
140
144
|
) -> str:
|
|
145
|
+
"""Serialize one typed command record using the protocol framing."""
|
|
141
146
|
return dumps_records(
|
|
142
147
|
record_type,
|
|
143
148
|
(record,),
|
|
@@ -153,6 +158,7 @@ def dumps_records(
|
|
|
153
158
|
protocol_header: str = PROTOCOL_HEADER,
|
|
154
159
|
registry: CommandSchemaRegistry | None = None,
|
|
155
160
|
) -> str:
|
|
161
|
+
"""Serialize a sequence of typed command records using the protocol framing."""
|
|
156
162
|
active_registry = registry or DEFAULT_SCHEMA_REGISTRY
|
|
157
163
|
schema = active_registry.schema(record_type)
|
|
158
164
|
if len(records) > MAX_RECORD_COUNT:
|
|
@@ -180,6 +186,7 @@ def loads_records(
|
|
|
180
186
|
protocol_header: str = PROTOCOL_HEADER,
|
|
181
187
|
registry: CommandSchemaRegistry | None = None,
|
|
182
188
|
) -> tuple[str, tuple[dict[str, RecordValue], ...]]:
|
|
189
|
+
"""Validate and decode protocol-framed command records."""
|
|
183
190
|
active_registry = registry or DEFAULT_SCHEMA_REGISTRY
|
|
184
191
|
# The wire framing is LF-delimited. `str.splitlines()` also accepts CR,
|
|
185
192
|
# vertical tab, form feed, and Unicode separators, which would make the
|
|
@@ -167,10 +167,12 @@ class Context(Generic[ConfigT, ApplicationStateT, ServicesT]):
|
|
|
167
167
|
def set_current_context(
|
|
168
168
|
context: Context[Any, Any, Any] | None,
|
|
169
169
|
) -> contextvars.Token[Context[Any, Any, Any] | None]:
|
|
170
|
+
"""Set the active context and return a token that can restore the prior value."""
|
|
170
171
|
return _current_context.set(context)
|
|
171
172
|
|
|
172
173
|
|
|
173
174
|
def reset_current_context(token: contextvars.Token[Context[Any, Any, Any] | None]) -> None:
|
|
175
|
+
"""Restore the context associated with a token, recovering safely if needed."""
|
|
174
176
|
try:
|
|
175
177
|
_current_context.reset(token)
|
|
176
178
|
except BaseException: # pylint: disable=broad-exception-caught
|
|
@@ -178,11 +180,13 @@ def reset_current_context(token: contextvars.Token[Context[Any, Any, Any] | None
|
|
|
178
180
|
|
|
179
181
|
|
|
180
182
|
def recover_current_context(token: contextvars.Token[Context[Any, Any, Any] | None]) -> None:
|
|
183
|
+
"""Best-effort restoration used when normal context-token reset fails."""
|
|
181
184
|
previous = token.old_value
|
|
182
185
|
_current_context.set(None if previous is contextvars.Token.MISSING else previous)
|
|
183
186
|
|
|
184
187
|
|
|
185
188
|
def get_current_context() -> Context[Any, Any, Any]:
|
|
189
|
+
"""Return the active command context or raise when called outside a command."""
|
|
186
190
|
context = _current_context.get()
|
|
187
191
|
if context is None:
|
|
188
192
|
raise RuntimeError("base_cli context is not active. Run inside a base_cli.App command.")
|
|
@@ -52,6 +52,7 @@ HISTORY_SCOPE_INTERNAL = "internal"
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def utc_now() -> datetime:
|
|
55
|
+
"""Return the current time as a timezone-aware UTC datetime."""
|
|
55
56
|
return datetime.now(timezone.utc)
|
|
56
57
|
|
|
57
58
|
|
|
@@ -62,6 +63,7 @@ def build_finished_record(
|
|
|
62
63
|
started_at: datetime,
|
|
63
64
|
exit_code: int,
|
|
64
65
|
) -> dict[str, Any]:
|
|
66
|
+
"""Build a redacted finished-invocation record from an active context."""
|
|
65
67
|
ended_at = utc_now()
|
|
66
68
|
safe_argv = redact_history_argv(argv, sensitive_options)
|
|
67
69
|
record: dict[str, Any] = {
|
|
@@ -248,20 +250,24 @@ def write_all(fd: int, data: bytes) -> None:
|
|
|
248
250
|
|
|
249
251
|
|
|
250
252
|
def format_timestamp(value: datetime) -> str:
|
|
253
|
+
"""Format a datetime as a second-precision UTC ISO-8601 timestamp."""
|
|
251
254
|
normalized = value.astimezone(timezone.utc)
|
|
252
255
|
return normalized.isoformat(timespec="seconds").replace("+00:00", "Z")
|
|
253
256
|
|
|
254
257
|
|
|
255
258
|
def duration_ms(started_at: datetime, ended_at: datetime) -> int:
|
|
259
|
+
"""Return the non-negative elapsed duration between two datetimes in milliseconds."""
|
|
256
260
|
return max(0, round((ended_at - started_at).total_seconds() * 1000))
|
|
257
261
|
|
|
258
262
|
|
|
259
263
|
def display_command(cli_name: str, argv: list[str]) -> str:
|
|
264
|
+
"""Return the stable, human-readable command label used in history."""
|
|
260
265
|
del argv
|
|
261
266
|
return cli_name.replace("_", "-")
|
|
262
267
|
|
|
263
268
|
|
|
264
269
|
def parse_positive_int(option: str, value: str) -> int:
|
|
270
|
+
"""Parse a decimal option value and require it to be greater than zero."""
|
|
265
271
|
if not value.isdecimal():
|
|
266
272
|
raise ValueError(f"Option '{option}' must be a positive integer.")
|
|
267
273
|
amount = int(value)
|
|
@@ -271,6 +277,7 @@ def parse_positive_int(option: str, value: str) -> int:
|
|
|
271
277
|
|
|
272
278
|
|
|
273
279
|
def parse_finished_history_record_line(line: str) -> dict[str, Any] | None:
|
|
280
|
+
"""Decode a line when it is a valid finished record for this schema version."""
|
|
274
281
|
try:
|
|
275
282
|
payload = json.loads(line)
|
|
276
283
|
except json.JSONDecodeError:
|
|
@@ -283,10 +290,12 @@ def parse_finished_history_record_line(line: str) -> dict[str, Any] | None:
|
|
|
283
290
|
|
|
284
291
|
|
|
285
292
|
def optional_string(value: Any) -> str | None:
|
|
293
|
+
"""Return a non-empty string value, or ``None`` for other values."""
|
|
286
294
|
return value if isinstance(value, str) and value else None
|
|
287
295
|
|
|
288
296
|
|
|
289
297
|
def optional_int(value: Any) -> int | None:
|
|
298
|
+
"""Return an integer value, or ``None`` when the value has another type."""
|
|
290
299
|
return value if isinstance(value, int) else None
|
|
291
300
|
|
|
292
301
|
|
|
@@ -304,10 +313,12 @@ def current_shell() -> str | None:
|
|
|
304
313
|
|
|
305
314
|
|
|
306
315
|
def redact_history_argv(argv: list[str], sensitive_options: set[str]) -> list[str]:
|
|
316
|
+
"""Redact sensitive arguments and compact home paths for history storage."""
|
|
307
317
|
return [redact_history_text(arg) for arg in redact_argv(argv, sensitive_options)]
|
|
308
318
|
|
|
309
319
|
|
|
310
320
|
def redact_history_text(value: str) -> str:
|
|
321
|
+
"""Redact sensitive option values and compact home paths in text."""
|
|
311
322
|
key, separator, _value = value.partition("=")
|
|
312
323
|
if separator and is_secret_key(option_name_to_parameter(key)):
|
|
313
324
|
return f"{key}={REDACTED}"
|
|
@@ -315,16 +326,19 @@ def redact_history_text(value: str) -> str:
|
|
|
315
326
|
|
|
316
327
|
|
|
317
328
|
def compact_optional_path(path: Path | None, *, home: Path | str | None = None) -> str | None:
|
|
329
|
+
"""Return a compact path string, preserving ``None`` for absent paths."""
|
|
318
330
|
if path is None:
|
|
319
331
|
return None
|
|
320
332
|
return compact_path(path, home=home)
|
|
321
333
|
|
|
322
334
|
|
|
323
335
|
def compact_path(path: Path, *, home: Path | str | None = None) -> str:
|
|
336
|
+
"""Resolve a path and replace the user's home directory with ``~``."""
|
|
324
337
|
return compact_home_text(str(path.expanduser().resolve(strict=False)), home=home)
|
|
325
338
|
|
|
326
339
|
|
|
327
340
|
def compact_home_text(value: str, *, home: Path | str | None = None) -> str:
|
|
341
|
+
"""Replace a home-directory prefix in text with the portable ``~`` marker."""
|
|
328
342
|
home_text = str(home) if home is not None else str(Path.home().expanduser().resolve(strict=False))
|
|
329
343
|
normalized_value = value.replace("\\", "/")
|
|
330
344
|
normalized_home = home_text.replace("\\", "/").rstrip("/")
|
|
@@ -21,6 +21,7 @@ except ImportError: # pragma: no cover - msvcrt is unavailable outside Windows.
|
|
|
21
21
|
|
|
22
22
|
from ._private_files import restrict_file
|
|
23
23
|
from .context import get_current_context
|
|
24
|
+
from .history import compact_home_text
|
|
24
25
|
from .json_contracts import JsonLogFormatter
|
|
25
26
|
from .paths import current_working_dir
|
|
26
27
|
from .redaction import redact_argv
|
|
@@ -261,6 +262,11 @@ def log_invocation(
|
|
|
261
262
|
sensitive_options: set[str] | None,
|
|
262
263
|
) -> None:
|
|
263
264
|
safe_argv = list(argv) if sensitive_options is None else redact_argv(argv, sensitive_options)
|
|
265
|
+
if safe_argv:
|
|
266
|
+
# Invocation logs are retained in diagnostic bundles. Keep the
|
|
267
|
+
# launcher path useful while avoiding disclosure of the local user's
|
|
268
|
+
# home directory and username.
|
|
269
|
+
safe_argv[0] = compact_home_text(safe_argv[0])
|
|
264
270
|
logger.debug("argv=%s", safe_argv)
|
|
265
271
|
logger.debug("platform=%s %s", platform.system(), platform.machine())
|
|
266
272
|
logger.debug("python=%s", sys.version.replace("\n", " "))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: base-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: A small, consumer-neutral Python CLI framework
|
|
5
5
|
Author: Base Foundry
|
|
6
6
|
License: Apache-2.0
|
|
@@ -58,7 +58,7 @@ Requires-Dist: mkdocs-material<10,>=9.5; extra == "docs"
|
|
|
58
58
|
|
|
59
59
|
| Version | License | Install | Release notes |
|
|
60
60
|
| --- | --- | --- | --- |
|
|
61
|
-
| `0.4.
|
|
61
|
+
| `0.4.2` | [Apache-2.0](LICENSE) | `python -m pip install base-cli` | [v0.4.2](https://github.com/basefoundry/base-cli/releases/tag/v0.4.2) |
|
|
62
62
|
|
|
63
63
|
`base-cli` is the PyPI distribution; import it in Python as `base_cli`.
|
|
64
64
|
|
|
@@ -84,6 +84,7 @@ class AppRunMetadataTests(unittest.TestCase):
|
|
|
84
84
|
("zero", 0, 0, "ok", "success"),
|
|
85
85
|
("usage", 2, 2, "error", "usage_error"),
|
|
86
86
|
("nonzero", 7, 7, "error", "nonzero_return"),
|
|
87
|
+
("returned-interrupted", 130, 130, "error", "nonzero_return"),
|
|
87
88
|
)
|
|
88
89
|
for name, returned, expected_code, expected_status, expected_outcome in cases:
|
|
89
90
|
with self.subTest(name=name), tempfile.TemporaryDirectory() as tmpdir:
|
|
@@ -237,6 +238,7 @@ class AppRunMetadataTests(unittest.TestCase):
|
|
|
237
238
|
|
|
238
239
|
cases = (
|
|
239
240
|
("click", click.exceptions.Exit(9), 9, "error", "nonzero_return", ""),
|
|
241
|
+
("click-interrupted-code", click.exceptions.Exit(130), 130, "error", "nonzero_return", ""),
|
|
240
242
|
("system-none", SystemExit(None), 0, "ok", "system_exit", ""),
|
|
241
243
|
("system-success", SystemExit(0), 0, "ok", "system_exit", ""),
|
|
242
244
|
("system-failure", SystemExit(5), 5, "error", "system_exit", ""),
|
|
@@ -13,6 +13,7 @@ from pathlib import Path
|
|
|
13
13
|
from unittest import mock
|
|
14
14
|
|
|
15
15
|
import base_cli
|
|
16
|
+
import base_cli.app as app_module
|
|
16
17
|
from base_cli.testing import invoke
|
|
17
18
|
|
|
18
19
|
|
|
@@ -125,7 +126,7 @@ def _load_run_artifacts(home: Path) -> tuple[dict[str, object], list[str], str]:
|
|
|
125
126
|
return metadata, logged_argv, log_text
|
|
126
127
|
|
|
127
128
|
|
|
128
|
-
def _observe_production(app: base_cli.App, args: list[str], home: Path) -> _Observation:
|
|
129
|
+
def _observe_production(app: base_cli.App, args: list[str] | None, home: Path) -> _Observation:
|
|
129
130
|
stdout = io.StringIO()
|
|
130
131
|
stderr = io.StringIO()
|
|
131
132
|
with mock.patch.dict(os.environ, _isolated_environment(home)), redirect_stdout(stdout), redirect_stderr(stderr):
|
|
@@ -171,6 +172,62 @@ def _stable_stderr(observation: _Observation) -> str:
|
|
|
171
172
|
|
|
172
173
|
@unittest.skipUnless(importlib.util.find_spec("click"), "Click is not installed")
|
|
173
174
|
class InvocationParityTests(unittest.TestCase):
|
|
175
|
+
def test_ambient_invocation_uses_display_command_in_logs_and_history(self) -> None:
|
|
176
|
+
observed_history_argv: list[str] = []
|
|
177
|
+
|
|
178
|
+
def history_writer(
|
|
179
|
+
_ctx: base_cli.Context,
|
|
180
|
+
argv: list[str],
|
|
181
|
+
_sensitive: set[str],
|
|
182
|
+
_started: object,
|
|
183
|
+
_exit_code: int,
|
|
184
|
+
) -> None:
|
|
185
|
+
observed_history_argv.extend(argv)
|
|
186
|
+
|
|
187
|
+
profile = replace(_profile(), history_writer=history_writer)
|
|
188
|
+
app = base_cli.App(name="internal-tool", profile=profile)
|
|
189
|
+
|
|
190
|
+
@app.command()
|
|
191
|
+
@base_cli.option("--name", required=True)
|
|
192
|
+
def main(ctx: base_cli.Context, name: str) -> None:
|
|
193
|
+
del ctx
|
|
194
|
+
print(f"hello {name}")
|
|
195
|
+
|
|
196
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
197
|
+
home = (Path(tmpdir) / "home").resolve()
|
|
198
|
+
home.mkdir()
|
|
199
|
+
launcher = home / ".venv" / "bin" / "internal-tool"
|
|
200
|
+
with mock.patch.object(app_module.sys, "argv", [str(launcher), "--name=Ada"]):
|
|
201
|
+
observation = _observe_production(app, None, home)
|
|
202
|
+
|
|
203
|
+
expected_argv = ["parity-tool", "--name=Ada"]
|
|
204
|
+
self.assertEqual(observation.logged_argv, expected_argv)
|
|
205
|
+
self.assertEqual(observed_history_argv, expected_argv)
|
|
206
|
+
self.assertNotIn(str(home), observation.log_text)
|
|
207
|
+
|
|
208
|
+
def test_production_compacts_launcher_home_path_in_retained_log(self) -> None:
|
|
209
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
210
|
+
home = (Path(tmpdir) / "home").resolve()
|
|
211
|
+
home.mkdir()
|
|
212
|
+
launcher = home / ".venv" / "bin" / "parity-tool"
|
|
213
|
+
profile = replace(
|
|
214
|
+
base_cli.CliProfile.generic(),
|
|
215
|
+
display_command=lambda: str(launcher),
|
|
216
|
+
)
|
|
217
|
+
app = base_cli.App(name="parity-tool", profile=profile)
|
|
218
|
+
|
|
219
|
+
@app.command()
|
|
220
|
+
@base_cli.option("--name", required=True)
|
|
221
|
+
def main(ctx: base_cli.Context, name: str) -> None:
|
|
222
|
+
del ctx
|
|
223
|
+
print(f"hello {name}")
|
|
224
|
+
|
|
225
|
+
with mock.patch.object(app_module.sys, "argv", [str(launcher), "--name=Ada"]):
|
|
226
|
+
observation = _observe_production(app, None, home)
|
|
227
|
+
|
|
228
|
+
self.assertEqual(observation.logged_argv[0], "~/.venv/bin/parity-tool")
|
|
229
|
+
self.assertNotIn(str(home), observation.log_text)
|
|
230
|
+
|
|
174
231
|
def test_production_and_testing_match_parser_usage_errors_without_bundles(self) -> None:
|
|
175
232
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
176
233
|
root = Path(tmpdir)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import inspect
|
|
3
4
|
import tempfile
|
|
4
5
|
import unittest
|
|
5
6
|
from pathlib import Path
|
|
@@ -11,6 +12,7 @@ from base_cli import (
|
|
|
11
12
|
command_filters,
|
|
12
13
|
command_protocol,
|
|
13
14
|
config,
|
|
15
|
+
context,
|
|
14
16
|
deprecations,
|
|
15
17
|
experimental,
|
|
16
18
|
history,
|
|
@@ -173,6 +175,15 @@ class PublicApiTests(unittest.TestCase):
|
|
|
173
175
|
self.assertTrue(base_cli.get_command_app.__doc__)
|
|
174
176
|
self.assertTrue(base_cli.run_app.__doc__)
|
|
175
177
|
|
|
178
|
+
def test_exported_callables_have_docstrings(self) -> None:
|
|
179
|
+
for module in (history, command_protocol, context):
|
|
180
|
+
for name in module.__all__:
|
|
181
|
+
exported = getattr(module, name)
|
|
182
|
+
if not (inspect.isclass(exported) or inspect.isfunction(exported)):
|
|
183
|
+
continue
|
|
184
|
+
with self.subTest(module=module.__name__, name=name):
|
|
185
|
+
self.assertTrue(inspect.getdoc(exported), f"{module.__name__}.{name} needs a docstring")
|
|
186
|
+
|
|
176
187
|
|
|
177
188
|
if __name__ == "__main__":
|
|
178
189
|
unittest.main()
|
base_cli-0.4.1/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.4.1
|
|
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
|
{base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/atlas_click/src/atlas_click/__init__.py
RENAMED
|
File without changes
|
{base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/atlas_click/src/atlas_click/cli.py
RENAMED
|
File without changes
|
{base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/atlas_click/tests/test_consumer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/beacon_typer/src/beacon_typer/__init__.py
RENAMED
|
File without changes
|
{base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/beacon_typer/src/beacon_typer/cli.py
RENAMED
|
File without changes
|
{base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/beacon_typer/tests/test_consumer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{base_cli-0.4.1 → base_cli-0.4.2}/compatibility/consumers/cinder_automation/tests/test_consumer.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
|
|
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
|
{base_cli-0.4.1 → base_cli-0.4.2}/examples/nested_click_app/src/nested_click_app/__init__.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
|
|
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
|
|
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
|