apcore-toolkit 0.4.1__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/PKG-INFO +2 -2
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/pyproject.toml +2 -2
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/ai_enhancer.py +64 -54
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/http_proxy_writer.py +6 -4
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/scanner.py +5 -5
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/.github/CODEOWNERS +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/.github/copilot-ignore +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/.github/workflows/ci.yml +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/.gitignore +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/.gitmessage +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/.pre-commit-config.yaml +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/CHANGELOG.md +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/README.md +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/__init__.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/convention_scanner.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/display/__init__.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/display/resolver.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/formatting/__init__.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/formatting/markdown.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/openapi.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/__init__.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/errors.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/python_writer.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/registry_writer.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/types.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/verifiers.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/yaml_writer.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/pydantic_utils.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/schema_utils.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/serializers.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/types.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/conftest.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_ai_enhancer.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_convention_scanner.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_display_resolver.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_http_proxy_writer.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_markdown.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_openapi.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_output_factory.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_pydantic_utils.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_python_writer.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_registry_writer.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_scanner.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_schema_utils.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_serializers.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_types.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_verifiers.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_write_error.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_write_result.py +0 -0
- {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_yaml_writer.py +0 -0
|
@@ -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
|
|
@@ -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
|
]
|
|
@@ -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
|
{apcore_toolkit-0.4.1 → 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.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|