|
1 | | -# JSONLT Python package |
| 1 | +# jsonlt |
2 | 2 |
|
3 | 3 | <!-- vale off --> |
| 4 | +[](https://pypi.org/project/jsonlt-python/) |
4 | 5 | [](https://github.com/jsonlt/jsonlt-python/actions/workflows/ci.yml) |
5 | 6 | [](https://codecov.io/gh/jsonlt/jsonlt-python) |
6 | | -[](https://codspeed.io/jsonlt/jsonlt-python?utm_source=badge) |
| 7 | +[](https://codspeed.io/jsonlt/jsonlt-python) |
7 | 8 | [](https://www.python.org/downloads/) |
8 | 9 | [](https://opensource.org/licenses/MIT) |
9 | 10 | <!-- vale on --> |
10 | 11 |
|
11 | | -**jsonlt** is the Python reference implementation of the [JSON Lines Table (JSONLT) specification][jsonlt]. |
12 | | - |
13 | | -JSONLT is a data format for storing keyed records in append-only files using [JSON Lines](https://jsonlines.org/). The format optimizes for version control diffs and human readability. |
| 12 | +The Python reference implementation of [JSONLT (JSON Lines Table)](https://jsonlt.org), a data format for storing keyed records in append-only files. JSONLT builds on [JSON Lines](https://jsonlines.org/) and optimizes for version control. Modifications append new lines rather than rewriting existing content, producing clean and meaningful diffs. |
14 | 13 |
|
15 | 14 | > [!NOTE] |
16 | | -> This package is in development and not yet ready for production use. |
| 15 | +> This package is under active development. The API may change before the 1.0 release. |
| 16 | +
|
| 17 | +## Resources |
| 18 | + |
| 19 | +- [Specification](https://spec.jsonlt.org): formal definition of the JSONLT format |
| 20 | +- [Documentation](https://docs.jsonlt.org): guides, tutorials, and API reference |
| 21 | +- [Conformance tests](https://github.com/jsonlt/jsonlt/tree/main/conformance): language-agnostic test suite |
17 | 22 |
|
18 | 23 | ## Installation |
19 | 24 |
|
20 | 25 | ```bash |
21 | | -pip install jsonlt-python |
| 26 | +pip install --pre jsonlt-python |
22 | 27 |
|
23 | 28 | # Or |
24 | 29 |
|
25 | | -uv add jsonlt-python |
| 30 | +uv add --pre jsonlt-python |
26 | 31 | ``` |
27 | 32 |
|
28 | | -## Quick start |
| 33 | +Requires Python 3.10 or later. |
29 | 34 |
|
30 | | -### Basic operations |
| 35 | +## Quick start |
31 | 36 |
|
32 | 37 | ```python |
33 | 38 | from jsonlt import Table |
34 | 39 |
|
35 | | -# Open or create a table with a simple key |
| 40 | +# Open or create a table |
36 | 41 | table = Table("users.jsonlt", key="id") |
37 | 42 |
|
38 | 43 | # Insert or update records |
39 | 44 | table.put({"id": "alice", "role": "admin", "email": "alice@example.com"}) |
40 | 45 | table.put({"id": "bob", "role": "user", "email": "bob@example.com"}) |
41 | 46 |
|
42 | | -# Read a record by key |
43 | | -user = table.get("alice") |
44 | | -print(user) # {"id": "alice", "role": "admin", "email": "alice@example.com"} |
45 | | - |
46 | | -# Check if a key exists |
47 | | -if table.has("bob"): |
48 | | - print("Bob exists") |
| 47 | +# Read records |
| 48 | +user = table.get("alice") # Returns the record or None |
| 49 | +exists = table.has("bob") # Returns True |
49 | 50 |
|
50 | | -# Delete a record |
| 51 | +# Delete records (appends a tombstone) |
51 | 52 | table.delete("bob") |
52 | 53 |
|
53 | | -# Get all records |
| 54 | +# Iterate over all records |
54 | 55 | for record in table.all(): |
55 | 56 | print(record) |
56 | 57 | ``` |
57 | 58 |
|
58 | | -### Compound keys |
| 59 | +The underlying file after these operations: |
59 | 60 |
|
60 | | -JSONLT supports multi-field compound keys: |
| 61 | +```jsonl |
| 62 | +{"id": "alice", "role": "admin", "email": "alice@example.com"} |
| 63 | +{"id": "bob", "role": "user", "email": "bob@example.com"} |
| 64 | +{"id": "bob", "$deleted": true} |
| 65 | +``` |
| 66 | + |
| 67 | +## When to use JSONLT |
| 68 | + |
| 69 | +JSONLT works well for configuration, metadata, and small-to-medium datasets where you want human-readable files that play nicely with Git. It's a good fit when you need keyed record storage but don't want the overhead of a database, and when you want to see exactly what changed in a pull request. |
| 70 | + |
| 71 | +JSONLT is not a database. For large datasets, high write throughput, query operations, or concurrent multi-process access, consider SQLite or a full-featured database. |
| 72 | + |
| 73 | +## Compound keys |
| 74 | + |
| 75 | +JSONLT supports multi-field compound keys for composite identifiers: |
61 | 76 |
|
62 | 77 | ```python |
63 | | -# Using a tuple of field names for compound keys |
64 | 78 | orders = Table("orders.jsonlt", key=("customer_id", "order_id")) |
65 | 79 |
|
66 | 80 | orders.put({"customer_id": "alice", "order_id": 1, "total": 99.99}) |
67 | 81 | orders.put({"customer_id": "alice", "order_id": 2, "total": 149.99}) |
68 | 82 |
|
69 | | -# Access with compound key |
70 | 83 | order = orders.get(("alice", 1)) |
71 | 84 | ``` |
72 | 85 |
|
73 | | -### Transactions |
| 86 | +## Transactions |
74 | 87 |
|
75 | | -Use transactions for atomic updates with conflict detection: |
| 88 | +Transactions provide snapshot isolation and atomic writes with conflict detection: |
76 | 89 |
|
77 | 90 | ```python |
78 | 91 | from jsonlt import Table, ConflictError |
79 | 92 |
|
80 | 93 | table = Table("counters.jsonlt", key="name") |
81 | 94 |
|
82 | | -# Context manager commits on success, aborts on exception |
83 | 95 | with table.transaction() as tx: |
84 | 96 | counter = tx.get("visits") |
85 | | - if counter: |
86 | | - tx.put({"name": "visits", "count": counter["count"] + 1}) |
87 | | - else: |
88 | | - tx.put({"name": "visits", "count": 1}) |
| 97 | + new_count = (counter["count"] + 1) if counter else 1 |
| 98 | + tx.put({"name": "visits", "count": new_count}) |
| 99 | +# Commits automatically; rolls back on exception |
89 | 100 |
|
90 | | -# Handle conflicts from concurrent modifications |
| 101 | +# Handle concurrent modification conflicts |
91 | 102 | try: |
92 | 103 | with table.transaction() as tx: |
93 | 104 | tx.put({"name": "counter", "value": 42}) |
94 | 105 | except ConflictError as e: |
95 | 106 | print(f"Conflict on key: {e.key}") |
96 | 107 | ``` |
97 | 108 |
|
98 | | -### Finding records |
| 109 | +## Finding records |
99 | 110 |
|
100 | 111 | ```python |
101 | | -from jsonlt import Table |
102 | | - |
103 | | -table = Table("products.jsonlt", key="sku") |
104 | | - |
105 | 112 | # Find all records matching a predicate |
106 | 113 | expensive = table.find(lambda r: r.get("price", 0) > 100) |
107 | 114 |
|
108 | 115 | # Find with limit |
109 | | -top_3 = table.find(lambda r: r.get("in_stock", False), limit=3) |
| 116 | +top_3 = table.find(lambda r: r.get("in_stock"), limit=3) |
110 | 117 |
|
111 | | -# Find the first matching record |
112 | | -first_match = table.find_one(lambda r: r.get("category") == "electronics") |
| 118 | +# Find the first match |
| 119 | +first = table.find_one(lambda r: r.get("category") == "electronics") |
113 | 120 | ``` |
114 | 121 |
|
115 | | -### Table maintenance |
| 122 | +## Maintenance |
116 | 123 |
|
117 | 124 | ```python |
118 | | -from jsonlt import Table |
119 | | - |
120 | | -table = Table("data.jsonlt", key="id") |
121 | | - |
122 | | -# Compact the table (removes tombstones and superseded records) |
| 125 | +# Compact the file (removes tombstones and superseded records) |
123 | 126 | table.compact() |
124 | 127 |
|
125 | | -# Clear all records (keeps header if present) |
| 128 | +# Clear all records |
126 | 129 | table.clear() |
127 | | -``` |
128 | 130 |
|
129 | | -## API overview |
130 | | - |
131 | | -### Table class |
132 | | - |
133 | | -The `Table` class is the primary interface for working with JSONLT files. |
| 131 | +# Force reload from disk |
| 132 | +table.reload() |
| 133 | +``` |
134 | 134 |
|
135 | | -| Method | Description | |
136 | | -|-------------------------------|--------------------------------------------------| |
137 | | -| `Table(path, key)` | Open or create a table at the given path | |
138 | | -| `get(key)` | Get a record by key, returns `None` if not found | |
139 | | -| `has(key)` | Check if a key exists | |
140 | | -| `put(record)` | Insert or update a record | |
141 | | -| `delete(key)` | Delete a record, returns whether it existed | |
142 | | -| `all()` | Get all records in key order | |
143 | | -| `keys()` | Get all keys in key order | |
144 | | -| `items()` | Get all (key, record) pairs in key order | |
145 | | -| `count()` | Get the number of records | |
146 | | -| `find(predicate, limit=None)` | Find records matching a predicate | |
147 | | -| `find_one(predicate)` | Find the first matching record | |
148 | | -| `transaction()` | Start a new transaction | |
149 | | -| `compact()` | Compact the table file | |
150 | | -| `clear()` | Remove all records | |
151 | | -| `reload()` | Force reload from disk | |
| 135 | +## API summary |
| 136 | + |
| 137 | +### Table |
| 138 | + |
| 139 | +| Method | Description | |
| 140 | +|-------------------------------|--------------------------------| |
| 141 | +| `Table(path, key)` | Open or create a table | |
| 142 | +| `get(key)` | Get a record by key, or `None` | |
| 143 | +| `has(key)` | Check if a key exists | |
| 144 | +| `put(record)` | Insert or update a record | |
| 145 | +| `delete(key)` | Delete a record | |
| 146 | +| `all()` | Iterate all records | |
| 147 | +| `keys()` | Iterate all keys | |
| 148 | +| `items()` | Iterate (key, record) pairs | |
| 149 | +| `count()` | Number of records | |
| 150 | +| `find(predicate, limit=None)` | Find matching records | |
| 151 | +| `find_one(predicate)` | Find first match | |
| 152 | +| `transaction()` | Start a transaction | |
| 153 | +| `compact()` | Remove historical entries | |
| 154 | +| `clear()` | Remove all records | |
| 155 | +| `reload()` | Reload from disk | |
| 156 | + |
| 157 | +The `Table` class also supports `len(table)`, `key in table`, and `for record in table`. |
| 158 | + |
| 159 | +### Transaction |
| 160 | + |
| 161 | +| Method | Description | |
| 162 | +|---------------|-------------------| |
| 163 | +| `get(key)` | Get from snapshot | |
| 164 | +| `has(key)` | Check in snapshot | |
| 165 | +| `put(record)` | Buffer a write | |
| 166 | +| `delete(key)` | Buffer a deletion | |
| 167 | +| `commit()` | Write to disk | |
| 168 | +| `abort()` | Discard changes | |
| 169 | + |
| 170 | +### Exceptions |
152 | 171 |
|
153 | | -The `Table` class also supports idiomatic Python operations: |
| 172 | +All exceptions inherit from `JSONLTError`: |
154 | 173 |
|
155 | | -- `len(table)` - number of records |
156 | | -- `key in table` - check if key exists |
157 | | -- `for record in table` - iterate over records |
| 174 | +| Exception | Description | |
| 175 | +|--------------------|---------------------------| |
| 176 | +| `ParseError` | Invalid file format | |
| 177 | +| `InvalidKeyError` | Invalid or missing key | |
| 178 | +| `FileError` | I/O error | |
| 179 | +| `LockError` | Cannot get lock | |
| 180 | +| `LimitError` | Size limit exceeded | |
| 181 | +| `TransactionError` | Invalid transaction state | |
| 182 | +| `ConflictError` | Write-write conflict | |
158 | 183 |
|
159 | | -### Transaction class |
| 184 | +## Conformance |
160 | 185 |
|
161 | | -The `Transaction` class provides snapshot isolation and buffered writes. |
| 186 | +This library implements the [JSONLT 1.0 Specification](https://jsonlt.org/spec). It provides both Lenient and Strict Parser conformance profiles, and generates Strict-conformant output by default. |
162 | 187 |
|
163 | | -| Method | Description | |
164 | | -|---------------|--------------------------------------------| |
165 | | -| `get(key)` | Get a record from the transaction snapshot | |
166 | | -| `has(key)` | Check if a key exists in the snapshot | |
167 | | -| `put(record)` | Buffer a record for commit | |
168 | | -| `delete(key)` | Buffer a deletion for commit | |
169 | | -| `commit()` | Write buffered changes to disk | |
170 | | -| `abort()` | Discard buffered changes | |
| 188 | +As the reference implementation, jsonlt-python passes the [JSONLT conformance test suite](https://github.com/jsonlt/jsonlt/tree/main/tests). Implementers of other JSONLT libraries can use these tests to verify compatibility. |
171 | 189 |
|
172 | | -### Exception hierarchy |
| 190 | +## Acknowledgements |
173 | 191 |
|
174 | | -All exceptions inherit from `JSONLTError`: |
| 192 | +The JSONLT format draws from related work including [BEADS](https://github.com/steveyegge/beads), which uses JSONL for git-backed structured storage. |
175 | 193 |
|
176 | | -| Exception | Description | |
177 | | -|--------------------|--------------------------------| |
178 | | -| `ParseError` | Invalid file format or content | |
179 | | -| `InvalidKeyError` | Invalid or missing key | |
180 | | -| `FileError` | File I/O error | |
181 | | -| `LockError` | Cannot obtain file lock | |
182 | | -| `LimitError` | Size limit exceeded | |
183 | | -| `TransactionError` | Transaction state error | |
184 | | -| `ConflictError` | Write-write conflict detected | |
| 194 | +### AI disclosure |
185 | 195 |
|
186 | | -## Documentation |
| 196 | +The development of this library involved AI language models, specifically Claude (Anthropic). AI tools contributed to drafting code, tests, and documentation. Human authors made all design decisions and final implementations, and they reviewed, edited, and validated AI-generated content. The authors take full responsibility for the correctness of this software. |
187 | 197 |
|
188 | | -For detailed documentation, tutorials, and the full specification, visit [jsonlt.org/docs](https://jsonlt.org/docs). |
| 198 | +This disclosure promotes transparency about modern software development practices. |
189 | 199 |
|
190 | 200 | ## License |
191 | 201 |
|
192 | 202 | MIT License. See [LICENSE](LICENSE) for details. |
193 | | - |
194 | | -[jsonlt]: https://jsonlt.org/ |
|
0 commit comments