Source on GitHub

Core templates

truewire init <name> --template <template> writes the hand-written half of a project: src/<pkg>/core/ and the [cores.*] and [python.cores.*] tables of truewire.toml that wire it to the code truewire generate python emits. A template is a working core for one common API shape, not a stub: a fresh project built on any of them passes truewire check and truewire generate python, and pyright accepts the core as written. Adapt the core to the API; the generated code never changes when you do, because the generator reads nothing from it (ADR 0011) and calls it only through the protocols in truewire_core.contract.

Template Transport Auth Envelope Errors
bearer (default) HTTP Authorization: Bearer <api_key> on non-public calls none ApiError on any non-2xx
hmac HTTP API key plus HMAC-SHA256 over timestamp, method, path and body, in headers none AuthError, RateLimited, BadRequest, ApiError by status
jsonrpc HTTP, one URL, POST only Authorization: Bearer <api_key> on non-public calls {jsonrpc, id, method, params} out, result back, error raised by JSON-RPC code, then by status
ws HTTP as bearer, plus one WebSocket connection as bearer none as bearer; ApiError on a subscribe error frame

Every template shares one shape, described in the core skill: Transport (how a request reaches the wire), ClientBase (the root class the generated main.py subclasses, with new(...) and the context manager) and Endpoint (the base every generated endpoint class subclasses, whose request() sends and validates). meta.py is generated from [cores.default].meta, which every template declares as { public: boolean }: an endpoint whose meta is {"public": true} is sent without credentials. truewire import openapi sets it for every operation with no security requirement.

bearer

The plain HTTP core. Transport.headers() returns {} for a public call and a bearer header otherwise; Transport.send() fills {name} placeholders in the path from the request, sends the rest as the query string (or a JSON body for POST/PUT/PATCH), and raises ApiError on a non-2xx status. Endpoint.request() validates the raw body against the generated response type.

What to change:

  • Headers every call needs (media type, API version, User-Agent) go into headers() unconditionally; only auth is conditional.
  • An API that wraps every response unwraps it in send() or request(), and its endpoints declare envelope.payload (authoring rule 6).
  • Map statuses the API distinguishes onto AuthError, RateLimited and BadRequest from truewire_core.exceptions; the hmac template's raise_for_status is the shape to copy.

hmac

The bearer core with request signing. Three pure functions carry the recipe:

  • signature_message(timestamp, method, path, body) returns the bytes the signature covers: timestamp + METHOD + path + body, where path includes the query string.
  • sign(secret, message) returns the hex HMAC-SHA256.
  • Transport.headers(method, path, body, public=...) puts the key, the timestamp and the signature into X-API-Key, X-Timestamp and X-Signature, and raises AuthError for a non-public call on a client built without credentials.

Transport.send() builds the query string itself so the signed path and the sent path are the same bytes. Transport.timestamp is the clock, a field a test replaces to sign a known value. ClientBase.new(base_url=..., api_key=..., api_secret=...) takes the credentials; the caller reads them from environment variables, the core never does.

What to change:

  • The order, separators and casing in signature_message; the digest or the encoding (base64, say) in sign; the header names at the top of the module.
  • Where the injected values travel. The template puts them in headers, which a recorded example never holds, so nothing is declared in the spec. An API that wants the timestamp, a nonce or the signature as a query or body field gets it added in send(), and every endpoint that carries it lists the field under redacted in its endpoint.json: a recorded example then never pins a value that changes on every call, and truewire mock ignores the field when matching a request. truewire capture records the parameters the call was made with, never the wire body, so an injected field is absent from the example either way; redacted is what tells the mock to ignore it on the wire.

jsonrpc

JSON-RPC 2.0 over HTTP. Every call is one POST to base_url carrying {"jsonrpc": "2.0", "id": <counter>, "method": <path>, "params": <request>}. In the spec, an endpoint's path is the JSON-RPC method name with no leading slash (getBalance), its method is POST, its response schema describes the whole reply frame, and it declares envelope: {"payload": "result"} so the generated method returns result (authoring rule 6). Adding "correlate": "id" to the envelope makes truewire mock echo the request id into the served frame, which the core checks.

The core:

  • build_request(id, method, params) builds the frame; params is the request's named parameters as a dict, or absent for a call without any.
  • unwrap(frame, id=..., method=...) returns result, raises ApiError on a frame that is not an object, answers another id, or carries neither result nor error, and hands an error member to raise_error.
  • raise_error(method, error) maps error.code through three tables: INVALID_REQUEST_CODES (JSON-RPC's own parse, invalid-request, method-not-found and invalid-params codes) to BadRequest, AUTH_CODES to AuthError, RATE_LIMIT_CODES to RateLimited, anything else to ApiError. The message carries the method, the API's message, the code and the first 200 characters of data.
  • Transport.call(method, params, public=...) posts, maps a non-2xx status through raise_for_status, decodes the frame and unwraps it. Endpoint.request() serializes the generated request through its validator (so declared formats apply), calls, and validates result against the generated response type. It accepts the HTTP method because the HttpEndpoint contract passes one, and never reads it.

What to change:

  • AUTH_CODES and RATE_LIMIT_CODES: put the API's own codes there. The defaults are placeholders, one uncommon code each.
  • Positional parameters: return a list from build_request, in the order the endpoint's request schema declares its properties. truewire mock compares a positional params element by element.
  • An error shape that is not {code, message, data}: change raise_error.
  • Batch requests, a method that lives on its own URL, an API-key query parameter: Transport.call and Transport.headers.

ws

The bearer HTTP core plus a WebSocket client, for an API with both. Two files:

  • core/__init__.py holds Transport, Endpoint, a ClientBase with two fields, client (HTTP) and socket, and StreamEndpoint, the base for every generated stream endpoint. StreamEndpoint.subscribe(channel, parameters, ...) serializes the parameters through their validator, fills {name} placeholders in the channel from them, and hands the rest to the socket as the subscribe frame's fields.
  • core/ws.py holds Connection, a truewire_core.ws.Streams subclass with the SerialReplies mixin, and SocketClient, which owns one connection and the validation default. ClientBase.new(base_url=..., ws_url=...) builds both; the socket opens on the first subscription and closes with the client.

The wiring in truewire.toml:

[cores.streams]                  # stream endpoints read nothing per call: no meta schema

[python.cores.root]
base = "feed.core:ClientBase"
children = { streams = "socket" }   # the streams composite is built on the root's socket field

[python.cores.streams]
base = "feed.core:StreamEndpoint"

init also writes spec/endpoints/streams/router.json naming the streams core, so every stream endpoint placed under spec/endpoints/streams/ subclasses StreamEndpoint and reaches the socket, while every other group keeps the HTTP transport. --ws-url sets the socket URL baked into new(); it defaults to --base-url with a ws/wss scheme.

Connection speaks the subscribe dialect truewire mock serves for an endpoint that records only parameters: it sends {"type": "subscribe", "channel": ..., ...params} and {"type": "unsubscribe", ...}, treats a frame whose type is subscribed, unsubscribed, ack or error as the reply to the most recent request (acks carry no correlation id, so SerialReplies pairs them by arrival order), and routes every other frame carrying channel to the subscription named by subscription_key(channel, frame): the channel, plus :<id> when the subscription was keyed by an id parameter. Each pushed frame is validated against the generated payload type unless validate=False.

What to change:

  • The two frames in request_subscription and request_unsubscription, ACK_TYPES, and the error check, to the API's own dialect. Stream endpoints then declare envelope.verb (and envelope.channel when the frame does not carry a top-level channel) so the mock recognizes the frames.
  • subscription_key: the one place that decides which local subscription a pushed frame belongs to.
  • Connection.ping: a protocol-level ping every 30 seconds by default; an API with its own heartbeat frame sends it here.
  • An API that correlates acks by a request id, or that carries request/reply methods over the same socket, is a truewire_core.ws.StreamsRpc; examples/kraken/src/kraken/core is a complete reference for that shape, including token authentication and two connections behind one root.
  • A private connection: build a second SocketClient in ClientBase.new, add a field for it, and route a second group to it with another children entry.

Proving a core

The core skill's gate applies to every template: truewire generate python (with a pyrightconfig.json in the project so it type-checks), then one truewire capture of the simplest public endpoint whose recording passes truewire check. The toolchain's own tests run every template through init, check and generate with pyright, call bearer, hmac and jsonrpc cores through truewire mock, and drive the ws core through a subscribe, push and unsubscribe round trip (packages/truewire/test/test_init_templates.py).