TSVZ 4.9__tar.gz → 5.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.
- {tsvz-4.9 → tsvz-5.0}/PKG-INFO +145 -21
- {tsvz-4.9 → tsvz-5.0}/README.md +140 -19
- {tsvz-4.9 → tsvz-5.0}/TSVZ.egg-info/PKG-INFO +145 -21
- {tsvz-4.9 → tsvz-5.0}/TSVZ.py +2638 -1397
- {tsvz-4.9 → tsvz-5.0}/setup.py +9 -6
- {tsvz-4.9 → tsvz-5.0}/LICENSE +0 -0
- {tsvz-4.9 → tsvz-5.0}/TSVZ.egg-info/SOURCES.txt +0 -0
- {tsvz-4.9 → tsvz-5.0}/TSVZ.egg-info/dependency_links.txt +0 -0
- {tsvz-4.9 → tsvz-5.0}/TSVZ.egg-info/entry_points.txt +0 -0
- {tsvz-4.9 → tsvz-5.0}/TSVZ.egg-info/requires.txt +0 -0
- {tsvz-4.9 → tsvz-5.0}/TSVZ.egg-info/top_level.txt +0 -0
- {tsvz-4.9 → tsvz-5.0}/setup.cfg +0 -0
{tsvz-4.9 → tsvz-5.0}/PKG-INFO
RENAMED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TSVZ
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0
|
|
4
4
|
Summary: Key-value store kept in a TSV / CSV / NSV / PSV file, with tsvz-spec-v1 support
|
|
5
|
-
Home-page: https://github.com/
|
|
5
|
+
Home-page: https://github.com/tsvz-org/TSVZ
|
|
6
6
|
Author: Yufei Pan
|
|
7
7
|
Author-email: pan@zopyr.us
|
|
8
8
|
License: GPLv3+
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.6
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
14
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
12
15
|
Classifier: Operating System :: OS Independent
|
|
13
16
|
Requires-Python: >=3.6
|
|
@@ -36,7 +39,7 @@ key-value store in a delimiter-separated text file**. The first column of each
|
|
|
36
39
|
row is the key; the file behaves like an ordered dictionary that is persisted to
|
|
37
40
|
disk as you change it.
|
|
38
41
|
|
|
39
|
-
**Version
|
|
42
|
+
**Version 5.0**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
|
|
40
43
|
next to your code and `import TSVZ`, or install it.
|
|
41
44
|
|
|
42
45
|
```bash
|
|
@@ -48,7 +51,7 @@ pip install -e . # editable, from this directory
|
|
|
48
51
|
|
|
49
52
|
| Extension | Rules |
|
|
50
53
|
|---|---|
|
|
51
|
-
| `.tsv` `.csv` `.nsv` `.psv` (and any other) | The TSVZ **3.39** format, unchanged. A 3.39 host can read every file
|
|
54
|
+
| `.tsv` `.csv` `.nsv` `.psv` (and any other) | The TSVZ **3.39** format, unchanged. A 3.39 host can read every file this version writes. |
|
|
52
55
|
| `.tsvz` `.csvz` `.nsvz` `.psvz` | The **[tsvz-spec-v1](tsvz-spec-v1.md)** append-only key-value log. |
|
|
53
56
|
|
|
54
57
|
The delimiter follows the extension: tab, comma, NUL, pipe. A trailing
|
|
@@ -94,7 +97,7 @@ tsvz people.tsvz append carol Carol 5 # the 3.39 form still works
|
|
|
94
97
|
tsvz -V
|
|
95
98
|
```
|
|
96
99
|
|
|
97
|
-
## What TSVZ
|
|
100
|
+
## What TSVZ does with `.tsvz` files
|
|
98
101
|
|
|
99
102
|
- **Reads** everything a conformant writer can produce: markers
|
|
100
103
|
(`#_defaults_#`, `#_strip_trailing_whites_#`, `#_fill_empty_with_default_#`,
|
|
@@ -121,7 +124,12 @@ tsvz -V
|
|
|
121
124
|
`setdefault` and `pop` behave like a dict.
|
|
122
125
|
- **`#` keys** are stored (`<#>key`). Keys of the form `#_name_#` are marker
|
|
123
126
|
writes: `t['#_defaults_#'] = [...]` sets the defaults, and so does
|
|
124
|
-
`setDefaults([...])` once the object is constructed.
|
|
127
|
+
`setDefaults([...])` once the object is constructed. `#_snapshot_#` is
|
|
128
|
+
refused (spec §19.10.2).
|
|
129
|
+
- **Other writers:** a `TSVZed` notices another process's change by the active
|
|
130
|
+
part's inode, size and times and by the list of parts, and reloads. Rows it
|
|
131
|
+
has queued but not yet written stay in memory across that reload, and a live
|
|
132
|
+
key is never missing while it runs.
|
|
125
133
|
|
|
126
134
|
## Command line
|
|
127
135
|
|
|
@@ -130,6 +138,9 @@ tsvz -V
|
|
|
130
138
|
lone `--`. The 3.39 form `tsvz STORE [OPERATION] [ARG ...]` works too; a store
|
|
131
139
|
named like an operation then needs a path prefix (`./read`).
|
|
132
140
|
|
|
141
|
+
- **One engine.** Every operation runs through the engine `tsvz serve` uses,
|
|
142
|
+
in its own process when no server runs, so a command prints the same with or
|
|
143
|
+
without a server.
|
|
133
144
|
- **stdout carries only rows.** It gets a table on a terminal and records
|
|
134
145
|
otherwise; `--format table|records` overrides. Records use the store's
|
|
135
146
|
delimiter and escaping, so `tsvz read A | tsvz set B -` copies a store of
|
|
@@ -198,7 +209,8 @@ with TSVZ.TSVZClient('people.tsvz') as people: # served, or in this process whe
|
|
|
198
209
|
`setDefaults`. Its writes return once they are written (raising `OSError` if they
|
|
199
210
|
cannot be), one client may be shared between threads, and without a server it
|
|
200
211
|
warns once and works on the files in-process, finishing its writes when the
|
|
201
|
-
program exits.
|
|
212
|
+
program exits. A server of a `.tsv`-family file names its delimiter in
|
|
213
|
+
`STORE.serve`, and `TSVZClient` splits string values on it.
|
|
202
214
|
|
|
203
215
|
## Fault tolerance
|
|
204
216
|
|
|
@@ -219,9 +231,116 @@ recovered, skipped or stops the read, as the store's `#_on_corruption_#` says,
|
|
|
219
231
|
part keeps the original as `<part>.damaged-<time>`), an unreadable part, and a
|
|
220
232
|
delimiter or encoding argument that conflicts with the extension.
|
|
221
233
|
|
|
234
|
+
Writing is stricter than reading: a key, value, header or marker that cannot be
|
|
235
|
+
encoded as UTF-8, such as the lone surrogate `os.fsdecode` gives for a
|
|
236
|
+
non-UTF-8 file name, raises a `ValueError` (`TSVZEncodeError`) that names it
|
|
237
|
+
and writes nothing, instead of storing `?`. Pass `errors='replace'` (or any codec
|
|
238
|
+
error handler) to the writing functions or to `TSVZed`, `TSVZedLite` and `TSVZClient`
|
|
239
|
+
to get the old `?`. `TSVZed` and `TSVZedLite` refuse such a row when it is set. A
|
|
240
|
+
rewrite that meets such text leaves the file as it is: `mapToFile()` and
|
|
241
|
+
`hardMapToFile()` raise the error, and the rewrites `TSVZed` makes itself report it
|
|
242
|
+
once. With a non-UTF-8 `encoding=`, U+FFFD (what the reader puts for a byte it could
|
|
243
|
+
not decode) is text that encoding may lack, and it is refused like any other: such a
|
|
244
|
+
store is not rewritten (`rewrite_on_load` leaves the file as it is, without failing),
|
|
245
|
+
appends of rows that can be written still work, and the delete or update of a key that
|
|
246
|
+
holds U+FFFD raises. Such a key stays until the file is edited, the
|
|
247
|
+
store cleared, or the store opened with the encoding it was written in; `errors='replace'`
|
|
248
|
+
would write `?` for U+FFFD, losing the bytes the file holds there. The `tsvz` command
|
|
249
|
+
exits 1 with the message.
|
|
250
|
+
|
|
251
|
+
A `TSVZed` writes a `.tsv`, `.csv`, `.nsv` or `.psv` file (compressed or not) from
|
|
252
|
+
its background thread only: `rewrite()`, `mapToFile()`, `hardMapToFile()`,
|
|
253
|
+
`commitAppendToFile()`, `clear()`, `clear_file()`, `reload()`, `close()` and the
|
|
254
|
+
constructor's load hand it the job and wait, so a Ctrl-C stops the wait, never the
|
|
255
|
+
write (the call still raises `KeyboardInterrupt`). At exit a write in progress is
|
|
256
|
+
finished: a Ctrl-C then says so and keeps waiting, and a second one abandons it. A
|
|
257
|
+
closed store is freed. (`get_file_obj()` and `release_file_obj()`, kept from 3.39,
|
|
258
|
+
hand the locked file to the calling thread.) A file another process removed is made
|
|
259
|
+
again from memory by the store's next write, an append included (a removal in the
|
|
260
|
+
microseconds as a write starts is followed as a rotation instead); while that rewrite
|
|
261
|
+
fails or is refused, the rows are appended to a new file there, and memory keeps all of
|
|
262
|
+
them for the rewrite that gets through. A compressed file another writer left damaged
|
|
263
|
+
is made again too (L5: every read stops at the damage, so rows
|
|
264
|
+
appended after it would be read by no one); the bytes after the damage go. While that
|
|
265
|
+
rewrite fails, the rows are appended after the damage and memory keeps them for the
|
|
266
|
+
next rewrite that gets through; while it is refused (a row changed in place to text
|
|
267
|
+
the encoding cannot hold), they are appended after it as in 4.9, and the next read
|
|
268
|
+
drops them. Either way `close()` tries the rewrite once more, whatever
|
|
269
|
+
`rewrite_on_exit` says. Appends that do not read the store first (`tsvz set`,
|
|
270
|
+
`appendTabularFile`, `TSVZedLite`, and a `TSVZed` with `monitor_external_changes=False`
|
|
271
|
+
for damage that lands after it loaded the file) still land after it, and so do a
|
|
272
|
+
`TSVZed`'s rows when a writer that takes no lock leaves the damage between the store's read
|
|
273
|
+
and its append ([docs/limitations.md](docs/limitations.md)).
|
|
274
|
+
|
|
275
|
+
It knows the size and SHA-256 of the bytes it last read or wrote. When another process
|
|
276
|
+
has changed the file, and it is the same file (device and inode) and still begins with
|
|
277
|
+
those bytes, nothing this store set is still waiting to be written, and
|
|
278
|
+
`setDefaults()` has not changed its defaults since, only what follows is read and
|
|
279
|
+
applied to memory, as a read of the file applies it: memory keeps an order set by
|
|
280
|
+
`move_to_end`, and its `#` keys. Any other change reloads the file, changing memory
|
|
281
|
+
in place, so a live key is never missing meanwhile.
|
|
282
|
+
|
|
283
|
+
A rewrite writes over the file and then cuts it to its new length, so a reader never
|
|
284
|
+
meets it emptied. One that fails part way (a full disk, an I/O error), or that cannot
|
|
285
|
+
make again a file that was removed, leaves the file damaged: the new bytes it wrote,
|
|
286
|
+
then the old ones after them. Memory then holds the only good copy, as it does when
|
|
287
|
+
the store appends after damage another writer left that it cannot make the file again
|
|
288
|
+
over yet (that rewrite failed, or waits out a failed one's back-off). Until a rewrite gets through, the file is not
|
|
289
|
+
reloaded over memory; the background writer still appends the rows set meanwhile, and
|
|
290
|
+
the next rewrite (the background writer's, `rewrite()`, `close()`) makes the file
|
|
291
|
+
again from memory. A change another process
|
|
292
|
+
made meanwhile is merged into memory first, as above, when the file is still the one
|
|
293
|
+
the damage was left in (for a file that was removed, any file made there since) and
|
|
294
|
+
still begins with exactly the bytes it left; after a damage that left the file empty
|
|
295
|
+
(or removed it), what follows must also begin with this store's header.
|
|
296
|
+
Otherwise that change is overwritten, with the warning `Overwriting external changes`.
|
|
297
|
+
What cannot be told from an append is merged as one: another process's rewrite of the
|
|
298
|
+
same file in place that begins with exactly the bytes the damage left, and, for a store
|
|
299
|
+
with no header (or `verifyHeader=False`), anything written to a file the damage left
|
|
300
|
+
empty; the keys it sets or deletes then take its values, and the keys it left out keep
|
|
301
|
+
memory's. An explicit `reload()` while the file is damaged replaces memory with the
|
|
302
|
+
damaged file, as asked, and warns.
|
|
303
|
+
|
|
304
|
+
## Upgrading to 5.0 from 4.9
|
|
305
|
+
|
|
306
|
+
- **Writes refuse text they cannot encode.** A key, value, header, defaults line or
|
|
307
|
+
marker the file's encoding cannot hold (UTF-8 for `.tsvz`; `encoding=` for a
|
|
308
|
+
`.tsv`), such as the lone surrogate `os.fsdecode` gives for a non-UTF-8 file name,
|
|
309
|
+
raises `TSVZEncodeError` (a `ValueError`) and writes nothing; 4.9 stored `?`.
|
|
310
|
+
`errors='replace'` brings `?` back. Under a non-UTF-8 `encoding=`, a key read as
|
|
311
|
+
U+FFFD cannot be deleted or updated. `tsvz` exits 1 with the message. See Fault
|
|
312
|
+
tolerance.
|
|
313
|
+
- **A Ctrl-C no longer cuts a `.tsv` write.** Every write of a `.tsv`-family file
|
|
314
|
+
runs on the store's background thread; a call waits for it, a Ctrl-C raises in the
|
|
315
|
+
caller and the write still finishes, and at exit a write in progress is finished
|
|
316
|
+
(a second Ctrl-C abandons it). In 4.9 a Ctrl-C while `TSVZed()` opened a
|
|
317
|
+
compressed store (its rewrite on load) could leave the file with no readable row.
|
|
318
|
+
A closed `TSVZed` or `TSVZedLite` is freed; 4.9 kept every one until exit.
|
|
319
|
+
- **Another process's change is taken in, not reloaded over memory.** An append to
|
|
320
|
+
a `.tsv`-family file is read from where the store left off, so an order set by
|
|
321
|
+
`move_to_end` and `#` keys survive it; any reload (both dialects) changes memory
|
|
322
|
+
in place, so another thread never finds a live key missing. After a rewrite of
|
|
323
|
+
the store's own fails part way, memory is kept and another process's appends are
|
|
324
|
+
merged into it (Fault tolerance).
|
|
325
|
+
- **`.tsvz` stores** see another process's append made right after their load, a
|
|
326
|
+
commit or a clear (4.9 missed it until the next change), keep the rows set while
|
|
327
|
+
they reload, refuse `#_snapshot_#` (spec §19.10.2), and raise a strict header
|
|
328
|
+
mismatch at the first read.
|
|
329
|
+
- **`.tsv` files:** an append to an existing empty file writes the header first, and
|
|
330
|
+
reading one gives an empty store; see L8 to L14 for the rest.
|
|
331
|
+
- **`tsvz`** runs every operation through the engine `tsvz serve` uses, with the same
|
|
332
|
+
output. A server of a `.tsv` (or other tab- or NUL-delimited) file is now found by
|
|
333
|
+
`tsvz` commands, `TSVZClient` and `tsvz stop`; 4.9's clients rejected its pointer
|
|
334
|
+
file. The 3.39 warning that a file name does not end in `.tsv` is printed only
|
|
335
|
+
when a command creates such a file, and a write that cannot be made reports the
|
|
336
|
+
operating system's error.
|
|
337
|
+
- **macOS and other systems that lock with `lockf`:** 4.9's close gate is gone, so
|
|
338
|
+
a read in another thread of the process can drop a write's lock
|
|
339
|
+
([docs/limitations.md](docs/limitations.md)).
|
|
340
|
+
|
|
222
341
|
## Breaking changes from 3.39
|
|
223
342
|
|
|
224
|
-
### `.tsv`, `.csv`, `.nsv`, `.psv`:
|
|
343
|
+
### `.tsv`, `.csv`, `.nsv`, `.psv`: the format is unchanged
|
|
225
344
|
|
|
226
345
|
| # | Change |
|
|
227
346
|
|---|---|
|
|
@@ -232,6 +351,13 @@ delimiter or encoding argument that conflicts with the extension.
|
|
|
232
351
|
| L5 | A damaged compressed file is read up to the damage instead of raising. |
|
|
233
352
|
| L6 | `mapToFile` no longer glues a row it adds at the end of the file to the next one. |
|
|
234
353
|
| L7 | `TSVZed`'s `in` and `get` find a key as `t[key]` does: the live key equal to it, else the key right-stripped. 3.39 matched them exactly, so `'x ' in t` was false while `t['x ']` read `x`, and `t.setdefault('x ', …)` overwrote `x`. |
|
|
354
|
+
| L8 | Appending (`TSVZed`, `TSVZedLite`, `appendTabularFile`, `appendLinesTabularFile`) to an existing empty file (no byte, or a compressed file holding only empty streams) writes the store's header first, as creating the file does; 3.39 wrote the rows alone (a read then took the first one for the header), and `appendTabularFile` with `strict` refused the file. |
|
|
355
|
+
| L9 | Reading such an empty file with a header (and `verifyHeader`) gives an empty store with that header: `readTabularFile`, `TSVZed` and `TSVZedLite` with `strict` no longer refuse it with `Header mismatch` (a `TSVZed` restarted after a rewrite that left the file empty failed in its constructor), and without `strict` no longer print a header mismatch. |
|
|
356
|
+
| L10 | Writing text the file's encoding cannot hold (a lone surrogate from `os.fsdecode`, a character the `encoding=` lacks) raises `TSVZEncodeError`, a `ValueError`, and writes nothing; 3.39 and 4.9 wrote `?`, which `errors='replace'` brings back. |
|
|
357
|
+
| L11 | `pop()` and `popitem()` of a `#` key, which lives in memory only, write no tombstone line to the file. |
|
|
358
|
+
| L12 | A rewrite of a file whose header line has no newline writes the rows on lines of their own; 3.39 wrote the first one onto the header line. |
|
|
359
|
+
| L13 | A compressed file is rewritten in members of at most 1 MiB, as appends already were: the bytes differ, the rows read are the same, and 3.39 reads them. |
|
|
360
|
+
| L14 | `TSVZed` opening a compressed file in a codec this Python lacks (zstd before Python 3.14) raises `ImportError` instead of reading the compressed bytes as text. |
|
|
235
361
|
|
|
236
362
|
### `.tsvz`, `.csvz`, `.nsvz`, `.psvz`: now tsvz-spec-v1
|
|
237
363
|
|
|
@@ -281,7 +407,7 @@ Conformance testing).
|
|
|
281
407
|
|
|
282
408
|
- §17.5: a numbered part opened directly is read alone, with a warning.
|
|
283
409
|
- §14.5: the defaults-on-missing read applies to `t[key]` only.
|
|
284
|
-
- §18.6: locking is one exclusive whole-file lock per write plus an in-process lock; readers do not lock. On Linux it is an open file description lock, which still excludes 3.39's `lockf`; elsewhere (macOS and other systems, or a Linux file system that refuses those locks) it is `lockf`, which closing any descriptor of the file in the process drops: there a read in another thread of the same process, such as a handler's or a `TSVZed`'s, can drop a write's lock while it is held. A known limitation,
|
|
410
|
+
- §18.6: locking is one exclusive whole-file lock per write plus an in-process lock; readers do not lock. On Linux it is an open file description lock, which still excludes 3.39's `lockf`; elsewhere (macOS and other systems, or a Linux file system that refuses those locks) it is `lockf`, which closing any descriptor of the file in the process drops: there a read in another thread of the same process, such as a handler's or a `TSVZed`'s, can drop a write's lock while it is held. A known limitation, deferred: see [docs/limitations.md](docs/limitations.md).
|
|
285
411
|
- §19.4: a scrub whose rows would read differently under the final markers writes them with stripping and fill-empty off, then restores the final markers after the rows.
|
|
286
412
|
- §20.7: `-q` also silences informational messages ("Created …"); only errors remain.
|
|
287
413
|
- §20.2: `scrub` of a multi-part store, and `clear` of one part of one, are refused with exit 1; `read`, `get` and `parts` of a numbered part read that part alone, with a warning.
|
|
@@ -296,16 +422,12 @@ Conformance testing).
|
|
|
296
422
|
|
|
297
423
|
## Website
|
|
298
424
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
python3 website/tsvz_site.py build /tmp/site # static files for any host
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
`deploy/tsvz-site.service` runs it under systemd; restart it to publish an edit.
|
|
425
|
+
The tsvz.org site lives in [tsvz-org/tsvz.org](https://github.com/tsvz-org/tsvz.org).
|
|
426
|
+
It renders `tsvz-spec-v1.md` from a checkout of this repository next to its own,
|
|
427
|
+
and its tests also check the spec: every § reference names a real section, and
|
|
428
|
+
the spec uses only markdown the site renders. Run them after editing the spec
|
|
429
|
+
(see Tests); CI runs them on every push here, and the site publishes `main`'s
|
|
430
|
+
spec each morning.
|
|
309
431
|
|
|
310
432
|
## Conformance testing
|
|
311
433
|
|
|
@@ -365,14 +487,16 @@ runs, because a single run can land in a slower mode as a whole.
|
|
|
365
487
|
|
|
366
488
|
```bash
|
|
367
489
|
python3 -m pytest TSVZ_test.py -q # everything, incl. 3.39 differential tests
|
|
368
|
-
python3 -m pytest
|
|
490
|
+
python3 -m pytest ../tsvz.org/tsvz_site_test.py -q # the spec as tsvz.org renders it (needs the tsvz.org checkout next to this one)
|
|
369
491
|
python3 -m pytest tsvzbench_test.py -q # the benchmark harness
|
|
370
492
|
python3 -m pytest tsvzconform_test.py -q # the conformance runner and cases, against the reference
|
|
371
493
|
python3 benchTSVZ.py /tmp/b.tsv -n 100000 --compare-old # benchmark (not a test)
|
|
372
494
|
```
|
|
373
495
|
|
|
374
496
|
`TSVZ_old.py` is the frozen 3.39 module the differential tests compare against;
|
|
375
|
-
it is not installed.
|
|
497
|
+
it is not installed. CI (`.github/workflows/test.yml`) runs the three test files on
|
|
498
|
+
Python 3.6, 3.9, 3.12 and 3.14. Accepted limitations are listed in
|
|
499
|
+
[docs/limitations.md](docs/limitations.md).
|
|
376
500
|
|
|
377
501
|
## License
|
|
378
502
|
|
{tsvz-4.9 → tsvz-5.0}/README.md
RENAMED
|
@@ -7,7 +7,7 @@ key-value store in a delimiter-separated text file**. The first column of each
|
|
|
7
7
|
row is the key; the file behaves like an ordered dictionary that is persisted to
|
|
8
8
|
disk as you change it.
|
|
9
9
|
|
|
10
|
-
**Version
|
|
10
|
+
**Version 5.0**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
|
|
11
11
|
next to your code and `import TSVZ`, or install it.
|
|
12
12
|
|
|
13
13
|
```bash
|
|
@@ -19,7 +19,7 @@ pip install -e . # editable, from this directory
|
|
|
19
19
|
|
|
20
20
|
| Extension | Rules |
|
|
21
21
|
|---|---|
|
|
22
|
-
| `.tsv` `.csv` `.nsv` `.psv` (and any other) | The TSVZ **3.39** format, unchanged. A 3.39 host can read every file
|
|
22
|
+
| `.tsv` `.csv` `.nsv` `.psv` (and any other) | The TSVZ **3.39** format, unchanged. A 3.39 host can read every file this version writes. |
|
|
23
23
|
| `.tsvz` `.csvz` `.nsvz` `.psvz` | The **[tsvz-spec-v1](tsvz-spec-v1.md)** append-only key-value log. |
|
|
24
24
|
|
|
25
25
|
The delimiter follows the extension: tab, comma, NUL, pipe. A trailing
|
|
@@ -65,7 +65,7 @@ tsvz people.tsvz append carol Carol 5 # the 3.39 form still works
|
|
|
65
65
|
tsvz -V
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
## What TSVZ
|
|
68
|
+
## What TSVZ does with `.tsvz` files
|
|
69
69
|
|
|
70
70
|
- **Reads** everything a conformant writer can produce: markers
|
|
71
71
|
(`#_defaults_#`, `#_strip_trailing_whites_#`, `#_fill_empty_with_default_#`,
|
|
@@ -92,7 +92,12 @@ tsvz -V
|
|
|
92
92
|
`setdefault` and `pop` behave like a dict.
|
|
93
93
|
- **`#` keys** are stored (`<#>key`). Keys of the form `#_name_#` are marker
|
|
94
94
|
writes: `t['#_defaults_#'] = [...]` sets the defaults, and so does
|
|
95
|
-
`setDefaults([...])` once the object is constructed.
|
|
95
|
+
`setDefaults([...])` once the object is constructed. `#_snapshot_#` is
|
|
96
|
+
refused (spec §19.10.2).
|
|
97
|
+
- **Other writers:** a `TSVZed` notices another process's change by the active
|
|
98
|
+
part's inode, size and times and by the list of parts, and reloads. Rows it
|
|
99
|
+
has queued but not yet written stay in memory across that reload, and a live
|
|
100
|
+
key is never missing while it runs.
|
|
96
101
|
|
|
97
102
|
## Command line
|
|
98
103
|
|
|
@@ -101,6 +106,9 @@ tsvz -V
|
|
|
101
106
|
lone `--`. The 3.39 form `tsvz STORE [OPERATION] [ARG ...]` works too; a store
|
|
102
107
|
named like an operation then needs a path prefix (`./read`).
|
|
103
108
|
|
|
109
|
+
- **One engine.** Every operation runs through the engine `tsvz serve` uses,
|
|
110
|
+
in its own process when no server runs, so a command prints the same with or
|
|
111
|
+
without a server.
|
|
104
112
|
- **stdout carries only rows.** It gets a table on a terminal and records
|
|
105
113
|
otherwise; `--format table|records` overrides. Records use the store's
|
|
106
114
|
delimiter and escaping, so `tsvz read A | tsvz set B -` copies a store of
|
|
@@ -169,7 +177,8 @@ with TSVZ.TSVZClient('people.tsvz') as people: # served, or in this process whe
|
|
|
169
177
|
`setDefaults`. Its writes return once they are written (raising `OSError` if they
|
|
170
178
|
cannot be), one client may be shared between threads, and without a server it
|
|
171
179
|
warns once and works on the files in-process, finishing its writes when the
|
|
172
|
-
program exits.
|
|
180
|
+
program exits. A server of a `.tsv`-family file names its delimiter in
|
|
181
|
+
`STORE.serve`, and `TSVZClient` splits string values on it.
|
|
173
182
|
|
|
174
183
|
## Fault tolerance
|
|
175
184
|
|
|
@@ -190,9 +199,116 @@ recovered, skipped or stops the read, as the store's `#_on_corruption_#` says,
|
|
|
190
199
|
part keeps the original as `<part>.damaged-<time>`), an unreadable part, and a
|
|
191
200
|
delimiter or encoding argument that conflicts with the extension.
|
|
192
201
|
|
|
202
|
+
Writing is stricter than reading: a key, value, header or marker that cannot be
|
|
203
|
+
encoded as UTF-8, such as the lone surrogate `os.fsdecode` gives for a
|
|
204
|
+
non-UTF-8 file name, raises a `ValueError` (`TSVZEncodeError`) that names it
|
|
205
|
+
and writes nothing, instead of storing `?`. Pass `errors='replace'` (or any codec
|
|
206
|
+
error handler) to the writing functions or to `TSVZed`, `TSVZedLite` and `TSVZClient`
|
|
207
|
+
to get the old `?`. `TSVZed` and `TSVZedLite` refuse such a row when it is set. A
|
|
208
|
+
rewrite that meets such text leaves the file as it is: `mapToFile()` and
|
|
209
|
+
`hardMapToFile()` raise the error, and the rewrites `TSVZed` makes itself report it
|
|
210
|
+
once. With a non-UTF-8 `encoding=`, U+FFFD (what the reader puts for a byte it could
|
|
211
|
+
not decode) is text that encoding may lack, and it is refused like any other: such a
|
|
212
|
+
store is not rewritten (`rewrite_on_load` leaves the file as it is, without failing),
|
|
213
|
+
appends of rows that can be written still work, and the delete or update of a key that
|
|
214
|
+
holds U+FFFD raises. Such a key stays until the file is edited, the
|
|
215
|
+
store cleared, or the store opened with the encoding it was written in; `errors='replace'`
|
|
216
|
+
would write `?` for U+FFFD, losing the bytes the file holds there. The `tsvz` command
|
|
217
|
+
exits 1 with the message.
|
|
218
|
+
|
|
219
|
+
A `TSVZed` writes a `.tsv`, `.csv`, `.nsv` or `.psv` file (compressed or not) from
|
|
220
|
+
its background thread only: `rewrite()`, `mapToFile()`, `hardMapToFile()`,
|
|
221
|
+
`commitAppendToFile()`, `clear()`, `clear_file()`, `reload()`, `close()` and the
|
|
222
|
+
constructor's load hand it the job and wait, so a Ctrl-C stops the wait, never the
|
|
223
|
+
write (the call still raises `KeyboardInterrupt`). At exit a write in progress is
|
|
224
|
+
finished: a Ctrl-C then says so and keeps waiting, and a second one abandons it. A
|
|
225
|
+
closed store is freed. (`get_file_obj()` and `release_file_obj()`, kept from 3.39,
|
|
226
|
+
hand the locked file to the calling thread.) A file another process removed is made
|
|
227
|
+
again from memory by the store's next write, an append included (a removal in the
|
|
228
|
+
microseconds as a write starts is followed as a rotation instead); while that rewrite
|
|
229
|
+
fails or is refused, the rows are appended to a new file there, and memory keeps all of
|
|
230
|
+
them for the rewrite that gets through. A compressed file another writer left damaged
|
|
231
|
+
is made again too (L5: every read stops at the damage, so rows
|
|
232
|
+
appended after it would be read by no one); the bytes after the damage go. While that
|
|
233
|
+
rewrite fails, the rows are appended after the damage and memory keeps them for the
|
|
234
|
+
next rewrite that gets through; while it is refused (a row changed in place to text
|
|
235
|
+
the encoding cannot hold), they are appended after it as in 4.9, and the next read
|
|
236
|
+
drops them. Either way `close()` tries the rewrite once more, whatever
|
|
237
|
+
`rewrite_on_exit` says. Appends that do not read the store first (`tsvz set`,
|
|
238
|
+
`appendTabularFile`, `TSVZedLite`, and a `TSVZed` with `monitor_external_changes=False`
|
|
239
|
+
for damage that lands after it loaded the file) still land after it, and so do a
|
|
240
|
+
`TSVZed`'s rows when a writer that takes no lock leaves the damage between the store's read
|
|
241
|
+
and its append ([docs/limitations.md](docs/limitations.md)).
|
|
242
|
+
|
|
243
|
+
It knows the size and SHA-256 of the bytes it last read or wrote. When another process
|
|
244
|
+
has changed the file, and it is the same file (device and inode) and still begins with
|
|
245
|
+
those bytes, nothing this store set is still waiting to be written, and
|
|
246
|
+
`setDefaults()` has not changed its defaults since, only what follows is read and
|
|
247
|
+
applied to memory, as a read of the file applies it: memory keeps an order set by
|
|
248
|
+
`move_to_end`, and its `#` keys. Any other change reloads the file, changing memory
|
|
249
|
+
in place, so a live key is never missing meanwhile.
|
|
250
|
+
|
|
251
|
+
A rewrite writes over the file and then cuts it to its new length, so a reader never
|
|
252
|
+
meets it emptied. One that fails part way (a full disk, an I/O error), or that cannot
|
|
253
|
+
make again a file that was removed, leaves the file damaged: the new bytes it wrote,
|
|
254
|
+
then the old ones after them. Memory then holds the only good copy, as it does when
|
|
255
|
+
the store appends after damage another writer left that it cannot make the file again
|
|
256
|
+
over yet (that rewrite failed, or waits out a failed one's back-off). Until a rewrite gets through, the file is not
|
|
257
|
+
reloaded over memory; the background writer still appends the rows set meanwhile, and
|
|
258
|
+
the next rewrite (the background writer's, `rewrite()`, `close()`) makes the file
|
|
259
|
+
again from memory. A change another process
|
|
260
|
+
made meanwhile is merged into memory first, as above, when the file is still the one
|
|
261
|
+
the damage was left in (for a file that was removed, any file made there since) and
|
|
262
|
+
still begins with exactly the bytes it left; after a damage that left the file empty
|
|
263
|
+
(or removed it), what follows must also begin with this store's header.
|
|
264
|
+
Otherwise that change is overwritten, with the warning `Overwriting external changes`.
|
|
265
|
+
What cannot be told from an append is merged as one: another process's rewrite of the
|
|
266
|
+
same file in place that begins with exactly the bytes the damage left, and, for a store
|
|
267
|
+
with no header (or `verifyHeader=False`), anything written to a file the damage left
|
|
268
|
+
empty; the keys it sets or deletes then take its values, and the keys it left out keep
|
|
269
|
+
memory's. An explicit `reload()` while the file is damaged replaces memory with the
|
|
270
|
+
damaged file, as asked, and warns.
|
|
271
|
+
|
|
272
|
+
## Upgrading to 5.0 from 4.9
|
|
273
|
+
|
|
274
|
+
- **Writes refuse text they cannot encode.** A key, value, header, defaults line or
|
|
275
|
+
marker the file's encoding cannot hold (UTF-8 for `.tsvz`; `encoding=` for a
|
|
276
|
+
`.tsv`), such as the lone surrogate `os.fsdecode` gives for a non-UTF-8 file name,
|
|
277
|
+
raises `TSVZEncodeError` (a `ValueError`) and writes nothing; 4.9 stored `?`.
|
|
278
|
+
`errors='replace'` brings `?` back. Under a non-UTF-8 `encoding=`, a key read as
|
|
279
|
+
U+FFFD cannot be deleted or updated. `tsvz` exits 1 with the message. See Fault
|
|
280
|
+
tolerance.
|
|
281
|
+
- **A Ctrl-C no longer cuts a `.tsv` write.** Every write of a `.tsv`-family file
|
|
282
|
+
runs on the store's background thread; a call waits for it, a Ctrl-C raises in the
|
|
283
|
+
caller and the write still finishes, and at exit a write in progress is finished
|
|
284
|
+
(a second Ctrl-C abandons it). In 4.9 a Ctrl-C while `TSVZed()` opened a
|
|
285
|
+
compressed store (its rewrite on load) could leave the file with no readable row.
|
|
286
|
+
A closed `TSVZed` or `TSVZedLite` is freed; 4.9 kept every one until exit.
|
|
287
|
+
- **Another process's change is taken in, not reloaded over memory.** An append to
|
|
288
|
+
a `.tsv`-family file is read from where the store left off, so an order set by
|
|
289
|
+
`move_to_end` and `#` keys survive it; any reload (both dialects) changes memory
|
|
290
|
+
in place, so another thread never finds a live key missing. After a rewrite of
|
|
291
|
+
the store's own fails part way, memory is kept and another process's appends are
|
|
292
|
+
merged into it (Fault tolerance).
|
|
293
|
+
- **`.tsvz` stores** see another process's append made right after their load, a
|
|
294
|
+
commit or a clear (4.9 missed it until the next change), keep the rows set while
|
|
295
|
+
they reload, refuse `#_snapshot_#` (spec §19.10.2), and raise a strict header
|
|
296
|
+
mismatch at the first read.
|
|
297
|
+
- **`.tsv` files:** an append to an existing empty file writes the header first, and
|
|
298
|
+
reading one gives an empty store; see L8 to L14 for the rest.
|
|
299
|
+
- **`tsvz`** runs every operation through the engine `tsvz serve` uses, with the same
|
|
300
|
+
output. A server of a `.tsv` (or other tab- or NUL-delimited) file is now found by
|
|
301
|
+
`tsvz` commands, `TSVZClient` and `tsvz stop`; 4.9's clients rejected its pointer
|
|
302
|
+
file. The 3.39 warning that a file name does not end in `.tsv` is printed only
|
|
303
|
+
when a command creates such a file, and a write that cannot be made reports the
|
|
304
|
+
operating system's error.
|
|
305
|
+
- **macOS and other systems that lock with `lockf`:** 4.9's close gate is gone, so
|
|
306
|
+
a read in another thread of the process can drop a write's lock
|
|
307
|
+
([docs/limitations.md](docs/limitations.md)).
|
|
308
|
+
|
|
193
309
|
## Breaking changes from 3.39
|
|
194
310
|
|
|
195
|
-
### `.tsv`, `.csv`, `.nsv`, `.psv`:
|
|
311
|
+
### `.tsv`, `.csv`, `.nsv`, `.psv`: the format is unchanged
|
|
196
312
|
|
|
197
313
|
| # | Change |
|
|
198
314
|
|---|---|
|
|
@@ -203,6 +319,13 @@ delimiter or encoding argument that conflicts with the extension.
|
|
|
203
319
|
| L5 | A damaged compressed file is read up to the damage instead of raising. |
|
|
204
320
|
| L6 | `mapToFile` no longer glues a row it adds at the end of the file to the next one. |
|
|
205
321
|
| L7 | `TSVZed`'s `in` and `get` find a key as `t[key]` does: the live key equal to it, else the key right-stripped. 3.39 matched them exactly, so `'x ' in t` was false while `t['x ']` read `x`, and `t.setdefault('x ', …)` overwrote `x`. |
|
|
322
|
+
| L8 | Appending (`TSVZed`, `TSVZedLite`, `appendTabularFile`, `appendLinesTabularFile`) to an existing empty file (no byte, or a compressed file holding only empty streams) writes the store's header first, as creating the file does; 3.39 wrote the rows alone (a read then took the first one for the header), and `appendTabularFile` with `strict` refused the file. |
|
|
323
|
+
| L9 | Reading such an empty file with a header (and `verifyHeader`) gives an empty store with that header: `readTabularFile`, `TSVZed` and `TSVZedLite` with `strict` no longer refuse it with `Header mismatch` (a `TSVZed` restarted after a rewrite that left the file empty failed in its constructor), and without `strict` no longer print a header mismatch. |
|
|
324
|
+
| L10 | Writing text the file's encoding cannot hold (a lone surrogate from `os.fsdecode`, a character the `encoding=` lacks) raises `TSVZEncodeError`, a `ValueError`, and writes nothing; 3.39 and 4.9 wrote `?`, which `errors='replace'` brings back. |
|
|
325
|
+
| L11 | `pop()` and `popitem()` of a `#` key, which lives in memory only, write no tombstone line to the file. |
|
|
326
|
+
| L12 | A rewrite of a file whose header line has no newline writes the rows on lines of their own; 3.39 wrote the first one onto the header line. |
|
|
327
|
+
| L13 | A compressed file is rewritten in members of at most 1 MiB, as appends already were: the bytes differ, the rows read are the same, and 3.39 reads them. |
|
|
328
|
+
| L14 | `TSVZed` opening a compressed file in a codec this Python lacks (zstd before Python 3.14) raises `ImportError` instead of reading the compressed bytes as text. |
|
|
206
329
|
|
|
207
330
|
### `.tsvz`, `.csvz`, `.nsvz`, `.psvz`: now tsvz-spec-v1
|
|
208
331
|
|
|
@@ -252,7 +375,7 @@ Conformance testing).
|
|
|
252
375
|
|
|
253
376
|
- §17.5: a numbered part opened directly is read alone, with a warning.
|
|
254
377
|
- §14.5: the defaults-on-missing read applies to `t[key]` only.
|
|
255
|
-
- §18.6: locking is one exclusive whole-file lock per write plus an in-process lock; readers do not lock. On Linux it is an open file description lock, which still excludes 3.39's `lockf`; elsewhere (macOS and other systems, or a Linux file system that refuses those locks) it is `lockf`, which closing any descriptor of the file in the process drops: there a read in another thread of the same process, such as a handler's or a `TSVZed`'s, can drop a write's lock while it is held. A known limitation,
|
|
378
|
+
- §18.6: locking is one exclusive whole-file lock per write plus an in-process lock; readers do not lock. On Linux it is an open file description lock, which still excludes 3.39's `lockf`; elsewhere (macOS and other systems, or a Linux file system that refuses those locks) it is `lockf`, which closing any descriptor of the file in the process drops: there a read in another thread of the same process, such as a handler's or a `TSVZed`'s, can drop a write's lock while it is held. A known limitation, deferred: see [docs/limitations.md](docs/limitations.md).
|
|
256
379
|
- §19.4: a scrub whose rows would read differently under the final markers writes them with stripping and fill-empty off, then restores the final markers after the rows.
|
|
257
380
|
- §20.7: `-q` also silences informational messages ("Created …"); only errors remain.
|
|
258
381
|
- §20.2: `scrub` of a multi-part store, and `clear` of one part of one, are refused with exit 1; `read`, `get` and `parts` of a numbered part read that part alone, with a warning.
|
|
@@ -267,16 +390,12 @@ Conformance testing).
|
|
|
267
390
|
|
|
268
391
|
## Website
|
|
269
392
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
python3 website/tsvz_site.py build /tmp/site # static files for any host
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
`deploy/tsvz-site.service` runs it under systemd; restart it to publish an edit.
|
|
393
|
+
The tsvz.org site lives in [tsvz-org/tsvz.org](https://github.com/tsvz-org/tsvz.org).
|
|
394
|
+
It renders `tsvz-spec-v1.md` from a checkout of this repository next to its own,
|
|
395
|
+
and its tests also check the spec: every § reference names a real section, and
|
|
396
|
+
the spec uses only markdown the site renders. Run them after editing the spec
|
|
397
|
+
(see Tests); CI runs them on every push here, and the site publishes `main`'s
|
|
398
|
+
spec each morning.
|
|
280
399
|
|
|
281
400
|
## Conformance testing
|
|
282
401
|
|
|
@@ -336,14 +455,16 @@ runs, because a single run can land in a slower mode as a whole.
|
|
|
336
455
|
|
|
337
456
|
```bash
|
|
338
457
|
python3 -m pytest TSVZ_test.py -q # everything, incl. 3.39 differential tests
|
|
339
|
-
python3 -m pytest
|
|
458
|
+
python3 -m pytest ../tsvz.org/tsvz_site_test.py -q # the spec as tsvz.org renders it (needs the tsvz.org checkout next to this one)
|
|
340
459
|
python3 -m pytest tsvzbench_test.py -q # the benchmark harness
|
|
341
460
|
python3 -m pytest tsvzconform_test.py -q # the conformance runner and cases, against the reference
|
|
342
461
|
python3 benchTSVZ.py /tmp/b.tsv -n 100000 --compare-old # benchmark (not a test)
|
|
343
462
|
```
|
|
344
463
|
|
|
345
464
|
`TSVZ_old.py` is the frozen 3.39 module the differential tests compare against;
|
|
346
|
-
it is not installed.
|
|
465
|
+
it is not installed. CI (`.github/workflows/test.yml`) runs the three test files on
|
|
466
|
+
Python 3.6, 3.9, 3.12 and 3.14. Accepted limitations are listed in
|
|
467
|
+
[docs/limitations.md](docs/limitations.md).
|
|
347
468
|
|
|
348
469
|
## License
|
|
349
470
|
|