apcore-cli 0.12.1__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.1 → apcore_cli-0.12.2}/CHANGELOG.md +6 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/PKG-INFO +3 -3
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/README.md +1 -1
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/pyproject.toml +2 -2
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_openapi_cmd.py +96 -33
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_openapi_source.py +4 -2
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/.github/CODEOWNERS +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/.github/copilot-ignore +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/.github/workflows/ci.yml +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/.gitignore +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/.gitmessage +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/.pre-commit-config.yaml +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/CLAUDE.md +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/LICENSE +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/Makefile +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/examples/README.md +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/examples/extensions/math/add.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/examples/extensions/math/multiply.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/examples/extensions/sysutil/disk.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/examples/extensions/sysutil/env.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/examples/extensions/sysutil/info.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/examples/extensions/text/reverse.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/examples/extensions/text/upper.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/examples/extensions/text/wordcount.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/examples/run_examples.sh +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/planning/approval-gate.md +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/planning/config-resolver.md +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/planning/core-dispatcher.md +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/planning/discovery.md +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/planning/exposure-filtering.md +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/planning/grouped-commands.md +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/planning/output-formatter.md +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/planning/overview.md +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/planning/schema-parser.md +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/planning/security-manager.md +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/planning/shell-integration.md +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/planning/state.json +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/__init__.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/__main__.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/_sandbox_runner.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/acl_cmd.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/acl_loader.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/approval.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/builtin_group.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/cli.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/config.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/discovery.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/display_helpers.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/exit_codes.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/exposure.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/factory.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/init_cmd.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/openapi_cmd.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/openapi_source.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/output.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/ref_resolver.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/schema_parser.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/security/__init__.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/security/audit.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/security/auth.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/security/config_encryptor.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/security/sandbox.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/shell.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/strategy.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/system_cmd.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/system_usage.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/src/apcore_cli/validate.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/__init__.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/conformance/__init__.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/conformance/test_apcli_visibility.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/conformance/test_snake_case_kwargs.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/conftest.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/shell_test_utils.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_acl_cmd.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_acl_loader.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_apcli_integration.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_approval.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_bugfixes.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_builtin_group.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_cli.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_config.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_discovery.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_discovery_fe13.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_display_helpers.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_e2e.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_exit_codes.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_exposure.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_factory_fe13.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_init_cmd.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_integration.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_list_command_filters.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_output.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_output_format_exec.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_output_format_markdown_skill.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_public_api.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_ref_resolver.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_sandbox_runner.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_schema_parser.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_security/__init__.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_security/test_audit.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_security/test_auth.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_security/test_config_encryptor.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_security/test_sandbox.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_shell.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_strategy.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_system_cmd.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_system_usage.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_toolkit_integration.py +0 -0
- {apcore_cli-0.12.1 → apcore_cli-0.12.2}/tests/test_validate.py +0 -0
|
@@ -5,6 +5,12 @@ 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
|
+
|
|
8
14
|
## [0.12.1] - 2026-09-24
|
|
9
15
|
|
|
10
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).
|
|
@@ -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,7 +21,7 @@ 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.
|
|
24
|
+
Requires-Dist: apcore-toolkit[http-proxy]>=0.13.0
|
|
25
25
|
Requires-Dist: apcore>=0.31.0
|
|
26
26
|
Requires-Dist: click>=8.1
|
|
27
27
|
Requires-Dist: cryptography>=41.0
|
|
@@ -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"
|
|
@@ -27,7 +27,7 @@ classifiers = [
|
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
29
|
"apcore>=0.31.0",
|
|
30
|
-
"apcore-toolkit[http-proxy]>=0.
|
|
30
|
+
"apcore-toolkit[http-proxy]>=0.13.0",
|
|
31
31
|
"click>=8.1",
|
|
32
32
|
"jsonschema>=4.20",
|
|
33
33
|
"rich>=13.0",
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|