feat(generator): emit Python SDK clients alongside Rust #45

Open
opened 2026-05-11 11:31:04 +00:00 by timur · 0 comments
Owner

Context

Python clients for OSIS (and other OpenRPC) services are currently generated at runtime by hero_router via the python_codegen module. The on-disk artefacts live in ~/.hero/var/router/python/ and are written by hero_router whenever it scans / a consumer hits router.python_client.

This works but couples hero_router to consumer-language codegen, which isn't really its job. The Rust SDK is already emitted by hero_rpc's generator into the configured sdk_dir/<domain>/ (see hero_osis_sdk for examples). Python should follow the same pattern.

Proposal

Extend hero_rpc's OschemaBuilder / generator to also emit Python clients into <sdk_dir>/python/ per domain. The codegen logic itself can be lifted from hero_router/crates/hero_router/src/python_codegen.rs — it already knows how to:

  • Generate @dataclass per components.schemas entry (with required fields first, optional fields with Optional[T] = None / field(default_factory=list) defaults)
  • Wrap CRUD method results: *_get returns the typed dataclass instance via Type(**_raw); array-of-$ref returns wraps each element
  • Escape Python reserved words in kwarg positions (fromfrom_)
  • Emit a compact *_interface.py LLM-context stub alongside the full *_client.py

Migration plan

  1. Move python_codegen.rs from hero_router into hero_rpc/crates/generator/src/python/ as a sibling of the Rust emitters.
  2. Add .with_python_sdk() (or an equivalent) to OschemaBuildConfig.
  3. Update each domain's build.rs (e.g. hero_osis_server/build.rs) to opt in.
  4. SDK consumers pip install (or path-import) the generated package; agents (e.g. service_agent's compile_stubs) read from the SDK rather than asking hero_router to generate.
  5. Keep hero_router's runtime codegen + cache as a fallback for ad-hoc / REPL use, but it stops being the canonical source.

Why backlog

The runtime codegen path works end-to-end today (verified in hero_logic/scripts/e2e_typed_client.py and the new python_codegen::test_dataclass_emission_and_method_wrapping lock-in test). So this is a refactor for ownership / coupling rather than a correctness fix — appropriate for the backlog.

Acceptance criteria

  • hero_osis build emits hero_osis_sdk/python/<domain>/<service>_client.py for each domain (mirrors the Rust SDK layout).
  • The emitted Python clients are byte-identical (or close to it) to what router.python_client returns today.
  • The existing hero_router/python_codegen regression test (test_dataclass_emission_and_method_wrapping) ports cleanly.
  • An e2e Python script (analogous to hero_logic/scripts/e2e_typed_client.py) imports from the SDK path, not from ~/.hero/var/router/python/.
  • hero_rpc 11e5c6a — base-field injection (sid/created_at/updated_at) in OpenRPC schemas
  • hero_router e7f902e + f34dbbe — current python_codegen impl + lock-in test
  • hero_osis PR #55 — Cargo.lock bump pulling the schema-merge into the running OSIS
## Context Python clients for OSIS (and other OpenRPC) services are currently generated **at runtime by `hero_router`** via the `python_codegen` module. The on-disk artefacts live in `~/.hero/var/router/python/` and are written by hero_router whenever it scans / a consumer hits `router.python_client`. This works but couples hero_router to consumer-language codegen, which isn't really its job. The Rust SDK is already emitted by `hero_rpc`'s generator into the configured `sdk_dir/<domain>/` (see `hero_osis_sdk` for examples). Python should follow the same pattern. ## Proposal Extend `hero_rpc`'s `OschemaBuilder` / generator to also emit Python clients into `<sdk_dir>/python/` per domain. The codegen logic itself can be lifted from `hero_router/crates/hero_router/src/python_codegen.rs` — it already knows how to: - Generate `@dataclass` per `components.schemas` entry (with required fields first, optional fields with `Optional[T] = None` / `field(default_factory=list)` defaults) - Wrap CRUD method results: `*_get` returns the typed dataclass instance via `Type(**_raw)`; array-of-`$ref` returns wraps each element - Escape Python reserved words in kwarg positions (`from` → `from_`) - Emit a compact `*_interface.py` LLM-context stub alongside the full `*_client.py` ## Migration plan 1. Move `python_codegen.rs` from hero_router into `hero_rpc/crates/generator/src/python/` as a sibling of the Rust emitters. 2. Add `.with_python_sdk()` (or an equivalent) to `OschemaBuildConfig`. 3. Update each domain's `build.rs` (e.g. `hero_osis_server/build.rs`) to opt in. 4. SDK consumers `pip install` (or path-import) the generated package; agents (e.g. service_agent's `compile_stubs`) read from the SDK rather than asking hero_router to generate. 5. Keep hero_router's runtime codegen + cache as a fallback for ad-hoc / REPL use, but it stops being the canonical source. ## Why backlog The runtime codegen path works end-to-end today (verified in `hero_logic/scripts/e2e_typed_client.py` and the new `python_codegen::test_dataclass_emission_and_method_wrapping` lock-in test). So this is a refactor for ownership / coupling rather than a correctness fix — appropriate for the backlog. ## Acceptance criteria - `hero_osis` build emits `hero_osis_sdk/python/<domain>/<service>_client.py` for each domain (mirrors the Rust SDK layout). - The emitted Python clients are byte-identical (or close to it) to what `router.python_client` returns today. - The existing `hero_router/python_codegen` regression test (`test_dataclass_emission_and_method_wrapping`) ports cleanly. - An e2e Python script (analogous to `hero_logic/scripts/e2e_typed_client.py`) imports from the SDK path, not from `~/.hero/var/router/python/`. ## Related - hero_rpc 11e5c6a — base-field injection (sid/created_at/updated_at) in OpenRPC schemas - hero_router e7f902e + f34dbbe — current python_codegen impl + lock-in test - hero_osis PR #55 — Cargo.lock bump pulling the schema-merge into the running OSIS
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_rpc#45
No description provided.