flattype 1.11.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.
- flattype-1.11.0/.gitignore +7 -0
- flattype-1.11.0/LICENSE +21 -0
- flattype-1.11.0/PKG-INFO +240 -0
- flattype-1.11.0/README.md +207 -0
- flattype-1.11.0/pyproject.toml +76 -0
- flattype-1.11.0/src/flattype/__init__.py +63 -0
- flattype-1.11.0/src/flattype/_decode.py +294 -0
- flattype-1.11.0/src/flattype/_encode.py +187 -0
- flattype-1.11.0/src/flattype/_flatten.py +55 -0
- flattype-1.11.0/src/flattype/_path.py +298 -0
- flattype-1.11.0/src/flattype/_syntax.py +171 -0
- flattype-1.11.0/src/flattype/ai.py +154 -0
- flattype-1.11.0/src/flattype/asgi.py +93 -0
- flattype-1.11.0/src/flattype/negotiate.py +47 -0
- flattype-1.11.0/src/flattype/py.typed +0 -0
- flattype-1.11.0/src/flattype/starlette.py +26 -0
flattype-1.11.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FlatType.js contributors
|
|
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.
|
flattype-1.11.0/PKG-INFO
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: flattype
|
|
3
|
+
Version: 1.11.0
|
|
4
|
+
Summary: JSON <-> Flat Notation: compact key: value text for APIs and LLM prompts. The Python side of @flattype/* on npm.
|
|
5
|
+
Project-URL: Homepage, https://karammm.github.io/flattype/docs/
|
|
6
|
+
Project-URL: Documentation, https://karammm.github.io/flattype/docs/guide/python
|
|
7
|
+
Project-URL: Repository, https://github.com/karammm/flattype
|
|
8
|
+
Project-URL: Issues, https://github.com/karammm/flattype/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/karammm/flattype/blob/main/CHANGELOG.md
|
|
10
|
+
Author: karammm
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: api,asgi,claude,fastapi,flatten,json,llm,serialization,tokens,toon
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Framework :: FastAPI
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
28
|
+
Classifier: Typing :: Typed
|
|
29
|
+
Requires-Python: >=3.9
|
|
30
|
+
Provides-Extra: starlette
|
|
31
|
+
Requires-Dist: starlette>=0.27; extra == 'starlette'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
<h1 align="center">flattype</h1>
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<b>JSON in, compact <code>key: value</code> text out.</b><br>
|
|
38
|
+
Flat Notation for Python APIs and LLM prompts: about half the size of JSON on lists, and easy for people and models to read.
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
<p align="center">
|
|
42
|
+
<a href="https://pypi.org/project/flattype/"><img src="https://img.shields.io/pypi/v/flattype?color=8b6cff" alt="PyPI"></a>
|
|
43
|
+
<a href="https://pypi.org/project/flattype/"><img src="https://img.shields.io/pypi/pyversions/flattype?color=8b6cff" alt="Python versions"></a>
|
|
44
|
+
<a href="https://github.com/karammm/flattype/actions/workflows/ci.yml"><img src="https://github.com/karammm/flattype/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
45
|
+
<img src="https://img.shields.io/badge/dependencies-0-2fd4b2" alt="No dependencies">
|
|
46
|
+
<a href="https://github.com/karammm/flattype/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
<p align="center">
|
|
50
|
+
<a href="https://github.com/karammm/flattype/raw/main/docs/assets/flattype-intro.mp4">
|
|
51
|
+
<img src="https://raw.githubusercontent.com/karammm/flattype/main/docs/assets/flattype-intro.gif" width="800" alt="FlatType in 92 seconds: an API response encoded to Flat Notation, typed models, a server answering in Flat Notation or JSON, framework apps, the CLI, a FastAPI app with FlatMiddleware, and the size comparison with JSON">
|
|
52
|
+
</a><br>
|
|
53
|
+
<sub>▶ <a href="https://github.com/karammm/flattype/raw/main/docs/assets/flattype-intro.mp4">Watch the 92-second intro</a>. The Python part (step 6) is a real FastAPI app.</sub>
|
|
54
|
+
</p>
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
This is the Python side of [FlatType](https://github.com/karammm/flattype), whose `@flattype/*` packages cover JavaScript, React, Vue, Angular, Svelte and Solid. Both write **exactly the same text**, so a FastAPI backend and a TypeScript frontend can talk Flat Notation to each other. A shared set of thousands of test cases checks this on every change.
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install flattype
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Python 3.9+, no dependencies. `pip install "flattype[starlette]"` adds the optional `FlatResponse` class.
|
|
67
|
+
|
|
68
|
+
## Quick start
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import flattype
|
|
72
|
+
|
|
73
|
+
flattype.encode({"name": "John", "age": 30, "city": "New York"})
|
|
74
|
+
# name: John
|
|
75
|
+
# age: 30
|
|
76
|
+
# city: New York
|
|
77
|
+
|
|
78
|
+
flattype.decode("name: John\nage: 30") # {'name': 'John', 'age': 30}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Lists, the shape of most API responses, become tables. Keys are written once, and nested dicts become dot paths:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
flattype.encode({
|
|
85
|
+
"page": 1,
|
|
86
|
+
"users": [
|
|
87
|
+
{"id": 1, "name": "Alice", "address": {"city": "Paris"}},
|
|
88
|
+
{"id": 2, "name": "Bob", "address": {"city": "Delhi"}},
|
|
89
|
+
],
|
|
90
|
+
})
|
|
91
|
+
# page: 1
|
|
92
|
+
# users[2]{id,name,address.city}:
|
|
93
|
+
# 1, Alice, Paris
|
|
94
|
+
# 2, Bob, Delhi
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`flattype.dumps` and `flattype.loads` are aliases, if you prefer the `json` names.
|
|
98
|
+
|
|
99
|
+
## FastAPI, Starlette and any ASGI app
|
|
100
|
+
|
|
101
|
+
One line. Clients that ask for `text/x-flat` get Flat Notation, and everyone else gets the same JSON as before:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from fastapi import FastAPI
|
|
105
|
+
from flattype.asgi import FlatMiddleware
|
|
106
|
+
|
|
107
|
+
app = FastAPI()
|
|
108
|
+
app.add_middleware(FlatMiddleware)
|
|
109
|
+
|
|
110
|
+
@app.get("/users")
|
|
111
|
+
def users():
|
|
112
|
+
return {"users": USERS} # plain FastAPI
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```console
|
|
116
|
+
$ curl localhost:3000/users
|
|
117
|
+
{"users":[{"id":1,"name":"Alice Martin","role":"admin"},{"id":2,"name":"Bob Singh","role":"editor"}]}
|
|
118
|
+
|
|
119
|
+
$ curl -H "Accept: text/x-flat" localhost:3000/users
|
|
120
|
+
users[2]{id,name,role}:
|
|
121
|
+
1, Alice Martin, admin
|
|
122
|
+
2, Bob Singh, editor
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- **How it decides:** only JSON responses (`application/json` or `+json`) are converted, and only when the request's `Accept` header names `text/x-flat` and prefers it to JSON. A browser's `*/*` never counts. These are the same rules as `@flattype/http`.
|
|
126
|
+
- **Caching:** `Vary: Accept` is added to every response, so caches keep the two formats apart.
|
|
127
|
+
- **Dependencies:** none. It's plain ASGI, so it also works with Litestar, Quart or a raw ASGI app.
|
|
128
|
+
- **Clients:** on the JavaScript side, `flatFetch` from [`@flattype/http`](https://www.npmjs.com/package/@flattype/http) or the React, Vue, Angular and Svelte hooks send that header for you.
|
|
129
|
+
|
|
130
|
+
To always answer in Flat Notation from one route, use the response class:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from flattype.starlette import FlatResponse
|
|
134
|
+
|
|
135
|
+
@app.get("/export", response_class=FlatResponse)
|
|
136
|
+
def export():
|
|
137
|
+
return {"users": USERS}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The [FastAPI example](https://github.com/karammm/flattype/tree/main/examples/python-fastapi) runs with one command.
|
|
141
|
+
|
|
142
|
+
## Typed models with Pydantic
|
|
143
|
+
|
|
144
|
+
`decode` returns plain dicts and lists, so Pydantic validates them like any parsed JSON:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from datetime import datetime
|
|
148
|
+
from pydantic import BaseModel, TypeAdapter
|
|
149
|
+
|
|
150
|
+
class User(BaseModel):
|
|
151
|
+
id: int
|
|
152
|
+
full_name: str
|
|
153
|
+
created_at: datetime
|
|
154
|
+
|
|
155
|
+
users = TypeAdapter(list[User]).validate_python(flattype.decode(text)["users"])
|
|
156
|
+
# [User(id=1, full_name='Alice', created_at=datetime.datetime(2026, 1, 1, 9, 0, tzinfo=TzInfo(0)))]
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Claude and other LLMs
|
|
160
|
+
|
|
161
|
+
You pay for every character you send to a model, and on lists Flat Notation is about half the characters of JSON. `flattype.ai` has the helpers from [`@flattype/ai`](https://www.npmjs.com/package/@flattype/ai), with the same output:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
import anthropic
|
|
165
|
+
from flattype.ai import to_prompt, output_instructions, from_model_output
|
|
166
|
+
|
|
167
|
+
client = anthropic.Anthropic()
|
|
168
|
+
message = client.messages.create(
|
|
169
|
+
model="claude-opus-5-5",
|
|
170
|
+
max_tokens=2048,
|
|
171
|
+
messages=[{
|
|
172
|
+
"role": "user",
|
|
173
|
+
"content": f"{to_prompt(orders, name='orders')}\n\nWhich customers ordered twice? "
|
|
174
|
+
+ output_instructions({"customers": [{"name": "Alice", "orders": 2}]}),
|
|
175
|
+
}],
|
|
176
|
+
)
|
|
177
|
+
reply = message.content[0].text
|
|
178
|
+
customers = from_model_output(reply) # or from_model_output(reply, Customer, many=True) with a Pydantic model
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- **`to_prompt(data)`:** the data as Flat Notation inside a `<data>` tag, with a one-line legend that explains the format.
|
|
182
|
+
- **`output_instructions(example)`:** asks the model to answer in Flat Notation, with an example of the shape you expect.
|
|
183
|
+
- **`from_model_output(reply)`:** finds the data in the reply (a `<data>` block, a code fence, or plain JSON instead) and skips prose lines around it. It runs in linear time, even on hostile replies.
|
|
184
|
+
- **`count_tokens(anthropic.Anthropic(), data)`:** compares JSON with Flat Notation using Claude's free token counter.
|
|
185
|
+
- **`tool_result(data)` and `claude_tool_result(id, data)`:** tool results for MCP servers and the Claude API.
|
|
186
|
+
|
|
187
|
+
## More
|
|
188
|
+
|
|
189
|
+
**Lenient decoding** for text from models or people: `decode(text, strict=False)` fills in what's missing instead of raising. Add `skip_invalid_lines=True` to drop lines that aren't Flat Notation.
|
|
190
|
+
|
|
191
|
+
**Streaming:** `Decoder` reads chunks as they arrive, and calls `on_row` for each table row as soon as its line is complete:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
d = flattype.Decoder(on_row=lambda row: print(row.path, row.index, row.value), keep_rows=False)
|
|
195
|
+
for chunk in response.iter_text():
|
|
196
|
+
d.write(chunk)
|
|
197
|
+
d.end()
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Shared objects and cycles:** `encode(x, refs=True)` writes an object that appears more than once only once, then `path = first.path` everywhere else. `decode(text, refs=True)` gives back the same objects, not copies. It's off by default on both ends, because a few reference lines can describe a graph that is endless when walked.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
team = {"lead": {"id": 1}}
|
|
204
|
+
team["members"] = [team["lead"]]
|
|
205
|
+
team["lead"]["team"] = team
|
|
206
|
+
flattype.encode(team, refs=True)
|
|
207
|
+
# lead.id: 1
|
|
208
|
+
# lead.team = ~
|
|
209
|
+
# members[0] = lead
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Other types:** `encode(x, default=fn)` works like `json.dumps(default=...)`. Tuples are written as lists. `encode`, `decode`, `delimiter="|"` / `"\t"`, `indent`, `tabular=False`, `flatten` / `unflatten` and `stats` match the JavaScript options.
|
|
213
|
+
|
|
214
|
+
## Same text as JavaScript
|
|
215
|
+
|
|
216
|
+
Every case in [`conformance/`](https://github.com/karammm/flattype/tree/main/conformance) is generated from `@flattype/core` and `@flattype/ai`, and the Python package must match it exactly. That's 990 encode cases, 3,215 decode cases (1,965 of them errors, compared message, line and column), 150 object graphs, and 425 prompt and model-reply cases. A few things follow from that:
|
|
217
|
+
|
|
218
|
+
- **Floats are written the way JavaScript writes them**, with `1e+21`, `1e-7` and `100`. So a float with no fractional part (`30.0`) decodes as the int `30`. Floats of 1e16 or more come back as an int with JavaScript's digits: the same number once converted to float, but not exactly equal to it.
|
|
219
|
+
- **Ints are exact, however large.** JavaScript would round anything past 2⁵³. A literal longer than 4,300 digits is read as a float, so it can't hit Python's slow and limited `int()` path.
|
|
220
|
+
- **Error columns count code points.** JavaScript counts UTF-16 units, so the two differ only after an emoji or another character outside the Basic Multilingual Plane.
|
|
221
|
+
|
|
222
|
+
## Performance
|
|
223
|
+
|
|
224
|
+
It's pure Python. For 1,000 users with a nested address (Python 3.13, Apple M-series):
|
|
225
|
+
|
|
226
|
+
| | Size | Encode | Decode |
|
|
227
|
+
| --- | ---: | ---: | ---: |
|
|
228
|
+
| `json` (minified, C accelerated) | 114 KB | 0.5 ms | 0.45 ms |
|
|
229
|
+
| `flattype` | 59 KB (−48%) | 4.6 ms | 3.9 ms |
|
|
230
|
+
|
|
231
|
+
The win is in bytes on the wire and tokens in a prompt, not CPU time. Linear-time tests on hostile input (long digit runs, deep paths, long lines streamed in tiny chunks, model replies full of prose or lookalike tags) run in CI.
|
|
232
|
+
|
|
233
|
+
## Links
|
|
234
|
+
|
|
235
|
+
- **Docs:** [karammm.github.io/flattype/docs](https://karammm.github.io/flattype/docs/), including the [format specification](https://karammm.github.io/flattype/docs/guide/format)
|
|
236
|
+
- **Playground:** [karammm.github.io/flattype](https://karammm.github.io/flattype/)
|
|
237
|
+
- **Security:** [policy and guarantees](https://karammm.github.io/flattype/docs/guide/security)
|
|
238
|
+
- **Changes:** [CHANGELOG](https://github.com/karammm/flattype/blob/main/CHANGELOG.md)
|
|
239
|
+
|
|
240
|
+
MIT © karammm
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
<h1 align="center">flattype</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<b>JSON in, compact <code>key: value</code> text out.</b><br>
|
|
5
|
+
Flat Notation for Python APIs and LLM prompts: about half the size of JSON on lists, and easy for people and models to read.
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<a href="https://pypi.org/project/flattype/"><img src="https://img.shields.io/pypi/v/flattype?color=8b6cff" alt="PyPI"></a>
|
|
10
|
+
<a href="https://pypi.org/project/flattype/"><img src="https://img.shields.io/pypi/pyversions/flattype?color=8b6cff" alt="Python versions"></a>
|
|
11
|
+
<a href="https://github.com/karammm/flattype/actions/workflows/ci.yml"><img src="https://github.com/karammm/flattype/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
12
|
+
<img src="https://img.shields.io/badge/dependencies-0-2fd4b2" alt="No dependencies">
|
|
13
|
+
<a href="https://github.com/karammm/flattype/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="https://github.com/karammm/flattype/raw/main/docs/assets/flattype-intro.mp4">
|
|
18
|
+
<img src="https://raw.githubusercontent.com/karammm/flattype/main/docs/assets/flattype-intro.gif" width="800" alt="FlatType in 92 seconds: an API response encoded to Flat Notation, typed models, a server answering in Flat Notation or JSON, framework apps, the CLI, a FastAPI app with FlatMiddleware, and the size comparison with JSON">
|
|
19
|
+
</a><br>
|
|
20
|
+
<sub>▶ <a href="https://github.com/karammm/flattype/raw/main/docs/assets/flattype-intro.mp4">Watch the 92-second intro</a>. The Python part (step 6) is a real FastAPI app.</sub>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
This is the Python side of [FlatType](https://github.com/karammm/flattype), whose `@flattype/*` packages cover JavaScript, React, Vue, Angular, Svelte and Solid. Both write **exactly the same text**, so a FastAPI backend and a TypeScript frontend can talk Flat Notation to each other. A shared set of thousands of test cases checks this on every change.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install flattype
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Python 3.9+, no dependencies. `pip install "flattype[starlette]"` adds the optional `FlatResponse` class.
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import flattype
|
|
39
|
+
|
|
40
|
+
flattype.encode({"name": "John", "age": 30, "city": "New York"})
|
|
41
|
+
# name: John
|
|
42
|
+
# age: 30
|
|
43
|
+
# city: New York
|
|
44
|
+
|
|
45
|
+
flattype.decode("name: John\nage: 30") # {'name': 'John', 'age': 30}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Lists, the shape of most API responses, become tables. Keys are written once, and nested dicts become dot paths:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
flattype.encode({
|
|
52
|
+
"page": 1,
|
|
53
|
+
"users": [
|
|
54
|
+
{"id": 1, "name": "Alice", "address": {"city": "Paris"}},
|
|
55
|
+
{"id": 2, "name": "Bob", "address": {"city": "Delhi"}},
|
|
56
|
+
],
|
|
57
|
+
})
|
|
58
|
+
# page: 1
|
|
59
|
+
# users[2]{id,name,address.city}:
|
|
60
|
+
# 1, Alice, Paris
|
|
61
|
+
# 2, Bob, Delhi
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`flattype.dumps` and `flattype.loads` are aliases, if you prefer the `json` names.
|
|
65
|
+
|
|
66
|
+
## FastAPI, Starlette and any ASGI app
|
|
67
|
+
|
|
68
|
+
One line. Clients that ask for `text/x-flat` get Flat Notation, and everyone else gets the same JSON as before:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from fastapi import FastAPI
|
|
72
|
+
from flattype.asgi import FlatMiddleware
|
|
73
|
+
|
|
74
|
+
app = FastAPI()
|
|
75
|
+
app.add_middleware(FlatMiddleware)
|
|
76
|
+
|
|
77
|
+
@app.get("/users")
|
|
78
|
+
def users():
|
|
79
|
+
return {"users": USERS} # plain FastAPI
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```console
|
|
83
|
+
$ curl localhost:3000/users
|
|
84
|
+
{"users":[{"id":1,"name":"Alice Martin","role":"admin"},{"id":2,"name":"Bob Singh","role":"editor"}]}
|
|
85
|
+
|
|
86
|
+
$ curl -H "Accept: text/x-flat" localhost:3000/users
|
|
87
|
+
users[2]{id,name,role}:
|
|
88
|
+
1, Alice Martin, admin
|
|
89
|
+
2, Bob Singh, editor
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- **How it decides:** only JSON responses (`application/json` or `+json`) are converted, and only when the request's `Accept` header names `text/x-flat` and prefers it to JSON. A browser's `*/*` never counts. These are the same rules as `@flattype/http`.
|
|
93
|
+
- **Caching:** `Vary: Accept` is added to every response, so caches keep the two formats apart.
|
|
94
|
+
- **Dependencies:** none. It's plain ASGI, so it also works with Litestar, Quart or a raw ASGI app.
|
|
95
|
+
- **Clients:** on the JavaScript side, `flatFetch` from [`@flattype/http`](https://www.npmjs.com/package/@flattype/http) or the React, Vue, Angular and Svelte hooks send that header for you.
|
|
96
|
+
|
|
97
|
+
To always answer in Flat Notation from one route, use the response class:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from flattype.starlette import FlatResponse
|
|
101
|
+
|
|
102
|
+
@app.get("/export", response_class=FlatResponse)
|
|
103
|
+
def export():
|
|
104
|
+
return {"users": USERS}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The [FastAPI example](https://github.com/karammm/flattype/tree/main/examples/python-fastapi) runs with one command.
|
|
108
|
+
|
|
109
|
+
## Typed models with Pydantic
|
|
110
|
+
|
|
111
|
+
`decode` returns plain dicts and lists, so Pydantic validates them like any parsed JSON:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from datetime import datetime
|
|
115
|
+
from pydantic import BaseModel, TypeAdapter
|
|
116
|
+
|
|
117
|
+
class User(BaseModel):
|
|
118
|
+
id: int
|
|
119
|
+
full_name: str
|
|
120
|
+
created_at: datetime
|
|
121
|
+
|
|
122
|
+
users = TypeAdapter(list[User]).validate_python(flattype.decode(text)["users"])
|
|
123
|
+
# [User(id=1, full_name='Alice', created_at=datetime.datetime(2026, 1, 1, 9, 0, tzinfo=TzInfo(0)))]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Claude and other LLMs
|
|
127
|
+
|
|
128
|
+
You pay for every character you send to a model, and on lists Flat Notation is about half the characters of JSON. `flattype.ai` has the helpers from [`@flattype/ai`](https://www.npmjs.com/package/@flattype/ai), with the same output:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
import anthropic
|
|
132
|
+
from flattype.ai import to_prompt, output_instructions, from_model_output
|
|
133
|
+
|
|
134
|
+
client = anthropic.Anthropic()
|
|
135
|
+
message = client.messages.create(
|
|
136
|
+
model="claude-opus-5-5",
|
|
137
|
+
max_tokens=2048,
|
|
138
|
+
messages=[{
|
|
139
|
+
"role": "user",
|
|
140
|
+
"content": f"{to_prompt(orders, name='orders')}\n\nWhich customers ordered twice? "
|
|
141
|
+
+ output_instructions({"customers": [{"name": "Alice", "orders": 2}]}),
|
|
142
|
+
}],
|
|
143
|
+
)
|
|
144
|
+
reply = message.content[0].text
|
|
145
|
+
customers = from_model_output(reply) # or from_model_output(reply, Customer, many=True) with a Pydantic model
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- **`to_prompt(data)`:** the data as Flat Notation inside a `<data>` tag, with a one-line legend that explains the format.
|
|
149
|
+
- **`output_instructions(example)`:** asks the model to answer in Flat Notation, with an example of the shape you expect.
|
|
150
|
+
- **`from_model_output(reply)`:** finds the data in the reply (a `<data>` block, a code fence, or plain JSON instead) and skips prose lines around it. It runs in linear time, even on hostile replies.
|
|
151
|
+
- **`count_tokens(anthropic.Anthropic(), data)`:** compares JSON with Flat Notation using Claude's free token counter.
|
|
152
|
+
- **`tool_result(data)` and `claude_tool_result(id, data)`:** tool results for MCP servers and the Claude API.
|
|
153
|
+
|
|
154
|
+
## More
|
|
155
|
+
|
|
156
|
+
**Lenient decoding** for text from models or people: `decode(text, strict=False)` fills in what's missing instead of raising. Add `skip_invalid_lines=True` to drop lines that aren't Flat Notation.
|
|
157
|
+
|
|
158
|
+
**Streaming:** `Decoder` reads chunks as they arrive, and calls `on_row` for each table row as soon as its line is complete:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
d = flattype.Decoder(on_row=lambda row: print(row.path, row.index, row.value), keep_rows=False)
|
|
162
|
+
for chunk in response.iter_text():
|
|
163
|
+
d.write(chunk)
|
|
164
|
+
d.end()
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Shared objects and cycles:** `encode(x, refs=True)` writes an object that appears more than once only once, then `path = first.path` everywhere else. `decode(text, refs=True)` gives back the same objects, not copies. It's off by default on both ends, because a few reference lines can describe a graph that is endless when walked.
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
team = {"lead": {"id": 1}}
|
|
171
|
+
team["members"] = [team["lead"]]
|
|
172
|
+
team["lead"]["team"] = team
|
|
173
|
+
flattype.encode(team, refs=True)
|
|
174
|
+
# lead.id: 1
|
|
175
|
+
# lead.team = ~
|
|
176
|
+
# members[0] = lead
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Other types:** `encode(x, default=fn)` works like `json.dumps(default=...)`. Tuples are written as lists. `encode`, `decode`, `delimiter="|"` / `"\t"`, `indent`, `tabular=False`, `flatten` / `unflatten` and `stats` match the JavaScript options.
|
|
180
|
+
|
|
181
|
+
## Same text as JavaScript
|
|
182
|
+
|
|
183
|
+
Every case in [`conformance/`](https://github.com/karammm/flattype/tree/main/conformance) is generated from `@flattype/core` and `@flattype/ai`, and the Python package must match it exactly. That's 990 encode cases, 3,215 decode cases (1,965 of them errors, compared message, line and column), 150 object graphs, and 425 prompt and model-reply cases. A few things follow from that:
|
|
184
|
+
|
|
185
|
+
- **Floats are written the way JavaScript writes them**, with `1e+21`, `1e-7` and `100`. So a float with no fractional part (`30.0`) decodes as the int `30`. Floats of 1e16 or more come back as an int with JavaScript's digits: the same number once converted to float, but not exactly equal to it.
|
|
186
|
+
- **Ints are exact, however large.** JavaScript would round anything past 2⁵³. A literal longer than 4,300 digits is read as a float, so it can't hit Python's slow and limited `int()` path.
|
|
187
|
+
- **Error columns count code points.** JavaScript counts UTF-16 units, so the two differ only after an emoji or another character outside the Basic Multilingual Plane.
|
|
188
|
+
|
|
189
|
+
## Performance
|
|
190
|
+
|
|
191
|
+
It's pure Python. For 1,000 users with a nested address (Python 3.13, Apple M-series):
|
|
192
|
+
|
|
193
|
+
| | Size | Encode | Decode |
|
|
194
|
+
| --- | ---: | ---: | ---: |
|
|
195
|
+
| `json` (minified, C accelerated) | 114 KB | 0.5 ms | 0.45 ms |
|
|
196
|
+
| `flattype` | 59 KB (−48%) | 4.6 ms | 3.9 ms |
|
|
197
|
+
|
|
198
|
+
The win is in bytes on the wire and tokens in a prompt, not CPU time. Linear-time tests on hostile input (long digit runs, deep paths, long lines streamed in tiny chunks, model replies full of prose or lookalike tags) run in CI.
|
|
199
|
+
|
|
200
|
+
## Links
|
|
201
|
+
|
|
202
|
+
- **Docs:** [karammm.github.io/flattype/docs](https://karammm.github.io/flattype/docs/), including the [format specification](https://karammm.github.io/flattype/docs/guide/format)
|
|
203
|
+
- **Playground:** [karammm.github.io/flattype](https://karammm.github.io/flattype/)
|
|
204
|
+
- **Security:** [policy and guarantees](https://karammm.github.io/flattype/docs/guide/security)
|
|
205
|
+
- **Changes:** [CHANGELOG](https://github.com/karammm/flattype/blob/main/CHANGELOG.md)
|
|
206
|
+
|
|
207
|
+
MIT © karammm
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "flattype"
|
|
7
|
+
version = "1.11.0"
|
|
8
|
+
description = "JSON <-> Flat Notation: compact key: value text for APIs and LLM prompts. The Python side of @flattype/* on npm."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
authors = [{ name = "karammm" }]
|
|
14
|
+
keywords = ["json", "flatten", "serialization", "llm", "tokens", "claude", "toon", "api", "fastapi", "asgi"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Framework :: FastAPI",
|
|
28
|
+
"Topic :: Software Development :: Libraries",
|
|
29
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
32
|
+
dependencies = []
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
starlette = ["starlette>=0.27"]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://karammm.github.io/flattype/docs/"
|
|
39
|
+
Documentation = "https://karammm.github.io/flattype/docs/guide/python"
|
|
40
|
+
Repository = "https://github.com/karammm/flattype"
|
|
41
|
+
Issues = "https://github.com/karammm/flattype/issues"
|
|
42
|
+
Changelog = "https://github.com/karammm/flattype/blob/main/CHANGELOG.md"
|
|
43
|
+
|
|
44
|
+
[dependency-groups]
|
|
45
|
+
dev = [
|
|
46
|
+
"pytest>=8",
|
|
47
|
+
"hypothesis>=6.100",
|
|
48
|
+
"anyio>=4",
|
|
49
|
+
"fastapi>=0.110",
|
|
50
|
+
"httpx>=0.27",
|
|
51
|
+
"mypy>=1.10",
|
|
52
|
+
"ruff>=0.6",
|
|
53
|
+
"pydantic>=2.13.5",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.wheel]
|
|
57
|
+
packages = ["src/flattype"]
|
|
58
|
+
|
|
59
|
+
[tool.hatch.build.targets.sdist]
|
|
60
|
+
include = ["src", "README.md", "LICENSE"]
|
|
61
|
+
|
|
62
|
+
[tool.pytest.ini_options]
|
|
63
|
+
testpaths = ["tests"]
|
|
64
|
+
pythonpath = ["tests"]
|
|
65
|
+
addopts = "-q"
|
|
66
|
+
|
|
67
|
+
[tool.mypy]
|
|
68
|
+
strict = true
|
|
69
|
+
files = ["src"]
|
|
70
|
+
|
|
71
|
+
[tool.ruff]
|
|
72
|
+
line-length = 130
|
|
73
|
+
target-version = "py39"
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint]
|
|
76
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""FlatType for Python: JSON <-> Flat Notation, the compact ``key: value`` format of the @flattype/* npm packages.
|
|
2
|
+
|
|
3
|
+
>>> import flattype
|
|
4
|
+
>>> print(flattype.encode({"name": "John", "age": 30, "city": "New York"}))
|
|
5
|
+
name: John
|
|
6
|
+
age: 30
|
|
7
|
+
city: New York
|
|
8
|
+
>>> flattype.decode("name: John\\nage: 30")
|
|
9
|
+
{'name': 'John', 'age': 30}
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from ._decode import Decoder, TableRow, decode
|
|
19
|
+
from ._encode import encode
|
|
20
|
+
from ._flatten import flatten, unflatten
|
|
21
|
+
from ._syntax import FlatDecodeError
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"MEDIA_TYPE",
|
|
25
|
+
"CONTENT_TYPE",
|
|
26
|
+
"Decoder",
|
|
27
|
+
"FlatDecodeError",
|
|
28
|
+
"FlatStats",
|
|
29
|
+
"TableRow",
|
|
30
|
+
"decode",
|
|
31
|
+
"dumps",
|
|
32
|
+
"encode",
|
|
33
|
+
"flatten",
|
|
34
|
+
"loads",
|
|
35
|
+
"stats",
|
|
36
|
+
"unflatten",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
__version__ = "1.11.0"
|
|
40
|
+
|
|
41
|
+
#: Media type for Flat Notation over HTTP.
|
|
42
|
+
MEDIA_TYPE = "text/x-flat"
|
|
43
|
+
#: ``Content-Type`` for Flat Notation responses.
|
|
44
|
+
CONTENT_TYPE = "text/x-flat; charset=utf-8"
|
|
45
|
+
|
|
46
|
+
#: ``json``-style aliases.
|
|
47
|
+
dumps = encode
|
|
48
|
+
loads = decode
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class FlatStats:
|
|
53
|
+
json_bytes: int
|
|
54
|
+
flat_bytes: int
|
|
55
|
+
#: Fraction saved, e.g. 0.45 for 45% smaller than minified JSON.
|
|
56
|
+
saved: float
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def stats(value: Any, **options: Any) -> FlatStats:
|
|
60
|
+
"""UTF-8 size of ``value`` as minified JSON and as Flat Notation."""
|
|
61
|
+
j = len(json.dumps(value, separators=(",", ":"), ensure_ascii=False).encode("utf-8", "surrogatepass"))
|
|
62
|
+
f = len(encode(value, **options).encode("utf-8", "surrogatepass"))
|
|
63
|
+
return FlatStats(j, f, 0.0 if j == 0 else 1 - f / j)
|