apcore-cli 0.12.0__tar.gz → 0.12.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.
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/CHANGELOG.md +18 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/PKG-INFO +4 -4
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/README.md +1 -1
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/pyproject.toml +3 -3
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/acl_loader.py +14 -40
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_acl_loader.py +14 -8
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_openapi_cmd.py +96 -33
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_openapi_source.py +4 -2
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/.github/CODEOWNERS +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/.github/copilot-ignore +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/.github/workflows/ci.yml +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/.gitignore +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/.gitmessage +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/.pre-commit-config.yaml +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/CLAUDE.md +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/LICENSE +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/Makefile +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/examples/README.md +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/examples/extensions/math/add.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/examples/extensions/math/multiply.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/examples/extensions/sysutil/disk.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/examples/extensions/sysutil/env.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/examples/extensions/sysutil/info.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/examples/extensions/text/reverse.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/examples/extensions/text/upper.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/examples/extensions/text/wordcount.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/examples/run_examples.sh +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/planning/approval-gate.md +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/planning/config-resolver.md +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/planning/core-dispatcher.md +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/planning/discovery.md +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/planning/exposure-filtering.md +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/planning/grouped-commands.md +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/planning/output-formatter.md +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/planning/overview.md +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/planning/schema-parser.md +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/planning/security-manager.md +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/planning/shell-integration.md +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/planning/state.json +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/__init__.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/__main__.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/_sandbox_runner.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/acl_cmd.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/approval.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/builtin_group.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/cli.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/config.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/discovery.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/display_helpers.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/exit_codes.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/exposure.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/factory.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/init_cmd.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/openapi_cmd.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/openapi_source.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/output.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/ref_resolver.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/schema_parser.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/security/__init__.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/security/audit.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/security/auth.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/security/config_encryptor.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/security/sandbox.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/shell.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/strategy.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/system_cmd.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/system_usage.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/src/apcore_cli/validate.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/__init__.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/conformance/__init__.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/conformance/test_apcli_visibility.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/conformance/test_snake_case_kwargs.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/conftest.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/shell_test_utils.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_acl_cmd.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_apcli_integration.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_approval.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_bugfixes.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_builtin_group.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_cli.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_config.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_discovery.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_discovery_fe13.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_display_helpers.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_e2e.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_exit_codes.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_exposure.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_factory_fe13.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_init_cmd.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_integration.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_list_command_filters.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_output.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_output_format_exec.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_output_format_markdown_skill.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_public_api.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_ref_resolver.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_sandbox_runner.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_schema_parser.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_security/__init__.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_security/test_audit.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_security/test_auth.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_security/test_config_encryptor.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_security/test_sandbox.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_shell.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_strategy.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_system_cmd.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_system_usage.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_toolkit_integration.py +0 -0
- {apcore_cli-0.12.0 → apcore_cli-0.12.2}/tests/test_validate.py +0 -0
|
@@ -5,6 +5,24 @@ All notable changes to apcore-cli (Python SDK) will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.12.2] - 2026-09-29
|
|
9
|
+
|
|
10
|
+
### Changed — BREAKING
|
|
11
|
+
|
|
12
|
+
- **`apcli openapi scan` / `generate` module IDs follow apcore-toolkit 0.13.0's normalisation; the toolkit floor is now `apcore-toolkit[http-proxy]>=0.13.0`.** The CLI passes the scanner's IDs through unchanged, so it inherits the toolkit's new derivation: IDs derived from camelCase or hyphenated names are now snake_case — `createPets` → `create_pets`, `showPetById` → `show_pet_by_id` — and `generate`'s `.binding.yaml` file names change with them (`createPets.binding.yaml` → `create_pets.binding.yaml`). An `operationId`-derived camelCase ID used to be emitted verbatim and was rejected by apcore's registry when the generated binding was loaded; it is now loadable for the first time. Path-derived IDs with a camelCase parameter change too, although they were already loadable (`/pets/{petId}`: `pets.petid.get` → `pets.pet_id.get`). A legal ID is never rewritten, and a `--prefix` value is normalised with the rest of the ID (`Pet-Store` → `pet_store`). **Migration:** `--include` / `--exclude` patterns and scripts keyed on the old IDs or file names need updating; the filters match the emitted, normalised ID. A segment that still begins with a digit (`POST /v1/2fa` → `v1.2fa.post`) is not repaired: the module is still emitted and carries the toolkit's legality warning, which `scan` renders like any other scanner warning (exit stays `0`; pinned by new test T-OAPI-28). No CLI code changed; 23 tests re-pinned to the new IDs.
|
|
13
|
+
|
|
14
|
+
## [0.12.1] - 2026-09-24
|
|
15
|
+
|
|
16
|
+
Patch release. Bumps the required `apcore` floor to `0.31.0` and `apcore-toolkit` to `[http-proxy]>=0.12.0`. All 1021 tests pass (6 xfailed).
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **`load_cli_acl` no longer rebuilds the ACL to attach the §4.8 audit callback.** apcore 0.31.0 (decision D-66) makes `ACL.load(path, audit_logger=…)` accept the callback directly; the `ACL(src.rules, src.default_effect, audit_logger=…)` rebuild this SDK relied on — the only lossless way to attach one before this release — is gone. `reload()` is now available on the ACL `load_cli_acl` returns whether or not `acl.audit.enabled` is set; previously it raised `ACLRuleError` on the auditing path.
|
|
21
|
+
|
|
22
|
+
### Security
|
|
23
|
+
|
|
24
|
+
- **`apcli openapi scan|generate` inherits apcore-toolkit 0.12.0's `$ref` sibling-key fix.** `OpenAPIScanner` (imported, never reimplemented, by `openapi_cmd.py`/`openapi_source.py`) previously discarded a `$ref` node's sibling keys during schema resolution, dropping `x-sensitive` and letting a field an OpenAPI document marked sensitive reach apcore with nothing to redact on. Fixed upstream; no code change was needed here.
|
|
25
|
+
|
|
8
26
|
## [0.12.0] - 2026-09-05
|
|
9
27
|
|
|
10
28
|
Two features: **FE-14 ACL Governance** and **FE-15a OpenAPI Import**. Bumps the required `apcore` floor to `0.30.0` and `apcore-toolkit` to `[http-proxy]>=0.11.1`. Full suite: 1004 passed, 5 xfailed (815 → 1004; 189 new tests). `APCLI_SUBCOMMAND_NAMES` grows from 13 to 15.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: apcore-cli
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.2
|
|
4
4
|
Summary: Terminal adapter for apcore — execute AI-Perceivable modules from the command line
|
|
5
5
|
Project-URL: Homepage, https://aiperceivable.com
|
|
6
6
|
Project-URL: Repository, https://github.com/aiperceivable/apcore-cli-python
|
|
@@ -21,8 +21,8 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
21
21
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
23
|
Requires-Python: >=3.11
|
|
24
|
-
Requires-Dist: apcore-toolkit[http-proxy]>=0.
|
|
25
|
-
Requires-Dist: apcore>=0.
|
|
24
|
+
Requires-Dist: apcore-toolkit[http-proxy]>=0.13.0
|
|
25
|
+
Requires-Dist: apcore>=0.31.0
|
|
26
26
|
Requires-Dist: click>=8.1
|
|
27
27
|
Requires-Dist: cryptography>=41.0
|
|
28
28
|
Requires-Dist: jsonschema>=4.20
|
|
@@ -89,7 +89,7 @@ Terminal adapter for apcore. Execute AI-Perceivable modules from the command lin
|
|
|
89
89
|
pip install apcore-cli
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
Requires Python 3.11+, `apcore >= 0.21.0`, and `apcore-toolkit >= 0.
|
|
92
|
+
Requires Python 3.11+, `apcore >= 0.21.0`, and `apcore-toolkit >= 0.13.0` (now a **required** runtime dependency as of v0.9.0 — previously optional; see the [tech-design ADR-09](https://github.com/aiperceivable/apcore-cli/blob/main/docs/tech-design.md) for the byte-equivalent toolkit-delegated tier rationale). The `[toolkit]` extras group is retained as a no-op for backward compat:
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
95
|
pip install "apcore-cli[toolkit]" # equivalent to plain `pip install apcore-cli`
|
|
@@ -48,7 +48,7 @@ Terminal adapter for apcore. Execute AI-Perceivable modules from the command lin
|
|
|
48
48
|
pip install apcore-cli
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
Requires Python 3.11+, `apcore >= 0.21.0`, and `apcore-toolkit >= 0.
|
|
51
|
+
Requires Python 3.11+, `apcore >= 0.21.0`, and `apcore-toolkit >= 0.13.0` (now a **required** runtime dependency as of v0.9.0 — previously optional; see the [tech-design ADR-09](https://github.com/aiperceivable/apcore-cli/blob/main/docs/tech-design.md) for the byte-equivalent toolkit-delegated tier rationale). The `[toolkit]` extras group is retained as a no-op for backward compat:
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
pip install "apcore-cli[toolkit]" # equivalent to plain `pip install apcore-cli`
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "apcore-cli"
|
|
7
|
-
version = "0.12.
|
|
7
|
+
version = "0.12.2"
|
|
8
8
|
description = "Terminal adapter for apcore — execute AI-Perceivable modules from the command line"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -26,8 +26,8 @@ classifiers = [
|
|
|
26
26
|
"Environment :: Console",
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
|
-
"apcore>=0.
|
|
30
|
-
"apcore-toolkit[http-proxy]>=0.
|
|
29
|
+
"apcore>=0.31.0",
|
|
30
|
+
"apcore-toolkit[http-proxy]>=0.13.0",
|
|
31
31
|
"click>=8.1",
|
|
32
32
|
"jsonschema>=4.20",
|
|
33
33
|
"rich>=13.0",
|
|
@@ -210,32 +210,13 @@ def make_acl_audit_logger(*, include_denied: bool = True) -> Callable[[AuditEntr
|
|
|
210
210
|
def load_cli_acl(root: str, *, audit_enabled: bool = True, include_denied: bool = True) -> ACL | None:
|
|
211
211
|
"""Load an :class:`~apcore.ACL` from *root*, or ``None`` for no enforcement.
|
|
212
212
|
|
|
213
|
-
|
|
214
|
-
§4.8
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
is the only lossless way to attach one. Two things about that sequence are
|
|
221
|
-
load-bearing:
|
|
222
|
-
|
|
223
|
-
1. ``default_effect`` is carried from ``src``, **never** a literal. A file
|
|
224
|
-
may legitimately declare ``default_effect: allow``, and passing the
|
|
225
|
-
constructor's own ``"deny"`` default would silently invert the governing
|
|
226
|
-
verdict for every call no rule matched — a rule set that grants by
|
|
227
|
-
default would start denying, with nothing in the output saying so.
|
|
228
|
-
2. The rebuilt ACL **loses** :meth:`~apcore.ACL.reload`, which needs the
|
|
229
|
-
``_yaml_path`` only ``ACL.load`` sets. This is accepted rather than
|
|
230
|
-
worked around: no apcore-cli SDK calls ``reload()`` on any path, and
|
|
231
|
-
writing the private attribute to fake the provenance would make
|
|
232
|
-
``reload()`` claim a file the object was not in fact loaded from. An
|
|
233
|
-
embedder that needs reloading passes its own ACL to
|
|
234
|
-
``create_cli(acl=…)``, which §4.2 attaches unchanged.
|
|
235
|
-
|
|
236
|
-
With auditing disabled the ``ACL.load`` result is returned **directly** —
|
|
237
|
-
no rebuild, no callback — so the ``reload()`` caveat applies only to the
|
|
238
|
-
auditing path.
|
|
213
|
+
``ACL.load`` takes ``audit_logger`` directly (apcore 0.31.0, D-66), so the
|
|
214
|
+
§4.8 callback is attached in the same call that parses the file — no
|
|
215
|
+
rebuild, and :meth:`~apcore.ACL.reload` stays available on the returned
|
|
216
|
+
object. (An earlier revision rebuilt the ACL via
|
|
217
|
+
``ACL(src.rules, src.default_effect, audit_logger=…)`` to attach the
|
|
218
|
+
callback after the fact, which lost ``reload()`` in the process; that
|
|
219
|
+
workaround is gone now that apcore accepts the callback at load time.)
|
|
239
220
|
|
|
240
221
|
Args:
|
|
241
222
|
root: Resolved ACL root (file or directory).
|
|
@@ -259,20 +240,13 @@ def load_cli_acl(root: str, *, audit_enabled: bool = True, include_denied: bool
|
|
|
259
240
|
if acl_file is None:
|
|
260
241
|
logger.debug("No ACL at '%s' — enforcement stays off.", root)
|
|
261
242
|
return None
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
src.default_effect,
|
|
270
|
-
audit_logger=make_acl_audit_logger(include_denied=include_denied),
|
|
271
|
-
)
|
|
272
|
-
logger.debug(
|
|
273
|
-
"ACL audit callback installed (include_denied=%s); reload() is unavailable on the rebuilt ACL.",
|
|
274
|
-
include_denied,
|
|
275
|
-
)
|
|
243
|
+
audit_logger = make_acl_audit_logger(include_denied=include_denied) if audit_enabled else None
|
|
244
|
+
acl = _ACL.load(acl_file, audit_logger=audit_logger)
|
|
245
|
+
logger.info("ACL loaded from %s (%d rules).", acl_file, len(acl.rules))
|
|
246
|
+
if audit_enabled:
|
|
247
|
+
logger.debug("ACL audit callback installed (include_denied=%s).", include_denied)
|
|
248
|
+
else:
|
|
249
|
+
logger.debug("acl.audit.enabled is false — no audit callback installed.")
|
|
276
250
|
return acl
|
|
277
251
|
|
|
278
252
|
|
|
@@ -659,8 +659,8 @@ class TestAuditWiring:
|
|
|
659
659
|
real_load = _ACL.load
|
|
660
660
|
captured: dict[str, object] = {}
|
|
661
661
|
|
|
662
|
-
def _spy(path: str):
|
|
663
|
-
result = real_load(path)
|
|
662
|
+
def _spy(path: str, audit_logger=None):
|
|
663
|
+
result = real_load(path, audit_logger=audit_logger)
|
|
664
664
|
captured["acl"] = result
|
|
665
665
|
return result
|
|
666
666
|
|
|
@@ -678,14 +678,20 @@ class TestAuditWiring:
|
|
|
678
678
|
assert acl.check_access("@external", "db.migrate", None).access == "allow"
|
|
679
679
|
assert _audit_records(audit_path) == []
|
|
680
680
|
|
|
681
|
-
def
|
|
682
|
-
"""§4.8 requirement 2
|
|
683
|
-
from apcore.errors import ACLRuleError
|
|
681
|
+
def test_auditing_path_reload_works(self, workdir, audit_path):
|
|
682
|
+
"""§4.8 requirement 2, updated for apcore 0.31.0 (D-66).
|
|
684
683
|
|
|
684
|
+
``ACL.load`` now takes ``audit_logger`` directly, so the auditing path
|
|
685
|
+
no longer rebuilds the ACL — ``reload()`` stays available, and the
|
|
686
|
+
installed audit callback survives it (apcore's own §6.3.2
|
|
687
|
+
requirement 7).
|
|
688
|
+
"""
|
|
685
689
|
_write_acl_dir(workdir)
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
690
|
+
acl = load_cli_acl(str(workdir / "acl"), audit_enabled=True)
|
|
691
|
+
acl.reload()
|
|
692
|
+
|
|
693
|
+
acl.check_access("@external", "system.control.disable", None)
|
|
694
|
+
assert [r["decision"] for r in _audit_records(audit_path)] == ["deny"]
|
|
689
695
|
|
|
690
696
|
def test_allow_default_survives_the_rebuild(self, workdir, audit_path):
|
|
691
697
|
"""T-ACL-27b: `default_effect: allow` + auditing enabled.
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""FE-15a ``apcli openapi`` subcommand group.
|
|
2
2
|
|
|
3
|
-
Covers T-OAPI-01..14, 18..
|
|
3
|
+
Covers T-OAPI-01..14, 18..28 — everything the CLI layer owns. Module-ID
|
|
4
4
|
derivation itself is the toolkit's conformance corpus and is deliberately not
|
|
5
5
|
re-tested here (§1.2): these assertions check that the CLI forwards options
|
|
6
|
-
verbatim and renders what comes back.
|
|
6
|
+
verbatim and renders what comes back. The IDs pinned below are the ones
|
|
7
|
+
apcore-toolkit >= 0.13.0 derives, normalised into apcore's Canonical ID
|
|
8
|
+
alphabet (``createPets`` -> ``create_pets``, ``DELETE /pets/{petId}`` -> ``pets.pet_id.delete``).
|
|
7
9
|
"""
|
|
8
10
|
|
|
9
11
|
from __future__ import annotations
|
|
@@ -63,6 +65,28 @@ info:
|
|
|
63
65
|
paths: {}
|
|
64
66
|
"""
|
|
65
67
|
|
|
68
|
+
#: A path segment beginning with a digit: the one case apcore-toolkit 0.13.0's
|
|
69
|
+
#: normalisation does not repair. No operationId, so the ID is path-derived.
|
|
70
|
+
_TWO_FA = """\
|
|
71
|
+
openapi: "3.1.0"
|
|
72
|
+
info:
|
|
73
|
+
title: TwoFactor
|
|
74
|
+
version: "1.0.0"
|
|
75
|
+
paths:
|
|
76
|
+
/v1/2fa:
|
|
77
|
+
post:
|
|
78
|
+
summary: Enable 2FA
|
|
79
|
+
responses:
|
|
80
|
+
"200":
|
|
81
|
+
description: ok
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
#: The toolkit's pinned legality warning for ``POST /v1/2fa``, byte for byte.
|
|
85
|
+
_TWO_FA_WARNING = (
|
|
86
|
+
"module_id 'v1.2fa.post' is not a legal apcore module ID: segment '2fa' must match "
|
|
87
|
+
"^[a-z][a-z0-9_]*$; name this operation with a derive_module_id or transform_module hook"
|
|
88
|
+
)
|
|
89
|
+
|
|
66
90
|
|
|
67
91
|
@pytest.fixture
|
|
68
92
|
def cli() -> click.Group:
|
|
@@ -107,16 +131,26 @@ class TestOpenapiScan:
|
|
|
107
131
|
json_path.write_text(json.dumps(doc), encoding="utf-8")
|
|
108
132
|
assert _scan_json(cli, str(json_path))["modules"] == _scan_json(cli, petstore)["modules"]
|
|
109
133
|
|
|
110
|
-
def
|
|
111
|
-
"""T-OAPI-03:
|
|
134
|
+
def test_ids_equal_toolkit_derive_module_id(self, cli, petstore):
|
|
135
|
+
"""T-OAPI-03: IDs equal the toolkit's derive_module_id output, unmodified by the CLI."""
|
|
136
|
+
from apcore_toolkit import derive_module_id
|
|
137
|
+
|
|
112
138
|
ids = [m["module_id"] for m in _scan_json(cli, petstore)["modules"]]
|
|
113
|
-
|
|
114
|
-
|
|
139
|
+
doc = yaml.safe_load(_PETSTORE)
|
|
140
|
+
expected = [
|
|
141
|
+
derive_module_id(path, method, operation)
|
|
142
|
+
for path, item in doc["paths"].items()
|
|
143
|
+
for method, operation in item.items()
|
|
144
|
+
]
|
|
145
|
+
assert ids == expected
|
|
146
|
+
# operationId `listPets` / `createPets`, snake_cased by the toolkit.
|
|
147
|
+
assert "list_pets" in ids
|
|
148
|
+
assert "create_pets" in ids
|
|
115
149
|
|
|
116
150
|
def test_path_and_method_algorithm_without_operation_id(self, cli, petstore):
|
|
117
151
|
"""T-OAPI-04."""
|
|
118
152
|
ids = [m["module_id"] for m in _scan_json(cli, petstore)["modules"]]
|
|
119
|
-
assert "pets.
|
|
153
|
+
assert "pets.pet_id.delete" in ids
|
|
120
154
|
|
|
121
155
|
def test_prefix_is_applied(self, cli, petstore):
|
|
122
156
|
"""T-OAPI-05."""
|
|
@@ -126,11 +160,12 @@ class TestOpenapiScan:
|
|
|
126
160
|
def test_include_filter(self, cli, petstore):
|
|
127
161
|
"""T-OAPI-06."""
|
|
128
162
|
ids = [m["module_id"] for m in _scan_json(cli, petstore, "--include", "^pets")["modules"]]
|
|
129
|
-
assert ids == ["pets.
|
|
163
|
+
assert ids == ["pets.pet_id.delete"]
|
|
130
164
|
|
|
131
165
|
def test_exclude_filter(self, cli, petstore):
|
|
132
166
|
ids = [m["module_id"] for m in _scan_json(cli, petstore, "--exclude", "^pets")["modules"]]
|
|
133
|
-
assert "pets.
|
|
167
|
+
assert "pets.pet_id.delete" not in ids
|
|
168
|
+
assert ids == ["list_pets", "create_pets", "show_pet_by_id"]
|
|
134
169
|
|
|
135
170
|
@pytest.mark.parametrize("flag", ["--include", "--exclude"])
|
|
136
171
|
def test_invalid_regex_exits_2(self, cli, petstore, flag):
|
|
@@ -141,9 +176,12 @@ class TestOpenapiScan:
|
|
|
141
176
|
|
|
142
177
|
def test_no_deprecated_omits_deprecated_operations(self, cli, petstore):
|
|
143
178
|
"""T-OAPI-08."""
|
|
179
|
+
# Without the flag the deprecated operation is present, so its absence
|
|
180
|
+
# below is the flag's doing rather than a mismatched name.
|
|
181
|
+
assert "show_pet_by_id" in [m["module_id"] for m in _scan_json(cli, petstore)["modules"]]
|
|
144
182
|
ids = [m["module_id"] for m in _scan_json(cli, petstore, "--no-deprecated")["modules"]]
|
|
145
|
-
assert "
|
|
146
|
-
assert "
|
|
183
|
+
assert "show_pet_by_id" not in ids
|
|
184
|
+
assert "list_pets" in ids
|
|
147
185
|
|
|
148
186
|
def test_string_false_deprecated_is_not_deprecated(self, cli, tmp_path):
|
|
149
187
|
"""T-OAPI-09: `deprecated: "false"` is a malformed boolean, not True."""
|
|
@@ -163,12 +201,12 @@ class TestOpenapiScan:
|
|
|
163
201
|
path = tmp_path / "o.json"
|
|
164
202
|
path.write_text(json.dumps(doc), encoding="utf-8")
|
|
165
203
|
ids = [m["module_id"] for m in _scan_json(cli, str(path), "--no-deprecated")["modules"]]
|
|
166
|
-
assert ids == ["
|
|
204
|
+
assert ids == ["op_x"]
|
|
167
205
|
|
|
168
206
|
def test_no_2xx_response_warns_but_keeps_the_module(self, cli, petstore):
|
|
169
207
|
"""T-OAPI-10."""
|
|
170
208
|
payload = _scan_json(cli, petstore)
|
|
171
|
-
delete = next(m for m in payload["modules"] if m["module_id"] == "pets.
|
|
209
|
+
delete = next(m for m in payload["modules"] if m["module_id"] == "pets.pet_id.delete")
|
|
172
210
|
assert any("no 2xx response" in w for w in delete["warnings"])
|
|
173
211
|
|
|
174
212
|
def test_external_ref_warning_is_rendered_and_not_fetched(self, cli, tmp_path):
|
|
@@ -214,7 +252,7 @@ class TestOpenapiScan:
|
|
|
214
252
|
assert all("warnings" in m for m in payload["modules"])
|
|
215
253
|
assert payload["hazards"] == [
|
|
216
254
|
{
|
|
217
|
-
"module_id": "
|
|
255
|
+
"module_id": "create_pets",
|
|
218
256
|
"http_method": "POST",
|
|
219
257
|
"url_path": "/pets",
|
|
220
258
|
"parameters": ["dryRun"],
|
|
@@ -226,13 +264,13 @@ class TestOpenapiScan:
|
|
|
226
264
|
"""T-OAPI-14: ScannedModule goes straight to format_modules."""
|
|
227
265
|
result = CliRunner().invoke(cli, ["openapi", "scan", petstore, "--format", style])
|
|
228
266
|
assert result.exit_code == 0
|
|
229
|
-
assert "
|
|
267
|
+
assert "list_pets" in result.output
|
|
230
268
|
|
|
231
269
|
@pytest.mark.parametrize("style", ["csv", "yaml", "jsonl"])
|
|
232
270
|
def test_tabular_styles_render(self, cli, petstore, style):
|
|
233
271
|
result = CliRunner().invoke(cli, ["openapi", "scan", petstore, "--format", style])
|
|
234
272
|
assert result.exit_code == 0
|
|
235
|
-
assert "
|
|
273
|
+
assert "list_pets" in result.output
|
|
236
274
|
|
|
237
275
|
def test_table_renders_banner_warnings_and_hazards(self, cli, petstore):
|
|
238
276
|
result = CliRunner().invoke(cli, ["openapi", "scan", petstore], env={"COLUMNS": "200"})
|
|
@@ -242,12 +280,31 @@ class TestOpenapiScan:
|
|
|
242
280
|
assert "GET /pets" in result.output
|
|
243
281
|
assert "1 warning" in result.output
|
|
244
282
|
assert "cannot be proxied by FE-15b" in result.output
|
|
245
|
-
assert "
|
|
283
|
+
assert "create_pets" in result.output
|
|
246
284
|
|
|
247
285
|
def test_hazards_do_not_change_the_exit_code(self, cli, petstore):
|
|
248
286
|
"""A partially-understood document is still a successful scan."""
|
|
249
287
|
assert CliRunner().invoke(cli, ["openapi", "scan", petstore]).exit_code == 0
|
|
250
288
|
|
|
289
|
+
def test_illegal_id_warning_is_rendered_verbatim(self, cli, tmp_path):
|
|
290
|
+
"""T-OAPI-28: a segment beginning with a digit is not repaired by the
|
|
291
|
+
toolkit; the module is still listed and its legality warning is
|
|
292
|
+
rendered like any other scanner warning, byte for byte, exit 0."""
|
|
293
|
+
path = tmp_path / "openapi.yaml"
|
|
294
|
+
path.write_text(_TWO_FA, encoding="utf-8")
|
|
295
|
+
|
|
296
|
+
table = CliRunner().invoke(cli, ["openapi", "scan", str(path)], env={"COLUMNS": "200"})
|
|
297
|
+
assert table.exit_code == 0, table.output
|
|
298
|
+
listing, _, warnings_block = table.output.partition("\n1 warning:\n")
|
|
299
|
+
assert "v1.2fa.post" in listing
|
|
300
|
+
# The whole line, so a renderer that ate `[a-z]` as markup would fail.
|
|
301
|
+
assert warnings_block.splitlines()[0] == " v1.2fa.post " + _TWO_FA_WARNING
|
|
302
|
+
|
|
303
|
+
payload = _scan_json(cli, str(path))
|
|
304
|
+
assert payload["modules"][0]["module_id"] == "v1.2fa.post"
|
|
305
|
+
assert payload["modules"][0]["warnings"] == [_TWO_FA_WARNING]
|
|
306
|
+
assert payload["hazards"] == []
|
|
307
|
+
|
|
251
308
|
|
|
252
309
|
# ---------------------------------------------------------------------------
|
|
253
310
|
# apcli openapi generate (§4.4)
|
|
@@ -262,10 +319,10 @@ class TestOpenapiGenerate:
|
|
|
262
319
|
assert result.exit_code == 0, result.output
|
|
263
320
|
written = sorted(p.name for p in out.glob("*.binding.yaml"))
|
|
264
321
|
assert written == [
|
|
265
|
-
"
|
|
266
|
-
"
|
|
267
|
-
"pets.
|
|
268
|
-
"
|
|
322
|
+
"create_pets.binding.yaml",
|
|
323
|
+
"list_pets.binding.yaml",
|
|
324
|
+
"pets.pet_id.delete.binding.yaml",
|
|
325
|
+
"show_pet_by_id.binding.yaml",
|
|
269
326
|
]
|
|
270
327
|
|
|
271
328
|
def test_dry_run_lists_paths_and_writes_nothing(self, cli, petstore, tmp_path):
|
|
@@ -273,7 +330,7 @@ class TestOpenapiGenerate:
|
|
|
273
330
|
out = tmp_path / "out"
|
|
274
331
|
result = CliRunner().invoke(cli, ["openapi", "generate", petstore, "-o", str(out), "--dry-run"])
|
|
275
332
|
assert result.exit_code == 0
|
|
276
|
-
assert "
|
|
333
|
+
assert "list_pets.binding.yaml" in result.output
|
|
277
334
|
assert not out.exists()
|
|
278
335
|
|
|
279
336
|
def test_artifact_carries_an_intact_routing_contract(self, cli, petstore, tmp_path):
|
|
@@ -283,14 +340,15 @@ class TestOpenapiGenerate:
|
|
|
283
340
|
out = tmp_path / "out"
|
|
284
341
|
CliRunner().invoke(cli, ["openapi", "generate", petstore, "-o", str(out)])
|
|
285
342
|
|
|
286
|
-
raw = yaml.safe_load((out / "
|
|
343
|
+
raw = yaml.safe_load((out / "create_pets.binding.yaml").read_text(encoding="utf-8"))
|
|
287
344
|
binding = raw["bindings"][0]
|
|
288
345
|
assert binding["target"] == "POST /pets"
|
|
289
346
|
assert binding["metadata"]["http_method"] == "POST"
|
|
290
347
|
assert binding["metadata"]["url_path"] == "/pets"
|
|
348
|
+
# The document's raw operationId, verbatim — only the module ID is normalised.
|
|
291
349
|
assert binding["metadata"]["openapi"]["operation_id"] == "createPets"
|
|
292
350
|
|
|
293
|
-
reloaded = BindingLoader().load(str(out / "
|
|
351
|
+
reloaded = BindingLoader().load(str(out / "create_pets.binding.yaml"))
|
|
294
352
|
assert reloaded[0].metadata["http_method"] == "POST"
|
|
295
353
|
assert reloaded[0].metadata["url_path"] == "/pets"
|
|
296
354
|
|
|
@@ -304,19 +362,24 @@ class TestOpenapiGenerate:
|
|
|
304
362
|
"""T-OAPI-21: non-destructive default, matching `apcli init`."""
|
|
305
363
|
out = tmp_path / "out"
|
|
306
364
|
CliRunner().invoke(cli, ["openapi", "generate", petstore, "-o", str(out)])
|
|
307
|
-
target = out / "
|
|
365
|
+
target = out / "list_pets.binding.yaml"
|
|
366
|
+
# The first run wrote this very file, so the sentinel below sits on a
|
|
367
|
+
# name the second run will actually try to write.
|
|
368
|
+
assert target.is_file()
|
|
308
369
|
target.write_text("SENTINEL", encoding="utf-8")
|
|
309
370
|
|
|
310
371
|
result = CliRunner().invoke(cli, ["openapi", "generate", petstore, "-o", str(out)])
|
|
311
372
|
assert result.exit_code == 0
|
|
312
373
|
assert "already exists" in result.output
|
|
374
|
+
assert f"{target} already exists" in result.output
|
|
313
375
|
assert target.read_text(encoding="utf-8") == "SENTINEL"
|
|
314
376
|
|
|
315
377
|
def test_force_overwrites(self, cli, petstore, tmp_path):
|
|
316
378
|
"""T-OAPI-22."""
|
|
317
379
|
out = tmp_path / "out"
|
|
318
380
|
CliRunner().invoke(cli, ["openapi", "generate", petstore, "-o", str(out)])
|
|
319
|
-
target = out / "
|
|
381
|
+
target = out / "list_pets.binding.yaml"
|
|
382
|
+
assert target.is_file()
|
|
320
383
|
target.write_text("SENTINEL", encoding="utf-8")
|
|
321
384
|
|
|
322
385
|
result = CliRunner().invoke(cli, ["openapi", "generate", petstore, "-o", str(out), "--force"])
|
|
@@ -392,9 +455,9 @@ class TestOpenapiGenerate:
|
|
|
392
455
|
gen = CliRunner().invoke(
|
|
393
456
|
cli, ["openapi", "generate", petstore, "-o", str(tmp_path / "out")], env={"COLUMNS": "200"}
|
|
394
457
|
)
|
|
395
|
-
assert "
|
|
458
|
+
assert "create_pets" in scan.output
|
|
396
459
|
assert "cannot be proxied by FE-15b" in gen.output
|
|
397
|
-
assert "
|
|
460
|
+
assert "create_pets" in gen.output
|
|
398
461
|
|
|
399
462
|
def test_missing_source_exits_47(self, cli, tmp_path):
|
|
400
463
|
result = CliRunner().invoke(
|
|
@@ -531,18 +594,18 @@ class TestOpenapiGenerateDryRunAndVerification:
|
|
|
531
594
|
def test_dry_run_does_not_list_a_preexisting_file_as_would_write(self, cli, petstore, tmp_path):
|
|
532
595
|
out = tmp_path / "out"
|
|
533
596
|
out.mkdir()
|
|
534
|
-
(out / "
|
|
597
|
+
(out / "list_pets.binding.yaml").write_text("SENTINEL", encoding="utf-8")
|
|
535
598
|
|
|
536
599
|
result = CliRunner().invoke(cli, ["openapi", "generate", petstore, "-o", str(out), "--dry-run"])
|
|
537
600
|
assert result.exit_code == 0, result.output
|
|
538
601
|
would_write_lines = [line for line in result.output.splitlines() if line.startswith("Would write")]
|
|
539
|
-
assert not any("
|
|
540
|
-
assert any("
|
|
602
|
+
assert not any("list_pets.binding.yaml" in line for line in would_write_lines)
|
|
603
|
+
assert any("create_pets.binding.yaml" in line for line in would_write_lines)
|
|
541
604
|
assert "3 file(s) would be written" in result.output
|
|
542
605
|
assert "1 skipped" in result.output
|
|
543
606
|
# --dry-run still writes nothing.
|
|
544
|
-
assert (out / "
|
|
545
|
-
assert not (out / "
|
|
607
|
+
assert (out / "list_pets.binding.yaml").read_text(encoding="utf-8") == "SENTINEL"
|
|
608
|
+
assert not (out / "create_pets.binding.yaml").exists()
|
|
546
609
|
|
|
547
610
|
def test_verification_failure_warns_and_fails_and_is_excluded_from_count(
|
|
548
611
|
self, cli, petstore, tmp_path, monkeypatch
|
|
@@ -180,7 +180,7 @@ class TestDetectProxyHazards:
|
|
|
180
180
|
hazards = detect_proxy_hazards(spec, modules)
|
|
181
181
|
assert len(hazards) == 1
|
|
182
182
|
hazard = hazards[0]
|
|
183
|
-
assert hazard.module_id == "
|
|
183
|
+
assert hazard.module_id == "create_pets"
|
|
184
184
|
assert hazard.http_method == "POST"
|
|
185
185
|
assert hazard.url_path == "/pets"
|
|
186
186
|
assert hazard.parameters == ("dryRun", "notify")
|
|
@@ -301,7 +301,9 @@ class TestDetectProxyHazards:
|
|
|
301
301
|
def test_filtered_out_operations_produce_no_hazard(self, tmp_path):
|
|
302
302
|
"""An operation with no module is nothing FE-15b could misroute."""
|
|
303
303
|
spec = load_openapi_source(_write(tmp_path, "o.yaml", _PETSTORE_YAML))
|
|
304
|
-
modules = OpenAPIScanner().scan(spec, include="^
|
|
304
|
+
modules = OpenAPIScanner().scan(spec, include="^list_pets$")
|
|
305
|
+
# The filter kept GET /pets and dropped POST /pets, the one hazard.
|
|
306
|
+
assert [m.module_id for m in modules] == ["list_pets"]
|
|
305
307
|
assert detect_proxy_hazards(spec, modules) == []
|
|
306
308
|
|
|
307
309
|
def test_empty_and_non_dict_specs_are_safe(self):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|