Skip to content

Latest commit

 

History

History
349 lines (248 loc) · 11.8 KB

File metadata and controls

349 lines (248 loc) · 11.8 KB

Output Writers

apcore-toolkit provides several ways to export the ScannedModule metadata into formats used by the apcore ecosystem.

YAMLWriter

Generates individual .binding.yaml files for each scanned module. These files are typically placed in an extensions/ or bindings/ directory and loaded by apcore.BindingLoader.

Features

  • File-per-Module: Each module is written to its own file for easy management.
  • Sanitized Filenames: Replaces periods and special characters in module IDs to ensure valid file names.
  • Header Injection: Optionally adds a "Generated by" header to all files.

=== "Python"

```python
from apcore_toolkit import YAMLWriter

writer = YAMLWriter()
writer.write(modules, output_dir="./bindings", dry_run=False)
```

=== "TypeScript"

```typescript
import { YAMLWriter } from "apcore-toolkit";

const writer = new YAMLWriter();
writer.write(modules, "./bindings", { dryRun: false });
```

PythonWriter / TypeScriptWriter

Generates source files containing decorator-based wrapper functions. This is useful for migrating legacy code to the apcore decorator pattern without manual re-writing.

Features

  • Auto-Decorators: Generates the decorator with all extracted metadata.
  • Target Integration: Points to the original view function as the target of the module.
  • Standard Formatting: Produces clean, idiomatic code for the target language.

=== "Python"

```python
from apcore_toolkit import PythonWriter

writer = PythonWriter()
writer.write(modules, output_dir="./generated_apcore", dry_run=False)
```

=== "TypeScript"

```typescript
import { TypeScriptWriter } from "apcore-toolkit";

const writer = new TypeScriptWriter();
writer.write(modules, "./generated_apcore", { dryRun: false });
```

RegistryWriter

Directly registers the scanned modules into an active apcore.Registry instance. This is ideal for "live" scanning where you want to expose existing endpoints without generating intermediate files.

Features

  • No Disk Usage: Modules are held only in memory.
  • Hot-Reload Ready: Can be re-run to refresh the Registry as endpoints change.

=== "Python"

```python
from apcore import Registry
from apcore_toolkit import RegistryWriter

registry = Registry()
writer = RegistryWriter()
writer.write(modules, registry, dry_run=False)
```

=== "TypeScript"

```typescript
import { Registry } from "apcore-js";
import { RegistryWriter } from "apcore-toolkit";

const registry = new Registry();
const writer = new RegistryWriter();
writer.write(modules, registry, { dryRun: false });
```

HTTPProxyRegistryWriter (Python only)

Registers scanned modules as HTTP proxy classes that forward requests to a running web API. This enables CLI execution without invoking route handlers directly (which depend on framework DI systems).

Features

  • No Direct Handler Invocation: Proxies requests over HTTP instead of calling view functions.
  • Path Parameter Substitution: Automatically maps path parameters (e.g., /items/{item_id}) from input values.
  • Auth Support: Pluggable auth_header_factory for Bearer tokens or custom headers.
  • Success Range: Accepts any 2xx response as success; 204 No Content returns {}.

Installation

Requires the httpx optional dependency:

pip install apcore-toolkit[http-proxy]

Usage

from apcore import Registry
from apcore_toolkit import HTTPProxyRegistryWriter

registry = Registry()
writer = HTTPProxyRegistryWriter(
    base_url="http://localhost:8000",
    auth_header_factory=lambda: {"Authorization": "Bearer xxx"},
)
writer.write(modules, registry)

get_writer() Factory

The get_writer(format) factory function returns the appropriate writer instance for a given output format, avoiding the need to import each writer class individually.

Supported Formats

Format Returns Language
"yaml" YAMLWriter instance Both
"python" PythonWriter instance Python
"typescript" TypeScriptWriter instance TypeScript
"registry" RegistryWriter instance Both
"http-proxy" HTTPProxyRegistryWriter instance Python

=== "Python"

```python
from apcore_toolkit.output import get_writer

writer = get_writer("yaml")       # YAMLWriter
writer = get_writer("python")     # PythonWriter
writer = get_writer("registry")   # RegistryWriter
writer = get_writer("http-proxy", base_url="http://localhost:8000")  # HTTPProxyRegistryWriter
```

=== "TypeScript"

```typescript
import { getWriter } from "apcore-toolkit";

const writer = getWriter("yaml");       // YAMLWriter
const writer = getWriter("typescript"); // TypeScriptWriter
const writer = getWriter("registry");   // RegistryWriter
```

Output Verification

Writers can optionally verify that their output artifacts are well-formed after writing. This prevents silent failures where a writer produces a file that apcore cannot load.

Verification by Writer Type

Writer Verification Checks
YAMLWriter File exists, YAML parses without error, contains required module_id and target fields
PythonWriter / TypeScriptWriter File exists, source code parses without syntax errors (AST/TS compiler check)
RegistryWriter Module ID is registered, registry.get(module_id) returns a valid module

Usage

Verification is enabled via the verify parameter. All writers support verify and verifiers:

=== "Python"

```python
from apcore_toolkit import YAMLWriter, PythonWriter, RegistryWriter

# YAMLWriter
writer = YAMLWriter()
results = writer.write(modules, output_dir="./bindings", verify=True, verifiers=[])

# PythonWriter
writer = PythonWriter()
results = writer.write(modules, output_dir="./generated", verify=True, verifiers=[])

# RegistryWriter
writer = RegistryWriter()
results = writer.write(modules, registry, verify=True, verifiers=[])

# results contains verification status per module
for r in results:
    if not r.verified:
        print(f"WARNING: {r.module_id} — {r.verification_error}")
```

=== "TypeScript"

```typescript
import { YAMLWriter, TypeScriptWriter, RegistryWriter } from "apcore-toolkit";

// YAMLWriter
const yamlWriter = new YAMLWriter();
const results1 = yamlWriter.write(modules, "./bindings", { verify: true, verifiers: [] });

// TypeScriptWriter
const tsWriter = new TypeScriptWriter();
const results2 = tsWriter.write(modules, "./generated", { verify: true, verifiers: [] });

// RegistryWriter
const regWriter = new RegistryWriter();
const results3 = regWriter.write(modules, registry, { verify: true, verifiers: [] });

for (const r of results1) {
  if (!r.verified) {
    console.warn(`WARNING: ${r.moduleId} — ${r.verificationError}`);
  }
}
```

Verification Result

Each write operation returns a list of WriteResult objects:

Field (Python / TypeScript) Python Type TypeScript Type Description
module_id / moduleId str string The module that was written
path str | None string | null Output file path (None/null for RegistryWriter)
verified bool boolean Whether verification passed (always True if verify=False)
verification_error / verificationError str | None string | null Error message if verification failed

!!! tip "Use in CI" Enable verification in CI pipelines to catch binding generation issues before deployment. A scan → write → verify cycle ensures that generated artifacts are always loadable by apcore.

Custom Verifiers

The built-in verification checks (YAML parsability, AST syntax, Registry lookup) cover common cases. For domain-specific needs, writers accept a pluggable Verifier interface.

Verifier Protocol

=== "Python"

```python
from typing import Protocol

class Verifier(Protocol):
    def verify(self, path: str, module_id: str) -> VerifyResult: ...

@dataclass
class VerifyResult:
    ok: bool
    error: str | None = None
```

=== "TypeScript"

```typescript
interface Verifier {
  verify(path: string, moduleId: string): VerifyResult;
}

interface VerifyResult {
  ok: boolean;
  error?: string;
}
```

Built-in Verifiers

Verifier Used By Checks
YAMLVerifier YAMLWriter YAML parses, required fields present
SyntaxVerifier PythonWriter / TypeScriptWriter Source code parses without errors
RegistryVerifier RegistryWriter Module registered and retrievable
MagicBytesVerifier (available for custom use) File header matches expected format
JSONVerifier (available for custom use) JSON parses, optional schema validation

Custom Verifier Example

=== "Python"

```python
from apcore_toolkit.output import YAMLWriter, Verifier, VerifyResult

class StrictYAMLVerifier:
    """Verify YAML bindings have descriptions for all parameters."""

    def verify(self, path: str, module_id: str) -> VerifyResult:
        with open(path) as f:
            data = yaml.safe_load(f)
        schema = data.get("input_schema", {})
        for prop, spec in schema.get("properties", {}).items():
            if "description" not in spec:
                return VerifyResult(ok=False, error=f"Missing description for {prop}")
        return VerifyResult(ok=True)

writer = YAMLWriter()
results = writer.write(modules, output_dir="./bindings", verify=True, verifiers=[StrictYAMLVerifier()])
```

Verifier Chain

When multiple verifiers are provided, they run in order. The first failure stops the chain and sets WriteResult.verified = False.

results = writer.write(
    modules,
    output_dir="./bindings",
    verify=True,
    verifiers=[YAMLVerifier(), StrictYAMLVerifier(), CustomSchemaVerifier()],
)

!!! tip "Lesson from CLI-Anything" The CLI-Anything project validates output with magic bytes, pixel analysis, and RMS audio levels — never trusting exit code alone. The same principle applies: always verify the artifact, not the process exit status. The MagicBytesVerifier is directly inspired by this approach.

Error Handling

Writers and verifiers follow a consistent error handling pattern:

Scenario Behavior
Write succeeds, verify succeeds WriteResult(verified=True)
Write succeeds, verify fails WriteResult(verified=False, verification_error="...") — artifact exists but is malformed
Write fails (I/O error) Raises WriteError with path and cause
Verifier itself throws Caught and wrapped as WriteResult(verified=False, verification_error="Verifier crashed: ...")

Writers never silently swallow errors. If verify=False, verification is skipped entirely (not suppressed).

=== "Python"

```python
from apcore_toolkit.output import WriteError

try:
    results = writer.write(modules, output_dir="./bindings", verify=True)
except WriteError as e:
    print(f"Failed to write {e.path}: {e.cause}")
```

Choosing a Writer

Use Case Recommended Writer
Configuration-first approach YAMLWriter
Migrating legacy views to decorators PythonWriter / TypeScriptWriter
Live, dynamic module exposure RegistryWriter
Fast, zero-config integration RegistryWriter
CLI execution against a running API (Python) HTTPProxyRegistryWriter