apcore-cli 0.10.2__tar.gz → 0.10.4__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.10.2 → apcore_cli-0.10.4}/CHANGELOG.md +16 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/PKG-INFO +4 -4
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/README.md +1 -1
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/pyproject.toml +3 -3
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/ref_resolver.py +24 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/schema_parser.py +35 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_ref_resolver.py +23 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_schema_parser.py +32 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/.github/CODEOWNERS +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/.github/copilot-ignore +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/.github/workflows/ci.yml +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/.gitignore +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/.gitmessage +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/.pre-commit-config.yaml +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/CLAUDE.md +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/LICENSE +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/Makefile +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/README.md +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/extensions/math/add.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/extensions/math/multiply.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/extensions/sysutil/disk.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/extensions/sysutil/env.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/extensions/sysutil/info.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/extensions/text/reverse.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/extensions/text/upper.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/extensions/text/wordcount.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/examples/run_examples.sh +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/approval-gate.md +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/config-resolver.md +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/core-dispatcher.md +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/discovery.md +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/exposure-filtering.md +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/grouped-commands.md +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/output-formatter.md +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/overview.md +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/schema-parser.md +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/security-manager.md +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/shell-integration.md +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/planning/state.json +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/__init__.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/__main__.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/_sandbox_runner.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/approval.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/builtin_group.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/cli.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/config.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/discovery.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/display_helpers.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/exit_codes.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/exposure.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/factory.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/init_cmd.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/output.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/security/__init__.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/security/audit.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/security/auth.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/security/config_encryptor.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/security/sandbox.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/shell.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/strategy.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/system_cmd.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/system_usage.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/src/apcore_cli/validate.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/__init__.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/conformance/__init__.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/conformance/test_apcli_visibility.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/conformance/test_snake_case_kwargs.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/conftest.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/shell_test_utils.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_apcli_integration.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_approval.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_bugfixes.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_builtin_group.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_cli.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_config.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_discovery.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_discovery_fe13.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_display_helpers.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_e2e.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_exit_codes.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_exposure.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_factory_fe13.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_init_cmd.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_integration.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_list_command_filters.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_output.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_output_format_exec.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_output_format_markdown_skill.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_public_api.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_sandbox_runner.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_security/__init__.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_security/test_audit.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_security/test_auth.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_security/test_config_encryptor.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_security/test_sandbox.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_shell.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_strategy.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_system_cmd.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_system_usage.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_toolkit_integration.py +0 -0
- {apcore_cli-0.10.2 → apcore_cli-0.10.4}/tests/test_validate.py +0 -0
|
@@ -5,6 +5,22 @@ 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.10.4] - 2026-07-14
|
|
9
|
+
|
|
10
|
+
Patch release. Bumps the required `apcore` floor to `0.26.0` to align the ecosystem on the 0.26.0 governance layer (Execution Policy, governance events, no-handler fail-loud — additive, no breaking changes). No code or API changes; all 798 tests pass (5 xfailed) unmodified against apcore 0.26.0.
|
|
11
|
+
|
|
12
|
+
## [0.10.3] - 2026-07-07
|
|
13
|
+
|
|
14
|
+
Patch release: fixes Pydantic v2 `Optional[...]` field type mapping and bumps the required `apcore-toolkit` floor. All 798 tests pass (5 xfailed).
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **`Optional[X]` fields mapped to the wrong Click type.** Pydantic v2 emits `Optional[X]` as `{"anyOf": [<X>, {"type": "null"}]}` rather than `{"type": "X"}`, so every optional CLI option fell through to `click.STRING` with a spurious "no type specified" warning — `Optional[bool]` lost its flag, `Optional[int]` lost `INT`, and the `*_file` convention stopped applying. `_map_type` (`schema_parser.py`) and the `anyOf`/`oneOf` branch of `_resolve_node` (`ref_resolver.py`) now recover the dominant non-null type from `anyOf`. JSON Schema list-form types (`{"type": ["string", "null"]}`) are likewise reduced to their first non-null string, so they no longer risk an unhashable-key `TypeError` in the type lookup. Regression tests added in `tests/test_schema_parser.py` and `tests/test_ref_resolver.py`.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Bumped the required `apcore-toolkit` floor to `>= 0.10.0` (which centralizes `RegistryWriter` field mapping and adds the shared annotation-preservation conformance verifier — additive, no breaking changes).
|
|
23
|
+
|
|
8
24
|
## [0.10.2] - 2026-06-24
|
|
9
25
|
|
|
10
26
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: apcore-cli
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.4
|
|
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>=0.
|
|
25
|
-
Requires-Dist: apcore>=0.
|
|
24
|
+
Requires-Dist: apcore-toolkit>=0.10.0
|
|
25
|
+
Requires-Dist: apcore>=0.26.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.10.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.10.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.10.
|
|
7
|
+
version = "0.10.4"
|
|
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>=0.
|
|
29
|
+
"apcore>=0.26.0",
|
|
30
|
+
"apcore-toolkit>=0.10.0",
|
|
31
31
|
"click>=8.1",
|
|
32
32
|
"jsonschema>=4.20",
|
|
33
33
|
"rich>=13.0",
|
|
@@ -117,8 +117,10 @@ def _resolve_node(
|
|
|
117
117
|
merged["properties"].update(node["properties"])
|
|
118
118
|
sibling_required: list[str] = list(node["required"]) if isinstance(node.get("required"), list) else []
|
|
119
119
|
all_required_sets: list[set[str]] = []
|
|
120
|
+
resolved_branches: list[dict] = []
|
|
120
121
|
for sub_schema in node[keyword]:
|
|
121
122
|
resolved = _resolve_node(sub_schema, defs, visited, depth + 1, max_depth, module_id)
|
|
123
|
+
resolved_branches.append(resolved)
|
|
122
124
|
if "properties" in resolved:
|
|
123
125
|
merged["properties"].update(resolved["properties"])
|
|
124
126
|
if "required" in resolved:
|
|
@@ -136,6 +138,28 @@ def _resolve_node(
|
|
|
136
138
|
for k, v in node.items():
|
|
137
139
|
if k not in (keyword, "properties", "required") and k not in merged:
|
|
138
140
|
merged[k] = v
|
|
141
|
+
# Pydantic v2 emits Optional[X] as {"anyOf": [<X-schema>, {"type": "null"}]}.
|
|
142
|
+
# The branch merge above copies properties but never the scalar `type` from
|
|
143
|
+
# branches, so Optional[str]/Optional[int]/etc. lose their type entirely.
|
|
144
|
+
# Recover it: if the merged result has no `type`, pick the type of the first
|
|
145
|
+
# non-null branch (ignoring {"type": "null"} sentinels).
|
|
146
|
+
if "type" not in merged:
|
|
147
|
+
for branch in resolved_branches:
|
|
148
|
+
branch_type = branch.get("type")
|
|
149
|
+
if isinstance(branch_type, str) and branch_type != "null":
|
|
150
|
+
merged["type"] = branch_type
|
|
151
|
+
break
|
|
152
|
+
if isinstance(branch_type, list):
|
|
153
|
+
# JSON Schema list-form type (e.g. ["string", "null"]):
|
|
154
|
+
# recover the first non-null string so downstream never
|
|
155
|
+
# sees an unhashable list type.
|
|
156
|
+
recovered = next(
|
|
157
|
+
(t for t in branch_type if isinstance(t, str) and t != "null"),
|
|
158
|
+
None,
|
|
159
|
+
)
|
|
160
|
+
if recovered is not None:
|
|
161
|
+
merged["type"] = recovered
|
|
162
|
+
break
|
|
139
163
|
return merged
|
|
140
164
|
|
|
141
165
|
# Recursively process every dict-valued child of this node.
|
|
@@ -34,10 +34,45 @@ RESERVED_PROPERTY_NAMES: frozenset[str] = frozenset(
|
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def _resolve_type_from_any_of(prop_schema: dict) -> str | None:
|
|
38
|
+
"""Extract the dominant non-null type from an anyOf schema.
|
|
39
|
+
|
|
40
|
+
Pydantic v2 emits Optional[X] as {"anyOf": [<X-schema>, {"type": "null"}]}.
|
|
41
|
+
We pick the first non-null branch and return its type string so the caller
|
|
42
|
+
can avoid a "no type" warning for what is effectively a typed optional field.
|
|
43
|
+
$ref entries (nested models) are treated as "object".
|
|
44
|
+
"""
|
|
45
|
+
branches = [b for b in prop_schema.get("anyOf", []) if b.get("type") != "null"]
|
|
46
|
+
if not branches:
|
|
47
|
+
return None
|
|
48
|
+
first = branches[0]
|
|
49
|
+
branch_type = first.get("type")
|
|
50
|
+
if isinstance(branch_type, str):
|
|
51
|
+
return branch_type
|
|
52
|
+
if isinstance(branch_type, list):
|
|
53
|
+
# JSON Schema list-form type (e.g. ["string", "null"]): first non-null string.
|
|
54
|
+
return next((t for t in branch_type if isinstance(t, str) and t != "null"), None)
|
|
55
|
+
if "$ref" in first: # nested model
|
|
56
|
+
return "object"
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
37
60
|
def _map_type(prop_name: str, prop_schema: dict) -> Any:
|
|
38
61
|
"""Map JSON Schema type to Click parameter type."""
|
|
39
62
|
schema_type = prop_schema.get("type")
|
|
40
63
|
|
|
64
|
+
# JSON Schema allows a list-form type (e.g. ["string", "null"]). Reduce it to
|
|
65
|
+
# the first non-null string so it maps cleanly and never reaches the
|
|
66
|
+
# (hashable-key) type_map lookup below as an unhashable list.
|
|
67
|
+
if isinstance(schema_type, list):
|
|
68
|
+
schema_type = next((t for t in schema_type if isinstance(t, str) and t != "null"), None)
|
|
69
|
+
|
|
70
|
+
# Pydantic v2 generates Optional[X] as {"anyOf": [<X-schema>, {"type": "null"}]}
|
|
71
|
+
# rather than {"type": "X"}. Resolve the dominant branch so we don't fall
|
|
72
|
+
# through to the "no type specified" warning for every optional field.
|
|
73
|
+
if schema_type is None and "anyOf" in prop_schema:
|
|
74
|
+
schema_type = _resolve_type_from_any_of(prop_schema)
|
|
75
|
+
|
|
41
76
|
# Check file convention
|
|
42
77
|
if schema_type == "string" and (prop_name.endswith("_file") or prop_schema.get("x-cli-file") is True):
|
|
43
78
|
return click.Path(exists=True)
|
|
@@ -415,3 +415,26 @@ class TestRefResolverExceptions:
|
|
|
415
415
|
cur = cur["properties"]["inner"]
|
|
416
416
|
depth_seen += 1
|
|
417
417
|
assert depth_seen == 50
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class TestOptionalTypeRecovery:
|
|
421
|
+
"""Optional[X] (Pydantic v2 anyOf-null) must recover the scalar `type` so the
|
|
422
|
+
schema parser does not fall through to a "no type" warning."""
|
|
423
|
+
|
|
424
|
+
def test_optional_scalar_recovers_type(self):
|
|
425
|
+
result = resolve_refs(
|
|
426
|
+
{
|
|
427
|
+
"type": "object",
|
|
428
|
+
"properties": {"name": {"anyOf": [{"type": "string"}, {"type": "null"}]}},
|
|
429
|
+
}
|
|
430
|
+
)
|
|
431
|
+
assert result["properties"]["name"]["type"] == "string"
|
|
432
|
+
|
|
433
|
+
def test_optional_list_form_type_recovers_string(self):
|
|
434
|
+
result = resolve_refs(
|
|
435
|
+
{
|
|
436
|
+
"type": "object",
|
|
437
|
+
"properties": {"name": {"anyOf": [{"type": ["string", "null"]}, {"type": "null"}]}},
|
|
438
|
+
}
|
|
439
|
+
)
|
|
440
|
+
assert result["properties"]["name"]["type"] == "string"
|
|
@@ -332,3 +332,35 @@ class TestNoFlagCollision:
|
|
|
332
332
|
}
|
|
333
333
|
result = schema_to_click_options(schema)
|
|
334
334
|
assert len(result) == 2
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class TestMapTypeOptional:
|
|
338
|
+
"""Pydantic v2 Optional[X] (anyOf-null) and JSON Schema list-form types."""
|
|
339
|
+
|
|
340
|
+
def test_optional_string(self):
|
|
341
|
+
assert _map_type("name", {"anyOf": [{"type": "string"}, {"type": "null"}]}) is click.STRING
|
|
342
|
+
|
|
343
|
+
def test_optional_integer(self):
|
|
344
|
+
assert _map_type("count", {"anyOf": [{"type": "integer"}, {"type": "null"}]}) is click.INT
|
|
345
|
+
|
|
346
|
+
def test_optional_boolean(self):
|
|
347
|
+
assert _map_type("flag", {"anyOf": [{"type": "boolean"}, {"type": "null"}]}) is _BOOLEAN_FLAG
|
|
348
|
+
|
|
349
|
+
def test_optional_model_ref_maps_to_string(self):
|
|
350
|
+
# Optional[Model] -> anyOf[$ref, null]; $ref treated as object -> STRING
|
|
351
|
+
assert _map_type("cfg", {"anyOf": [{"$ref": "#/$defs/M"}, {"type": "null"}]}) is click.STRING
|
|
352
|
+
|
|
353
|
+
def test_optional_string_file_convention(self):
|
|
354
|
+
# The *_file convention must still apply through an Optional[str].
|
|
355
|
+
result = _map_type("data_file", {"anyOf": [{"type": "string"}, {"type": "null"}]})
|
|
356
|
+
assert isinstance(result, click.Path)
|
|
357
|
+
|
|
358
|
+
def test_list_form_type_does_not_raise(self):
|
|
359
|
+
# JSON Schema {"type": ["string", "null"]} must reduce to STRING, not TypeError.
|
|
360
|
+
assert _map_type("name", {"type": ["string", "null"]}) is click.STRING
|
|
361
|
+
|
|
362
|
+
def test_anyof_only_null_falls_back_to_string(self, caplog):
|
|
363
|
+
with caplog.at_level(logging.WARNING, logger="apcore_cli.schema_parser"):
|
|
364
|
+
result = _map_type("field", {"anyOf": [{"type": "null"}]})
|
|
365
|
+
assert result is click.STRING
|
|
366
|
+
assert "No type specified" in caplog.text
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|