Skip to content

Commit ed44b75

Browse files
committed
feat: introduce APCore unified client and update global convenience functions to support version_hint
1 parent 33964c5 commit ed44b75

2 files changed

Lines changed: 27 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88

9-
## [0.18.0] - 2026-04-08
9+
## [0.18.0] - 2026-04-09
1010

1111
### Added
1212

13+
- **`APCore` unified client class** (`apcore.client.APCore`) — High-level facade over `Registry` + `Executor` providing a single entry point for all module operations. Constructor accepts optional `registry`, `executor`, `config`, and `metrics_collector` (auto-created when `sys_modules.enabled`). Public API surface:
14+
- **Module management**: `module()` decorator, `register()`, `list_modules(tags=, prefix=)`, `discover()`, `describe()`
15+
- **Execution**: `call()`, `call_async()`, `stream()`, `validate()` — all accept `version_hint` for semver negotiation (A14)
16+
- **Middleware**: `use()`, `use_before()`, `use_after()`, `remove()``use`/`use_before`/`use_after` return `self` for chaining
17+
- **Events**: `events` property, `on(event_type, handler)`, `off(subscriber)` — requires `sys_modules.events.enabled` in config
18+
- **Module toggle**: `disable(module_id, reason=)`, `enable(module_id, reason=)` — wrappers around `system.control.toggle_feature`
19+
- Cross-language parity: matches apcore-typescript `APCore` class and apcore-rust `APCore` struct public API surface
20+
- **Package-level global convenience functions** (`apcore.call`, `apcore.call_async`, `apcore.stream`, `apcore.validate`, `apcore.register`, `apcore.describe`, `apcore.use`, `apcore.use_before`, `apcore.use_after`, `apcore.remove`, `apcore.discover`, `apcore.list_modules`, `apcore.on`, `apcore.off`, `apcore.disable`, `apcore.enable`, `apcore.module`) — delegate to a module-level `_default_client = APCore()` instance for zero-setup usage (`import apcore; apcore.call("math.add", {"a": 1, "b": 2})`). Python-specific ergonomic; apcore-typescript and apcore-rust require explicit client construction.
1321
- **Pipeline preset builders re-exported at package root**`build_standard_strategy`, `build_internal_strategy`, `build_testing_strategy`, `build_performance_strategy`, `build_minimal_strategy` are now importable directly from `apcore`. These functions existed in `apcore.builtin_steps` but were not previously in `apcore.__all__`. Parity with apcore-typescript (`buildXxxStrategy`) and apcore-rust (`build_xxx_strategy` at the crate root).
1422
- **`TestRegisterInternalValidation`** test class in `tests/registry/test_registry.py` (6 parity tests covering empty rejection, pattern rejection, over-length rejection, reserved-word bypass, duplicate rejection, accept-at-max-length) plus `test_pipeline_preset_builders_*` in `tests/test_public_api.py`.
1523

@@ -50,6 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5058

5159
### Fixed
5260

61+
- **Global convenience functions `call()`, `call_async()`, `stream()` missing `version_hint` parameter** — These `apcore/__init__.py` wrappers previously forwarded only `(module_id, inputs, context)` to the `APCore` client, silently dropping `version_hint`. Users calling `apcore.call(..., version_hint=">=1.0.0")` would have had the hint ignored. Now all three wrappers accept and forward `version_hint: str | None = None`, matching the `APCore` class signature and cross-language SDKs.
5362
- **Spec §4.13 annotation merge — YAML annotations are no longer silently dropped at registration.** Two coupled bugs were repaired: (1) `registry/metadata.py:merge_module_metadata` was doing whole-replacement of the `annotations` field instead of the field-level merge mandated by §4.13 ("If YAML only defines `readonly: true`, other fields **must** retain values from code or defaults."), and (2) `registry/registry.py:get_definition` was ignoring even that broken merge result and reading directly from the module's class attribute. The fix wires the previously-unwired `apcore.schema.annotations.merge_annotations` and `merge_examples` (which were defined and unit-tested but never called from production) into the registry pipeline, and updates `get_definition` to consume the merged metadata. **User-observable behavior change:** modules that supplied `annotations:` in their `*_meta.yaml` companion files were previously seeing those annotations silently ignored. Those annotations will now be honored. Modules that relied on the broken behavior should audit their `*_meta.yaml`. Adds 5 regression tests covering field-level merge, YAML-only, neither-defined, examples override, and an end-to-end `discover() → get_definition()` round-trip.
5463
- **`ModuleAnnotations.from_dict` precedence inversion** — Per PROTOCOL_SPEC §4.4.1 rule 7, when the same key appears both in a nested `extra` object and as a top-level overflow key, the **nested value now wins** (previously the top-level overflow would silently overwrite it). Behavior change is observable only in the pathological case where an input contains both forms of the same key — no conformant producer emits this. Top-level overflow keys are still tolerated and merged into `extra` for backward compatibility.
5564

src/apcore/__init__.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -231,16 +231,24 @@
231231
_default_client = APCore()
232232

233233

234-
def call(module_id: str, inputs: dict[str, Any] | None = None, context: Context | None = None) -> dict[str, Any]:
234+
def call(
235+
module_id: str,
236+
inputs: dict[str, Any] | None = None,
237+
context: Context | None = None,
238+
version_hint: str | None = None,
239+
) -> dict[str, Any]:
235240
"""Global convenience for _default_client.call()."""
236-
return _default_client.call(module_id, inputs, context)
241+
return _default_client.call(module_id, inputs, context, version_hint=version_hint)
237242

238243

239244
async def call_async(
240-
module_id: str, inputs: dict[str, Any] | None = None, context: Context | None = None
245+
module_id: str,
246+
inputs: dict[str, Any] | None = None,
247+
context: Context | None = None,
248+
version_hint: str | None = None,
241249
) -> dict[str, Any]:
242250
"""Global convenience for _default_client.call_async()."""
243-
return await _default_client.call_async(module_id, inputs, context)
251+
return await _default_client.call_async(module_id, inputs, context, version_hint=version_hint)
244252

245253

246254
def module(
@@ -282,10 +290,13 @@ def module(
282290

283291

284292
async def stream(
285-
module_id: str, inputs: dict[str, Any] | None = None, context: Context | None = None
293+
module_id: str,
294+
inputs: dict[str, Any] | None = None,
295+
context: Context | None = None,
296+
version_hint: str | None = None,
286297
) -> AsyncIterator[dict[str, Any]]:
287298
"""Global convenience for _default_client.stream()."""
288-
async for chunk in _default_client.stream(module_id, inputs, context):
299+
async for chunk in _default_client.stream(module_id, inputs, context, version_hint=version_hint):
289300
yield chunk
290301

291302

0 commit comments

Comments
 (0)