apcore-toolkit 0.4.0__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/CHANGELOG.md +11 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/PKG-INFO +12 -3
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/README.md +10 -1
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/pyproject.toml +2 -2
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/__init__.py +2 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/ai_enhancer.py +64 -54
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/openapi.py +21 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/http_proxy_writer.py +6 -4
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/scanner.py +5 -5
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_openapi.py +96 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/.github/CODEOWNERS +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/.github/copilot-ignore +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/.github/workflows/ci.yml +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/.gitignore +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/.gitmessage +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/.pre-commit-config.yaml +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/convention_scanner.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/display/__init__.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/display/resolver.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/formatting/__init__.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/formatting/markdown.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/__init__.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/errors.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/python_writer.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/registry_writer.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/types.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/verifiers.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/yaml_writer.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/pydantic_utils.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/schema_utils.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/serializers.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/types.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/conftest.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_ai_enhancer.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_convention_scanner.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_display_resolver.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_http_proxy_writer.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_markdown.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_output_factory.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_pydantic_utils.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_python_writer.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_registry_writer.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_scanner.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_schema_utils.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_serializers.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_types.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_verifiers.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_write_error.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_write_result.py +0 -0
- {apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/tests/test_yaml_writer.py +0 -0
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.4.1] - 2026-03-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`deep_resolve_refs()`** — public API for recursive `$ref` resolution in OpenAPI schemas (previously internal `_deep_resolve_refs`). Resolves nested `allOf`/`anyOf`/`oneOf`, `items`, and `properties`. Depth-limited to 16 levels.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- README: apcore dependency version updated from `>= 0.13.1` to `>= 0.14.0` (matches pyproject.toml).
|
|
14
|
+
- README: Core Modules table now lists all public API functions (added 10 missing entries).
|
|
15
|
+
|
|
5
16
|
## [0.4.0] - 2026-03-23
|
|
6
17
|
|
|
7
18
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: apcore-toolkit
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Shared scanner, schema extraction, and output toolkit for apcore framework adapters
|
|
5
5
|
Project-URL: Homepage, https://aiperceivable.com
|
|
6
6
|
Project-URL: Repository, https://github.com/aiperceivable/apcore-toolkit-python
|
|
@@ -10,7 +10,7 @@ Author-email: aiperceivable <tercel.yi@gmail.com>
|
|
|
10
10
|
License-Expression: Apache-2.0
|
|
11
11
|
Keywords: apcore,mcp,openapi,pydantic,scanner,schema,toolkit,yaml
|
|
12
12
|
Requires-Python: >=3.11
|
|
13
|
-
Requires-Dist: apcore>=0.
|
|
13
|
+
Requires-Dist: apcore>=0.18.0
|
|
14
14
|
Requires-Dist: pydantic>=2.0
|
|
15
15
|
Requires-Dist: pyyaml>=6.0
|
|
16
16
|
Provides-Extra: dev
|
|
@@ -69,6 +69,15 @@ pip install apcore-toolkit
|
|
|
69
69
|
| `get_writer` | Factory function for writer instances |
|
|
70
70
|
| `DisplayResolver` | Sparse binding.yaml display overlay — resolves surface-facing alias, description, guidance, tags into `metadata["display"]` (§5.13) |
|
|
71
71
|
| `ConventionScanner` | Scans a `commands/` directory of plain Python files for public functions and converts them to `ScannedModule` instances with schema inferred from type annotations (§5.14) |
|
|
72
|
+
| `extract_input_schema` | Merges OpenAPI query, path, and request body params into a single JSON Schema |
|
|
73
|
+
| `extract_output_schema` | Extracts response schema from OpenAPI operation objects |
|
|
74
|
+
| `resolve_ref` | Resolves a single internal `$ref` JSON pointer |
|
|
75
|
+
| `resolve_schema` | Resolves a top-level `$ref` in a schema |
|
|
76
|
+
| `deep_resolve_refs` | Recursively resolves all `$ref` pointers, depth-limited to 16 levels |
|
|
77
|
+
| `annotations_to_dict` | Converts `ModuleAnnotations` to a plain dict |
|
|
78
|
+
| `module_to_dict` | Converts a `ScannedModule` to a dict for JSON/YAML serialization |
|
|
79
|
+
| `modules_to_dicts` | Batch version of `module_to_dict` |
|
|
80
|
+
| `run_verifier_chain` | Runs multiple verifiers in sequence, stopping on first failure |
|
|
72
81
|
|
|
73
82
|
## Usage
|
|
74
83
|
|
|
@@ -240,7 +249,7 @@ Input and output schemas are inferred from PEP 484 type annotations. Use `includ
|
|
|
240
249
|
## Requirements
|
|
241
250
|
|
|
242
251
|
- Python >= 3.11
|
|
243
|
-
- apcore >= 0.
|
|
252
|
+
- apcore >= 0.14.0
|
|
244
253
|
- pydantic >= 2.0
|
|
245
254
|
- PyYAML >= 6.0
|
|
246
255
|
|
|
@@ -43,6 +43,15 @@ pip install apcore-toolkit
|
|
|
43
43
|
| `get_writer` | Factory function for writer instances |
|
|
44
44
|
| `DisplayResolver` | Sparse binding.yaml display overlay — resolves surface-facing alias, description, guidance, tags into `metadata["display"]` (§5.13) |
|
|
45
45
|
| `ConventionScanner` | Scans a `commands/` directory of plain Python files for public functions and converts them to `ScannedModule` instances with schema inferred from type annotations (§5.14) |
|
|
46
|
+
| `extract_input_schema` | Merges OpenAPI query, path, and request body params into a single JSON Schema |
|
|
47
|
+
| `extract_output_schema` | Extracts response schema from OpenAPI operation objects |
|
|
48
|
+
| `resolve_ref` | Resolves a single internal `$ref` JSON pointer |
|
|
49
|
+
| `resolve_schema` | Resolves a top-level `$ref` in a schema |
|
|
50
|
+
| `deep_resolve_refs` | Recursively resolves all `$ref` pointers, depth-limited to 16 levels |
|
|
51
|
+
| `annotations_to_dict` | Converts `ModuleAnnotations` to a plain dict |
|
|
52
|
+
| `module_to_dict` | Converts a `ScannedModule` to a dict for JSON/YAML serialization |
|
|
53
|
+
| `modules_to_dicts` | Batch version of `module_to_dict` |
|
|
54
|
+
| `run_verifier_chain` | Runs multiple verifiers in sequence, stopping on first failure |
|
|
46
55
|
|
|
47
56
|
## Usage
|
|
48
57
|
|
|
@@ -214,7 +223,7 @@ Input and output schemas are inferred from PEP 484 type annotations. Use `includ
|
|
|
214
223
|
## Requirements
|
|
215
224
|
|
|
216
225
|
- Python >= 3.11
|
|
217
|
-
- apcore >= 0.
|
|
226
|
+
- apcore >= 0.14.0
|
|
218
227
|
- pydantic >= 2.0
|
|
219
228
|
- PyYAML >= 6.0
|
|
220
229
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "apcore-toolkit"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "Shared scanner, schema extraction, and output toolkit for apcore framework adapters"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
readme = "README.md"
|
|
@@ -23,7 +23,7 @@ keywords = [
|
|
|
23
23
|
"toolkit",
|
|
24
24
|
]
|
|
25
25
|
dependencies = [
|
|
26
|
-
"apcore>=0.
|
|
26
|
+
"apcore>=0.18.0",
|
|
27
27
|
"pydantic>=2.0",
|
|
28
28
|
"PyYAML>=6.0",
|
|
29
29
|
]
|
|
@@ -9,6 +9,7 @@ from apcore_toolkit.ai_enhancer import AIEnhancer, Enhancer
|
|
|
9
9
|
from apcore_toolkit.display import DisplayResolver
|
|
10
10
|
from apcore_toolkit.formatting import to_markdown
|
|
11
11
|
from apcore_toolkit.openapi import (
|
|
12
|
+
deep_resolve_refs,
|
|
12
13
|
extract_input_schema,
|
|
13
14
|
extract_output_schema,
|
|
14
15
|
resolve_ref,
|
|
@@ -62,6 +63,7 @@ __all__ = [
|
|
|
62
63
|
"YAMLVerifier",
|
|
63
64
|
"YAMLWriter",
|
|
64
65
|
"annotations_to_dict",
|
|
66
|
+
"deep_resolve_refs",
|
|
65
67
|
"enrich_schema_descriptions",
|
|
66
68
|
"extract_input_schema",
|
|
67
69
|
"extract_output_schema",
|
|
@@ -10,13 +10,16 @@ metadata dict for auditability.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import dataclasses
|
|
13
14
|
import json
|
|
14
15
|
import logging
|
|
15
16
|
import os
|
|
17
|
+
import types
|
|
18
|
+
import typing
|
|
16
19
|
from dataclasses import replace
|
|
17
|
-
from typing import Any, Protocol
|
|
20
|
+
from typing import Any, Callable, Protocol
|
|
18
21
|
|
|
19
|
-
from apcore import ModuleAnnotations
|
|
22
|
+
from apcore import DEFAULT_ANNOTATIONS, ModuleAnnotations
|
|
20
23
|
|
|
21
24
|
from apcore_toolkit.types import ScannedModule
|
|
22
25
|
|
|
@@ -27,7 +30,53 @@ _DEFAULT_MODEL = "qwen:0.6b"
|
|
|
27
30
|
_DEFAULT_THRESHOLD = 0.7
|
|
28
31
|
_DEFAULT_BATCH_SIZE = 5
|
|
29
32
|
_DEFAULT_TIMEOUT = 30
|
|
30
|
-
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# SLM must never write to ModuleAnnotations.extra: it is reserved for adapter
|
|
36
|
+
# extensions and is not user-facing semantic content.
|
|
37
|
+
_SLM_EXCLUDED_ANNOTATION_FIELDS = frozenset({"extra"})
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _build_annotation_field_validators() -> dict[str, Callable[[Any], bool]]:
|
|
41
|
+
"""Derive per-field type validators from ``ModuleAnnotations`` at import time.
|
|
42
|
+
|
|
43
|
+
Introspecting the dataclass instead of hardcoding a whitelist ensures that
|
|
44
|
+
when apcore adds new annotation fields the AI Enhancer automatically picks
|
|
45
|
+
them up (still subject to confidence gating). ``extra`` is excluded so the
|
|
46
|
+
SLM cannot inject arbitrary keys via that escape hatch.
|
|
47
|
+
|
|
48
|
+
The order of ``isinstance`` checks matters: ``bool`` is a subclass of
|
|
49
|
+
``int`` in Python, so a boolean must not be silently accepted as an int
|
|
50
|
+
field.
|
|
51
|
+
"""
|
|
52
|
+
hints = typing.get_type_hints(ModuleAnnotations)
|
|
53
|
+
validators: dict[str, Callable[[Any], bool]] = {}
|
|
54
|
+
for field in dataclasses.fields(ModuleAnnotations):
|
|
55
|
+
if field.name in _SLM_EXCLUDED_ANNOTATION_FIELDS:
|
|
56
|
+
continue
|
|
57
|
+
hint = hints.get(field.name)
|
|
58
|
+
origin = typing.get_origin(hint)
|
|
59
|
+
# Strip Optional[X] / X | None — SLM returns concrete values, not None.
|
|
60
|
+
# PEP 604 (X | None) yields types.UnionType; typing.Optional yields typing.Union.
|
|
61
|
+
if origin is typing.Union or origin is types.UnionType:
|
|
62
|
+
args = [a for a in typing.get_args(hint) if a is not type(None)]
|
|
63
|
+
if len(args) == 1:
|
|
64
|
+
hint = args[0]
|
|
65
|
+
origin = typing.get_origin(hint)
|
|
66
|
+
if hint is bool:
|
|
67
|
+
validators[field.name] = lambda v: isinstance(v, bool)
|
|
68
|
+
elif hint is int:
|
|
69
|
+
validators[field.name] = lambda v: isinstance(v, int) and not isinstance(v, bool)
|
|
70
|
+
elif hint is str:
|
|
71
|
+
validators[field.name] = lambda v: isinstance(v, str)
|
|
72
|
+
elif origin in (list, tuple):
|
|
73
|
+
validators[field.name] = lambda v: isinstance(v, list)
|
|
74
|
+
else:
|
|
75
|
+
logger.debug("AIEnhancer: skipping ModuleAnnotations field %r with unsupported type %r", field.name, hint)
|
|
76
|
+
return validators
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_ANNOTATION_FIELD_VALIDATORS = _build_annotation_field_validators()
|
|
31
80
|
|
|
32
81
|
|
|
33
82
|
class Enhancer(Protocol):
|
|
@@ -152,7 +201,7 @@ class AIEnhancer:
|
|
|
152
201
|
gaps.append("description")
|
|
153
202
|
if not module.documentation:
|
|
154
203
|
gaps.append("documentation")
|
|
155
|
-
if module.annotations is None or module.annotations ==
|
|
204
|
+
if module.annotations is None or module.annotations == DEFAULT_ANNOTATIONS:
|
|
156
205
|
gaps.append("annotations")
|
|
157
206
|
if not module.input_schema.get("properties"):
|
|
158
207
|
gaps.append("input_schema")
|
|
@@ -186,65 +235,26 @@ class AIEnhancer:
|
|
|
186
235
|
else:
|
|
187
236
|
warnings.append(f"Low confidence ({doc_conf:.2f}) for documentation — skipped. Review manually.")
|
|
188
237
|
|
|
189
|
-
# Apply annotations if above threshold
|
|
238
|
+
# Apply annotations if above threshold. Field set is derived from
|
|
239
|
+
# ModuleAnnotations at import time, so adding new fields upstream
|
|
240
|
+
# automatically widens what the SLM may populate (extra excluded).
|
|
190
241
|
if "annotations" in gaps and "annotations" in parsed and isinstance(parsed["annotations"], dict):
|
|
191
242
|
ann_data = parsed["annotations"]
|
|
192
243
|
ann_conf = parsed.get("confidence", {})
|
|
193
244
|
accepted: dict[str, Any] = {}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"
|
|
198
|
-
"
|
|
199
|
-
"open_world",
|
|
200
|
-
"streaming",
|
|
201
|
-
"cacheable",
|
|
202
|
-
"paginated",
|
|
203
|
-
)
|
|
204
|
-
for field in _BOOL_FIELDS:
|
|
205
|
-
if field in ann_data and isinstance(ann_data[field], bool):
|
|
206
|
-
field_conf = ann_conf.get(f"annotations.{field}", ann_conf.get(field, 0.0))
|
|
207
|
-
confidence[f"annotations.{field}"] = field_conf
|
|
208
|
-
if field_conf >= self.threshold:
|
|
209
|
-
accepted[field] = ann_data[field]
|
|
210
|
-
else:
|
|
211
|
-
warnings.append(
|
|
212
|
-
f"Low confidence ({field_conf:.2f}) for annotations.{field} — skipped. Review manually."
|
|
213
|
-
)
|
|
214
|
-
# Handle non-boolean annotation fields
|
|
215
|
-
_INT_FIELDS = ("cache_ttl",)
|
|
216
|
-
for field in _INT_FIELDS:
|
|
217
|
-
if field in ann_data and isinstance(ann_data[field], int):
|
|
218
|
-
field_conf = ann_conf.get(f"annotations.{field}", ann_conf.get(field, 0.0))
|
|
219
|
-
confidence[f"annotations.{field}"] = field_conf
|
|
220
|
-
if field_conf >= self.threshold:
|
|
221
|
-
accepted[field] = ann_data[field]
|
|
222
|
-
else:
|
|
223
|
-
warnings.append(
|
|
224
|
-
f"Low confidence ({field_conf:.2f}) for annotations.{field} — skipped. Review manually."
|
|
225
|
-
)
|
|
226
|
-
_STR_FIELDS = ("pagination_style",)
|
|
227
|
-
for field in _STR_FIELDS:
|
|
228
|
-
if field in ann_data and isinstance(ann_data[field], str):
|
|
229
|
-
field_conf = ann_conf.get(f"annotations.{field}", ann_conf.get(field, 0.0))
|
|
230
|
-
confidence[f"annotations.{field}"] = field_conf
|
|
231
|
-
if field_conf >= self.threshold:
|
|
232
|
-
accepted[field] = ann_data[field]
|
|
233
|
-
else:
|
|
234
|
-
warnings.append(
|
|
235
|
-
f"Low confidence ({field_conf:.2f}) for annotations.{field} — skipped. Review manually."
|
|
236
|
-
)
|
|
237
|
-
if "cache_key_fields" in ann_data and isinstance(ann_data["cache_key_fields"], list):
|
|
238
|
-
field_conf = ann_conf.get("annotations.cache_key_fields", ann_conf.get("cache_key_fields", 0.0))
|
|
239
|
-
confidence["annotations.cache_key_fields"] = field_conf
|
|
245
|
+
for field_name, validate in _ANNOTATION_FIELD_VALIDATORS.items():
|
|
246
|
+
if field_name not in ann_data or not validate(ann_data[field_name]):
|
|
247
|
+
continue
|
|
248
|
+
field_conf = ann_conf.get(f"annotations.{field_name}", ann_conf.get(field_name, 0.0))
|
|
249
|
+
confidence[f"annotations.{field_name}"] = field_conf
|
|
240
250
|
if field_conf >= self.threshold:
|
|
241
|
-
accepted[
|
|
251
|
+
accepted[field_name] = ann_data[field_name]
|
|
242
252
|
else:
|
|
243
253
|
warnings.append(
|
|
244
|
-
f"Low confidence ({field_conf:.2f}) for annotations.
|
|
254
|
+
f"Low confidence ({field_conf:.2f}) for annotations.{field_name} — skipped. Review manually."
|
|
245
255
|
)
|
|
246
256
|
if accepted:
|
|
247
|
-
base = module.annotations or
|
|
257
|
+
base = module.annotations or DEFAULT_ANNOTATIONS
|
|
248
258
|
updates["annotations"] = replace(base, **accepted)
|
|
249
259
|
|
|
250
260
|
# Apply input_schema if above threshold
|
|
@@ -85,6 +85,27 @@ def _deep_resolve_refs(
|
|
|
85
85
|
return result
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
def deep_resolve_refs(
|
|
89
|
+
schema: dict[str, Any],
|
|
90
|
+
openapi_doc: dict[str, Any],
|
|
91
|
+
depth: int = 0,
|
|
92
|
+
) -> dict[str, Any]:
|
|
93
|
+
"""Recursively resolve all ``$ref`` pointers in a schema.
|
|
94
|
+
|
|
95
|
+
Handles nested ``$ref``, ``allOf``, ``anyOf``, ``oneOf``, and ``items``.
|
|
96
|
+
Depth-limited to 16 levels to prevent infinite recursion on circular refs.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
schema: A JSON Schema dict (possibly containing ``$ref`` pointers).
|
|
100
|
+
openapi_doc: The full OpenAPI document dict.
|
|
101
|
+
depth: Current recursion depth (callers should not set this).
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
A new schema dict with all ``$ref`` pointers resolved.
|
|
105
|
+
"""
|
|
106
|
+
return _deep_resolve_refs(schema, openapi_doc, depth)
|
|
107
|
+
|
|
108
|
+
|
|
88
109
|
def extract_input_schema(
|
|
89
110
|
operation: dict[str, Any],
|
|
90
111
|
openapi_doc: dict[str, Any] | None = None,
|
{apcore_toolkit-0.4.0 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/http_proxy_writer.py
RENAMED
|
@@ -29,7 +29,7 @@ import re
|
|
|
29
29
|
from collections.abc import Callable
|
|
30
30
|
from typing import Any
|
|
31
31
|
|
|
32
|
-
from apcore import
|
|
32
|
+
from apcore import DEFAULT_ANNOTATIONS, Registry
|
|
33
33
|
from apcore_toolkit.output.types import WriteResult
|
|
34
34
|
from apcore_toolkit.types import ScannedModule
|
|
35
35
|
|
|
@@ -104,7 +104,7 @@ class HTTPProxyRegistryWriter:
|
|
|
104
104
|
auth_factory = self._auth_header_factory
|
|
105
105
|
timeout = self._timeout
|
|
106
106
|
|
|
107
|
-
annotations = mod.annotations or
|
|
107
|
+
annotations = mod.annotations or DEFAULT_ANNOTATIONS
|
|
108
108
|
|
|
109
109
|
# Raw dict schemas are auto-wrapped by Registry._ensure_schema_adapter
|
|
110
110
|
# (apcore >= 0.13.1) during register(), so no manual wrapping needed.
|
|
@@ -152,11 +152,13 @@ class HTTPProxyRegistryWriter:
|
|
|
152
152
|
return resp.json() # type: ignore[no-any-return]
|
|
153
153
|
|
|
154
154
|
error_msg = _extract_error_message(resp)
|
|
155
|
+
from apcore import ErrorCodes
|
|
155
156
|
from apcore.errors import ModuleError
|
|
156
157
|
|
|
157
158
|
raise ModuleError(
|
|
158
|
-
code=
|
|
159
|
-
message=error_msg,
|
|
159
|
+
code=ErrorCodes.MODULE_EXECUTE_ERROR,
|
|
160
|
+
message=f"HTTP {resp.status_code}: {error_msg}",
|
|
161
|
+
details={"http_status": resp.status_code},
|
|
160
162
|
)
|
|
161
163
|
|
|
162
164
|
ProxyModule.__name__ = mod.module_id.replace(".", "_")
|
|
@@ -12,7 +12,7 @@ from abc import ABC, abstractmethod
|
|
|
12
12
|
from dataclasses import replace
|
|
13
13
|
from typing import Any
|
|
14
14
|
|
|
15
|
-
from apcore import ModuleAnnotations, parse_docstring
|
|
15
|
+
from apcore import DEFAULT_ANNOTATIONS, ModuleAnnotations, parse_docstring
|
|
16
16
|
from apcore_toolkit.types import ScannedModule
|
|
17
17
|
|
|
18
18
|
|
|
@@ -97,12 +97,12 @@ class BaseScanner(ABC):
|
|
|
97
97
|
"""
|
|
98
98
|
method_upper = method.upper()
|
|
99
99
|
if method_upper == "GET":
|
|
100
|
-
return
|
|
100
|
+
return replace(DEFAULT_ANNOTATIONS, readonly=True, cacheable=True)
|
|
101
101
|
elif method_upper == "DELETE":
|
|
102
|
-
return
|
|
102
|
+
return replace(DEFAULT_ANNOTATIONS, destructive=True)
|
|
103
103
|
elif method_upper == "PUT":
|
|
104
|
-
return
|
|
105
|
-
return
|
|
104
|
+
return replace(DEFAULT_ANNOTATIONS, idempotent=True)
|
|
105
|
+
return DEFAULT_ANNOTATIONS
|
|
106
106
|
|
|
107
107
|
def deduplicate_ids(self, modules: list[ScannedModule]) -> list[ScannedModule]:
|
|
108
108
|
"""Resolve duplicate module IDs by appending _2, _3, etc.
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from apcore_toolkit.openapi import (
|
|
6
6
|
_deep_resolve_refs,
|
|
7
|
+
deep_resolve_refs,
|
|
7
8
|
extract_input_schema,
|
|
8
9
|
extract_output_schema,
|
|
9
10
|
resolve_ref,
|
|
@@ -168,6 +169,101 @@ class TestDeepResolveRefs:
|
|
|
168
169
|
assert OPENAPI_DOC["components"]["schemas"]["Address"]["properties"] == original_props
|
|
169
170
|
|
|
170
171
|
|
|
172
|
+
class TestDeepResolveRefsPublic:
|
|
173
|
+
"""Tests for the public deep_resolve_refs wrapper."""
|
|
174
|
+
|
|
175
|
+
def test_top_level_ref(self) -> None:
|
|
176
|
+
schema = {"$ref": "#/components/schemas/User"}
|
|
177
|
+
result = deep_resolve_refs(schema, OPENAPI_DOC)
|
|
178
|
+
assert result["type"] == "object"
|
|
179
|
+
assert "id" in result["properties"]
|
|
180
|
+
assert "name" in result["properties"]
|
|
181
|
+
|
|
182
|
+
def test_nested_ref_in_properties(self) -> None:
|
|
183
|
+
schema = {
|
|
184
|
+
"type": "object",
|
|
185
|
+
"properties": {
|
|
186
|
+
"address": {"$ref": "#/components/schemas/Address"},
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
result = deep_resolve_refs(schema, OPENAPI_DOC)
|
|
190
|
+
assert result["properties"]["address"]["type"] == "object"
|
|
191
|
+
assert "street" in result["properties"]["address"]["properties"]
|
|
192
|
+
|
|
193
|
+
def test_ref_in_allof(self) -> None:
|
|
194
|
+
schema = {
|
|
195
|
+
"allOf": [
|
|
196
|
+
{"$ref": "#/components/schemas/User"},
|
|
197
|
+
{"type": "object", "properties": {"extra": {"type": "boolean"}}},
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
result = deep_resolve_refs(schema, OPENAPI_DOC)
|
|
201
|
+
assert result["allOf"][0]["type"] == "object"
|
|
202
|
+
assert "id" in result["allOf"][0]["properties"]
|
|
203
|
+
|
|
204
|
+
def test_ref_in_anyof(self) -> None:
|
|
205
|
+
schema = {
|
|
206
|
+
"anyOf": [
|
|
207
|
+
{"$ref": "#/components/schemas/User"},
|
|
208
|
+
{"$ref": "#/components/schemas/Address"},
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
result = deep_resolve_refs(schema, OPENAPI_DOC)
|
|
212
|
+
assert "name" in result["anyOf"][0]["properties"]
|
|
213
|
+
assert "street" in result["anyOf"][1]["properties"]
|
|
214
|
+
|
|
215
|
+
def test_ref_in_oneof(self) -> None:
|
|
216
|
+
schema = {
|
|
217
|
+
"oneOf": [
|
|
218
|
+
{"$ref": "#/components/schemas/User"},
|
|
219
|
+
{"$ref": "#/components/schemas/Address"},
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
result = deep_resolve_refs(schema, OPENAPI_DOC)
|
|
223
|
+
assert result["oneOf"][0]["type"] == "object"
|
|
224
|
+
assert "name" in result["oneOf"][0]["properties"]
|
|
225
|
+
assert "city" in result["oneOf"][1]["properties"]
|
|
226
|
+
|
|
227
|
+
def test_ref_in_array_items(self) -> None:
|
|
228
|
+
schema = {"type": "array", "items": {"$ref": "#/components/schemas/User"}}
|
|
229
|
+
result = deep_resolve_refs(schema, OPENAPI_DOC)
|
|
230
|
+
assert result["items"]["type"] == "object"
|
|
231
|
+
assert "id" in result["items"]["properties"]
|
|
232
|
+
|
|
233
|
+
def test_deeply_nested_ref(self) -> None:
|
|
234
|
+
result = deep_resolve_refs({"$ref": "#/components/schemas/UserWithAddress"}, OPENAPI_DOC)
|
|
235
|
+
assert result["properties"]["address"]["type"] == "object"
|
|
236
|
+
assert "street" in result["properties"]["address"]["properties"]
|
|
237
|
+
|
|
238
|
+
def test_circular_ref_depth_limit(self) -> None:
|
|
239
|
+
result = deep_resolve_refs({"$ref": "#/components/schemas/SelfRef"}, OPENAPI_DOC)
|
|
240
|
+
assert result["type"] == "object"
|
|
241
|
+
assert "child" in result["properties"]
|
|
242
|
+
|
|
243
|
+
def test_no_mutation_of_original(self) -> None:
|
|
244
|
+
original_address = OPENAPI_DOC["components"]["schemas"]["Address"]
|
|
245
|
+
original_props = dict(original_address.get("properties", {}))
|
|
246
|
+
deep_resolve_refs({"$ref": "#/components/schemas/UserWithAddress"}, OPENAPI_DOC)
|
|
247
|
+
assert OPENAPI_DOC["components"]["schemas"]["Address"]["properties"] == original_props
|
|
248
|
+
|
|
249
|
+
def test_custom_depth_parameter(self) -> None:
|
|
250
|
+
"""Passing depth=17 should short-circuit immediately."""
|
|
251
|
+
schema = {"$ref": "#/components/schemas/User"}
|
|
252
|
+
result = deep_resolve_refs(schema, OPENAPI_DOC, depth=17)
|
|
253
|
+
# Should return the unresolved schema since depth > 16
|
|
254
|
+
assert "$ref" in result
|
|
255
|
+
|
|
256
|
+
def test_plain_schema_no_refs(self) -> None:
|
|
257
|
+
schema = {"type": "string", "description": "A simple string"}
|
|
258
|
+
result = deep_resolve_refs(schema, OPENAPI_DOC)
|
|
259
|
+
assert result == {"type": "string", "description": "A simple string"}
|
|
260
|
+
|
|
261
|
+
def test_importable_from_package(self) -> None:
|
|
262
|
+
from apcore_toolkit import deep_resolve_refs as public_fn
|
|
263
|
+
|
|
264
|
+
assert callable(public_fn)
|
|
265
|
+
|
|
266
|
+
|
|
171
267
|
class TestExtractInputSchema:
|
|
172
268
|
def test_query_and_path_params(self) -> None:
|
|
173
269
|
operation = {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|