Skip to main content

taut — one declarative IR -> native types, deterministic CBOR wire, IR-driven codec + service contracts, verified by a golden conformance corpus (import: `taut`)

Project description

taut

One tiny declarative contract → native types, a deterministic wire codec, and service stubs across Python, TypeScript, Rust, C++, Swift, Go, Kotlin, JS, and Java — verified byte-for-byte by a shared golden corpus.

taut is a cross-language data + service protocol mechanism: protobuf's essential value (a single governed schema, generated types, a portable wire format, an evolution gate) with roughly 90% less of its weight (no plugin toolchain, no .proto grammar, no required runtime, a wire you can read in an afternoon). The schema is authored as a small, restricted Python DSL — or any .ir.json — and everything else is projected from it.

Status: alpha. The model, codec, generators, corpus, and evolution gate are implemented across nine languages; APIs may still move.

pip install taut-proto      # distribution name (PyPI)
import taut                 # import name
tautc gen api.taut.py -o gen/   # CLI codegen

The distribution is named taut-proto (PyPI rejects the bare taut); the import package and CLI are taut / tautc — the Pillow→PIL pattern. Pure Python, no runtime dependencies.


taut vs proto3

A schema language earns its keep by removing ambiguity between services in different languages. proto3 (with gRPC) does that but drags in a code-gen plugin architecture, a bespoke grammar, per-language runtimes, an HTTP/2 RPC stack, and a wire format few can read by hand. taut keeps the parts that matter and drops the rest:

Dimension proto3 / gRPC taut
Schema source .proto DSL + protoc + a plugin per language a few lines of typed Python, or .ir.json
Toolchain protoc compiler + language plugins one pip install; tautc for compiled targets
Wire format varint tag-length-value; not guaranteed canonical across implementations (maps are explicitly unordered) frozen deterministic CBOR (RFC 8949 §4.2) — byte-identical everywhere, corpus-pinned
Codegen required? yes for most languages (or descriptor-based reflection) no for Python/TS (IR-driven runtime codec); generated for the compiled targets (Rust/C++/Swift/Go/Kotlin/JS/Java)
Runtime dependency a protobuf runtime per language none (stdlib; hand-rolled codec per target)
Field presence scalars had none; optional presence re-added in 3.15 optional → null; presence is clean (fields always emitted)
required removed in proto3 kept as a governance assertion, enforced by the evolution gate (never a decode error)
Forward-compat (unknown fields) dropped in 3.0, restored in 3.5 — and binary only (the JSON mapping drops them) opt-in unknown-field preservation (raw tags re-emitted in canonical order)
Services / RPC service/rpc, but the transport is gRPC: HTTP/2 + codegen + runtime (name, in, out, shape); transport-agnostic (reference: JSON envelope + CBOR payload)
Streaming gRPC client/server/bidi streams first-class delivery shapes: atom / log / stream / swmr / snapshot_delta / crdt
Schema evolution field-number rules + reserved; breaking-change detection is external (buf, protolock) a built-in structural breaking-change gate, runnable in CI
Extensions proto2 had them; proto3 dropped them (use Any) declared, typed side-channels at a reserved tag band
Read the whole spec large surface the IR fits on a screen

Because the IR is data — not a Turing-complete program — taut can diff two versions of a contract and tell you mechanically what broke.

What taut deliberately doesn't have (yet): oneof types, well-known types (Timestamp/Any/Duration), a canonical JSON profile (deferred), descriptor/reflection services, and — above all — proto's ecosystem maturity, scale hardening, and gRPC's battle-tested multi-language RPC runtime. taut is a focused mechanism, not a drop-in protobuf replacement for every use case. If you need proto's breadth, use proto; taut is the lighter contract when you want ~10% of the load for the parts that actually bite.

The contract

A schema is enums + messages + services, authored declaratively:

from taut.ir.dsl import INT, STR, BYTES, Enum, F, Msg, Params, Ref, List, method, service, schema

SCHEMA = schema(
    service("Razel",
        method("build", role="in", params=Params(target=STR), out=Ref.BuildResult),
        method("build.subscribe", role="out", shape="atom", out=Ref.BuildResult)),

    BuildStatus=Enum(cached=0, built=1, failed=2),

    OutputArtifact=Msg(
        path=F(1, STR),
        digest=F(2, BYTES),
        next_id=3),

    BuildResult=Msg(
        target=F(1, STR),
        status=F(2, Ref.BuildStatus),
        outputs=F(3, List(Ref.OutputArtifact)),
        message=F(4, STR, optional=True),
        next_id=5),
)
  • Fields carry an explicit integer tag, a type, and flags (optional, transient, CRDT merge). Types are a closed set: scalars (int/str/bytes/ bool), enum/message refs, List, and Map (a key-sorted, deterministic keyed collection — K scalar, V scalar/enum/message).
  • Keyword-named enums, messages, fields, Ref.Name references, and Params(...) method params are preferred. The explicit string forms remain available for names that are not Python identifiers, Python keywords, or Msg(...) control names such as reserved and next_id.
  • reserved + next_id are first-class, validated message features (not comments) — retired tags/names can never be reused, and next_id is checked.
  • Methods are the minimal contract (name, in, out, shape). shape is the sole discriminator: unary (request→response, the default) is just the degenerate member of an open shape registry, alongside streaming shapes atom (latest-wins state), log (append-only), stream (live), swmr (snapshot+delta), snapshot_delta, and crdt. out binds a type per the shape's delivery slots. There is no separate "kind" axis to disagree — illegal states are unrepresentable. (See Reference §5–6.)

A validator rejects incoherent IR (dangling refs, duplicate tags, out-slots that don't match a shape, …), so downstream mechanism can be derived without review.

The wire

A hand-rolled, frozen deterministic CBOR subset: definite-length, shortest- form integers, ascending integer map keys. The same value encodes to the same bytes in every language — pinned by a golden conformance corpus (value → exact hex). Python and TypeScript run a fully IR-driven codec (instantiate a client or server from JSON alone, zero codegen); Rust, C++, Swift, Go, Kotlin, JS, and Java get generated native types with encoders/decoders (compiled targets need types ahead of time). The C++ corpus is a wall of static_asserts — compiling is the test.

Opinionated about the wire, not the API

Small-and-tight vs. bloated is only half the pitch. The other half: taut pins exactly what cross-language interop requires — the deterministic wire, the (name, in, out, shape) contract, and the evolution gate — and nothing more. Those must be fixed, so they are.

How that contract surfaces in your code is your call. The IR is the governed artifact (a tiny JSON); projecting it into types, clients, and servers is a choice, not a mandate:

  • The bundled tautc generators are reference projections. Use them, swap them, or write your own against the exported .ir.json — pydantic models, plain dataclasses, a transactional/ORM wrapper, different client ergonomics, whatever fits your app.
  • Or generate nothing: Python and TypeScript drive the IR straight through the runtime codec.

Where protoc hands you its message classes and gRPC its stubs, taut hands you the bytes and the schema and gets out of the way.

tautc — the reference codegen CLI

tautc gen IR --out DIR [--lang python,typescript,rust,cpp] [--service A,B] [--api-only]

Loads a .taut.py or .ir.json, validates it, and emits per language: api (native types + encoders/decoders), plus client/server stubs per service. --api-only emits just the struct defs + codec — the drop-in for a build script on a compiled target. --with-runtime additionally emits the vendored CBOR runtime for compiled targets (rustcbor.rs, cpptaut/cbor.hpp), so the generated output compiles standalone — no hand-vendoring. Example: razel (a Rust build daemon) authors ir/razel.taut.py and generates its Rust wire layer with tautc gen ir/razel.taut.py -o razel/gen --lang rust --api-only.

These are the reference emitters; they read the exported .ir.json, and so can a generator you write — tautc is a convenience, not a requirement.

Conformancetautc corpus IR -o DIR [--lang rust] [--check] derives a golden corpus from the IR (deterministic coverage values, one per message, encoded by the canonical codec → golden.json) plus a per-language byte-parity harness (Rust vectors.rs today). A target conforms iff its codec reproduces those bytes; --check is the CI drift gate. No hand-authored vectors.

CBOR ↔ JSONtautc json IR -m MESSAGE converts the deterministic-CBOR wire to/from JSON via the IR (stdin/stdout or -i/-o files; --from-json reverses; --indent pretty-prints). proto3-style conventions (int64→string, bytes→base64, enum→name); cbor → json → cbor is byte-identical. Handy for debugging and JSON-native gateways:

some-producer | tautc json api.taut.py -m BuildResult            # CBOR -> JSON
tautc json api.taut.py -m BuildResult --from-json < x.json       # JSON -> CBOR

Evolution

  • Breaking-change gate — structural diff of two IR versions classifies each change breaking / compatible (remove field, retag, change a method's shape → breaking; add a field/method → compatible). Run it in CI.
  • Forward-compatibility — unknown-field preservation: a decoder keeps unrecognized tags as raw CBOR and re-emits them in canonical order, so an old reader relaying a new message loses nothing. Default-on in the Python/TS runtime codec; opt-in for generated structs (Rust + C++) via tautc gen --forward-compat (a wire_residual field). Proven across all four languages by a cross-version test: a v1 struct round-trips a v2-only field byte-for-byte — even when the unknown tag interleaves between known ones.
  • Extensions — declared, typed side-channels at a reserved tag band (BAND_START = 2^20); infrastructure reads/writes them on the wire without the app schema knowing.

Layout

src/taut/        the builder (pure Python)
  ir/            model, DSL, validator, export/load, breaking-change gate, shapes
  wire/          deterministic CBOR + IR-driven codec
  gen/           generators: Rust, C++, and the per-language scaffold
  cli.py         the `tautc` command
ir/              authored IR modules — the only governed artifact
                 (griplab.taut.py, razel.taut.py)
corpus/          generated golden vectors (the oracle)
docs/            Overview, GettingStarted, Reference, Server + a runnable example
dev-docs/        design notes + the decisions log (Taut*.md)
.github/         the PyPI publish workflow (Trusted Publishing on release)

Reference target implementations and the full cross-language interop matrix (TS/Rust/Python clients × Python/Rust servers, plus the C++ oracle) live in a companion trial/ repo, each validated against this repo's corpus.

Docs

  • Overview — the mental model and the shape catalog.
  • Getting Started — author → validate → encode → generate.
  • Reference — every DSL helper, the shapes, the validator rules.
  • Building a Server — handlers, shape engines, the WS loop.
  • examples/tasks — a complete runnable API with generated code for all four languages.

Develop

python run_tests.py        # regenerate the corpus, run the suite

Design notes and the decisions log live in dev-docs/.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

taut_proto-0.5.0.tar.gz (69.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

taut_proto-0.5.0-py3-none-any.whl (81.6 kB view details)

Uploaded Python 3

File details

Details for the file taut_proto-0.5.0.tar.gz.

File metadata

  • Download URL: taut_proto-0.5.0.tar.gz
  • Upload date:
  • Size: 69.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for taut_proto-0.5.0.tar.gz
Algorithm Hash digest
SHA256 e9eff188a994eb9833c905d070170465b7acf3c0b94ef96f1b532e5dd6b6c33f
MD5 4235b23df4258af374cb12c95fd73526
BLAKE2b-256 e600190a71a800ff7d2002587b6febc413f0ca61834e88f4ace8edd28958aa2f

See more details on using hashes here.

Provenance

The following attestation bundles were made for taut_proto-0.5.0.tar.gz:

Publisher: publish.yml on owebeeone/taut

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file taut_proto-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: taut_proto-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 81.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for taut_proto-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a7f343be6c793d7a8c15132f85991df1d22208ba65229d50df6cffaceb81db50
MD5 7e4c88e80e9f3651ba5be8eec1ec0bf3
BLAKE2b-256 7504e427a128a2d3e5808073873b1d2b38d2b142b2785e8b4d6a81ac9a2556f2

See more details on using hashes here.

Provenance

The following attestation bundles were made for taut_proto-0.5.0-py3-none-any.whl:

Publisher: publish.yml on owebeeone/taut

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page