TSVZ 4.9__tar.gz → 5.1__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.
@@ -1,13 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TSVZ
3
- Version: 4.9
3
+ Version: 5.1
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/yufei-pan/TSVZ
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 4.9**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
42
+ **Version 5.1**, 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 4.x writes. |
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 4 does with `.tsvz` files
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,195 @@ 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.1 from 5.0
305
+
306
+ The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
307
+ 5.1 writes. Most of the changes are in scrub and clear.
308
+
309
+ - **Scrub and clear of a `.tsv` write over the file.** `scrubTabularFile`,
310
+ `clearTabularFile`, `tsvz scrub` and `tsvz clear` (on the command line or through a
311
+ `tsvz serve` handler), `TSVZClient.clear()` and `TSVZedLite.clear_file()` of a
312
+ `.tsv`-family file write the new content over the file and then cut it, as
313
+ `TSVZed`'s rewrites do, so a reader that takes no lock never meets the file emptied.
314
+ They take the file's lock, so they wait while another writer holds it. 5.0 emptied
315
+ the file first, with no lock (`TSVZedLite` truncated its own descriptor).
316
+ - **A scrub keeps another process's rows.** A scrub of a `.tsv` writes only if the
317
+ file is still what its read saw. Otherwise it reads again, up to three times, and if
318
+ the file kept changing it writes nothing and warns: `tsvz scrub` exits 1 (`scrub
319
+ refused; nothing was written`), as for a `.tsvz` (C6), and a handler answers refused.
320
+ `scrubTabularFile` still returns the rows it read. 5.0 wrote over a row appended
321
+ after its read.
322
+ - **`scrubTabularFile(lastLineOnly=True)` is a read.** It returns the last row and
323
+ writes nothing, as for a `.tsvz`. 5.0 and 3.39 wrote that row back as deletion lines
324
+ (`id\tv\na\t\n3\t\n`), and the store was gone.
325
+ - **A scrub keeps the file's `#_defaults_#` lines.** The defaults in effect at the end
326
+ of the read are written after the rows: the last non-empty defaults line, its empty
327
+ cells filled from the line before it as a reader fills them. An empty defaults line
328
+ that was read (it turns defaults off) is written before the rows, padded to their
329
+ width. Every row reads as it did to a reader given the scrub's defaults and
330
+ `strict`. Defaults a caller passes still fill the rows' empty cells, as in 5.0, but
331
+ are not written as a defaults line, and the caller's list is not changed. 5.0 and
332
+ 3.39 dropped the lines, so rows appended later got no defaults.
333
+ - **The header stays on line 1.** A scrub given no header (`tsvz scrub` of a `.tsv`,
334
+ `scrubTabularFile(path)`) and a `TSVZed` given none, of a file
335
+ that has a header line, keep that line first, with a `TSVZed`'s defaults line right
336
+ after it. 5.0 and 3.39 put an empty line in front of it, so a reader given the header
337
+ read the header as a row. A header-less file keeps the empty first line it begins
338
+ with, and new files and clears given no header still get one, as in 5.0.
339
+ - **Interrupts.** A scrub or clear of either dialect keeps a copy of only the bytes it
340
+ overwrites, to put back if it is interrupted before the file is cut to its new
341
+ length; once the cut is made the rewrite is done and the new content stays. 5.0 kept
342
+ a copy of the whole `.tsvz` part and put it all back. (A scrub still reads the whole
343
+ store, and a `.tsvz` clear reads it too, to keep its markers.)
344
+ - **A compressed store cleared to nothing is one empty stream.** A `.tsvz.gz` (or
345
+ `.bz2`, `.xz`, `.zst`) cleared or scrubbed to nothing holds one empty compressed
346
+ stream, which `bzcat`, `xzcat` and Python's `bz2` and `lzma` can read; 5.0 left 0
347
+ bytes.
348
+ - **A header-checking clear of an empty file writes the header it was given.** A clear
349
+ that checks the header (`verifyHeader=True`, `tsvz clear --x-header H --x-strict`) of a
350
+ 0-byte `.tsv`, or of a compressed file with no byte or only empty streams (another
351
+ tool's), takes it for an empty store with that header, as every reader does (L9), and
352
+ writes H without a "Header mismatch" warning. 5.0 wrote an empty line and warned of a
353
+ mismatch, so a strict `TSVZed` given H then refused the file, and raised on a 0-byte
354
+ `.bz2`, `.xz` or `.zst`.
355
+ - **`tsvz --help`** says what `clear` keeps: a `.tsvz` its header and markers, a
356
+ `.tsv` the `--x-header` it is given. (With `--x-strict` too, a `.tsv` clear keeps the
357
+ file's own header line.)
358
+ `TSVZed`'s message for a refused rewrite lists `hardMapToFile()` among the rewrites
359
+ a program asks for.
360
+ - **Known: a scrub of a `.tsv` whose rows were all deleted does not compact it** when
361
+ the scrub is given the file's header (`scrubTabularFile(path, header=H)`) or the file
362
+ has no header line: it leaves the file as it is, deletion lines included; reads are
363
+ right, as in 3.39 and 5.0. (`tsvz scrub` of a headed file reads the header line as a
364
+ row, so it does compact it.)
365
+ ([limitations](docs/limitations.md#a-scrub-of-a-tsv-whose-rows-were-all-deleted-does-not-compact-it)).
366
+ - **Known: a scrub run with other defaults than the store's program can change how
367
+ early rows read.** In a `.tsv` with an empty `#_defaults_#` line and rows with empty
368
+ cells before any defaults line, the reset line the scrub writes before the rows keeps
369
+ those cells empty for a program that opens the store with its own defaults; run the
370
+ scrub with the defaults that program uses
371
+ ([limitations](docs/limitations.md#a-scrub-run-with-other-defaults-than-the-stores-program-can-change-how-early-rows-read)).
372
+ - **Running programs keep 5.0** until they are restarted: long-running programs that
373
+ import TSVZ, and running `tsvz serve` handlers.
374
+
375
+ ## Upgrading to 5.0 from 4.9
376
+
377
+ - **Writes refuse text they cannot encode.** A key, value, header, defaults line or
378
+ marker the file's encoding cannot hold (UTF-8 for `.tsvz`; `encoding=` for a
379
+ `.tsv`), such as the lone surrogate `os.fsdecode` gives for a non-UTF-8 file name,
380
+ raises `TSVZEncodeError` (a `ValueError`) and writes nothing; 4.9 stored `?` (its
381
+ `tsvz` stored U+FFFD for a non-UTF-8 argument, and its `.tsvz` file functions raised
382
+ `UnicodeEncodeError`).
383
+ `errors='replace'` brings `?` back. Under a non-UTF-8 `encoding=`, a key read as
384
+ U+FFFD cannot be deleted or updated. `tsvz` exits 1 with the message. See Fault
385
+ tolerance.
386
+ - **A Ctrl-C no longer cuts a `.tsv` write.** Every write of a `.tsv`-family file
387
+ runs on the store's background thread; a call waits for it, a Ctrl-C raises in the
388
+ caller and the write still finishes, and at exit a write in progress is finished
389
+ (a second Ctrl-C abandons it). In 4.9 a Ctrl-C while `TSVZed()` opened a
390
+ compressed store (its rewrite on load) could leave the file with no readable row.
391
+ A closed `TSVZed` or `TSVZedLite` is freed; 4.9 kept every one until exit.
392
+ - **Another process's change is taken in, not reloaded over memory.** An append to
393
+ a `.tsv`-family file is read from where the store left off, so an order set by
394
+ `move_to_end` and `#` keys survive it; any reload (both dialects) changes memory
395
+ in place, so another thread never finds a live key missing. After a rewrite of
396
+ the store's own fails part way, memory is kept and another process's appends are
397
+ merged into it (Fault tolerance).
398
+ - **`.tsvz` stores** see another process's append made right after their load, a
399
+ commit or a clear (4.9 missed it until the next change), keep the rows set while
400
+ they reload, and raise a strict header mismatch at the first read.
401
+ - **`.tsv` files:** an append to an existing empty file writes the header first, and
402
+ reading one gives an empty store; see L8 to L14 for the rest.
403
+ - **`tsvz`** runs every operation through the engine `tsvz serve` uses, with the same
404
+ output. A server of a `.tsv` (or other tab- or NUL-delimited) file is now found by
405
+ `tsvz` commands, `TSVZClient` and `tsvz stop`; 4.9's clients rejected its pointer
406
+ file. The 3.39 warning that a file name does not end in `.tsv` is printed only
407
+ when a command creates such a file, and a write that cannot be made reports the
408
+ operating system's error.
409
+ - **Running programs keep 4.9.** Long-running programs that import TSVZ, and running
410
+ `tsvz serve` handlers, keep 4.9 until they are restarted. 5.0's `tsvz stop` cannot
411
+ stop a 4.9 handler of a `.tsv` (or other tab- or NUL-delimited) file: 4.9 wrote the
412
+ delimiter into the pointer file's `x-delimiter` as `<sep>` (a NUL as `<NUL>`), which
413
+ 5.0 decodes to the TAB or NUL itself and refuses as a control character. Send that
414
+ handler SIGTERM instead: its pid is on the `pid` line of its pointer file
415
+ (`STORE.serve`), and it finishes its writes and exits.
416
+ - **macOS and other systems that lock with `lockf`:** as in 4.9, a read in another
417
+ thread of the process can still drop a write's lock
418
+ ([docs/limitations.md](docs/limitations.md)).
419
+
222
420
  ## Breaking changes from 3.39
223
421
 
224
- ### `.tsv`, `.csv`, `.nsv`, `.psv`: bug fixes only
422
+ ### `.tsv`, `.csv`, `.nsv`, `.psv`: the format is unchanged
225
423
 
226
424
  | # | Change |
227
425
  |---|---|
@@ -232,6 +430,18 @@ delimiter or encoding argument that conflicts with the extension.
232
430
  | L5 | A damaged compressed file is read up to the damage instead of raising. |
233
431
  | L6 | `mapToFile` no longer glues a row it adds at the end of the file to the next one. |
234
432
  | 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`. |
433
+ | 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. |
434
+ | 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. |
435
+ | 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. |
436
+ | L11 | `pop()` and `popitem()` of a `#` key, which lives in memory only, write no tombstone line to the file. |
437
+ | 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. |
438
+ | 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. |
439
+ | 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. |
440
+ | L15 | `scrubTabularFile`, `clearTabularFile`, `tsvz scrub`, `tsvz clear` and `TSVZedLite.clear_file()` write the new content over the file and then cut it, under the file's lock; 3.39 emptied the file first, with no lock, so a reader could meet it empty. |
441
+ | L16 | A scrub writes only if the file is still what its read saw: it reads again up to three times, then writes nothing, with a warning (`tsvz scrub` exits 1, C6); 3.39 wrote over rows appended after its read. |
442
+ | L17 | `scrubTabularFile(lastLineOnly=True)` returns the last row and writes nothing; 3.39 wrote that row back as deletion lines, and the store was gone. |
443
+ | L18 | A scrub keeps the file's `#_defaults_#` lines (an empty one before the rows, the defaults in effect after them), so every row reads as it did; 3.39 dropped them, so rows appended later got no defaults. |
444
+ | L19 | A scrub given no header, and a `TSVZed` given none, keep a file's header line first; 3.39 put an empty line in front of it, and a reader given the header then read the header as a row. |
235
445
 
236
446
  ### `.tsvz`, `.csvz`, `.nsvz`, `.psvz`: now tsvz-spec-v1
237
447
 
@@ -281,7 +491,7 @@ Conformance testing).
281
491
 
282
492
  - §17.5: a numbered part opened directly is read alone, with a warning.
283
493
  - §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, to be fixed separately.
494
+ - §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
495
  - §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
496
  - §20.7: `-q` also silences informational messages ("Created …"); only errors remain.
287
497
  - §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 +506,12 @@ Conformance testing).
296
506
 
297
507
  ## Website
298
508
 
299
- `website/` holds the tsvz.org site. `website/tsvz_site.py` (standard library
300
- only, Python 3.8+) renders `website/index.md`, `website/spec-glance.md` and
301
- `tsvz-spec-v1.md`, and serves HTML to browsers and markdown to agents.
302
-
303
- ```bash
304
- python3 website/tsvz_site.py serve -p 8765 # http://127.0.0.1:8765/
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.
509
+ The tsvz.org site lives in [tsvz-org/tsvz.org](https://github.com/tsvz-org/tsvz.org).
510
+ It renders `tsvz-spec-v1.md` from a checkout of this repository next to its own,
511
+ and its tests also check the spec: every § reference names a real section, and
512
+ the spec uses only markdown the site renders. Run them after editing the spec
513
+ (see Tests); CI runs them on every push here, and the site publishes `main`'s
514
+ spec each morning.
309
515
 
310
516
  ## Conformance testing
311
517
 
@@ -365,14 +571,16 @@ runs, because a single run can land in a slower mode as a whole.
365
571
 
366
572
  ```bash
367
573
  python3 -m pytest TSVZ_test.py -q # everything, incl. 3.39 differential tests
368
- python3 -m pytest website/tsvz_site_test.py -q # the tsvz.org site
574
+ 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
575
  python3 -m pytest tsvzbench_test.py -q # the benchmark harness
370
576
  python3 -m pytest tsvzconform_test.py -q # the conformance runner and cases, against the reference
371
577
  python3 benchTSVZ.py /tmp/b.tsv -n 100000 --compare-old # benchmark (not a test)
372
578
  ```
373
579
 
374
580
  `TSVZ_old.py` is the frozen 3.39 module the differential tests compare against;
375
- it is not installed.
581
+ it is not installed. CI (`.github/workflows/test.yml`) runs the three test files on
582
+ Python 3.6, 3.9, 3.12 and 3.14. Accepted limitations are listed in
583
+ [docs/limitations.md](docs/limitations.md).
376
584
 
377
585
  ## License
378
586