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.
- {tsvz-4.9 → tsvz-5.1}/PKG-INFO +229 -21
- {tsvz-4.9 → tsvz-5.1}/README.md +224 -19
- {tsvz-4.9 → tsvz-5.1}/TSVZ.egg-info/PKG-INFO +229 -21
- {tsvz-4.9 → tsvz-5.1}/TSVZ.py +2845 -1435
- {tsvz-4.9 → tsvz-5.1}/setup.py +9 -6
- {tsvz-4.9 → tsvz-5.1}/LICENSE +0 -0
- {tsvz-4.9 → tsvz-5.1}/TSVZ.egg-info/SOURCES.txt +0 -0
- {tsvz-4.9 → tsvz-5.1}/TSVZ.egg-info/dependency_links.txt +0 -0
- {tsvz-4.9 → tsvz-5.1}/TSVZ.egg-info/entry_points.txt +0 -0
- {tsvz-4.9 → tsvz-5.1}/TSVZ.egg-info/requires.txt +0 -0
- {tsvz-4.9 → tsvz-5.1}/TSVZ.egg-info/top_level.txt +0 -0
- {tsvz-4.9 → tsvz-5.1}/setup.cfg +0 -0
{tsvz-4.9 → tsvz-5.1}/PKG-INFO
RENAMED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TSVZ
|
|
3
|
-
Version:
|
|
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/
|
|
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.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
|
|
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,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`:
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|