apcore-toolkit 0.3.0__tar.gz → 0.4.0__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.3.0 → apcore_toolkit-0.4.0}/CHANGELOG.md +38 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/PKG-INFO +85 -10
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/README.md +79 -4
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/pyproject.toml +6 -6
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/__init__.py +4 -0
- apcore_toolkit-0.4.0/src/apcore_toolkit/convention_scanner.py +215 -0
- apcore_toolkit-0.4.0/src/apcore_toolkit/display/__init__.py +10 -0
- apcore_toolkit-0.4.0/src/apcore_toolkit/display/resolver.py +227 -0
- apcore_toolkit-0.4.0/tests/test_convention_scanner.py +118 -0
- apcore_toolkit-0.4.0/tests/test_display_resolver.py +419 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/.github/CODEOWNERS +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/.github/copilot-ignore +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/.github/workflows/ci.yml +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/.gitignore +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/.gitmessage +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/.pre-commit-config.yaml +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/ai_enhancer.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/formatting/__init__.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/formatting/markdown.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/openapi.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/output/__init__.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/output/errors.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/output/http_proxy_writer.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/output/python_writer.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/output/registry_writer.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/output/types.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/output/verifiers.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/output/yaml_writer.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/pydantic_utils.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/scanner.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/schema_utils.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/serializers.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/src/apcore_toolkit/types.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/tests/conftest.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/tests/test_ai_enhancer.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/tests/test_http_proxy_writer.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/tests/test_markdown.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/tests/test_openapi.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/tests/test_output_factory.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/tests/test_pydantic_utils.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/tests/test_python_writer.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/tests/test_registry_writer.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/tests/test_scanner.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/tests/test_schema_utils.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/tests/test_serializers.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/tests/test_types.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/tests/test_verifiers.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/tests/test_write_error.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/tests/test_write_result.py +0 -0
- {apcore_toolkit-0.3.0 → apcore_toolkit-0.4.0}/tests/test_yaml_writer.py +0 -0
|
@@ -2,6 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.4.0] - 2026-03-23
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`DisplayResolver`** (`apcore_toolkit.display`) — sparse binding.yaml display overlay (§5.13). Merges per-surface presentation fields (alias, description, guidance, tags, documentation) into `ScannedModule.metadata["display"]` for downstream CLI/MCP/A2A surfaces.
|
|
10
|
+
- Resolution chain per field: surface-specific override > `display` default > binding-level field > scanner value.
|
|
11
|
+
- `resolve(modules, *, binding_path=..., binding_data=...)` — accepts pre-parsed dict or a path to a `.binding.yaml` file / directory of `*.binding.yaml` files. `binding_data` takes precedence over `binding_path`.
|
|
12
|
+
- MCP alias auto-sanitization: replaces characters outside `[a-zA-Z0-9_-]` with `_`; prepends `_` if result starts with a digit.
|
|
13
|
+
- MCP alias hard limit: raises `ValueError` if sanitized alias exceeds 64 characters.
|
|
14
|
+
- CLI alias validation: warns and falls back to `display.alias` when user-explicitly-set alias does not match `^[a-z][a-z0-9_-]*$` (module_id fallback always accepted without warning).
|
|
15
|
+
- `suggested_alias` in `ScannedModule.metadata` (emitted by `simplify_ids=True` scanner) used as fallback when no `display.alias` is set.
|
|
16
|
+
- Match-count logging: `INFO` for match count, `WARNING` when binding map loaded but zero modules matched.
|
|
17
|
+
- **`DisplayResolver` public export** — available as `from apcore_toolkit.display import DisplayResolver` and `from apcore_toolkit import DisplayResolver`.
|
|
18
|
+
|
|
19
|
+
### Tests
|
|
20
|
+
|
|
21
|
+
- 30 new tests in `tests/test_display_resolver.py` covering: no-binding fallthrough, alias-only overlay, surface-specific overrides, MCP sanitization, MCP 64-char limit, `suggested_alias` fallback, sparse overlay (10 modules / 1 binding), tags resolution, `binding_path` file and directory loading, guidance chain, CLI invalid alias warning and fallback, `binding_data` vs `binding_path` precedence.
|
|
22
|
+
|
|
23
|
+
### Added (Convention Module Discovery — §5.14)
|
|
24
|
+
|
|
25
|
+
- **`ConventionScanner`** — scans a `commands/` directory of plain Python files for public functions and converts them to `ScannedModule` instances with schema inferred from type annotations.
|
|
26
|
+
- Module ID: `{file_prefix}.{function_name}` with `MODULE_PREFIX` override.
|
|
27
|
+
- Description from first line of docstring (`"(no description)"` fallback).
|
|
28
|
+
- `input_schema` / `output_schema` inferred from PEP 484 type hints.
|
|
29
|
+
- `CLI_GROUP` and `TAGS` module-level constants stored in metadata.
|
|
30
|
+
- `include` / `exclude` regex filters on module IDs.
|
|
31
|
+
- **`ConventionScanner` public export** — available as `from apcore_toolkit import ConventionScanner`.
|
|
32
|
+
|
|
33
|
+
### Tests (Convention Module Discovery)
|
|
34
|
+
|
|
35
|
+
- 15 new tests in `tests/test_convention_scanner.py`.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
## [0.3.1] - 2026-03-22
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
- Rebrand: aipartnerup → aiperceivable
|
|
42
|
+
|
|
5
43
|
## [0.3.0] - 2026-03-19
|
|
6
44
|
|
|
7
45
|
### Added
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: apcore-toolkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Shared scanner, schema extraction, and output toolkit for apcore framework adapters
|
|
5
|
-
Project-URL: Homepage, https://
|
|
6
|
-
Project-URL: Repository, https://github.com/
|
|
7
|
-
Project-URL: Documentation, https://github.com/
|
|
8
|
-
Project-URL: Issues, https://github.com/
|
|
9
|
-
Author-email:
|
|
5
|
+
Project-URL: Homepage, https://aiperceivable.com
|
|
6
|
+
Project-URL: Repository, https://github.com/aiperceivable/apcore-toolkit-python
|
|
7
|
+
Project-URL: Documentation, https://github.com/aiperceivable/apcore-toolkit-python#readme
|
|
8
|
+
Project-URL: Issues, https://github.com/aiperceivable/apcore-toolkit-python/issues
|
|
9
|
+
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
|
|
@@ -25,12 +25,12 @@ Requires-Dist: httpx>=0.24; extra == 'http-proxy'
|
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
|
|
27
27
|
<div align="center">
|
|
28
|
-
<img src="https://raw.githubusercontent.com/
|
|
28
|
+
<img src="https://raw.githubusercontent.com/aiperceivable/apcore-toolkit/main/apcore-toolkit-logo.svg" alt="apcore-toolkit logo" width="200"/>
|
|
29
29
|
</div>
|
|
30
30
|
|
|
31
31
|
# apcore-toolkit-python
|
|
32
32
|
|
|
33
|
-
Python implementation of the [apcore-toolkit](https://github.com/
|
|
33
|
+
Python implementation of the [apcore-toolkit](https://github.com/aiperceivable/apcore-toolkit).
|
|
34
34
|
|
|
35
35
|
Extracts ~1,400 lines of duplicated framework-agnostic logic from `django-apcore` and `flask-apcore` into a standalone Python package.
|
|
36
36
|
|
|
@@ -67,6 +67,8 @@ pip install apcore-toolkit
|
|
|
67
67
|
| `resolve_target` | Resolves "module.path:function_name" to callable |
|
|
68
68
|
| `enrich_schema_descriptions` | Merges descriptions into JSON Schema properties |
|
|
69
69
|
| `get_writer` | Factory function for writer instances |
|
|
70
|
+
| `DisplayResolver` | Sparse binding.yaml display overlay — resolves surface-facing alias, description, guidance, tags into `metadata["display"]` (§5.13) |
|
|
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) |
|
|
70
72
|
|
|
71
73
|
## Usage
|
|
72
74
|
|
|
@@ -162,16 +164,89 @@ from apcore_toolkit import to_markdown
|
|
|
162
164
|
md = to_markdown({"name": "Alice", "role": "admin"}, title="User Info")
|
|
163
165
|
```
|
|
164
166
|
|
|
167
|
+
### Display Overlay (§5.13)
|
|
168
|
+
|
|
169
|
+
`DisplayResolver` applies a sparse `binding.yaml` display overlay to a list of `ScannedModule` instances, populating `metadata["display"]` with surface-facing presentation fields (alias, description, guidance, tags) for CLI, MCP, and A2A surfaces.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from apcore_toolkit.display import DisplayResolver
|
|
173
|
+
|
|
174
|
+
resolver = DisplayResolver()
|
|
175
|
+
|
|
176
|
+
# Apply overlay from a directory of *.binding.yaml files
|
|
177
|
+
modules = resolver.resolve(scanned_modules, binding_path="bindings/")
|
|
178
|
+
|
|
179
|
+
# Or from a pre-parsed dict
|
|
180
|
+
modules = resolver.resolve(
|
|
181
|
+
scanned_modules,
|
|
182
|
+
binding_data={
|
|
183
|
+
"bindings": [
|
|
184
|
+
{
|
|
185
|
+
"module_id": "product.get",
|
|
186
|
+
"display": {
|
|
187
|
+
"alias": "product-get",
|
|
188
|
+
"description": "Get a product by ID",
|
|
189
|
+
"cli": {"alias": "get-product"},
|
|
190
|
+
"mcp": {"alias": "get_product"},
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
]
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Resolved fields are in metadata["display"]
|
|
198
|
+
mod = modules[0]
|
|
199
|
+
print(mod.metadata["display"]["cli"]["alias"]) # "get-product"
|
|
200
|
+
print(mod.metadata["display"]["mcp"]["alias"]) # "get_product"
|
|
201
|
+
print(mod.metadata["display"]["a2a"]["alias"]) # "product-get"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Resolution chain** (per field): surface-specific override > `display` default > binding-level field > scanner value.
|
|
205
|
+
|
|
206
|
+
**MCP alias constraints**: automatically sanitized (non-`[a-zA-Z0-9_-]` chars replaced with `_`; leading digit prefixed with `_`); raises `ValueError` if result exceeds 64 characters.
|
|
207
|
+
|
|
208
|
+
**CLI alias validation**: warns and falls back to `display.alias` when a user-explicitly-set alias does not match `^[a-z][a-z0-9_-]*$`.
|
|
209
|
+
|
|
210
|
+
### Convention Module Discovery (§5.14)
|
|
211
|
+
|
|
212
|
+
`ConventionScanner` scans a directory of plain Python files for public functions and converts them to `ScannedModule` instances. No decorators, no base classes, no imports from apcore -- just functions with type hints.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from apcore_toolkit import ConventionScanner
|
|
216
|
+
|
|
217
|
+
scanner = ConventionScanner()
|
|
218
|
+
modules = scanner.scan(commands_dir="commands/")
|
|
219
|
+
|
|
220
|
+
# Each public function becomes a module:
|
|
221
|
+
# commands/deploy.py -> deploy.deploy
|
|
222
|
+
# commands/deploy.py -> deploy.rollback (if rollback() exists)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Module-level constants customize behavior:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
# commands/deploy.py
|
|
229
|
+
MODULE_PREFIX = "ops" # override file-based prefix -> ops.deploy
|
|
230
|
+
CLI_GROUP = "operations" # group hint for CLI surface
|
|
231
|
+
TAGS = ["infra", "deploy"] # tags stored in metadata
|
|
232
|
+
|
|
233
|
+
def deploy(env: str, tag: str = "latest") -> dict:
|
|
234
|
+
"""Deploy the app to the given environment."""
|
|
235
|
+
return {"status": "deployed", "env": env}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Input and output schemas are inferred from PEP 484 type annotations. Use `include` / `exclude` regex filters to control which module IDs are registered.
|
|
239
|
+
|
|
165
240
|
## Requirements
|
|
166
241
|
|
|
167
242
|
- Python >= 3.11
|
|
168
|
-
- apcore >= 0.13.
|
|
243
|
+
- apcore >= 0.13.1
|
|
169
244
|
- pydantic >= 2.0
|
|
170
245
|
- PyYAML >= 6.0
|
|
171
246
|
|
|
172
247
|
## Documentation
|
|
173
248
|
|
|
174
|
-
Full documentation is available at [https://github.com/
|
|
249
|
+
Full documentation is available at [https://github.com/aiperceivable/apcore-toolkit](https://github.com/aiperceivable/apcore-toolkit).
|
|
175
250
|
|
|
176
251
|
## License
|
|
177
252
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/
|
|
2
|
+
<img src="https://raw.githubusercontent.com/aiperceivable/apcore-toolkit/main/apcore-toolkit-logo.svg" alt="apcore-toolkit logo" width="200"/>
|
|
3
3
|
</div>
|
|
4
4
|
|
|
5
5
|
# apcore-toolkit-python
|
|
6
6
|
|
|
7
|
-
Python implementation of the [apcore-toolkit](https://github.com/
|
|
7
|
+
Python implementation of the [apcore-toolkit](https://github.com/aiperceivable/apcore-toolkit).
|
|
8
8
|
|
|
9
9
|
Extracts ~1,400 lines of duplicated framework-agnostic logic from `django-apcore` and `flask-apcore` into a standalone Python package.
|
|
10
10
|
|
|
@@ -41,6 +41,8 @@ pip install apcore-toolkit
|
|
|
41
41
|
| `resolve_target` | Resolves "module.path:function_name" to callable |
|
|
42
42
|
| `enrich_schema_descriptions` | Merges descriptions into JSON Schema properties |
|
|
43
43
|
| `get_writer` | Factory function for writer instances |
|
|
44
|
+
| `DisplayResolver` | Sparse binding.yaml display overlay — resolves surface-facing alias, description, guidance, tags into `metadata["display"]` (§5.13) |
|
|
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) |
|
|
44
46
|
|
|
45
47
|
## Usage
|
|
46
48
|
|
|
@@ -136,16 +138,89 @@ from apcore_toolkit import to_markdown
|
|
|
136
138
|
md = to_markdown({"name": "Alice", "role": "admin"}, title="User Info")
|
|
137
139
|
```
|
|
138
140
|
|
|
141
|
+
### Display Overlay (§5.13)
|
|
142
|
+
|
|
143
|
+
`DisplayResolver` applies a sparse `binding.yaml` display overlay to a list of `ScannedModule` instances, populating `metadata["display"]` with surface-facing presentation fields (alias, description, guidance, tags) for CLI, MCP, and A2A surfaces.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from apcore_toolkit.display import DisplayResolver
|
|
147
|
+
|
|
148
|
+
resolver = DisplayResolver()
|
|
149
|
+
|
|
150
|
+
# Apply overlay from a directory of *.binding.yaml files
|
|
151
|
+
modules = resolver.resolve(scanned_modules, binding_path="bindings/")
|
|
152
|
+
|
|
153
|
+
# Or from a pre-parsed dict
|
|
154
|
+
modules = resolver.resolve(
|
|
155
|
+
scanned_modules,
|
|
156
|
+
binding_data={
|
|
157
|
+
"bindings": [
|
|
158
|
+
{
|
|
159
|
+
"module_id": "product.get",
|
|
160
|
+
"display": {
|
|
161
|
+
"alias": "product-get",
|
|
162
|
+
"description": "Get a product by ID",
|
|
163
|
+
"cli": {"alias": "get-product"},
|
|
164
|
+
"mcp": {"alias": "get_product"},
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Resolved fields are in metadata["display"]
|
|
172
|
+
mod = modules[0]
|
|
173
|
+
print(mod.metadata["display"]["cli"]["alias"]) # "get-product"
|
|
174
|
+
print(mod.metadata["display"]["mcp"]["alias"]) # "get_product"
|
|
175
|
+
print(mod.metadata["display"]["a2a"]["alias"]) # "product-get"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Resolution chain** (per field): surface-specific override > `display` default > binding-level field > scanner value.
|
|
179
|
+
|
|
180
|
+
**MCP alias constraints**: automatically sanitized (non-`[a-zA-Z0-9_-]` chars replaced with `_`; leading digit prefixed with `_`); raises `ValueError` if result exceeds 64 characters.
|
|
181
|
+
|
|
182
|
+
**CLI alias validation**: warns and falls back to `display.alias` when a user-explicitly-set alias does not match `^[a-z][a-z0-9_-]*$`.
|
|
183
|
+
|
|
184
|
+
### Convention Module Discovery (§5.14)
|
|
185
|
+
|
|
186
|
+
`ConventionScanner` scans a directory of plain Python files for public functions and converts them to `ScannedModule` instances. No decorators, no base classes, no imports from apcore -- just functions with type hints.
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from apcore_toolkit import ConventionScanner
|
|
190
|
+
|
|
191
|
+
scanner = ConventionScanner()
|
|
192
|
+
modules = scanner.scan(commands_dir="commands/")
|
|
193
|
+
|
|
194
|
+
# Each public function becomes a module:
|
|
195
|
+
# commands/deploy.py -> deploy.deploy
|
|
196
|
+
# commands/deploy.py -> deploy.rollback (if rollback() exists)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Module-level constants customize behavior:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
# commands/deploy.py
|
|
203
|
+
MODULE_PREFIX = "ops" # override file-based prefix -> ops.deploy
|
|
204
|
+
CLI_GROUP = "operations" # group hint for CLI surface
|
|
205
|
+
TAGS = ["infra", "deploy"] # tags stored in metadata
|
|
206
|
+
|
|
207
|
+
def deploy(env: str, tag: str = "latest") -> dict:
|
|
208
|
+
"""Deploy the app to the given environment."""
|
|
209
|
+
return {"status": "deployed", "env": env}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Input and output schemas are inferred from PEP 484 type annotations. Use `include` / `exclude` regex filters to control which module IDs are registered.
|
|
213
|
+
|
|
139
214
|
## Requirements
|
|
140
215
|
|
|
141
216
|
- Python >= 3.11
|
|
142
|
-
- apcore >= 0.13.
|
|
217
|
+
- apcore >= 0.13.1
|
|
143
218
|
- pydantic >= 2.0
|
|
144
219
|
- PyYAML >= 6.0
|
|
145
220
|
|
|
146
221
|
## Documentation
|
|
147
222
|
|
|
148
|
-
Full documentation is available at [https://github.com/
|
|
223
|
+
Full documentation is available at [https://github.com/aiperceivable/apcore-toolkit](https://github.com/aiperceivable/apcore-toolkit).
|
|
149
224
|
|
|
150
225
|
## License
|
|
151
226
|
|
|
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "apcore-toolkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
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"
|
|
11
11
|
license = "Apache-2.0"
|
|
12
12
|
authors = [
|
|
13
|
-
{ name = "
|
|
13
|
+
{ name = "aiperceivable", email = "tercel.yi@gmail.com" },
|
|
14
14
|
]
|
|
15
15
|
keywords = [
|
|
16
16
|
"apcore",
|
|
@@ -40,10 +40,10 @@ dev = [
|
|
|
40
40
|
]
|
|
41
41
|
|
|
42
42
|
[project.urls]
|
|
43
|
-
Homepage = "https://
|
|
44
|
-
Repository = "https://github.com/
|
|
45
|
-
Documentation = "https://github.com/
|
|
46
|
-
Issues = "https://github.com/
|
|
43
|
+
Homepage = "https://aiperceivable.com"
|
|
44
|
+
Repository = "https://github.com/aiperceivable/apcore-toolkit-python"
|
|
45
|
+
Documentation = "https://github.com/aiperceivable/apcore-toolkit-python#readme"
|
|
46
|
+
Issues = "https://github.com/aiperceivable/apcore-toolkit-python/issues"
|
|
47
47
|
|
|
48
48
|
[tool.hatch.build.targets.wheel]
|
|
49
49
|
packages = ["src/apcore_toolkit"]
|
|
@@ -6,6 +6,7 @@ Public API re-exports for convenient access to core types and utilities.
|
|
|
6
6
|
from importlib.metadata import PackageNotFoundError
|
|
7
7
|
from importlib.metadata import version as _get_version
|
|
8
8
|
from apcore_toolkit.ai_enhancer import AIEnhancer, Enhancer
|
|
9
|
+
from apcore_toolkit.display import DisplayResolver
|
|
9
10
|
from apcore_toolkit.formatting import to_markdown
|
|
10
11
|
from apcore_toolkit.openapi import (
|
|
11
12
|
extract_input_schema,
|
|
@@ -29,6 +30,7 @@ from apcore_toolkit.output.verifiers import (
|
|
|
29
30
|
)
|
|
30
31
|
from apcore_toolkit.output.yaml_writer import YAMLWriter
|
|
31
32
|
from apcore_toolkit.pydantic_utils import flatten_pydantic_params, resolve_target
|
|
33
|
+
from apcore_toolkit.convention_scanner import ConventionScanner
|
|
32
34
|
from apcore_toolkit.scanner import BaseScanner
|
|
33
35
|
from apcore_toolkit.schema_utils import enrich_schema_descriptions
|
|
34
36
|
from apcore_toolkit.serializers import annotations_to_dict, module_to_dict, modules_to_dicts
|
|
@@ -41,7 +43,9 @@ except PackageNotFoundError:
|
|
|
41
43
|
|
|
42
44
|
__all__ = [
|
|
43
45
|
"AIEnhancer",
|
|
46
|
+
"DisplayResolver",
|
|
44
47
|
"BaseScanner",
|
|
48
|
+
"ConventionScanner",
|
|
45
49
|
"HTTPProxyRegistryWriter",
|
|
46
50
|
"Enhancer",
|
|
47
51
|
"JSONVerifier",
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Convention Module Scanner — discovers plain functions as apcore modules (§5.14)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import inspect
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, get_type_hints
|
|
12
|
+
|
|
13
|
+
from apcore_toolkit.types import ScannedModule
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("apcore_toolkit")
|
|
16
|
+
|
|
17
|
+
# Type hint → JSON Schema mapping (subset of §5.11.5)
|
|
18
|
+
_TYPE_MAP: dict[type, dict[str, Any]] = {
|
|
19
|
+
str: {"type": "string"},
|
|
20
|
+
int: {"type": "integer"},
|
|
21
|
+
float: {"type": "number"},
|
|
22
|
+
bool: {"type": "boolean"},
|
|
23
|
+
list: {"type": "array"},
|
|
24
|
+
dict: {"type": "object"},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConventionScanner:
|
|
29
|
+
"""Scan a directory of plain Python files for public functions.
|
|
30
|
+
|
|
31
|
+
Converts each discovered function into a ScannedModule with
|
|
32
|
+
schema inferred from type annotations and description from docstrings.
|
|
33
|
+
|
|
34
|
+
Usage::
|
|
35
|
+
|
|
36
|
+
scanner = ConventionScanner()
|
|
37
|
+
modules = scanner.scan("commands/")
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def scan(
|
|
41
|
+
self,
|
|
42
|
+
commands_dir: str | Path,
|
|
43
|
+
*,
|
|
44
|
+
include: str | None = None,
|
|
45
|
+
exclude: str | None = None,
|
|
46
|
+
) -> list[ScannedModule]:
|
|
47
|
+
"""Scan a commands directory for convention modules.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
commands_dir: Path to the commands directory.
|
|
51
|
+
include: Regex pattern to include module IDs.
|
|
52
|
+
exclude: Regex pattern to exclude module IDs.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
List of ScannedModule instances.
|
|
56
|
+
"""
|
|
57
|
+
commands_path = Path(commands_dir)
|
|
58
|
+
if not commands_path.is_dir():
|
|
59
|
+
logger.warning("ConventionScanner: commands directory not found: %s", commands_path)
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
modules: list[ScannedModule] = []
|
|
63
|
+
for py_file in sorted(commands_path.rglob("*.py")):
|
|
64
|
+
if py_file.name.startswith("_"):
|
|
65
|
+
continue
|
|
66
|
+
try:
|
|
67
|
+
file_modules = self._scan_file(py_file, commands_path)
|
|
68
|
+
modules.extend(file_modules)
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
logger.warning("ConventionScanner: failed to scan %s: %s", py_file, exc)
|
|
71
|
+
|
|
72
|
+
# Apply include/exclude filters
|
|
73
|
+
if include:
|
|
74
|
+
pattern = re.compile(include)
|
|
75
|
+
modules = [m for m in modules if pattern.search(m.module_id)]
|
|
76
|
+
if exclude:
|
|
77
|
+
pattern = re.compile(exclude)
|
|
78
|
+
modules = [m for m in modules if not pattern.search(m.module_id)]
|
|
79
|
+
|
|
80
|
+
logger.info("ConventionScanner: discovered %d modules from %s", len(modules), commands_path)
|
|
81
|
+
return modules
|
|
82
|
+
|
|
83
|
+
def _scan_file(self, py_file: Path, base_dir: Path) -> list[ScannedModule]:
|
|
84
|
+
"""Scan a single Python file for public functions."""
|
|
85
|
+
# Derive prefix from relative path
|
|
86
|
+
rel = py_file.relative_to(base_dir)
|
|
87
|
+
parts = list(rel.with_suffix("").parts)
|
|
88
|
+
file_prefix = ".".join(parts)
|
|
89
|
+
|
|
90
|
+
# Load the module
|
|
91
|
+
spec = importlib.util.spec_from_file_location(f"_convention_{file_prefix}", py_file)
|
|
92
|
+
if spec is None or spec.loader is None:
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
mod = importlib.util.module_from_spec(spec)
|
|
96
|
+
# Add parent directory to sys.path temporarily for imports
|
|
97
|
+
parent = str(py_file.parent)
|
|
98
|
+
added_path = parent not in sys.path
|
|
99
|
+
if added_path:
|
|
100
|
+
sys.path.insert(0, parent)
|
|
101
|
+
try:
|
|
102
|
+
spec.loader.exec_module(mod)
|
|
103
|
+
finally:
|
|
104
|
+
if added_path and parent in sys.path:
|
|
105
|
+
sys.path.remove(parent)
|
|
106
|
+
|
|
107
|
+
# Read module-level constants
|
|
108
|
+
module_prefix = getattr(mod, "MODULE_PREFIX", None)
|
|
109
|
+
cli_group = getattr(mod, "CLI_GROUP", None)
|
|
110
|
+
file_tags = getattr(mod, "TAGS", None) or []
|
|
111
|
+
|
|
112
|
+
prefix = module_prefix if isinstance(module_prefix, str) else file_prefix
|
|
113
|
+
|
|
114
|
+
# Discover public functions
|
|
115
|
+
results: list[ScannedModule] = []
|
|
116
|
+
for name, obj in inspect.getmembers(mod, inspect.isfunction):
|
|
117
|
+
# Skip private functions and imported functions
|
|
118
|
+
if name.startswith("_"):
|
|
119
|
+
continue
|
|
120
|
+
if getattr(obj, "__module__", None) != mod.__name__:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
module_id = f"{prefix}.{name}"
|
|
124
|
+
description = self._extract_description(obj)
|
|
125
|
+
input_schema = self._build_input_schema(obj)
|
|
126
|
+
output_schema = self._build_output_schema(obj)
|
|
127
|
+
|
|
128
|
+
metadata: dict[str, Any] = {}
|
|
129
|
+
if isinstance(cli_group, str):
|
|
130
|
+
metadata.setdefault("display", {}).setdefault("cli", {})["group"] = cli_group
|
|
131
|
+
|
|
132
|
+
results.append(
|
|
133
|
+
ScannedModule(
|
|
134
|
+
module_id=module_id,
|
|
135
|
+
description=description,
|
|
136
|
+
input_schema=input_schema,
|
|
137
|
+
output_schema=output_schema,
|
|
138
|
+
tags=list(file_tags),
|
|
139
|
+
target=f"{py_file}:{name}",
|
|
140
|
+
metadata=metadata,
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return results
|
|
145
|
+
|
|
146
|
+
def _extract_description(self, func: Any) -> str:
|
|
147
|
+
"""Extract first line of docstring as description."""
|
|
148
|
+
doc = inspect.getdoc(func)
|
|
149
|
+
if not doc:
|
|
150
|
+
return "(no description)"
|
|
151
|
+
return doc.split("\n")[0].strip()
|
|
152
|
+
|
|
153
|
+
def _build_input_schema(self, func: Any) -> dict[str, Any]:
|
|
154
|
+
"""Build JSON Schema from function parameter type hints."""
|
|
155
|
+
try:
|
|
156
|
+
hints = get_type_hints(func)
|
|
157
|
+
except Exception:
|
|
158
|
+
hints = {}
|
|
159
|
+
|
|
160
|
+
sig = inspect.signature(func)
|
|
161
|
+
properties: dict[str, Any] = {}
|
|
162
|
+
required: list[str] = []
|
|
163
|
+
|
|
164
|
+
for param_name, param in sig.parameters.items():
|
|
165
|
+
if param_name in ("self", "cls", "ctx", "context"):
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
type_hint = hints.get(param_name)
|
|
169
|
+
prop = self._type_to_schema(type_hint)
|
|
170
|
+
|
|
171
|
+
if param.default is inspect.Parameter.empty:
|
|
172
|
+
required.append(param_name)
|
|
173
|
+
else:
|
|
174
|
+
if param.default is not None:
|
|
175
|
+
prop["default"] = param.default
|
|
176
|
+
|
|
177
|
+
properties[param_name] = prop
|
|
178
|
+
|
|
179
|
+
schema: dict[str, Any] = {"type": "object", "properties": properties}
|
|
180
|
+
if required:
|
|
181
|
+
schema["required"] = required
|
|
182
|
+
return schema
|
|
183
|
+
|
|
184
|
+
def _build_output_schema(self, func: Any) -> dict[str, Any]:
|
|
185
|
+
"""Build output schema from return type hint."""
|
|
186
|
+
try:
|
|
187
|
+
hints = get_type_hints(func)
|
|
188
|
+
except Exception:
|
|
189
|
+
return {}
|
|
190
|
+
|
|
191
|
+
ret = hints.get("return")
|
|
192
|
+
if ret is None or ret is type(None):
|
|
193
|
+
return {}
|
|
194
|
+
return self._type_to_schema(ret)
|
|
195
|
+
|
|
196
|
+
def _type_to_schema(self, type_hint: Any) -> dict[str, Any]:
|
|
197
|
+
"""Convert a Python type hint to JSON Schema."""
|
|
198
|
+
if type_hint is None:
|
|
199
|
+
return {"type": "string"}
|
|
200
|
+
|
|
201
|
+
# Direct type mapping
|
|
202
|
+
if type_hint in _TYPE_MAP:
|
|
203
|
+
return dict(_TYPE_MAP[type_hint])
|
|
204
|
+
|
|
205
|
+
# Handle Optional[X] / X | None
|
|
206
|
+
origin = getattr(type_hint, "__origin__", None)
|
|
207
|
+
args = getattr(type_hint, "__args__", None)
|
|
208
|
+
|
|
209
|
+
if origin is list and args:
|
|
210
|
+
return {"type": "array", "items": self._type_to_schema(args[0])}
|
|
211
|
+
if origin is dict:
|
|
212
|
+
return {"type": "object"}
|
|
213
|
+
|
|
214
|
+
# Fallback
|
|
215
|
+
return {"type": "string"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Display overlay resolver for surface-facing presentation.
|
|
2
|
+
|
|
3
|
+
Implements §5.13 of the apcore PROTOCOL_SPEC: sparse binding.yaml display
|
|
4
|
+
section that controls how modules appear in CLI, MCP, and A2A surfaces
|
|
5
|
+
without changing the canonical module_id.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from apcore_toolkit.display.resolver import DisplayResolver
|
|
9
|
+
|
|
10
|
+
__all__ = ["DisplayResolver"]
|