taut-proto 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- taut_proto-0.1.0/LICENSE +21 -0
- taut_proto-0.1.0/PKG-INFO +228 -0
- taut_proto-0.1.0/README.md +208 -0
- taut_proto-0.1.0/pyproject.toml +39 -0
- taut_proto-0.1.0/setup.cfg +4 -0
- taut_proto-0.1.0/src/taut/__init__.py +0 -0
- taut_proto-0.1.0/src/taut/cli.py +72 -0
- taut_proto-0.1.0/src/taut/corpus/__init__.py +0 -0
- taut_proto-0.1.0/src/taut/corpus/build.py +94 -0
- taut_proto-0.1.0/src/taut/crdt/__init__.py +3 -0
- taut_proto-0.1.0/src/taut/crdt/engine.py +111 -0
- taut_proto-0.1.0/src/taut/ext.py +46 -0
- taut_proto-0.1.0/src/taut/gen/__init__.py +0 -0
- taut_proto-0.1.0/src/taut/gen/cpp.py +232 -0
- taut_proto-0.1.0/src/taut/gen/rust.py +174 -0
- taut_proto-0.1.0/src/taut/gen/scaffold.py +354 -0
- taut_proto-0.1.0/src/taut/ir/__init__.py +0 -0
- taut_proto-0.1.0/src/taut/ir/compat.py +192 -0
- taut_proto-0.1.0/src/taut/ir/dsl.py +125 -0
- taut_proto-0.1.0/src/taut/ir/export.py +77 -0
- taut_proto-0.1.0/src/taut/ir/load.py +84 -0
- taut_proto-0.1.0/src/taut/ir/model.py +134 -0
- taut_proto-0.1.0/src/taut/ir/shapes.py +89 -0
- taut_proto-0.1.0/src/taut/ir/validate.py +124 -0
- taut_proto-0.1.0/src/taut/shapes/__init__.py +0 -0
- taut_proto-0.1.0/src/taut/wire/__init__.py +0 -0
- taut_proto-0.1.0/src/taut/wire/cbor.py +150 -0
- taut_proto-0.1.0/src/taut/wire/codec.py +83 -0
- taut_proto-0.1.0/src/taut_proto.egg-info/PKG-INFO +228 -0
- taut_proto-0.1.0/src/taut_proto.egg-info/SOURCES.txt +31 -0
- taut_proto-0.1.0/src/taut_proto.egg-info/dependency_links.txt +1 -0
- taut_proto-0.1.0/src/taut_proto.egg-info/entry_points.txt +2 -0
- taut_proto-0.1.0/src/taut_proto.egg-info/top_level.txt +1 -0
taut_proto-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gianni Mariani
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: taut-proto
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: taut — one declarative IR -> native types, deterministic CBOR wire, IR-driven codec + service contracts, verified by a golden conformance corpus (import: `taut`)
|
|
5
|
+
Author-email: Gianni Mariani <gianni@mariani.ws>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/owebeeone/taut
|
|
8
|
+
Project-URL: Repository, https://github.com/owebeeone/taut
|
|
9
|
+
Keywords: idl,protocol,schema,serialization,cbor,rpc,contract
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Classifier: Topic :: System :: Networking
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# taut
|
|
22
|
+
|
|
23
|
+
**One tiny declarative contract → native types, a deterministic wire codec, and
|
|
24
|
+
service stubs across Python, TypeScript, Rust, and C++ — verified byte-for-byte
|
|
25
|
+
by a shared golden corpus.**
|
|
26
|
+
|
|
27
|
+
taut is a cross-language data + service protocol mechanism: protobuf's essential
|
|
28
|
+
value (a single governed schema, generated types, a portable wire format, an
|
|
29
|
+
evolution gate) with roughly 90% less of its weight (no plugin toolchain, no
|
|
30
|
+
`.proto` grammar, no required runtime, a wire you can read in an afternoon). The
|
|
31
|
+
schema is authored as a small, restricted Python DSL — or any `.ir.json` — and
|
|
32
|
+
everything else is *projected* from it.
|
|
33
|
+
|
|
34
|
+
> Status: **alpha**. The model, codec, generators, corpus, and evolution gate are
|
|
35
|
+
> built and tested end-to-end across four languages (57 tests green); APIs may
|
|
36
|
+
> still move.
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
pip install taut-proto # distribution name (PyPI)
|
|
40
|
+
import taut # import name
|
|
41
|
+
tautc gen api.taut.py -o gen/ # CLI codegen
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The distribution is named `taut-proto` (PyPI rejects the bare `taut`); the import
|
|
45
|
+
package and CLI are `taut` / `tautc` — the Pillow→PIL pattern. Pure Python, no
|
|
46
|
+
runtime dependencies.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## taut vs proto3
|
|
51
|
+
|
|
52
|
+
A schema language earns its keep by removing ambiguity between services in
|
|
53
|
+
different languages. proto3 (with gRPC) does that but drags in a code-gen plugin
|
|
54
|
+
architecture, a bespoke grammar, per-language runtimes, an HTTP/2 RPC stack, and
|
|
55
|
+
a wire format few can read by hand. taut keeps the parts that matter and drops
|
|
56
|
+
the rest:
|
|
57
|
+
|
|
58
|
+
| Dimension | proto3 / gRPC | taut |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| **Schema source** | `.proto` DSL + `protoc` + a plugin per language | a few lines of typed Python, or `.ir.json` |
|
|
61
|
+
| **Toolchain** | `protoc` compiler + language plugins | one `pip install`; `tautc` for compiled targets |
|
|
62
|
+
| **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 |
|
|
63
|
+
| **Codegen required?** | yes for most languages (or descriptor-based reflection) | **no** for Python/TS (IR-driven runtime codec); generated for Rust/C++ |
|
|
64
|
+
| **Runtime dependency** | a protobuf runtime per language | **none** (stdlib; hand-rolled codec per target) |
|
|
65
|
+
| **Field presence** | scalars had none; `optional` presence re-added in **3.15** | `optional` → null; presence is clean (fields always emitted) |
|
|
66
|
+
| **`required`** | removed in proto3 | kept as a *governance assertion*, enforced by the evolution gate (never a decode error) |
|
|
67
|
+
| **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) |
|
|
68
|
+
| **Services / RPC** | `service`/`rpc`, but the transport *is* gRPC: HTTP/2 + codegen + runtime | `(name, in, out, shape)`; transport-agnostic (reference: JSON envelope + CBOR payload) |
|
|
69
|
+
| **Streaming** | gRPC client/server/bidi streams | first-class **delivery shapes**: `atom` / `log` / `stream` / `swmr` / `snapshot_delta` / `crdt` |
|
|
70
|
+
| **Schema evolution** | field-number rules + `reserved`; breaking-change detection is **external** (buf, protolock) | a **built-in structural breaking-change gate**, runnable in CI |
|
|
71
|
+
| **Extensions** | proto2 had them; proto3 dropped them (use `Any`) | declared, typed **side-channels** at a reserved tag band |
|
|
72
|
+
| **Read the whole spec** | large surface | the IR fits on a screen |
|
|
73
|
+
|
|
74
|
+
Because the IR is data — not a Turing-complete program — taut can *diff* two
|
|
75
|
+
versions of a contract and tell you mechanically what broke.
|
|
76
|
+
|
|
77
|
+
**What taut deliberately doesn't have (yet):** `map` and `oneof` types,
|
|
78
|
+
well-known types (`Timestamp`/`Any`/`Duration`), a canonical JSON profile
|
|
79
|
+
(deferred), descriptor/reflection services, and — above all — proto's ecosystem
|
|
80
|
+
maturity, scale hardening, and gRPC's battle-tested multi-language RPC runtime.
|
|
81
|
+
taut is a focused mechanism, not a drop-in protobuf replacement for every use
|
|
82
|
+
case. If you need proto's breadth, use proto; taut is the lighter contract when
|
|
83
|
+
you want ~10% of the load for the parts that actually bite.
|
|
84
|
+
|
|
85
|
+
## The contract
|
|
86
|
+
|
|
87
|
+
A schema is **enums + messages + services**, authored declaratively:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from taut.ir.dsl import INT, STR, BYTES, Enum, F, Msg, Ref, List, method, service, schema
|
|
91
|
+
|
|
92
|
+
SCHEMA = schema(
|
|
93
|
+
Enum("BuildStatus", cached=0, built=1, failed=2),
|
|
94
|
+
|
|
95
|
+
Msg("OutputArtifact",
|
|
96
|
+
F("path", 1, STR),
|
|
97
|
+
F("digest", 2, BYTES),
|
|
98
|
+
next_id=3),
|
|
99
|
+
|
|
100
|
+
Msg("BuildResult",
|
|
101
|
+
F("target", 1, STR),
|
|
102
|
+
F("status", 2, Ref("BuildStatus")),
|
|
103
|
+
F("outputs", 3, List(Ref("OutputArtifact"))),
|
|
104
|
+
F("message", 4, STR, optional=True),
|
|
105
|
+
next_id=5),
|
|
106
|
+
|
|
107
|
+
service("Razel",
|
|
108
|
+
method("build", role="in", params=[("target", STR)], out=Ref("BuildResult")),
|
|
109
|
+
method("build.subscribe", role="out", shape="atom", out=Ref("BuildState"))),
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- **Fields** carry an explicit integer `tag`, a type, and flags (`optional`,
|
|
114
|
+
`transient`, CRDT `merge`). Types are a closed set: scalars (`int/str/bytes/
|
|
115
|
+
bool`), enum/message refs, and `List`.
|
|
116
|
+
- **`reserved` + `next_id`** are first-class, validated message features (not
|
|
117
|
+
comments) — retired tags/names can never be reused, and `next_id` is checked.
|
|
118
|
+
- **Methods are the minimal contract `(name, in, out, shape)`.** `shape` is the
|
|
119
|
+
*sole* discriminator: `unary` (request→response, the default) is just the
|
|
120
|
+
degenerate member of an **open shape registry**, alongside streaming shapes
|
|
121
|
+
`atom` (latest-wins state), `log` (append-only), `stream` (live), `swmr`
|
|
122
|
+
(snapshot+delta), `snapshot_delta`, and `crdt`. `out` binds a type per the
|
|
123
|
+
shape's delivery slots. There is no separate "kind" axis to disagree — illegal
|
|
124
|
+
states are unrepresentable. (See [Reference §5–6](docs/Reference.md).)
|
|
125
|
+
|
|
126
|
+
A **validator** rejects incoherent IR (dangling refs, duplicate tags, out-slots
|
|
127
|
+
that don't match a shape, …), so downstream mechanism can be derived without
|
|
128
|
+
review.
|
|
129
|
+
|
|
130
|
+
## The wire
|
|
131
|
+
|
|
132
|
+
A hand-rolled, frozen **deterministic CBOR** subset: definite-length, shortest-
|
|
133
|
+
form integers, ascending integer map keys. The same value encodes to the same
|
|
134
|
+
bytes in every language — pinned by a **golden conformance corpus** (value →
|
|
135
|
+
exact hex). Python and TypeScript run a fully **IR-driven codec** (instantiate a
|
|
136
|
+
client or server from JSON alone, zero codegen); Rust and C++ get **generated
|
|
137
|
+
native types with encoders/decoders** (compiled targets need types ahead of
|
|
138
|
+
time). The C++ corpus is a wall of `static_assert`s — *compiling is the test*.
|
|
139
|
+
|
|
140
|
+
## Opinionated about the wire, not the API
|
|
141
|
+
|
|
142
|
+
Small-and-tight vs. bloated is only half the pitch. The other half: taut pins
|
|
143
|
+
*exactly* what cross-language interop requires — the deterministic wire, the
|
|
144
|
+
`(name, in, out, shape)` contract, and the evolution gate — and **nothing more**.
|
|
145
|
+
Those must be fixed, so they are.
|
|
146
|
+
|
|
147
|
+
How that contract surfaces in *your* code is your call. The IR is the governed
|
|
148
|
+
artifact (a tiny JSON); projecting it into types, clients, and servers is a
|
|
149
|
+
choice, not a mandate:
|
|
150
|
+
|
|
151
|
+
- The bundled `tautc` generators are **reference** projections. Use them, swap
|
|
152
|
+
them, or write your own against the exported `.ir.json` — pydantic models, plain
|
|
153
|
+
dataclasses, a transactional/ORM wrapper, different client ergonomics, whatever
|
|
154
|
+
fits your app.
|
|
155
|
+
- Or generate nothing: Python and TypeScript drive the IR straight through the
|
|
156
|
+
runtime codec.
|
|
157
|
+
|
|
158
|
+
Where `protoc` hands you *its* message classes and gRPC *its* stubs, taut hands
|
|
159
|
+
you the bytes and the schema and gets out of the way.
|
|
160
|
+
|
|
161
|
+
## `tautc` — the reference codegen CLI
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
tautc gen IR --out DIR [--lang python,typescript,rust,cpp] [--service A,B] [--api-only]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Loads a `.taut.py` or `.ir.json`, validates it, and emits per language:
|
|
168
|
+
`api` (native types + encoders/decoders), plus `client`/`server` stubs per
|
|
169
|
+
service. `--api-only` emits just the struct defs + codec — the drop-in for a
|
|
170
|
+
build script on a compiled target. Example: razel (a Rust build daemon) authors
|
|
171
|
+
`ir/razel.taut.py` and generates its Rust wire layer with
|
|
172
|
+
`tautc gen ir/razel.taut.py -o razel/gen --lang rust --api-only`.
|
|
173
|
+
|
|
174
|
+
These are the *reference* emitters; they read the exported `.ir.json`, and so can
|
|
175
|
+
a generator you write — `tautc` is a convenience, not a requirement.
|
|
176
|
+
|
|
177
|
+
## Evolution
|
|
178
|
+
|
|
179
|
+
- **Breaking-change gate** — structural diff of two IR versions classifies each
|
|
180
|
+
change `breaking` / `compatible` (remove field, retag, change a method's
|
|
181
|
+
shape → breaking; add a field/method → compatible). Run it in CI.
|
|
182
|
+
- **Forward-compatibility** — opt-in unknown-field preservation: a decoder keeps
|
|
183
|
+
unrecognized tags as raw CBOR and re-emits them in canonical order, so an old
|
|
184
|
+
hop relaying a new message loses nothing.
|
|
185
|
+
- **Extensions** — declared, typed side-channels at a reserved tag band
|
|
186
|
+
(`BAND_START = 2^20`); infrastructure reads/writes them on the wire without the
|
|
187
|
+
app schema knowing.
|
|
188
|
+
|
|
189
|
+
## Layout
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
src/taut/ the builder (pure Python)
|
|
193
|
+
ir/ model, DSL, validator, export/load, breaking-change gate, shapes
|
|
194
|
+
wire/ deterministic CBOR + IR-driven codec
|
|
195
|
+
gen/ generators: Rust, C++, and the per-language scaffold
|
|
196
|
+
cli.py the `tautc` command
|
|
197
|
+
ir/ authored IR modules — the only governed artifact
|
|
198
|
+
(griplab.taut.py, razel.taut.py)
|
|
199
|
+
corpus/ generated golden vectors (the oracle)
|
|
200
|
+
docs/ Overview, GettingStarted, Reference, Server + a runnable example
|
|
201
|
+
dev-docs/ design notes + the decisions log (Taut*.md)
|
|
202
|
+
.github/ the PyPI publish workflow (Trusted Publishing on release)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Reference target implementations and the full cross-language interop matrix
|
|
206
|
+
(TS/Rust/Python clients × Python/Rust servers, plus the C++ oracle) live in a
|
|
207
|
+
companion `trial/` repo, each validated against this repo's corpus.
|
|
208
|
+
|
|
209
|
+
## Docs
|
|
210
|
+
|
|
211
|
+
- [Overview](docs/Overview.md) — the mental model and the shape catalog.
|
|
212
|
+
- [Getting Started](docs/GettingStarted.md) — author → validate → encode → generate.
|
|
213
|
+
- [Reference](docs/Reference.md) — every DSL helper, the shapes, the validator rules.
|
|
214
|
+
- [Building a Server](docs/Server.md) — handlers, shape engines, the WS loop.
|
|
215
|
+
- [examples/tasks](docs/examples/tasks/) — a complete runnable API with generated
|
|
216
|
+
code for all four languages.
|
|
217
|
+
|
|
218
|
+
## Develop
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
python run_tests.py # regenerate the corpus, run the suite
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Design notes and the decisions log live in [dev-docs/](dev-docs/).
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# taut
|
|
2
|
+
|
|
3
|
+
**One tiny declarative contract → native types, a deterministic wire codec, and
|
|
4
|
+
service stubs across Python, TypeScript, Rust, and C++ — verified byte-for-byte
|
|
5
|
+
by a shared golden corpus.**
|
|
6
|
+
|
|
7
|
+
taut is a cross-language data + service protocol mechanism: protobuf's essential
|
|
8
|
+
value (a single governed schema, generated types, a portable wire format, an
|
|
9
|
+
evolution gate) with roughly 90% less of its weight (no plugin toolchain, no
|
|
10
|
+
`.proto` grammar, no required runtime, a wire you can read in an afternoon). The
|
|
11
|
+
schema is authored as a small, restricted Python DSL — or any `.ir.json` — and
|
|
12
|
+
everything else is *projected* from it.
|
|
13
|
+
|
|
14
|
+
> Status: **alpha**. The model, codec, generators, corpus, and evolution gate are
|
|
15
|
+
> built and tested end-to-end across four languages (57 tests green); APIs may
|
|
16
|
+
> still move.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
pip install taut-proto # distribution name (PyPI)
|
|
20
|
+
import taut # import name
|
|
21
|
+
tautc gen api.taut.py -o gen/ # CLI codegen
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The distribution is named `taut-proto` (PyPI rejects the bare `taut`); the import
|
|
25
|
+
package and CLI are `taut` / `tautc` — the Pillow→PIL pattern. Pure Python, no
|
|
26
|
+
runtime dependencies.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## taut vs proto3
|
|
31
|
+
|
|
32
|
+
A schema language earns its keep by removing ambiguity between services in
|
|
33
|
+
different languages. proto3 (with gRPC) does that but drags in a code-gen plugin
|
|
34
|
+
architecture, a bespoke grammar, per-language runtimes, an HTTP/2 RPC stack, and
|
|
35
|
+
a wire format few can read by hand. taut keeps the parts that matter and drops
|
|
36
|
+
the rest:
|
|
37
|
+
|
|
38
|
+
| Dimension | proto3 / gRPC | taut |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| **Schema source** | `.proto` DSL + `protoc` + a plugin per language | a few lines of typed Python, or `.ir.json` |
|
|
41
|
+
| **Toolchain** | `protoc` compiler + language plugins | one `pip install`; `tautc` for compiled targets |
|
|
42
|
+
| **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 |
|
|
43
|
+
| **Codegen required?** | yes for most languages (or descriptor-based reflection) | **no** for Python/TS (IR-driven runtime codec); generated for Rust/C++ |
|
|
44
|
+
| **Runtime dependency** | a protobuf runtime per language | **none** (stdlib; hand-rolled codec per target) |
|
|
45
|
+
| **Field presence** | scalars had none; `optional` presence re-added in **3.15** | `optional` → null; presence is clean (fields always emitted) |
|
|
46
|
+
| **`required`** | removed in proto3 | kept as a *governance assertion*, enforced by the evolution gate (never a decode error) |
|
|
47
|
+
| **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) |
|
|
48
|
+
| **Services / RPC** | `service`/`rpc`, but the transport *is* gRPC: HTTP/2 + codegen + runtime | `(name, in, out, shape)`; transport-agnostic (reference: JSON envelope + CBOR payload) |
|
|
49
|
+
| **Streaming** | gRPC client/server/bidi streams | first-class **delivery shapes**: `atom` / `log` / `stream` / `swmr` / `snapshot_delta` / `crdt` |
|
|
50
|
+
| **Schema evolution** | field-number rules + `reserved`; breaking-change detection is **external** (buf, protolock) | a **built-in structural breaking-change gate**, runnable in CI |
|
|
51
|
+
| **Extensions** | proto2 had them; proto3 dropped them (use `Any`) | declared, typed **side-channels** at a reserved tag band |
|
|
52
|
+
| **Read the whole spec** | large surface | the IR fits on a screen |
|
|
53
|
+
|
|
54
|
+
Because the IR is data — not a Turing-complete program — taut can *diff* two
|
|
55
|
+
versions of a contract and tell you mechanically what broke.
|
|
56
|
+
|
|
57
|
+
**What taut deliberately doesn't have (yet):** `map` and `oneof` types,
|
|
58
|
+
well-known types (`Timestamp`/`Any`/`Duration`), a canonical JSON profile
|
|
59
|
+
(deferred), descriptor/reflection services, and — above all — proto's ecosystem
|
|
60
|
+
maturity, scale hardening, and gRPC's battle-tested multi-language RPC runtime.
|
|
61
|
+
taut is a focused mechanism, not a drop-in protobuf replacement for every use
|
|
62
|
+
case. If you need proto's breadth, use proto; taut is the lighter contract when
|
|
63
|
+
you want ~10% of the load for the parts that actually bite.
|
|
64
|
+
|
|
65
|
+
## The contract
|
|
66
|
+
|
|
67
|
+
A schema is **enums + messages + services**, authored declaratively:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from taut.ir.dsl import INT, STR, BYTES, Enum, F, Msg, Ref, List, method, service, schema
|
|
71
|
+
|
|
72
|
+
SCHEMA = schema(
|
|
73
|
+
Enum("BuildStatus", cached=0, built=1, failed=2),
|
|
74
|
+
|
|
75
|
+
Msg("OutputArtifact",
|
|
76
|
+
F("path", 1, STR),
|
|
77
|
+
F("digest", 2, BYTES),
|
|
78
|
+
next_id=3),
|
|
79
|
+
|
|
80
|
+
Msg("BuildResult",
|
|
81
|
+
F("target", 1, STR),
|
|
82
|
+
F("status", 2, Ref("BuildStatus")),
|
|
83
|
+
F("outputs", 3, List(Ref("OutputArtifact"))),
|
|
84
|
+
F("message", 4, STR, optional=True),
|
|
85
|
+
next_id=5),
|
|
86
|
+
|
|
87
|
+
service("Razel",
|
|
88
|
+
method("build", role="in", params=[("target", STR)], out=Ref("BuildResult")),
|
|
89
|
+
method("build.subscribe", role="out", shape="atom", out=Ref("BuildState"))),
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- **Fields** carry an explicit integer `tag`, a type, and flags (`optional`,
|
|
94
|
+
`transient`, CRDT `merge`). Types are a closed set: scalars (`int/str/bytes/
|
|
95
|
+
bool`), enum/message refs, and `List`.
|
|
96
|
+
- **`reserved` + `next_id`** are first-class, validated message features (not
|
|
97
|
+
comments) — retired tags/names can never be reused, and `next_id` is checked.
|
|
98
|
+
- **Methods are the minimal contract `(name, in, out, shape)`.** `shape` is the
|
|
99
|
+
*sole* discriminator: `unary` (request→response, the default) is just the
|
|
100
|
+
degenerate member of an **open shape registry**, alongside streaming shapes
|
|
101
|
+
`atom` (latest-wins state), `log` (append-only), `stream` (live), `swmr`
|
|
102
|
+
(snapshot+delta), `snapshot_delta`, and `crdt`. `out` binds a type per the
|
|
103
|
+
shape's delivery slots. There is no separate "kind" axis to disagree — illegal
|
|
104
|
+
states are unrepresentable. (See [Reference §5–6](docs/Reference.md).)
|
|
105
|
+
|
|
106
|
+
A **validator** rejects incoherent IR (dangling refs, duplicate tags, out-slots
|
|
107
|
+
that don't match a shape, …), so downstream mechanism can be derived without
|
|
108
|
+
review.
|
|
109
|
+
|
|
110
|
+
## The wire
|
|
111
|
+
|
|
112
|
+
A hand-rolled, frozen **deterministic CBOR** subset: definite-length, shortest-
|
|
113
|
+
form integers, ascending integer map keys. The same value encodes to the same
|
|
114
|
+
bytes in every language — pinned by a **golden conformance corpus** (value →
|
|
115
|
+
exact hex). Python and TypeScript run a fully **IR-driven codec** (instantiate a
|
|
116
|
+
client or server from JSON alone, zero codegen); Rust and C++ get **generated
|
|
117
|
+
native types with encoders/decoders** (compiled targets need types ahead of
|
|
118
|
+
time). The C++ corpus is a wall of `static_assert`s — *compiling is the test*.
|
|
119
|
+
|
|
120
|
+
## Opinionated about the wire, not the API
|
|
121
|
+
|
|
122
|
+
Small-and-tight vs. bloated is only half the pitch. The other half: taut pins
|
|
123
|
+
*exactly* what cross-language interop requires — the deterministic wire, the
|
|
124
|
+
`(name, in, out, shape)` contract, and the evolution gate — and **nothing more**.
|
|
125
|
+
Those must be fixed, so they are.
|
|
126
|
+
|
|
127
|
+
How that contract surfaces in *your* code is your call. The IR is the governed
|
|
128
|
+
artifact (a tiny JSON); projecting it into types, clients, and servers is a
|
|
129
|
+
choice, not a mandate:
|
|
130
|
+
|
|
131
|
+
- The bundled `tautc` generators are **reference** projections. Use them, swap
|
|
132
|
+
them, or write your own against the exported `.ir.json` — pydantic models, plain
|
|
133
|
+
dataclasses, a transactional/ORM wrapper, different client ergonomics, whatever
|
|
134
|
+
fits your app.
|
|
135
|
+
- Or generate nothing: Python and TypeScript drive the IR straight through the
|
|
136
|
+
runtime codec.
|
|
137
|
+
|
|
138
|
+
Where `protoc` hands you *its* message classes and gRPC *its* stubs, taut hands
|
|
139
|
+
you the bytes and the schema and gets out of the way.
|
|
140
|
+
|
|
141
|
+
## `tautc` — the reference codegen CLI
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
tautc gen IR --out DIR [--lang python,typescript,rust,cpp] [--service A,B] [--api-only]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Loads a `.taut.py` or `.ir.json`, validates it, and emits per language:
|
|
148
|
+
`api` (native types + encoders/decoders), plus `client`/`server` stubs per
|
|
149
|
+
service. `--api-only` emits just the struct defs + codec — the drop-in for a
|
|
150
|
+
build script on a compiled target. Example: razel (a Rust build daemon) authors
|
|
151
|
+
`ir/razel.taut.py` and generates its Rust wire layer with
|
|
152
|
+
`tautc gen ir/razel.taut.py -o razel/gen --lang rust --api-only`.
|
|
153
|
+
|
|
154
|
+
These are the *reference* emitters; they read the exported `.ir.json`, and so can
|
|
155
|
+
a generator you write — `tautc` is a convenience, not a requirement.
|
|
156
|
+
|
|
157
|
+
## Evolution
|
|
158
|
+
|
|
159
|
+
- **Breaking-change gate** — structural diff of two IR versions classifies each
|
|
160
|
+
change `breaking` / `compatible` (remove field, retag, change a method's
|
|
161
|
+
shape → breaking; add a field/method → compatible). Run it in CI.
|
|
162
|
+
- **Forward-compatibility** — opt-in unknown-field preservation: a decoder keeps
|
|
163
|
+
unrecognized tags as raw CBOR and re-emits them in canonical order, so an old
|
|
164
|
+
hop relaying a new message loses nothing.
|
|
165
|
+
- **Extensions** — declared, typed side-channels at a reserved tag band
|
|
166
|
+
(`BAND_START = 2^20`); infrastructure reads/writes them on the wire without the
|
|
167
|
+
app schema knowing.
|
|
168
|
+
|
|
169
|
+
## Layout
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
src/taut/ the builder (pure Python)
|
|
173
|
+
ir/ model, DSL, validator, export/load, breaking-change gate, shapes
|
|
174
|
+
wire/ deterministic CBOR + IR-driven codec
|
|
175
|
+
gen/ generators: Rust, C++, and the per-language scaffold
|
|
176
|
+
cli.py the `tautc` command
|
|
177
|
+
ir/ authored IR modules — the only governed artifact
|
|
178
|
+
(griplab.taut.py, razel.taut.py)
|
|
179
|
+
corpus/ generated golden vectors (the oracle)
|
|
180
|
+
docs/ Overview, GettingStarted, Reference, Server + a runnable example
|
|
181
|
+
dev-docs/ design notes + the decisions log (Taut*.md)
|
|
182
|
+
.github/ the PyPI publish workflow (Trusted Publishing on release)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Reference target implementations and the full cross-language interop matrix
|
|
186
|
+
(TS/Rust/Python clients × Python/Rust servers, plus the C++ oracle) live in a
|
|
187
|
+
companion `trial/` repo, each validated against this repo's corpus.
|
|
188
|
+
|
|
189
|
+
## Docs
|
|
190
|
+
|
|
191
|
+
- [Overview](docs/Overview.md) — the mental model and the shape catalog.
|
|
192
|
+
- [Getting Started](docs/GettingStarted.md) — author → validate → encode → generate.
|
|
193
|
+
- [Reference](docs/Reference.md) — every DSL helper, the shapes, the validator rules.
|
|
194
|
+
- [Building a Server](docs/Server.md) — handlers, shape engines, the WS loop.
|
|
195
|
+
- [examples/tasks](docs/examples/tasks/) — a complete runnable API with generated
|
|
196
|
+
code for all four languages.
|
|
197
|
+
|
|
198
|
+
## Develop
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
python run_tests.py # regenerate the corpus, run the suite
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Design notes and the decisions log live in [dev-docs/](dev-docs/).
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Distribution name is `taut-proto` (PyPI rejects `taut` as "isn't allowed").
|
|
2
|
+
# Import name stays `taut`: pip install taut-proto -> import taut -> CLI `taut`.
|
|
3
|
+
[project]
|
|
4
|
+
name = "taut-proto"
|
|
5
|
+
version = "0.1.0"
|
|
6
|
+
description = "taut — one declarative IR -> native types, deterministic CBOR wire, IR-driven codec + service contracts, verified by a golden conformance corpus (import: `taut`)"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
license-files = ["LICENSE"]
|
|
11
|
+
authors = [{ name = "Gianni Mariani", email = "gianni@mariani.ws" }]
|
|
12
|
+
keywords = ["idl", "protocol", "schema", "serialization", "cbor", "rpc", "contract"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
"Topic :: Software Development :: Libraries",
|
|
19
|
+
"Topic :: System :: Networking",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [] # stdlib only (hand-rolled deterministic CBOR; no runtime deps)
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/owebeeone/taut"
|
|
25
|
+
Repository = "https://github.com/owebeeone/taut"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
tautc = "taut.cli:main" # codegen CLI: `tautc gen IR --out DIR [--lang ...]`
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["setuptools>=77"] # PEP 639 SPDX license expression support
|
|
32
|
+
build-backend = "setuptools.build_meta"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
36
|
+
include = ["taut*"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["src/tests"]
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""``tautc`` — the taut codegen CLI.
|
|
2
|
+
|
|
3
|
+
Generate native types + encoders/decoders (and optional client/server stubs) from
|
|
4
|
+
a taut IR, so a build script can produce ahead-of-time struct defs for compiled
|
|
5
|
+
targets (Rust/C++) and typed surfaces for Python/TS. The IR is the single governed
|
|
6
|
+
artifact; this just projects it.
|
|
7
|
+
|
|
8
|
+
tautc gen IR --out DIR [--lang python,rust,...] [--service NAME,...] [--api-only]
|
|
9
|
+
|
|
10
|
+
Examples:
|
|
11
|
+
tautc gen api.taut.py -o gen/ # all languages, api + client/server
|
|
12
|
+
tautc gen api.taut.py -o gen/ -l rust,cpp --api-only # just structs + codecs
|
|
13
|
+
tautc gen api.ir.json -o gen/ -l rust -s Tasks # one language, one service
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from .gen import scaffold
|
|
24
|
+
from .ir.load import load_schema, schema_from_json
|
|
25
|
+
from .ir.validate import validate_or_raise
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load(path: Path):
|
|
29
|
+
"""Load a schema from a `.taut.py` DSL module or an exported `.ir.json`."""
|
|
30
|
+
if path.suffix == ".json":
|
|
31
|
+
return schema_from_json(json.loads(path.read_text()))
|
|
32
|
+
return load_schema(path)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _split(arg: str | None) -> list[str] | None:
|
|
36
|
+
return [p.strip() for p in arg.split(",") if p.strip()] if arg else None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _cmd_gen(args: argparse.Namespace) -> int:
|
|
40
|
+
schema = _load(Path(args.ir))
|
|
41
|
+
validate_or_raise(schema) # never generate from incoherent IR
|
|
42
|
+
if args.api_only:
|
|
43
|
+
services: list[str] | None = []
|
|
44
|
+
else:
|
|
45
|
+
services = _split(args.service) # None => all services in the IR
|
|
46
|
+
written = scaffold.emit(
|
|
47
|
+
schema, Path(args.out), langs=_split(args.lang), services=services
|
|
48
|
+
)
|
|
49
|
+
for p in written:
|
|
50
|
+
print(p)
|
|
51
|
+
print(f"# {len(written)} files generated", file=sys.stderr)
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main(argv: list[str] | None = None) -> int:
|
|
56
|
+
p = argparse.ArgumentParser(prog="tautc", description="taut codegen — IR -> code")
|
|
57
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
58
|
+
|
|
59
|
+
g = sub.add_parser("gen", help="generate native types + codec (and client/server) from an IR")
|
|
60
|
+
g.add_argument("ir", help="path to a taut IR (.taut.py DSL module or .ir.json)")
|
|
61
|
+
g.add_argument("-o", "--out", required=True, help="output directory")
|
|
62
|
+
g.add_argument("-l", "--lang", help="comma-separated targets (default: all): python,typescript,rust,cpp")
|
|
63
|
+
g.add_argument("-s", "--service", help="comma-separated services for client/server stubs (default: all in the IR)")
|
|
64
|
+
g.add_argument("--api-only", action="store_true", help="emit only api (types + encoders/decoders), no client/server")
|
|
65
|
+
g.set_defaults(func=_cmd_gen)
|
|
66
|
+
|
|
67
|
+
args = p.parse_args(argv)
|
|
68
|
+
return args.func(args)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
raise SystemExit(main())
|
|
File without changes
|