TSVZ 5.0__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-5.0 → tsvz-5.1}/PKG-INFO +91 -7
- {tsvz-5.0 → tsvz-5.1}/README.md +90 -6
- {tsvz-5.0 → tsvz-5.1}/TSVZ.egg-info/PKG-INFO +91 -7
- {tsvz-5.0 → tsvz-5.1}/TSVZ.py +245 -76
- {tsvz-5.0 → tsvz-5.1}/LICENSE +0 -0
- {tsvz-5.0 → tsvz-5.1}/TSVZ.egg-info/SOURCES.txt +0 -0
- {tsvz-5.0 → tsvz-5.1}/TSVZ.egg-info/dependency_links.txt +0 -0
- {tsvz-5.0 → tsvz-5.1}/TSVZ.egg-info/entry_points.txt +0 -0
- {tsvz-5.0 → tsvz-5.1}/TSVZ.egg-info/requires.txt +0 -0
- {tsvz-5.0 → tsvz-5.1}/TSVZ.egg-info/top_level.txt +0 -0
- {tsvz-5.0 → tsvz-5.1}/setup.cfg +0 -0
- {tsvz-5.0 → tsvz-5.1}/setup.py +0 -0
{tsvz-5.0 → tsvz-5.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TSVZ
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.1
|
|
4
4
|
Summary: Key-value store kept in a TSV / CSV / NSV / PSV file, with tsvz-spec-v1 support
|
|
5
5
|
Home-page: https://github.com/tsvz-org/TSVZ
|
|
6
6
|
Author: Yufei Pan
|
|
@@ -39,7 +39,7 @@ key-value store in a delimiter-separated text file**. The first column of each
|
|
|
39
39
|
row is the key; the file behaves like an ordered dictionary that is persisted to
|
|
40
40
|
disk as you change it.
|
|
41
41
|
|
|
42
|
-
**Version 5.
|
|
42
|
+
**Version 5.1**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
|
|
43
43
|
next to your code and `import TSVZ`, or install it.
|
|
44
44
|
|
|
45
45
|
```bash
|
|
@@ -301,12 +301,85 @@ empty; the keys it sets or deletes then take its values, and the keys it left ou
|
|
|
301
301
|
memory's. An explicit `reload()` while the file is damaged replaces memory with the
|
|
302
302
|
damaged file, as asked, and warns.
|
|
303
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
|
+
|
|
304
375
|
## Upgrading to 5.0 from 4.9
|
|
305
376
|
|
|
306
377
|
- **Writes refuse text they cannot encode.** A key, value, header, defaults line or
|
|
307
378
|
marker the file's encoding cannot hold (UTF-8 for `.tsvz`; `encoding=` for a
|
|
308
379
|
`.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
|
|
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`).
|
|
310
383
|
`errors='replace'` brings `?` back. Under a non-UTF-8 `encoding=`, a key read as
|
|
311
384
|
U+FFFD cannot be deleted or updated. `tsvz` exits 1 with the message. See Fault
|
|
312
385
|
tolerance.
|
|
@@ -324,8 +397,7 @@ damaged file, as asked, and warns.
|
|
|
324
397
|
merged into it (Fault tolerance).
|
|
325
398
|
- **`.tsvz` stores** see another process's append made right after their load, a
|
|
326
399
|
commit or a clear (4.9 missed it until the next change), keep the rows set while
|
|
327
|
-
they reload,
|
|
328
|
-
mismatch at the first read.
|
|
400
|
+
they reload, and raise a strict header mismatch at the first read.
|
|
329
401
|
- **`.tsv` files:** an append to an existing empty file writes the header first, and
|
|
330
402
|
reading one gives an empty store; see L8 to L14 for the rest.
|
|
331
403
|
- **`tsvz`** runs every operation through the engine `tsvz serve` uses, with the same
|
|
@@ -334,8 +406,15 @@ damaged file, as asked, and warns.
|
|
|
334
406
|
file. The 3.39 warning that a file name does not end in `.tsv` is printed only
|
|
335
407
|
when a command creates such a file, and a write that cannot be made reports the
|
|
336
408
|
operating system's error.
|
|
337
|
-
- **
|
|
338
|
-
|
|
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
|
|
339
418
|
([docs/limitations.md](docs/limitations.md)).
|
|
340
419
|
|
|
341
420
|
## Breaking changes from 3.39
|
|
@@ -358,6 +437,11 @@ damaged file, as asked, and warns.
|
|
|
358
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. |
|
|
359
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. |
|
|
360
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. |
|
|
361
445
|
|
|
362
446
|
### `.tsvz`, `.csvz`, `.nsvz`, `.psvz`: now tsvz-spec-v1
|
|
363
447
|
|
{tsvz-5.0 → tsvz-5.1}/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 5.
|
|
10
|
+
**Version 5.1**, 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
|
|
@@ -269,12 +269,85 @@ empty; the keys it sets or deletes then take its values, and the keys it left ou
|
|
|
269
269
|
memory's. An explicit `reload()` while the file is damaged replaces memory with the
|
|
270
270
|
damaged file, as asked, and warns.
|
|
271
271
|
|
|
272
|
+
## Upgrading to 5.1 from 5.0
|
|
273
|
+
|
|
274
|
+
The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
|
|
275
|
+
5.1 writes. Most of the changes are in scrub and clear.
|
|
276
|
+
|
|
277
|
+
- **Scrub and clear of a `.tsv` write over the file.** `scrubTabularFile`,
|
|
278
|
+
`clearTabularFile`, `tsvz scrub` and `tsvz clear` (on the command line or through a
|
|
279
|
+
`tsvz serve` handler), `TSVZClient.clear()` and `TSVZedLite.clear_file()` of a
|
|
280
|
+
`.tsv`-family file write the new content over the file and then cut it, as
|
|
281
|
+
`TSVZed`'s rewrites do, so a reader that takes no lock never meets the file emptied.
|
|
282
|
+
They take the file's lock, so they wait while another writer holds it. 5.0 emptied
|
|
283
|
+
the file first, with no lock (`TSVZedLite` truncated its own descriptor).
|
|
284
|
+
- **A scrub keeps another process's rows.** A scrub of a `.tsv` writes only if the
|
|
285
|
+
file is still what its read saw. Otherwise it reads again, up to three times, and if
|
|
286
|
+
the file kept changing it writes nothing and warns: `tsvz scrub` exits 1 (`scrub
|
|
287
|
+
refused; nothing was written`), as for a `.tsvz` (C6), and a handler answers refused.
|
|
288
|
+
`scrubTabularFile` still returns the rows it read. 5.0 wrote over a row appended
|
|
289
|
+
after its read.
|
|
290
|
+
- **`scrubTabularFile(lastLineOnly=True)` is a read.** It returns the last row and
|
|
291
|
+
writes nothing, as for a `.tsvz`. 5.0 and 3.39 wrote that row back as deletion lines
|
|
292
|
+
(`id\tv\na\t\n3\t\n`), and the store was gone.
|
|
293
|
+
- **A scrub keeps the file's `#_defaults_#` lines.** The defaults in effect at the end
|
|
294
|
+
of the read are written after the rows: the last non-empty defaults line, its empty
|
|
295
|
+
cells filled from the line before it as a reader fills them. An empty defaults line
|
|
296
|
+
that was read (it turns defaults off) is written before the rows, padded to their
|
|
297
|
+
width. Every row reads as it did to a reader given the scrub's defaults and
|
|
298
|
+
`strict`. Defaults a caller passes still fill the rows' empty cells, as in 5.0, but
|
|
299
|
+
are not written as a defaults line, and the caller's list is not changed. 5.0 and
|
|
300
|
+
3.39 dropped the lines, so rows appended later got no defaults.
|
|
301
|
+
- **The header stays on line 1.** A scrub given no header (`tsvz scrub` of a `.tsv`,
|
|
302
|
+
`scrubTabularFile(path)`) and a `TSVZed` given none, of a file
|
|
303
|
+
that has a header line, keep that line first, with a `TSVZed`'s defaults line right
|
|
304
|
+
after it. 5.0 and 3.39 put an empty line in front of it, so a reader given the header
|
|
305
|
+
read the header as a row. A header-less file keeps the empty first line it begins
|
|
306
|
+
with, and new files and clears given no header still get one, as in 5.0.
|
|
307
|
+
- **Interrupts.** A scrub or clear of either dialect keeps a copy of only the bytes it
|
|
308
|
+
overwrites, to put back if it is interrupted before the file is cut to its new
|
|
309
|
+
length; once the cut is made the rewrite is done and the new content stays. 5.0 kept
|
|
310
|
+
a copy of the whole `.tsvz` part and put it all back. (A scrub still reads the whole
|
|
311
|
+
store, and a `.tsvz` clear reads it too, to keep its markers.)
|
|
312
|
+
- **A compressed store cleared to nothing is one empty stream.** A `.tsvz.gz` (or
|
|
313
|
+
`.bz2`, `.xz`, `.zst`) cleared or scrubbed to nothing holds one empty compressed
|
|
314
|
+
stream, which `bzcat`, `xzcat` and Python's `bz2` and `lzma` can read; 5.0 left 0
|
|
315
|
+
bytes.
|
|
316
|
+
- **A header-checking clear of an empty file writes the header it was given.** A clear
|
|
317
|
+
that checks the header (`verifyHeader=True`, `tsvz clear --x-header H --x-strict`) of a
|
|
318
|
+
0-byte `.tsv`, or of a compressed file with no byte or only empty streams (another
|
|
319
|
+
tool's), takes it for an empty store with that header, as every reader does (L9), and
|
|
320
|
+
writes H without a "Header mismatch" warning. 5.0 wrote an empty line and warned of a
|
|
321
|
+
mismatch, so a strict `TSVZed` given H then refused the file, and raised on a 0-byte
|
|
322
|
+
`.bz2`, `.xz` or `.zst`.
|
|
323
|
+
- **`tsvz --help`** says what `clear` keeps: a `.tsvz` its header and markers, a
|
|
324
|
+
`.tsv` the `--x-header` it is given. (With `--x-strict` too, a `.tsv` clear keeps the
|
|
325
|
+
file's own header line.)
|
|
326
|
+
`TSVZed`'s message for a refused rewrite lists `hardMapToFile()` among the rewrites
|
|
327
|
+
a program asks for.
|
|
328
|
+
- **Known: a scrub of a `.tsv` whose rows were all deleted does not compact it** when
|
|
329
|
+
the scrub is given the file's header (`scrubTabularFile(path, header=H)`) or the file
|
|
330
|
+
has no header line: it leaves the file as it is, deletion lines included; reads are
|
|
331
|
+
right, as in 3.39 and 5.0. (`tsvz scrub` of a headed file reads the header line as a
|
|
332
|
+
row, so it does compact it.)
|
|
333
|
+
([limitations](docs/limitations.md#a-scrub-of-a-tsv-whose-rows-were-all-deleted-does-not-compact-it)).
|
|
334
|
+
- **Known: a scrub run with other defaults than the store's program can change how
|
|
335
|
+
early rows read.** In a `.tsv` with an empty `#_defaults_#` line and rows with empty
|
|
336
|
+
cells before any defaults line, the reset line the scrub writes before the rows keeps
|
|
337
|
+
those cells empty for a program that opens the store with its own defaults; run the
|
|
338
|
+
scrub with the defaults that program uses
|
|
339
|
+
([limitations](docs/limitations.md#a-scrub-run-with-other-defaults-than-the-stores-program-can-change-how-early-rows-read)).
|
|
340
|
+
- **Running programs keep 5.0** until they are restarted: long-running programs that
|
|
341
|
+
import TSVZ, and running `tsvz serve` handlers.
|
|
342
|
+
|
|
272
343
|
## Upgrading to 5.0 from 4.9
|
|
273
344
|
|
|
274
345
|
- **Writes refuse text they cannot encode.** A key, value, header, defaults line or
|
|
275
346
|
marker the file's encoding cannot hold (UTF-8 for `.tsvz`; `encoding=` for a
|
|
276
347
|
`.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
|
|
348
|
+
raises `TSVZEncodeError` (a `ValueError`) and writes nothing; 4.9 stored `?` (its
|
|
349
|
+
`tsvz` stored U+FFFD for a non-UTF-8 argument, and its `.tsvz` file functions raised
|
|
350
|
+
`UnicodeEncodeError`).
|
|
278
351
|
`errors='replace'` brings `?` back. Under a non-UTF-8 `encoding=`, a key read as
|
|
279
352
|
U+FFFD cannot be deleted or updated. `tsvz` exits 1 with the message. See Fault
|
|
280
353
|
tolerance.
|
|
@@ -292,8 +365,7 @@ damaged file, as asked, and warns.
|
|
|
292
365
|
merged into it (Fault tolerance).
|
|
293
366
|
- **`.tsvz` stores** see another process's append made right after their load, a
|
|
294
367
|
commit or a clear (4.9 missed it until the next change), keep the rows set while
|
|
295
|
-
they reload,
|
|
296
|
-
mismatch at the first read.
|
|
368
|
+
they reload, and raise a strict header mismatch at the first read.
|
|
297
369
|
- **`.tsv` files:** an append to an existing empty file writes the header first, and
|
|
298
370
|
reading one gives an empty store; see L8 to L14 for the rest.
|
|
299
371
|
- **`tsvz`** runs every operation through the engine `tsvz serve` uses, with the same
|
|
@@ -302,8 +374,15 @@ damaged file, as asked, and warns.
|
|
|
302
374
|
file. The 3.39 warning that a file name does not end in `.tsv` is printed only
|
|
303
375
|
when a command creates such a file, and a write that cannot be made reports the
|
|
304
376
|
operating system's error.
|
|
305
|
-
- **
|
|
306
|
-
|
|
377
|
+
- **Running programs keep 4.9.** Long-running programs that import TSVZ, and running
|
|
378
|
+
`tsvz serve` handlers, keep 4.9 until they are restarted. 5.0's `tsvz stop` cannot
|
|
379
|
+
stop a 4.9 handler of a `.tsv` (or other tab- or NUL-delimited) file: 4.9 wrote the
|
|
380
|
+
delimiter into the pointer file's `x-delimiter` as `<sep>` (a NUL as `<NUL>`), which
|
|
381
|
+
5.0 decodes to the TAB or NUL itself and refuses as a control character. Send that
|
|
382
|
+
handler SIGTERM instead: its pid is on the `pid` line of its pointer file
|
|
383
|
+
(`STORE.serve`), and it finishes its writes and exits.
|
|
384
|
+
- **macOS and other systems that lock with `lockf`:** as in 4.9, a read in another
|
|
385
|
+
thread of the process can still drop a write's lock
|
|
307
386
|
([docs/limitations.md](docs/limitations.md)).
|
|
308
387
|
|
|
309
388
|
## Breaking changes from 3.39
|
|
@@ -326,6 +405,11 @@ damaged file, as asked, and warns.
|
|
|
326
405
|
| 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
406
|
| 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
407
|
| 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. |
|
|
408
|
+
| 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. |
|
|
409
|
+
| 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. |
|
|
410
|
+
| 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. |
|
|
411
|
+
| 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. |
|
|
412
|
+
| 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. |
|
|
329
413
|
|
|
330
414
|
### `.tsvz`, `.csvz`, `.nsvz`, `.psvz`: now tsvz-spec-v1
|
|
331
415
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TSVZ
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.1
|
|
4
4
|
Summary: Key-value store kept in a TSV / CSV / NSV / PSV file, with tsvz-spec-v1 support
|
|
5
5
|
Home-page: https://github.com/tsvz-org/TSVZ
|
|
6
6
|
Author: Yufei Pan
|
|
@@ -39,7 +39,7 @@ key-value store in a delimiter-separated text file**. The first column of each
|
|
|
39
39
|
row is the key; the file behaves like an ordered dictionary that is persisted to
|
|
40
40
|
disk as you change it.
|
|
41
41
|
|
|
42
|
-
**Version 5.
|
|
42
|
+
**Version 5.1**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
|
|
43
43
|
next to your code and `import TSVZ`, or install it.
|
|
44
44
|
|
|
45
45
|
```bash
|
|
@@ -301,12 +301,85 @@ empty; the keys it sets or deletes then take its values, and the keys it left ou
|
|
|
301
301
|
memory's. An explicit `reload()` while the file is damaged replaces memory with the
|
|
302
302
|
damaged file, as asked, and warns.
|
|
303
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
|
+
|
|
304
375
|
## Upgrading to 5.0 from 4.9
|
|
305
376
|
|
|
306
377
|
- **Writes refuse text they cannot encode.** A key, value, header, defaults line or
|
|
307
378
|
marker the file's encoding cannot hold (UTF-8 for `.tsvz`; `encoding=` for a
|
|
308
379
|
`.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
|
|
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`).
|
|
310
383
|
`errors='replace'` brings `?` back. Under a non-UTF-8 `encoding=`, a key read as
|
|
311
384
|
U+FFFD cannot be deleted or updated. `tsvz` exits 1 with the message. See Fault
|
|
312
385
|
tolerance.
|
|
@@ -324,8 +397,7 @@ damaged file, as asked, and warns.
|
|
|
324
397
|
merged into it (Fault tolerance).
|
|
325
398
|
- **`.tsvz` stores** see another process's append made right after their load, a
|
|
326
399
|
commit or a clear (4.9 missed it until the next change), keep the rows set while
|
|
327
|
-
they reload,
|
|
328
|
-
mismatch at the first read.
|
|
400
|
+
they reload, and raise a strict header mismatch at the first read.
|
|
329
401
|
- **`.tsv` files:** an append to an existing empty file writes the header first, and
|
|
330
402
|
reading one gives an empty store; see L8 to L14 for the rest.
|
|
331
403
|
- **`tsvz`** runs every operation through the engine `tsvz serve` uses, with the same
|
|
@@ -334,8 +406,15 @@ damaged file, as asked, and warns.
|
|
|
334
406
|
file. The 3.39 warning that a file name does not end in `.tsv` is printed only
|
|
335
407
|
when a command creates such a file, and a write that cannot be made reports the
|
|
336
408
|
operating system's error.
|
|
337
|
-
- **
|
|
338
|
-
|
|
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
|
|
339
418
|
([docs/limitations.md](docs/limitations.md)).
|
|
340
419
|
|
|
341
420
|
## Breaking changes from 3.39
|
|
@@ -358,6 +437,11 @@ damaged file, as asked, and warns.
|
|
|
358
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. |
|
|
359
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. |
|
|
360
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. |
|
|
361
445
|
|
|
362
446
|
### `.tsvz`, `.csvz`, `.nsvz`, `.psvz`: now tsvz-spec-v1
|
|
363
447
|
|
{tsvz-5.0 → tsvz-5.1}/TSVZ.py
RENAMED
|
@@ -42,10 +42,10 @@ if os.name == 'nt':
|
|
|
42
42
|
elif os.name == 'posix':
|
|
43
43
|
import fcntl
|
|
44
44
|
|
|
45
|
-
version = '5.
|
|
45
|
+
version = '5.1'
|
|
46
46
|
__version__ = version
|
|
47
47
|
author = 'pan@zopyr.us'
|
|
48
|
-
COMMIT_DATE = '2026-
|
|
48
|
+
COMMIT_DATE = '2026-11-01'
|
|
49
49
|
|
|
50
50
|
DEFAULT_DELIMITER = '\t'
|
|
51
51
|
DEFAULTS_INDICATOR_KEY = '#_defaults_#'
|
|
@@ -203,8 +203,9 @@ class _RewriteRows(object):
|
|
|
203
203
|
in place after the snapshot is written as it was then.
|
|
204
204
|
"""
|
|
205
205
|
|
|
206
|
-
def __init__(self, first, values, delimiter, encoding, errors, header):
|
|
206
|
+
def __init__(self, first, values, delimiter, encoding, errors, header, later=()):
|
|
207
207
|
self.first = first
|
|
208
|
+
self.later = later # lines written after the first row (or alone, when there is none)
|
|
208
209
|
cells = []
|
|
209
210
|
lengths = array.array('I')
|
|
210
211
|
extend, append = cells.extend, lengths.append
|
|
@@ -249,8 +250,14 @@ class _RewriteRows(object):
|
|
|
249
250
|
for line in self.first:
|
|
250
251
|
yield line
|
|
251
252
|
encode, delimiter = self.encode, self.delimiter
|
|
253
|
+
later = self.later
|
|
252
254
|
for row in self._rows():
|
|
253
255
|
yield encode(_sanitize(row,delimiter=delimiter))
|
|
256
|
+
for line in later:
|
|
257
|
+
yield line
|
|
258
|
+
later = ()
|
|
259
|
+
for line in later:
|
|
260
|
+
yield line
|
|
254
261
|
|
|
255
262
|
|
|
256
263
|
def _fileIdentity(st):
|
|
@@ -1330,6 +1337,7 @@ def _processLine(line,taskDic,correctColumnNum,strict = True,delimiter = ...,def
|
|
|
1330
1337
|
lineCache = _unsanitize(line.split(delimiter),delimiter)
|
|
1331
1338
|
if not lineCache or not lineCache[0]:
|
|
1332
1339
|
return correctColumnNum , []
|
|
1340
|
+
asRead = lineCache[:] if lineCache[0] == DEFAULTS_INDICATOR_KEY else None # (filled and fitted to the columns below)
|
|
1333
1341
|
if correctColumnNum == -1:
|
|
1334
1342
|
if defaults and len(defaults) > 1:
|
|
1335
1343
|
correctColumnNum = len(defaults)
|
|
@@ -1345,6 +1353,9 @@ def _processLine(line,taskDic,correctColumnNum,strict = True,delimiter = ...,def
|
|
|
1345
1353
|
# __teePrintOrNot(f"Empty defaults line found: {line}",teeLogger=teeLogger)
|
|
1346
1354
|
defaults.clear()
|
|
1347
1355
|
defaults[:] = [DEFAULTS_INDICATOR_KEY]
|
|
1356
|
+
if isinstance(defaults, _DefaultsList):
|
|
1357
|
+
defaults.line = [DEFAULTS_INDICATOR_KEY]
|
|
1358
|
+
defaults.reset = True
|
|
1348
1359
|
else:
|
|
1349
1360
|
# if verbose:
|
|
1350
1361
|
# __teePrintOrNot(f"Key {lineCache[0]} found with empty value, deleting such key's representaion",teeLogger=teeLogger)
|
|
@@ -1373,6 +1384,8 @@ def _processLine(line,taskDic,correctColumnNum,strict = True,delimiter = ...,def
|
|
|
1373
1384
|
# if verbose:
|
|
1374
1385
|
# __teePrintOrNot(f"Defaults line found: {line}",teeLogger=teeLogger)
|
|
1375
1386
|
defaults[:] = lineCache
|
|
1387
|
+
if isinstance(defaults, _DefaultsList):
|
|
1388
|
+
defaults.line = _chainFill(asRead, defaults.line)
|
|
1376
1389
|
return correctColumnNum , []
|
|
1377
1390
|
taskDic[lineCache[0]] = lineCache if not storeOffset else offset
|
|
1378
1391
|
# if verbose:
|
|
@@ -1995,9 +2008,14 @@ def _legacyRows(linesToAppend, delimiter):
|
|
|
1995
2008
|
return formatedLines
|
|
1996
2009
|
|
|
1997
2010
|
|
|
1998
|
-
def _legacyFormatPayload(
|
|
1999
|
-
|
|
2000
|
-
|
|
2011
|
+
def _legacyFormatPayload(*args, **kwargs):
|
|
2012
|
+
"""Format rows as 3.39's ``appendLinesTabularFile`` writes them; return ``(payload, count)`` (_legacyFormatRows)."""
|
|
2013
|
+
return _legacyFormatRows(*args, **kwargs)[:2]
|
|
2014
|
+
|
|
2015
|
+
def _legacyFormatRows(fileName, linesToAppend, teeLogger, header, verifyHeader, verbose, encoding, strict, delimiter,
|
|
2016
|
+
errors='strict', rows=None, assume=None):
|
|
2017
|
+
"""Format rows as 3.39's ``appendLinesTabularFile`` writes them; return ``(payload, count, columns)``: ``columns``
|
|
2018
|
+
is the column count they are padded or cut to (0 for no row).
|
|
2001
2019
|
|
|
2002
2020
|
``header`` is a formatted header list and ``delimiter`` a resolved
|
|
2003
2021
|
delimiter. Rows are sanitised and padded or cut to the column count: the
|
|
@@ -2010,14 +2028,13 @@ def _legacyFormatPayload(fileName, linesToAppend, teeLogger, header, verifyHeade
|
|
|
2010
2028
|
"""
|
|
2011
2029
|
formatedLines = _legacyRows(linesToAppend, delimiter) if rows is None else rows
|
|
2012
2030
|
if not formatedLines:
|
|
2013
|
-
return b'', 0
|
|
2031
|
+
return b'', 0, 0
|
|
2014
2032
|
correctColumnNum = max([len(line) for line in formatedLines])
|
|
2015
2033
|
if any(header) and verifyHeader:
|
|
2016
2034
|
if assume is not None:
|
|
2017
2035
|
line = delimiter.join(assume)
|
|
2018
2036
|
else:
|
|
2019
|
-
|
|
2020
|
-
line = file.readline().decode(encoding=encoding,errors='replace')
|
|
2037
|
+
line = _legacyFirstLine(fileName, encoding, teeLogger)
|
|
2021
2038
|
if _lineContainHeader(header,line,verbose = verbose,teeLogger = teeLogger,strict = strict,delimiter = delimiter):
|
|
2022
2039
|
correctColumnNum = len(header)
|
|
2023
2040
|
if verbose:
|
|
@@ -2031,17 +2048,27 @@ def _legacyFormatPayload(fileName, linesToAppend, teeLogger, header, verifyHeade
|
|
|
2031
2048
|
line = line[:correctColumnNum]
|
|
2032
2049
|
fitted.append(line)
|
|
2033
2050
|
payload = b'\n'.join([_encodeRow(line,delimiter,encoding,errors,header) for line in fitted]) + b'\n'
|
|
2034
|
-
return payload, len(fitted)
|
|
2051
|
+
return payload, len(fitted), correctColumnNum
|
|
2052
|
+
|
|
2053
|
+
def _legacyFirstLine(fileName, encoding, teeLogger):
|
|
2054
|
+
"""The first line of the legacy file ``fileName`` (which exists) as text; '' for a file that holds nothing: no byte,
|
|
2055
|
+
or, compressed, only empty streams (a 0-byte .bz2 or .xz is no stream to read, as _fileHoldsNothing's readers know)."""
|
|
2056
|
+
if _isCompressedFile(fileName) and _fileHoldsNothing(fileName):
|
|
2057
|
+
return ''
|
|
2058
|
+
with openFileAsCompressed(fileName, mode ='rb',encoding=encoding,teeLogger=teeLogger) as file:
|
|
2059
|
+
return file.readline().decode(encoding=encoding,errors='replace')
|
|
2035
2060
|
|
|
2036
2061
|
def _legacyKeptHeader(fileName, header, verifyHeader, verbose, teeLogger, encoding, strict, delimiter, warn=True):
|
|
2037
|
-
"""The header ``clearTabularFile`` leaves in the file: the file's own when a header is given and
|
|
2062
|
+
"""The header ``clearTabularFile`` leaves in the file: the file's own first line when a header is given and
|
|
2063
|
+
``verifyHeader`` (the header given for a file that holds nothing), else ``header``; [] when it has no cell: the file
|
|
2064
|
+
is then left with no header line."""
|
|
2038
2065
|
if any(header) and verifyHeader:
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
return header
|
|
2066
|
+
line = _legacyFirstLine(fileName, encoding, teeLogger)
|
|
2067
|
+
if line: # a file that holds nothing ('') is an empty store with any header, as the readers take it: the one given is kept
|
|
2068
|
+
if warn and not _lineContainHeader(header,line,verbose = verbose,teeLogger = teeLogger,strict = strict,delimiter = delimiter):
|
|
2069
|
+
__teePrintOrNot(f'Warning: Header mismatch in {fileName}. Keeping original header in file...','warning',teeLogger)
|
|
2070
|
+
header = _formatHeader(line,verbose = verbose,teeLogger = teeLogger,delimiter=delimiter)
|
|
2071
|
+
return header if any(header) else [] # no header: no header line (3.39 wrote an empty one, read back as a row)
|
|
2045
2072
|
|
|
2046
2073
|
|
|
2047
2074
|
def clearTSV(fileName,teeLogger = None,header = '',verifyHeader = False,verbose = False,encoding = 'utf8',strict = False,delimiter = '\t',errors = 'strict'):
|
|
@@ -2082,16 +2109,74 @@ def clearTabularFile(fileName,teeLogger = None,header = '',verifyHeader = False,
|
|
|
2082
2109
|
header = _formatHeader(header,verbose = verbose,teeLogger = teeLogger,delimiter=delimiter)
|
|
2083
2110
|
if not _verifyFileExistence(fileName,createIfNotExist = True,teeLogger = teeLogger,header = header,encoding = encoding,strict = False,delimiter=delimiter,errors = errors):
|
|
2084
2111
|
raise FileNotFoundError("Something catastrophic happened! File still not found after creation")
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
with openFileAsCompressed(fileName, mode ='wb',encoding=encoding,teeLogger=teeLogger) as file:
|
|
2090
|
-
if headerBytes is not None:
|
|
2091
|
-
file.write(headerBytes+b'\n')
|
|
2112
|
+
kept = _legacyKeptHeader(fileName, header, verifyHeader, verbose, teeLogger, encoding, strict, delimiter)
|
|
2113
|
+
# With no header the file keeps the empty first line that stands for it, as 3.39 and 5.0 left it (a reader given a
|
|
2114
|
+
# header the file lacks takes that line for it, rather than a row).
|
|
2115
|
+
_legacyReplace(fileName, kept, True, encoding, delimiter, errors)
|
|
2092
2116
|
if verbose:
|
|
2093
2117
|
__teePrintOrNot(f"Cleared {fileName}",teeLogger=teeLogger)
|
|
2094
2118
|
|
|
2119
|
+
def _isRowLine(text, delimiter):
|
|
2120
|
+
"""True when the line ``text`` is a data row to a reader told no header: not what ``_processLine`` skips (empty, a
|
|
2121
|
+
comment, a line with no key) and not a defaults line. A first line that is not one is where a reader given a header
|
|
2122
|
+
the file lacks loses nothing: it takes that line for the header."""
|
|
2123
|
+
line = text.strip('\x00').rstrip('\r\n')
|
|
2124
|
+
if not line or (line.startswith('#') and not line.startswith(DEFAULTS_INDICATOR_KEY)):
|
|
2125
|
+
return False
|
|
2126
|
+
cells = _unsanitize(line.split(delimiter),delimiter)
|
|
2127
|
+
return bool(cells and cells[0] and cells[0] != DEFAULTS_INDICATOR_KEY)
|
|
2128
|
+
|
|
2129
|
+
def _legacyBeginsBlank(fileName, encoding, teeLogger, delimiter):
|
|
2130
|
+
"""True when the legacy file ``fileName`` holds nothing, cannot be read, or begins with a line that is not a data row
|
|
2131
|
+
(_isRowLine): then a header-less whole write puts the empty line that stands for a header in front of its rows, as
|
|
2132
|
+
3.39 and 5.0 always do; never in front of a first line that is a row (it would push a header to line 2)."""
|
|
2133
|
+
try:
|
|
2134
|
+
return not _isRowLine(_legacyFirstLine(fileName, encoding, teeLogger), delimiter)
|
|
2135
|
+
except Exception:
|
|
2136
|
+
return True
|
|
2137
|
+
|
|
2138
|
+
def _legacyBeginsBlankIn(f, encoding, delimiter):
|
|
2139
|
+
"""_legacyBeginsBlank for the open binary legacy file ``f`` (at its raw level), read through that descriptor and left
|
|
2140
|
+
open: a lockf lock is dropped by closing any descriptor of the file, so a writer that holds one must not open the
|
|
2141
|
+
file again to look. ``f`` is left at offset 0."""
|
|
2142
|
+
codec = _fileCodec(f.name)
|
|
2143
|
+
try:
|
|
2144
|
+
f.flush()
|
|
2145
|
+
if _holdsNothing(f, codec, os.fstat(f.fileno()).st_size):
|
|
2146
|
+
return True
|
|
2147
|
+
f.seek(0)
|
|
2148
|
+
stream = _openStreams(codec, f) if codec else f
|
|
2149
|
+
try:
|
|
2150
|
+
line = stream.readline()
|
|
2151
|
+
finally:
|
|
2152
|
+
if codec:
|
|
2153
|
+
stream.close()
|
|
2154
|
+
return not _isRowLine(line.decode(encoding or 'utf8', errors='replace'), delimiter)
|
|
2155
|
+
except Exception:
|
|
2156
|
+
return True
|
|
2157
|
+
finally:
|
|
2158
|
+
f.seek(0)
|
|
2159
|
+
|
|
2160
|
+
def _legacyReplace(fileName, kept, blank, encoding, delimiter, errors, payload=b'', expect=None):
|
|
2161
|
+
"""Make the existing legacy file ``fileName`` hold ``kept``, the header list it keeps (_legacyKeptHeader), and then
|
|
2162
|
+
``payload`` (whole lines): the one write of ``clearTabularFile`` and ``scrubTabularFile``. ``delimiter`` is a
|
|
2163
|
+
resolved delimiter. Where ``kept`` is empty, ``blank`` says whether the file begins with an empty line in its
|
|
2164
|
+
place (never in front of a first line that is a row).
|
|
2165
|
+
|
|
2166
|
+
The new bytes are made whole first, then written over the file from its start and the file is cut there, under
|
|
2167
|
+
its locks (_specRewriteInPlace: spec §18.7 / §19.8, as ``TSVZed``'s whole rewrites do), so a reader that takes no
|
|
2168
|
+
lock never meets the file emptied. A compressed file is written as whole members of at most 1 MiB. Nothing is
|
|
2169
|
+
written when the header cannot be encoded. ``expect``, the ``_specStamp`` of the file as a caller read it: the
|
|
2170
|
+
file is left alone when it no longer matches under the lock (another writer committed since), and None is
|
|
2171
|
+
returned; else the result is not None."""
|
|
2172
|
+
# Encoded before the file is written to, which would leave it cut short.
|
|
2173
|
+
headerBytes = _encodeRow(_sanitize(kept,delimiter=delimiter),delimiter,encoding,errors,what='header') + b'\n' if kept else b'\n' if blank else b''
|
|
2174
|
+
if not stat.S_ISREG(os.stat(fileName).st_mode):
|
|
2175
|
+
with open(fileName, 'wb') as f: # /dev/null holds nothing to empty, and cannot be cut or given space
|
|
2176
|
+
f.write(headerBytes + payload)
|
|
2177
|
+
return True, None
|
|
2178
|
+
return _specRewriteInPlace(fileName, headerBytes + payload, expect=expect)
|
|
2179
|
+
|
|
2095
2180
|
def getFileUpdateTimeNs(fileName):
|
|
2096
2181
|
# return 0 if the file does not exist
|
|
2097
2182
|
if not os.path.isfile(fileName):
|
|
@@ -2179,22 +2264,96 @@ def scrubTabularFile(fileName,teeLogger = None,header = '',createIfNotExist = Fa
|
|
|
2179
2264
|
lastLineOnly=lastLineOnly, verifyHeader=verifyHeader, verbose=verbose,
|
|
2180
2265
|
taskDic=taskDic, encoding=encoding, strict=strict, delimiter=delimiter,
|
|
2181
2266
|
defaults=defaults, correctColumnNum=correctColumnNum, errors=errors)
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2267
|
+
if lastLineOnly: # a read option, as for a .tsvz (a scrub of the last line alone would leave the file holding that)
|
|
2268
|
+
return readTabularFile(fileName,teeLogger = teeLogger,header = header,createIfNotExist = createIfNotExist,
|
|
2269
|
+
lastLineOnly = True,verifyHeader = verifyHeader,verbose = verbose,taskDic = taskDic,
|
|
2270
|
+
encoding = encoding,strict = strict,delimiter = delimiter,defaults=defaults,correctColumnNum = correctColumnNum,errors = errors)
|
|
2271
|
+
return _legacyScrub(fileName,teeLogger = teeLogger,header = header,createIfNotExist = createIfNotExist,verifyHeader = verifyHeader,
|
|
2272
|
+
verbose = verbose,taskDic = taskDic,encoding = encoding,strict = strict,delimiter = delimiter,
|
|
2273
|
+
defaults=defaults,correctColumnNum = correctColumnNum,errors = errors)[0]
|
|
2274
|
+
|
|
2275
|
+
|
|
2276
|
+
def _chainFill(line, previous):
|
|
2277
|
+
"""The cells of a ``#_defaults_#`` line as they stand once its empty cells are filled from the line before it in the
|
|
2278
|
+
file (``previous``, None for the first, or after an empty one): the defaults in effect after it."""
|
|
2279
|
+
if not previous or len(previous) < 2:
|
|
2280
|
+
return line
|
|
2281
|
+
cells = list(line) + [''] * (len(previous) - len(line)) # (a shorter line has the empty cells it lacks)
|
|
2282
|
+
return [cell if cell or i == 0 or i >= len(previous) else previous[i] for i, cell in enumerate(cells)]
|
|
2283
|
+
|
|
2284
|
+
|
|
2285
|
+
class _DefaultsList(list):
|
|
2286
|
+
"""The defaults list of a read: ``found`` says whether the file's own ``#_defaults_#`` line set it (_processLine
|
|
2287
|
+
replaces its cells in place), not the caller's defaults it began with."""
|
|
2288
|
+
found = False
|
|
2289
|
+
reset = False # an empty defaults line was read (it stays one whatever follows it)
|
|
2290
|
+
line = None # the cells of that line as the file has them (``defaults`` holds them filled and fitted to the columns)
|
|
2291
|
+
|
|
2292
|
+
def __setitem__(self, index, value):
|
|
2293
|
+
self.found = True
|
|
2294
|
+
list.__setitem__(self, index, value)
|
|
2295
|
+
|
|
2296
|
+
|
|
2297
|
+
def _legacyScrub(fileName,teeLogger = None,header = '',createIfNotExist = False,verifyHeader = True,verbose = False,
|
|
2298
|
+
taskDic = None,encoding = 'utf8',strict = False,delimiter = ...,defaults = ...,correctColumnNum = -1,errors = 'strict'):
|
|
2299
|
+
"""Compact a legacy file in place; return ``(taskDic, done)``: the rows read, and whether the file was written (or
|
|
2300
|
+
had nothing to write). ``done`` is False when nothing was written because the file kept changing (the shape of
|
|
2301
|
+
``_specScrub``): it is rewritten only if it is what the read saw (spec §19.8), so a row another process appends
|
|
2302
|
+
after the read is not scrubbed away; the read is made again, up to ``_SPEC_REWRITE_ATTEMPTS`` times.
|
|
2303
|
+
|
|
2304
|
+
The file's own ``#_defaults_#`` lines are written as the defaults in effect at the end of the read: the last
|
|
2305
|
+
non-empty one after the rows (those before it keep their empty cells, those after it were filled when read), so
|
|
2306
|
+
that every row reads as it did and the header, if any, stays on the first line. An empty one that was read is
|
|
2307
|
+
written before the rows (_legacyScrub, ``reset``), as wide as they are. The defaults the caller passed are not
|
|
2308
|
+
written. With no header the file begins with an empty line only if the first line of the file was not a row."""
|
|
2309
|
+
given = list(taskDic.items()) if taskDic else []
|
|
2310
|
+
givenDefaults = list(defaults) if defaults not in (..., None) else []
|
|
2311
|
+
for tries in range(_SPEC_REWRITE_ATTEMPTS):
|
|
2312
|
+
if tries and taskDic is not None:
|
|
2313
|
+
# Read again as the caller passed it.
|
|
2314
|
+
taskDic.clear()
|
|
2315
|
+
taskDic.update(given)
|
|
2316
|
+
readDefaults = _DefaultsList(givenDefaults)
|
|
2317
|
+
# The stamp is taken before the read, and compared under the file lock.
|
|
2318
|
+
stamp = _specFileStamp(fileName)
|
|
2319
|
+
file = readTabularFile(fileName,teeLogger = teeLogger,header = header,createIfNotExist = createIfNotExist,
|
|
2320
|
+
verifyHeader = verifyHeader,verbose = verbose,taskDic = taskDic,
|
|
2321
|
+
encoding = encoding,strict = strict,delimiter = delimiter,defaults=readDefaults,correctColumnNum = correctColumnNum,errors = errors)
|
|
2322
|
+
if not file:
|
|
2323
|
+
return file, True
|
|
2188
2324
|
dlm = get_delimiter(delimiter,file_name=fileName)
|
|
2189
2325
|
formatted = _formatHeader(header,delimiter=dlm)
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2326
|
+
if not _verifyFileExistence(fileName,createIfNotExist = True,teeLogger = teeLogger,header = formatted,encoding = encoding,strict = False,delimiter=dlm,errors = errors):
|
|
2327
|
+
raise FileNotFoundError("Something catastrophic happened! File still not found after creation") # (removed since the read: made again)
|
|
2328
|
+
kept = _legacyKeptHeader(fileName, formatted, verifyHeader, verbose, teeLogger, encoding, strict, dlm)
|
|
2329
|
+
# Everything that is to be written is looked at, and encoded, before the file is touched:
|
|
2330
|
+
# rows that took the caller's defaults can hold text it cannot write.
|
|
2331
|
+
payload, count, columns = _legacyFormatRows(fileName, file, teeLogger, formatted, verifyHeader, verbose, encoding,
|
|
2332
|
+
strict, dlm, errors, assume=_sanitize(kept,delimiter=dlm) if kept else [])
|
|
2333
|
+
blank = not kept and _legacyBeginsBlank(fileName, encoding, teeLogger, dlm)
|
|
2334
|
+
if readDefaults.reset and columns > 1:
|
|
2335
|
+
# An empty defaults line was read: no defaults reach the rows that follow it, whoever reads them (the rows
|
|
2336
|
+
# were read with it, and any the caller of that read gave are not written). Written before the rows, so
|
|
2337
|
+
# that none are filled: after the header, or the empty line standing for one, or else the first row (a
|
|
2338
|
+
# header, to the store owning the file); as wide as the rows, which is the column count a reader takes from
|
|
2339
|
+
# the first line it reads, and still an empty defaults line to one that reads it anywhere.
|
|
2340
|
+
reset = _encodeRow([DEFAULTS_INDICATOR_KEY] + [''] * (columns - 1),dlm,encoding,errors,what='defaults') + b'\n'
|
|
2341
|
+
if kept or blank:
|
|
2342
|
+
payload = reset + payload
|
|
2343
|
+
else:
|
|
2344
|
+
first, newline, rest = payload.partition(b'\n')
|
|
2345
|
+
payload = first + newline + reset + rest
|
|
2346
|
+
last = readDefaults.line if readDefaults.found else None
|
|
2347
|
+
if last and len(last) > 1:
|
|
2348
|
+
payload += _encodeRow(_sanitize(last,delimiter=dlm),dlm,encoding,errors,what='defaults') + b'\n'
|
|
2349
|
+
if _legacyReplace(fileName, kept, blank, encoding, dlm, errors, payload, expect=stamp) is None:
|
|
2350
|
+
continue # another writer changed the file after the read
|
|
2351
|
+
if verbose:
|
|
2352
|
+
__teePrintOrNot(f"Cleared {fileName}",teeLogger=teeLogger)
|
|
2353
|
+
__teePrintOrNot(f"Appended {count} lines to {fileName}" if count else f"No lines to append to {fileName}",teeLogger=teeLogger)
|
|
2354
|
+
return file, True
|
|
2355
|
+
__teePrintOrNot(f'Warning: scrub of {fileName} skipped: the file kept changing while it was being scrubbed; nothing was written','warning',teeLogger)
|
|
2356
|
+
return file, False
|
|
2198
2357
|
|
|
2199
2358
|
# ===========================================================================
|
|
2200
2359
|
# TSVZ spec v1 dialect (.tsvz / .csvz / .nsvz / .psvz) -- see tsvz-spec-v1.md
|
|
@@ -3586,7 +3745,8 @@ def _matchOwner(fd, st):
|
|
|
3586
3745
|
|
|
3587
3746
|
|
|
3588
3747
|
def _specRewriteInPlace(path, content, expect=None, memberSize=None):
|
|
3589
|
-
"""Replace a part's bytes in place (same inode, spec §18.7 / §19.8)
|
|
3748
|
+
"""Replace a part's bytes in place (same inode, spec §18.7 / §19.8): the write of a spec store's scrub and
|
|
3749
|
+
clear, and of a legacy file's (_legacyReplace).
|
|
3590
3750
|
|
|
3591
3751
|
``content`` is uncompressed; it is compressed for a compressed part. The
|
|
3592
3752
|
file is left alone when its bytes already match. Returns ``(written, stamp)``:
|
|
@@ -3597,7 +3757,7 @@ def _specRewriteInPlace(path, content, expect=None, memberSize=None):
|
|
|
3597
3757
|
"""
|
|
3598
3758
|
codec = _parsePartName(path).codec
|
|
3599
3759
|
_requireCodec(codec)
|
|
3600
|
-
raw = _compressBytes(codec, content, memberSize=memberSize) if codec
|
|
3760
|
+
raw = _compressBytes(codec, content, memberSize=memberSize) if codec else content # (nothing: one empty member)
|
|
3601
3761
|
with _pathLock(path):
|
|
3602
3762
|
with open(path, 'r+b') as f:
|
|
3603
3763
|
_lockFile(f)
|
|
@@ -3606,19 +3766,29 @@ def _specRewriteInPlace(path, content, expect=None, memberSize=None):
|
|
|
3606
3766
|
if expect is not None and _specStamp(st) != expect:
|
|
3607
3767
|
return None
|
|
3608
3768
|
fd = f.fileno()
|
|
3609
|
-
#
|
|
3610
|
-
#
|
|
3611
|
-
old
|
|
3612
|
-
|
|
3769
|
+
# Read unbuffered, as the file is written below: a read-ahead in f would leave it a stale position to
|
|
3770
|
+
# close at. What a failed rewrite puts back is kept: the bytes the write overwrites (from the start to
|
|
3771
|
+
# the shorter end) and the old size to cut back to. A clear of a huge file reads none of it.
|
|
3772
|
+
size = st.st_size
|
|
3773
|
+
old = _readFrom0(fd, min(size, len(raw)))
|
|
3774
|
+
if size == len(raw) and old == raw:
|
|
3613
3775
|
return False, _specStamp(st)
|
|
3614
3776
|
try:
|
|
3615
3777
|
_reserveSpace(fd, len(raw)) # spec §19.8: a full disk refuses the rewrite before any byte changes
|
|
3616
3778
|
_writeFrom0(fd, raw)
|
|
3617
3779
|
except BaseException as e:
|
|
3780
|
+
# Once the cut is made the rewrite is done: only the cut makes the file shorter, and it follows the
|
|
3781
|
+
# whole write, so the new bytes are complete (what was cut off is not kept to put back).
|
|
3782
|
+
if os.fstat(fd).st_size < size:
|
|
3783
|
+
raise
|
|
3618
3784
|
# Put the old bytes back rather than leave a half-rewritten part (or one a failed
|
|
3619
|
-
# reservation lengthened).
|
|
3785
|
+
# reservation lengthened): the write left what it did not overwrite as it was.
|
|
3620
3786
|
try:
|
|
3621
|
-
|
|
3787
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
3788
|
+
view = memoryview(old)
|
|
3789
|
+
while view:
|
|
3790
|
+
view = view[os.write(fd, view):]
|
|
3791
|
+
os.ftruncate(fd, size)
|
|
3622
3792
|
os.fsync(fd)
|
|
3623
3793
|
except OSError as undo:
|
|
3624
3794
|
damaged = '{}: the rewrite failed ({!r}) and the old bytes could not be put back ({}): the part is damaged'.format(
|
|
@@ -4970,7 +5140,7 @@ class TSVZed(OrderedDict):
|
|
|
4970
5140
|
"""clear_file() for a legacy file; ``dropRows`` (clear()) also empties memory and drops the queued rows, once the
|
|
4971
5141
|
file is open and locked (when it cannot be, nothing changed), under _memLock: no set is half done, and no row
|
|
4972
5142
|
set before it is kept or appended after the emptied file. Queued defaults lines are kept: the defaults stay."""
|
|
4973
|
-
header = self._headerLine()
|
|
5143
|
+
header = self._headerLine(None, clearing=True)
|
|
4974
5144
|
|
|
4975
5145
|
def emptied():
|
|
4976
5146
|
if dropRows:
|
|
@@ -5224,7 +5394,7 @@ memoryOnly:{self.memoryOnly}
|
|
|
5224
5394
|
f"in a row up to a minute.")
|
|
5225
5395
|
else:
|
|
5226
5396
|
retry = ("With rewrite_interval=0 none is tried again on its own, only one asked for (mapToFile(), "
|
|
5227
|
-
"rewrite(force=True), rewrite_on_exit), one made after another process changes the file, or, while "
|
|
5397
|
+
"hardMapToFile(), rewrite(force=True), rewrite_on_exit), one made after another process changes the file, or, while "
|
|
5228
5398
|
"damage another writer left is to be made good, one at a later append.")
|
|
5229
5399
|
damaged = (" The file is damaged (a rewrite of this object's failed) or was removed (rows are appended to a new "
|
|
5230
5400
|
"one): until a rewrite gets through, memory holds the only good copy." if self._damaged else "")
|
|
@@ -5281,31 +5451,38 @@ memoryOnly:{self.memoryOnly}
|
|
|
5281
5451
|
now = _specClock()
|
|
5282
5452
|
return any(state is not None and now < state[1] for state in map(self._failures.get, kinds))
|
|
5283
5453
|
|
|
5284
|
-
def _headerLine(self):
|
|
5454
|
+
def _headerLine(self, f, clearing=False):
|
|
5285
5455
|
"""The header line a legacy file of this store begins with, as bytes without its newline; None for no header.
|
|
5286
5456
|
|
|
5287
|
-
Checked by the constructor: it can be written.
|
|
5457
|
+
Checked by the constructor: it can be written. A store with no header (['']) begins the file with the empty line
|
|
5458
|
+
that stands for one (3.39 and 5.0 did, always) when the file holds nothing or begins with a line that is not a
|
|
5459
|
+
data row, and when it is cleared; it never puts one in front of a first line that is a row. ``f`` is the open,
|
|
5460
|
+
locked file a rewrite is to write: looked at through it (not by name, which would drop a lockf lock)."""
|
|
5288
5461
|
if not self.header:
|
|
5289
5462
|
return None
|
|
5463
|
+
if not clearing and not any(self.header) and not _legacyBeginsBlankIn(f, self.encoding, self.delimiter):
|
|
5464
|
+
return None
|
|
5290
5465
|
return _encodeRow(_sanitize(list(self.header),delimiter=self.delimiter),self.delimiter,self.encoding,self.errors,what='header')
|
|
5291
5466
|
|
|
5292
|
-
def _snapshotForRewrite(self):
|
|
5467
|
+
def _snapshotForRewrite(self, f):
|
|
5293
5468
|
"""``(header, lines, queued)``: the header line (None for none), the defaults line and the rows (_RewriteRows, a
|
|
5294
5469
|
flat snapshot taken under _memLock and checked: a TSVZEncodeError changes nothing), and the queued rows, taken
|
|
5295
5470
|
from the queue: memory holds them, so the rewrite writes them; _writeFile puts them back if it stops."""
|
|
5296
5471
|
delimiter, encoding, errors = self.delimiter, self.encoding, self.errors
|
|
5297
|
-
header = self._headerLine()
|
|
5472
|
+
header = self._headerLine(f)
|
|
5298
5473
|
with self._memLock:
|
|
5299
5474
|
values = list(self.values())
|
|
5300
5475
|
defaults = list(self.defaults)
|
|
5301
5476
|
queued, self.appendQueue = self.appendQueue, deque()
|
|
5302
5477
|
try:
|
|
5303
|
-
first = []
|
|
5478
|
+
first = later = []
|
|
5304
5479
|
if defaults and len(defaults) > 1:
|
|
5305
5480
|
# The defaults line comes right after the header, so it survives a
|
|
5306
|
-
# full rewrite -- it is not a regular mapping key.
|
|
5307
|
-
|
|
5308
|
-
|
|
5481
|
+
# full rewrite -- it is not a regular mapping key. With no header line written the first line
|
|
5482
|
+
# is a row (a header, to the store that owns the file): after that.
|
|
5483
|
+
line = _encodeRow(_sanitize(defaults,delimiter=delimiter),delimiter,encoding,errors,what='defaults')
|
|
5484
|
+
first, later = ([line], []) if header is not None else ([], [line])
|
|
5485
|
+
rows = _RewriteRows(first, values, delimiter, encoding, errors, self.header, later)
|
|
5309
5486
|
del values # the snapshot holds the cells
|
|
5310
5487
|
rows.check()
|
|
5311
5488
|
except BaseException:
|
|
@@ -5362,7 +5539,7 @@ memoryOnly:{self.memoryOnly}
|
|
|
5362
5539
|
self._syncFrom(f, True)
|
|
5363
5540
|
elif sync is False and self._externalChange():
|
|
5364
5541
|
self.__teePrintOrNot(f"Warning: Overwriting external changes in {self._fileName}",'warning')
|
|
5365
|
-
header, lines, queued = (snapshot
|
|
5542
|
+
header, lines, queued = snapshot() if snapshot else self._snapshotForRewrite(f)
|
|
5366
5543
|
if not sync or self._damaged or attempt == 2 or not self._changedSince(f):
|
|
5367
5544
|
break
|
|
5368
5545
|
# A writer that takes no lock appended while the rows were taken: taken in too, and taken again.
|
|
@@ -5857,7 +6034,7 @@ memoryOnly:{self.memoryOnly}
|
|
|
5857
6034
|
reporter = _Reporter(self._fileName, self.teeLogger)
|
|
5858
6035
|
written = []
|
|
5859
6036
|
try:
|
|
5860
|
-
_appendOpen(f, b''.join(chunks), codec, self._headerLine if any(self.header) else None, 'newline',
|
|
6037
|
+
_appendOpen(f, b''.join(chunks), codec, (lambda: self._headerLine(f)) if any(self.header) else None, 'newline',
|
|
5861
6038
|
reporter, written=written)
|
|
5862
6039
|
_fsync(f.fileno()) # a sync that fails fails the append: the rows stay queued
|
|
5863
6040
|
finally:
|
|
@@ -6363,23 +6540,15 @@ class TSVZedLite(MutableMapping):
|
|
|
6363
6540
|
if not cleared:
|
|
6364
6541
|
raise OSError('{} was not cleared: the clear was refused (see the warning above)'.format(self._fileName))
|
|
6365
6542
|
return self
|
|
6366
|
-
if self.fileObj is None:
|
|
6367
|
-
clearTabularFile(self._fileName, header=self.header, verbose=self.verbose,
|
|
6368
|
-
encoding=self.encoding, delimiter=self.delimiter, errors=self.errors)
|
|
6369
|
-
return self
|
|
6370
6543
|
if self.verbose:
|
|
6371
6544
|
eprint(f"Clearing {self._fileName}")
|
|
6372
|
-
|
|
6373
|
-
self.
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
location = self.__writeValues(self.header, headerFirst=False)
|
|
6378
|
-
if self.verbose:
|
|
6379
|
-
eprint(f"Header {self.header} written to {self._fileName}")
|
|
6380
|
-
eprint(f"At {location} size: {self.fileObj.tell()}")
|
|
6545
|
+
# Written over the file and cut under its lock, as TSVZed's rewrites are (the file object stays the file's).
|
|
6546
|
+
clearTabularFile(self._fileName, header=self.header, verbose=self.verbose,
|
|
6547
|
+
encoding=self.encoding, delimiter=self.delimiter, errors=self.errors)
|
|
6548
|
+
if self.fileObj is not None:
|
|
6549
|
+
self.fileObj.seek(0, os.SEEK_END) # (and drops what it had read ahead)
|
|
6381
6550
|
return self
|
|
6382
|
-
|
|
6551
|
+
|
|
6383
6552
|
def switchFile(self,newFileName,createIfNotExist = ...,verifyHeader = ...):
|
|
6384
6553
|
if createIfNotExist is ...:
|
|
6385
6554
|
createIfNotExist = self.createIfNotExist
|
|
@@ -7349,9 +7518,8 @@ class _ServeStore(object):
|
|
|
7349
7518
|
if self.spec:
|
|
7350
7519
|
return _specScrub(self.path, teeLogger=log, verifyHeader=False, verbose=self.verbose,
|
|
7351
7520
|
strict=self.strict, delimiter=self.delimiter, defaults=self.defaults)[1]
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
return True
|
|
7521
|
+
return _legacyScrub(self.path, teeLogger=log, verifyHeader=False, verbose=self.verbose,
|
|
7522
|
+
strict=self.strict, delimiter=self.delimiter, defaults=self.defaults)[1]
|
|
7355
7523
|
finally:
|
|
7356
7524
|
self.parts = None # read again at the next request
|
|
7357
7525
|
|
|
@@ -8424,7 +8592,8 @@ operations:
|
|
|
8424
8592
|
set STORE KEY [VALUE ...] write a row; KEY alone deletes it; '-' reads rows from stdin
|
|
8425
8593
|
append ... same as set
|
|
8426
8594
|
delete STORE KEY [KEY ...] delete KEYs; '-' reads keys from stdin
|
|
8427
|
-
clear STORE empty the store
|
|
8595
|
+
clear STORE empty the store; a .tsvz keeps its header and
|
|
8596
|
+
markers, a .tsv the --x-header it is given
|
|
8428
8597
|
scrub STORE compact the store in place (archival maintenance)
|
|
8429
8598
|
verify STORE check #_checksum_*_# segments (exit 4 on a mismatch)
|
|
8430
8599
|
parts STORE list the parts of a multi-part store
|
{tsvz-5.0 → tsvz-5.1}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tsvz-5.0 → tsvz-5.1}/setup.cfg
RENAMED
|
File without changes
|
{tsvz-5.0 → tsvz-5.1}/setup.py
RENAMED
|
File without changes
|