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.
Files changed (50) hide show
  1. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/PKG-INFO +2 -2
  2. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/pyproject.toml +2 -2
  3. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/ai_enhancer.py +64 -54
  4. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/http_proxy_writer.py +6 -4
  5. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/scanner.py +5 -5
  6. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/.github/CODEOWNERS +0 -0
  7. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/.github/copilot-ignore +0 -0
  8. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/.github/workflows/ci.yml +0 -0
  9. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/.gitignore +0 -0
  10. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/.gitmessage +0 -0
  11. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/.pre-commit-config.yaml +0 -0
  12. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/CHANGELOG.md +0 -0
  13. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/README.md +0 -0
  14. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/__init__.py +0 -0
  15. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/convention_scanner.py +0 -0
  16. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/display/__init__.py +0 -0
  17. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/display/resolver.py +0 -0
  18. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/formatting/__init__.py +0 -0
  19. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/formatting/markdown.py +0 -0
  20. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/openapi.py +0 -0
  21. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/__init__.py +0 -0
  22. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/errors.py +0 -0
  23. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/python_writer.py +0 -0
  24. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/registry_writer.py +0 -0
  25. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/types.py +0 -0
  26. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/verifiers.py +0 -0
  27. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/output/yaml_writer.py +0 -0
  28. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/pydantic_utils.py +0 -0
  29. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/schema_utils.py +0 -0
  30. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/serializers.py +0 -0
  31. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/src/apcore_toolkit/types.py +0 -0
  32. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/conftest.py +0 -0
  33. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_ai_enhancer.py +0 -0
  34. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_convention_scanner.py +0 -0
  35. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_display_resolver.py +0 -0
  36. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_http_proxy_writer.py +0 -0
  37. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_markdown.py +0 -0
  38. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_openapi.py +0 -0
  39. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_output_factory.py +0 -0
  40. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_pydantic_utils.py +0 -0
  41. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_python_writer.py +0 -0
  42. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_registry_writer.py +0 -0
  43. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_scanner.py +0 -0
  44. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_schema_utils.py +0 -0
  45. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_serializers.py +0 -0
  46. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_types.py +0 -0
  47. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_verifiers.py +0 -0
  48. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_write_error.py +0 -0
  49. {apcore_toolkit-0.4.1 → apcore_toolkit-0.4.2}/tests/test_write_result.py +0 -0
  50. {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.1
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.14.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.1"
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.14.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
- _DEFAULT_ANNOTATIONS = ModuleAnnotations()
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 == _DEFAULT_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
- _BOOL_FIELDS = (
195
- "readonly",
196
- "destructive",
197
- "idempotent",
198
- "requires_approval",
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["cache_key_fields"] = ann_data["cache_key_fields"]
251
+ accepted[field_name] = ann_data[field_name]
242
252
  else:
243
253
  warnings.append(
244
- f"Low confidence ({field_conf:.2f}) for annotations.cache_key_fields — skipped. Review manually."
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 ModuleAnnotations()
257
+ base = module.annotations or DEFAULT_ANNOTATIONS
248
258
  updates["annotations"] = replace(base, **accepted)
249
259
 
250
260
  # Apply input_schema if above threshold
@@ -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 ModuleAnnotations, Registry
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 ModuleAnnotations()
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=f"HTTP_{resp.status_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 ModuleAnnotations(readonly=True, cacheable=True)
100
+ return replace(DEFAULT_ANNOTATIONS, readonly=True, cacheable=True)
101
101
  elif method_upper == "DELETE":
102
- return ModuleAnnotations(destructive=True)
102
+ return replace(DEFAULT_ANNOTATIONS, destructive=True)
103
103
  elif method_upper == "PUT":
104
- return ModuleAnnotations(idempotent=True)
105
- return ModuleAnnotations()
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