TSVZ 5.1__tar.gz → 5.2__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.1 → tsvz-5.2}/PKG-INFO +130 -9
- {tsvz-5.1 → tsvz-5.2}/README.md +129 -8
- {tsvz-5.1 → tsvz-5.2}/TSVZ.egg-info/PKG-INFO +130 -9
- {tsvz-5.1 → tsvz-5.2}/TSVZ.py +154 -39
- {tsvz-5.1 → tsvz-5.2}/LICENSE +0 -0
- {tsvz-5.1 → tsvz-5.2}/TSVZ.egg-info/SOURCES.txt +0 -0
- {tsvz-5.1 → tsvz-5.2}/TSVZ.egg-info/dependency_links.txt +0 -0
- {tsvz-5.1 → tsvz-5.2}/TSVZ.egg-info/entry_points.txt +0 -0
- {tsvz-5.1 → tsvz-5.2}/TSVZ.egg-info/requires.txt +0 -0
- {tsvz-5.1 → tsvz-5.2}/TSVZ.egg-info/top_level.txt +0 -0
- {tsvz-5.1 → tsvz-5.2}/setup.cfg +0 -0
- {tsvz-5.1 → tsvz-5.2}/setup.py +0 -0
{tsvz-5.1 → tsvz-5.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TSVZ
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2
|
|
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
|
|
@@ -163,6 +163,20 @@ named like an operation then needs a path prefix (`./read`).
|
|
|
163
163
|
with a warning.
|
|
164
164
|
- **TSVZ extensions:**
|
|
165
165
|
- `--x-header H` and `--x-defaults KEY<d>D1<d>...` decode backslash escapes such as `\t`;
|
|
166
|
+
- `--x-header H` makes line 1 of a `.tsv`-family file its header for every operation
|
|
167
|
+
when line 1 is H: the same number of cells, each equal to H's once the whitespace
|
|
168
|
+
around it is taken off (a trailing empty cell counts: `id\tv\t` is not `id\tv`; a UTF-8
|
|
169
|
+
byte order mark before line 1, as Excel writes one, is skipped). It
|
|
170
|
+
is then never a key, row or count, `scrub` and `clear` keep it, and
|
|
171
|
+
`read --format table` shows it above the rows. When line 1 differs, the file has no
|
|
172
|
+
header and line 1 is a row, with 3.39's "Header mismatch" warning (`--x-strict`: an
|
|
173
|
+
error, exit 1, nothing written). A missing or empty file gets H first. Without
|
|
174
|
+
`--x-header`, line 1 is a row, as in 3.39. (The library's own header check, a
|
|
175
|
+
`header=` given to `readTabularFile`, `appendTabularFile`, `clearTabularFile`,
|
|
176
|
+
`scrubTabularFile` or `TSVZed`, is 3.39's, looser one: each cell of H within the
|
|
177
|
+
cell of line 1 under it, so `idaho\tvalue` passes for `id\tv`.) A rewrite without
|
|
178
|
+
`--x-header` can put a row equal to H on line 1
|
|
179
|
+
([limitations](docs/limitations.md#a-rewrite-without---x-header-can-put-a-row-equal-to-h-on-line-1)).
|
|
166
180
|
- `--x-strict` / `--x-force` turn 3.39's column and header checks on or off.
|
|
167
181
|
|
|
168
182
|
## Serving a store
|
|
@@ -193,7 +207,13 @@ status stay the same.
|
|
|
193
207
|
and `fsync`s everything and removes `STORE.serve`.
|
|
194
208
|
- **Bypassing it.** `--x-direct` makes a `tsvz` command use the files. Command
|
|
195
209
|
lines with `--x-header`, `--x-defaults` or `--x-strict`, or another delimiter
|
|
196
|
-
than the server's, use the files too.
|
|
210
|
+
than the server's, use the files too. A server started with `--x-header H`
|
|
211
|
+
answers every request by the `--x-header` rule above, and one started with
|
|
212
|
+
`--x-strict` too answers as a direct `--x-strict` command does: a line 1 that is
|
|
213
|
+
not H fails the request (status 1, nothing written), and reads and scrubs drop
|
|
214
|
+
the rows of another width. It does not start when line 1 is not H then. The
|
|
215
|
+
table of a routed `read --format table` shows no H line: its first row is set
|
|
216
|
+
as the table's header, as with no `--x-header`.
|
|
197
217
|
|
|
198
218
|
```python
|
|
199
219
|
import TSVZ
|
|
@@ -261,7 +281,9 @@ microseconds as a write starts is followed as a rotation instead); while that re
|
|
|
261
281
|
fails or is refused, the rows are appended to a new file there, and memory keeps all of
|
|
262
282
|
them for the rewrite that gets through. A compressed file another writer left damaged
|
|
263
283
|
is made again too (L5: every read stops at the damage, so rows
|
|
264
|
-
appended after it would be read by no one)
|
|
284
|
+
appended after it would be read by no one): what the read did not get goes, the bytes after
|
|
285
|
+
the damage and also the lines of the damaged member before it that the codec never handed
|
|
286
|
+
over (Python's gzip gives none of a first member cut short, where `zcat` shows them). While that
|
|
265
287
|
rewrite fails, the rows are appended after the damage and memory keeps them for the
|
|
266
288
|
next rewrite that gets through; while it is refused (a row changed in place to text
|
|
267
289
|
the encoding cannot hold), they are appended after it as in 4.9, and the next read
|
|
@@ -301,6 +323,106 @@ empty; the keys it sets or deletes then take its values, and the keys it left ou
|
|
|
301
323
|
memory's. An explicit `reload()` while the file is damaged replaces memory with the
|
|
302
324
|
damaged file, as asked, and warns.
|
|
303
325
|
|
|
326
|
+
## Upgrading to 5.2 from 5.1
|
|
327
|
+
|
|
328
|
+
The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
|
|
329
|
+
5.2 writes.
|
|
330
|
+
|
|
331
|
+
- **A scrub of a `.tsv` whose rows were all deleted shrinks it.** `scrubTabularFile`,
|
|
332
|
+
`tsvz scrub` (also through a `tsvz serve` handler) of a `.tsv`-family file (`.tsv`,
|
|
333
|
+
`.csv`, `.nsv`, `.psv` and their compressed forms) in which every key has been deleted
|
|
334
|
+
leaves the file as an empty store: the header line it has, and its `#_defaults_#`
|
|
335
|
+
lines as a scrub of a file with rows keeps them (an empty one after the header, the
|
|
336
|
+
last non-empty one after it, both as wide as the header, or as the column count the
|
|
337
|
+
read took, which caller defaults set when given). A file with no header line keeps its
|
|
338
|
+
empty first line, or becomes 0 bytes when its first line was a row; a first line that
|
|
339
|
+
is no row (a comment, say) becomes an empty line. A defaults line it keeps follows an
|
|
340
|
+
empty first line, which the scrub then writes. Comment lines go, as a scrub of a file
|
|
341
|
+
with rows already drops them. A scrub that is given
|
|
342
|
+
a header (and checks it, the default) keeps the file's first line as its header, an
|
|
343
|
+
empty one included, with a "Header mismatch" warning when it differs; the header it is
|
|
344
|
+
given is written only with `verifyHeader=False`, or when the file's first line cannot be
|
|
345
|
+
decoded (plain text named `.gz`); `tsvz scrub --x-header` follows the `--x-header` rule
|
|
346
|
+
below instead. A scrub of a path that is not there
|
|
347
|
+
makes no file and returns `{}`, as in 5.1, unless `createIfNotExist=True`. A compressed
|
|
348
|
+
file whose read decodes no row (plain text named `.gz`, a first member cut short) is
|
|
349
|
+
made again as an empty store, as a scrub of a damaged file with rows already was in
|
|
350
|
+
5.1: what the read did not get goes, the bytes after the damage and also the lines of the
|
|
351
|
+
damaged member before it that the codec never handed over (Python's gzip gives none of
|
|
352
|
+
a first member cut short, where `zcat` shows them) (see the damaged-stream notes above), and
|
|
353
|
+
`tsvz clear --x-header H --x-strict` (`clearTabularFile(verifyHeader=True)`) now clears
|
|
354
|
+
such a file, where 5.1 exited 1 with the codec's error.
|
|
355
|
+
5.1 and 5.0 left such a file as it was, deletion lines included, so a writer that
|
|
356
|
+
appended keys, deleted them all and scrubbed, over and over, saw the file grow without
|
|
357
|
+
limit (205, 405, 605 bytes, ... for 20 keys a cycle); now it is the same size after
|
|
358
|
+
every scrub. A file with no byte (or a compressed one with only empty streams) is
|
|
359
|
+
still left alone, and so is a file whose strict scrub (`strict=True`, `--x-strict`) read
|
|
360
|
+
dropped a line for its width and found no row: that line is not a deletion, as 5.1 left
|
|
361
|
+
it. A scrub that finds the file changed since its read still reads
|
|
362
|
+
again and writes nothing if it keeps changing, as for a file with rows.
|
|
363
|
+
- **`get_file_obj('ab')` on a compressed store writes its trailer before the lock is let
|
|
364
|
+
go.** A program that calls it on a `.tsv.gz`, `.bz2`, `.xz` or `.zst` store and writes
|
|
365
|
+
through the file now has all of it, the gzip trailer and the bz2, xz and zstd data included,
|
|
366
|
+
in the file when `release_file_obj()` unlocks it. 5.1 and earlier wrote them after the
|
|
367
|
+
unlock, where another writer could append in between and corrupt the stream.
|
|
368
|
+
- **A `TSVZed` no longer lets a file grow with every session of sets and deletes.** It
|
|
369
|
+
rewrites a `.tsv`-family file at open when the file holds a key's row and the deletion
|
|
370
|
+
of it, as 3.39 did only for a key with two values, so a program that sets keys and
|
|
371
|
+
deletes them in each session (default settings, or `rewrite_interval`) keeps the file
|
|
372
|
+
the same size (205, 405, 605 bytes, ... for 20 keys a session before). Every session
|
|
373
|
+
that opens such a file now rewrites it once. With `rewrite_on_load=False` the open
|
|
374
|
+
does not, and the next exit or periodic rewrite does it (`rewrite_on_exit=True`,
|
|
375
|
+
`rewrite_interval`). What such a rewrite does, it now does at a plain open:
|
|
376
|
+
- it cuts a row wider than the header, and the cut stays
|
|
377
|
+
([limitations](docs/limitations.md#scrubs-and-rewrites-read-rows-at-one-column-width-deferred-to-the-width-design)):
|
|
378
|
+
`id\tv\na\t1\t2\nb\t1\nb\t\n` becomes `id\tv\na\t1 \n`, and
|
|
379
|
+
`id\tv\na\t\t7\nb\t1\nb\t\n` becomes `id\tv\na\t \n`, the 7 lost;
|
|
380
|
+
- it writes over the file in place and pads with spaces (`a\t1 ` above): TSVZ readers
|
|
381
|
+
strip them, tools that read the raw bytes (`awk`, `cut`) see them;
|
|
382
|
+
- on a file it cannot write (read-only), it prints `Failed to open file … Permission
|
|
383
|
+
denied` twice and leaves the file as it is, as 5.1 did for a key with two values.
|
|
384
|
+
- **`--x-header H` makes line 1 the header for every `tsvz` operation.** On a
|
|
385
|
+
`.tsv`-family file whose line 1 is H (an exact match: the same cells, each without the
|
|
386
|
+
whitespace around it, a trailing empty cell included), line 1 is the header: `keys`,
|
|
387
|
+
`len`, `read`, `get`, `has`, `pop`, `popitem` and `delete` never list, count, print,
|
|
388
|
+
pop or delete it, and `scrub` and `clear` keep it. When line 1 differs, the file has no
|
|
389
|
+
header and line 1 is a row, with 3.39's "Header mismatch" warning, for the reads and
|
|
390
|
+
the scrub alike, so a scrub keeps the rows a read shows (but for the column-width cases
|
|
391
|
+
below), and in such a file it never writes a row that is H as line 1: it puts an empty
|
|
392
|
+
line first (`a\t1\nid\tv\na\t\n` becomes `\nid\tv\n`), so the reads still take that row
|
|
393
|
+
for a row. With `--x-strict` a line 1 that is not H is an error (exit 1, nothing
|
|
394
|
+
written) for every operation, through a handler too. A missing or empty file gets H
|
|
395
|
+
first at its first write, as in 5.1. A `tsvz serve STORE --x-header H` handler applies
|
|
396
|
+
the rule to every request it answers, routed `tsvz` commands included; with
|
|
397
|
+
`--x-strict` it is strict for them as above (5.1 ignored `--x-strict` at `serve`): such
|
|
398
|
+
a handler refuses to start on a file whose line 1 is not H, its routed `.tsv` reads and
|
|
399
|
+
scrubs drop lines of another column count, and its `.tsvz` reads are strict, as for a
|
|
400
|
+
direct `--x-strict` command. The library's own header check (`header=` to
|
|
401
|
+
`readTabularFile`, `appendTabularFile`, `clearTabularFile`, `scrubTabularFile`,
|
|
402
|
+
`TSVZed`) is unchanged, 3.39's containment check. The CLI's writes check line 1 more
|
|
403
|
+
strictly than 5.1's did: 5.1's `set`, `append`, `delete` and `clear` with `--x-header`
|
|
404
|
+
silently took a line 1 that merely held H's cells (`id\tvalue` for `id\tv`) for the
|
|
405
|
+
header and fitted rows to H's width; now they warn and write the row at its own width,
|
|
406
|
+
and with `--x-strict` exit 1. A rewrite without `--x-header` (a scrub without it,
|
|
407
|
+
`scrubTabularFile`, a `TSVZed` rewrite) can still put a row equal to H on line 1 of a
|
|
408
|
+
header-less file
|
|
409
|
+
([limitations](docs/limitations.md#a-rewrite-without---x-header-can-put-a-row-equal-to-h-on-line-1)).
|
|
410
|
+
In 5.1 only `set`, `append`, `delete` and `clear` used `--x-header`: the reads and
|
|
411
|
+
`scrub` took line 1 as a row (`tsvz keys`
|
|
412
|
+
printed `id a b` for two keys, `len` said 3, `popitem first` popped the header), and
|
|
413
|
+
so did every request to a handler. Without `--x-header` nothing changed: line 1 is a
|
|
414
|
+
row, as in 5.1 and 3.39. In a `.tsvz` store `pop`, `popitem` and `setdefault` now check
|
|
415
|
+
`--x-header` as `set` does; nothing else changed there.
|
|
416
|
+
- **Known, mostly as in 5.1: a scrub, a rewrite or a read takes one column count for every row,
|
|
417
|
+
and can lose cells or keys.** A scrub given defaults narrower than the rows, a row wider
|
|
418
|
+
than the header, and a `.tsv` that begins with a bare `#_defaults_#` line (read without
|
|
419
|
+
defaults) are examples; the entry lists each, and which is new in 5.2
|
|
420
|
+
([limitations](docs/limitations.md#scrubs-and-rewrites-read-rows-at-one-column-width-deferred-to-the-width-design)).
|
|
421
|
+
A strict scrub of a file whose read dropped a line for its width and found no row leaves it,
|
|
422
|
+
so it grows again over append, delete-all and scrub cycles
|
|
423
|
+
([limitations](docs/limitations.md#a-strict-scrub-leaves-a-file-whose-read-dropped-a-line-for-its-width)).
|
|
424
|
+
Both wait for the column-width design after 5.2.
|
|
425
|
+
|
|
304
426
|
## Upgrading to 5.1 from 5.0
|
|
305
427
|
|
|
306
428
|
The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
|
|
@@ -357,12 +479,11 @@ The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family
|
|
|
357
479
|
file's own header line.)
|
|
358
480
|
`TSVZed`'s message for a refused rewrite lists `hardMapToFile()` among the rewrites
|
|
359
481
|
a program asks for.
|
|
360
|
-
- **Known: a scrub of a `.tsv` whose rows were all deleted
|
|
361
|
-
the scrub
|
|
362
|
-
|
|
363
|
-
right, as in 3.39 and 5.0. (`tsvz scrub` of a headed file
|
|
364
|
-
row, so it
|
|
365
|
-
([limitations](docs/limitations.md#a-scrub-of-a-tsv-whose-rows-were-all-deleted-does-not-compact-it)).
|
|
482
|
+
- **Known in 5.1, fixed in 5.2: a scrub of a `.tsv` whose rows were all deleted did not
|
|
483
|
+
compact it** when the scrub was given the file's header (`scrubTabularFile(path, header=H)`)
|
|
484
|
+
or the file had no header line: it left the file as it was, deletion lines included;
|
|
485
|
+
reads were right, as in 3.39 and 5.0. (`tsvz scrub` of a headed file read the header
|
|
486
|
+
line as a row, so it did compact it.)
|
|
366
487
|
- **Known: a scrub run with other defaults than the store's program can change how
|
|
367
488
|
early rows read.** In a `.tsv` with an empty `#_defaults_#` line and rows with empty
|
|
368
489
|
cells before any defaults line, the reset line the scrub writes before the rows keeps
|
{tsvz-5.1 → tsvz-5.2}/README.md
RENAMED
|
@@ -131,6 +131,20 @@ named like an operation then needs a path prefix (`./read`).
|
|
|
131
131
|
with a warning.
|
|
132
132
|
- **TSVZ extensions:**
|
|
133
133
|
- `--x-header H` and `--x-defaults KEY<d>D1<d>...` decode backslash escapes such as `\t`;
|
|
134
|
+
- `--x-header H` makes line 1 of a `.tsv`-family file its header for every operation
|
|
135
|
+
when line 1 is H: the same number of cells, each equal to H's once the whitespace
|
|
136
|
+
around it is taken off (a trailing empty cell counts: `id\tv\t` is not `id\tv`; a UTF-8
|
|
137
|
+
byte order mark before line 1, as Excel writes one, is skipped). It
|
|
138
|
+
is then never a key, row or count, `scrub` and `clear` keep it, and
|
|
139
|
+
`read --format table` shows it above the rows. When line 1 differs, the file has no
|
|
140
|
+
header and line 1 is a row, with 3.39's "Header mismatch" warning (`--x-strict`: an
|
|
141
|
+
error, exit 1, nothing written). A missing or empty file gets H first. Without
|
|
142
|
+
`--x-header`, line 1 is a row, as in 3.39. (The library's own header check, a
|
|
143
|
+
`header=` given to `readTabularFile`, `appendTabularFile`, `clearTabularFile`,
|
|
144
|
+
`scrubTabularFile` or `TSVZed`, is 3.39's, looser one: each cell of H within the
|
|
145
|
+
cell of line 1 under it, so `idaho\tvalue` passes for `id\tv`.) A rewrite without
|
|
146
|
+
`--x-header` can put a row equal to H on line 1
|
|
147
|
+
([limitations](docs/limitations.md#a-rewrite-without---x-header-can-put-a-row-equal-to-h-on-line-1)).
|
|
134
148
|
- `--x-strict` / `--x-force` turn 3.39's column and header checks on or off.
|
|
135
149
|
|
|
136
150
|
## Serving a store
|
|
@@ -161,7 +175,13 @@ status stay the same.
|
|
|
161
175
|
and `fsync`s everything and removes `STORE.serve`.
|
|
162
176
|
- **Bypassing it.** `--x-direct` makes a `tsvz` command use the files. Command
|
|
163
177
|
lines with `--x-header`, `--x-defaults` or `--x-strict`, or another delimiter
|
|
164
|
-
than the server's, use the files too.
|
|
178
|
+
than the server's, use the files too. A server started with `--x-header H`
|
|
179
|
+
answers every request by the `--x-header` rule above, and one started with
|
|
180
|
+
`--x-strict` too answers as a direct `--x-strict` command does: a line 1 that is
|
|
181
|
+
not H fails the request (status 1, nothing written), and reads and scrubs drop
|
|
182
|
+
the rows of another width. It does not start when line 1 is not H then. The
|
|
183
|
+
table of a routed `read --format table` shows no H line: its first row is set
|
|
184
|
+
as the table's header, as with no `--x-header`.
|
|
165
185
|
|
|
166
186
|
```python
|
|
167
187
|
import TSVZ
|
|
@@ -229,7 +249,9 @@ microseconds as a write starts is followed as a rotation instead); while that re
|
|
|
229
249
|
fails or is refused, the rows are appended to a new file there, and memory keeps all of
|
|
230
250
|
them for the rewrite that gets through. A compressed file another writer left damaged
|
|
231
251
|
is made again too (L5: every read stops at the damage, so rows
|
|
232
|
-
appended after it would be read by no one)
|
|
252
|
+
appended after it would be read by no one): what the read did not get goes, the bytes after
|
|
253
|
+
the damage and also the lines of the damaged member before it that the codec never handed
|
|
254
|
+
over (Python's gzip gives none of a first member cut short, where `zcat` shows them). While that
|
|
233
255
|
rewrite fails, the rows are appended after the damage and memory keeps them for the
|
|
234
256
|
next rewrite that gets through; while it is refused (a row changed in place to text
|
|
235
257
|
the encoding cannot hold), they are appended after it as in 4.9, and the next read
|
|
@@ -269,6 +291,106 @@ empty; the keys it sets or deletes then take its values, and the keys it left ou
|
|
|
269
291
|
memory's. An explicit `reload()` while the file is damaged replaces memory with the
|
|
270
292
|
damaged file, as asked, and warns.
|
|
271
293
|
|
|
294
|
+
## Upgrading to 5.2 from 5.1
|
|
295
|
+
|
|
296
|
+
The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
|
|
297
|
+
5.2 writes.
|
|
298
|
+
|
|
299
|
+
- **A scrub of a `.tsv` whose rows were all deleted shrinks it.** `scrubTabularFile`,
|
|
300
|
+
`tsvz scrub` (also through a `tsvz serve` handler) of a `.tsv`-family file (`.tsv`,
|
|
301
|
+
`.csv`, `.nsv`, `.psv` and their compressed forms) in which every key has been deleted
|
|
302
|
+
leaves the file as an empty store: the header line it has, and its `#_defaults_#`
|
|
303
|
+
lines as a scrub of a file with rows keeps them (an empty one after the header, the
|
|
304
|
+
last non-empty one after it, both as wide as the header, or as the column count the
|
|
305
|
+
read took, which caller defaults set when given). A file with no header line keeps its
|
|
306
|
+
empty first line, or becomes 0 bytes when its first line was a row; a first line that
|
|
307
|
+
is no row (a comment, say) becomes an empty line. A defaults line it keeps follows an
|
|
308
|
+
empty first line, which the scrub then writes. Comment lines go, as a scrub of a file
|
|
309
|
+
with rows already drops them. A scrub that is given
|
|
310
|
+
a header (and checks it, the default) keeps the file's first line as its header, an
|
|
311
|
+
empty one included, with a "Header mismatch" warning when it differs; the header it is
|
|
312
|
+
given is written only with `verifyHeader=False`, or when the file's first line cannot be
|
|
313
|
+
decoded (plain text named `.gz`); `tsvz scrub --x-header` follows the `--x-header` rule
|
|
314
|
+
below instead. A scrub of a path that is not there
|
|
315
|
+
makes no file and returns `{}`, as in 5.1, unless `createIfNotExist=True`. A compressed
|
|
316
|
+
file whose read decodes no row (plain text named `.gz`, a first member cut short) is
|
|
317
|
+
made again as an empty store, as a scrub of a damaged file with rows already was in
|
|
318
|
+
5.1: what the read did not get goes, the bytes after the damage and also the lines of the
|
|
319
|
+
damaged member before it that the codec never handed over (Python's gzip gives none of
|
|
320
|
+
a first member cut short, where `zcat` shows them) (see the damaged-stream notes above), and
|
|
321
|
+
`tsvz clear --x-header H --x-strict` (`clearTabularFile(verifyHeader=True)`) now clears
|
|
322
|
+
such a file, where 5.1 exited 1 with the codec's error.
|
|
323
|
+
5.1 and 5.0 left such a file as it was, deletion lines included, so a writer that
|
|
324
|
+
appended keys, deleted them all and scrubbed, over and over, saw the file grow without
|
|
325
|
+
limit (205, 405, 605 bytes, ... for 20 keys a cycle); now it is the same size after
|
|
326
|
+
every scrub. A file with no byte (or a compressed one with only empty streams) is
|
|
327
|
+
still left alone, and so is a file whose strict scrub (`strict=True`, `--x-strict`) read
|
|
328
|
+
dropped a line for its width and found no row: that line is not a deletion, as 5.1 left
|
|
329
|
+
it. A scrub that finds the file changed since its read still reads
|
|
330
|
+
again and writes nothing if it keeps changing, as for a file with rows.
|
|
331
|
+
- **`get_file_obj('ab')` on a compressed store writes its trailer before the lock is let
|
|
332
|
+
go.** A program that calls it on a `.tsv.gz`, `.bz2`, `.xz` or `.zst` store and writes
|
|
333
|
+
through the file now has all of it, the gzip trailer and the bz2, xz and zstd data included,
|
|
334
|
+
in the file when `release_file_obj()` unlocks it. 5.1 and earlier wrote them after the
|
|
335
|
+
unlock, where another writer could append in between and corrupt the stream.
|
|
336
|
+
- **A `TSVZed` no longer lets a file grow with every session of sets and deletes.** It
|
|
337
|
+
rewrites a `.tsv`-family file at open when the file holds a key's row and the deletion
|
|
338
|
+
of it, as 3.39 did only for a key with two values, so a program that sets keys and
|
|
339
|
+
deletes them in each session (default settings, or `rewrite_interval`) keeps the file
|
|
340
|
+
the same size (205, 405, 605 bytes, ... for 20 keys a session before). Every session
|
|
341
|
+
that opens such a file now rewrites it once. With `rewrite_on_load=False` the open
|
|
342
|
+
does not, and the next exit or periodic rewrite does it (`rewrite_on_exit=True`,
|
|
343
|
+
`rewrite_interval`). What such a rewrite does, it now does at a plain open:
|
|
344
|
+
- it cuts a row wider than the header, and the cut stays
|
|
345
|
+
([limitations](docs/limitations.md#scrubs-and-rewrites-read-rows-at-one-column-width-deferred-to-the-width-design)):
|
|
346
|
+
`id\tv\na\t1\t2\nb\t1\nb\t\n` becomes `id\tv\na\t1 \n`, and
|
|
347
|
+
`id\tv\na\t\t7\nb\t1\nb\t\n` becomes `id\tv\na\t \n`, the 7 lost;
|
|
348
|
+
- it writes over the file in place and pads with spaces (`a\t1 ` above): TSVZ readers
|
|
349
|
+
strip them, tools that read the raw bytes (`awk`, `cut`) see them;
|
|
350
|
+
- on a file it cannot write (read-only), it prints `Failed to open file … Permission
|
|
351
|
+
denied` twice and leaves the file as it is, as 5.1 did for a key with two values.
|
|
352
|
+
- **`--x-header H` makes line 1 the header for every `tsvz` operation.** On a
|
|
353
|
+
`.tsv`-family file whose line 1 is H (an exact match: the same cells, each without the
|
|
354
|
+
whitespace around it, a trailing empty cell included), line 1 is the header: `keys`,
|
|
355
|
+
`len`, `read`, `get`, `has`, `pop`, `popitem` and `delete` never list, count, print,
|
|
356
|
+
pop or delete it, and `scrub` and `clear` keep it. When line 1 differs, the file has no
|
|
357
|
+
header and line 1 is a row, with 3.39's "Header mismatch" warning, for the reads and
|
|
358
|
+
the scrub alike, so a scrub keeps the rows a read shows (but for the column-width cases
|
|
359
|
+
below), and in such a file it never writes a row that is H as line 1: it puts an empty
|
|
360
|
+
line first (`a\t1\nid\tv\na\t\n` becomes `\nid\tv\n`), so the reads still take that row
|
|
361
|
+
for a row. With `--x-strict` a line 1 that is not H is an error (exit 1, nothing
|
|
362
|
+
written) for every operation, through a handler too. A missing or empty file gets H
|
|
363
|
+
first at its first write, as in 5.1. A `tsvz serve STORE --x-header H` handler applies
|
|
364
|
+
the rule to every request it answers, routed `tsvz` commands included; with
|
|
365
|
+
`--x-strict` it is strict for them as above (5.1 ignored `--x-strict` at `serve`): such
|
|
366
|
+
a handler refuses to start on a file whose line 1 is not H, its routed `.tsv` reads and
|
|
367
|
+
scrubs drop lines of another column count, and its `.tsvz` reads are strict, as for a
|
|
368
|
+
direct `--x-strict` command. The library's own header check (`header=` to
|
|
369
|
+
`readTabularFile`, `appendTabularFile`, `clearTabularFile`, `scrubTabularFile`,
|
|
370
|
+
`TSVZed`) is unchanged, 3.39's containment check. The CLI's writes check line 1 more
|
|
371
|
+
strictly than 5.1's did: 5.1's `set`, `append`, `delete` and `clear` with `--x-header`
|
|
372
|
+
silently took a line 1 that merely held H's cells (`id\tvalue` for `id\tv`) for the
|
|
373
|
+
header and fitted rows to H's width; now they warn and write the row at its own width,
|
|
374
|
+
and with `--x-strict` exit 1. A rewrite without `--x-header` (a scrub without it,
|
|
375
|
+
`scrubTabularFile`, a `TSVZed` rewrite) can still put a row equal to H on line 1 of a
|
|
376
|
+
header-less file
|
|
377
|
+
([limitations](docs/limitations.md#a-rewrite-without---x-header-can-put-a-row-equal-to-h-on-line-1)).
|
|
378
|
+
In 5.1 only `set`, `append`, `delete` and `clear` used `--x-header`: the reads and
|
|
379
|
+
`scrub` took line 1 as a row (`tsvz keys`
|
|
380
|
+
printed `id a b` for two keys, `len` said 3, `popitem first` popped the header), and
|
|
381
|
+
so did every request to a handler. Without `--x-header` nothing changed: line 1 is a
|
|
382
|
+
row, as in 5.1 and 3.39. In a `.tsvz` store `pop`, `popitem` and `setdefault` now check
|
|
383
|
+
`--x-header` as `set` does; nothing else changed there.
|
|
384
|
+
- **Known, mostly as in 5.1: a scrub, a rewrite or a read takes one column count for every row,
|
|
385
|
+
and can lose cells or keys.** A scrub given defaults narrower than the rows, a row wider
|
|
386
|
+
than the header, and a `.tsv` that begins with a bare `#_defaults_#` line (read without
|
|
387
|
+
defaults) are examples; the entry lists each, and which is new in 5.2
|
|
388
|
+
([limitations](docs/limitations.md#scrubs-and-rewrites-read-rows-at-one-column-width-deferred-to-the-width-design)).
|
|
389
|
+
A strict scrub of a file whose read dropped a line for its width and found no row leaves it,
|
|
390
|
+
so it grows again over append, delete-all and scrub cycles
|
|
391
|
+
([limitations](docs/limitations.md#a-strict-scrub-leaves-a-file-whose-read-dropped-a-line-for-its-width)).
|
|
392
|
+
Both wait for the column-width design after 5.2.
|
|
393
|
+
|
|
272
394
|
## Upgrading to 5.1 from 5.0
|
|
273
395
|
|
|
274
396
|
The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
|
|
@@ -325,12 +447,11 @@ The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family
|
|
|
325
447
|
file's own header line.)
|
|
326
448
|
`TSVZed`'s message for a refused rewrite lists `hardMapToFile()` among the rewrites
|
|
327
449
|
a program asks for.
|
|
328
|
-
- **Known: a scrub of a `.tsv` whose rows were all deleted
|
|
329
|
-
the scrub
|
|
330
|
-
|
|
331
|
-
right, as in 3.39 and 5.0. (`tsvz scrub` of a headed file
|
|
332
|
-
row, so it
|
|
333
|
-
([limitations](docs/limitations.md#a-scrub-of-a-tsv-whose-rows-were-all-deleted-does-not-compact-it)).
|
|
450
|
+
- **Known in 5.1, fixed in 5.2: a scrub of a `.tsv` whose rows were all deleted did not
|
|
451
|
+
compact it** when the scrub was given the file's header (`scrubTabularFile(path, header=H)`)
|
|
452
|
+
or the file had no header line: it left the file as it was, deletion lines included;
|
|
453
|
+
reads were right, as in 3.39 and 5.0. (`tsvz scrub` of a headed file read the header
|
|
454
|
+
line as a row, so it did compact it.)
|
|
334
455
|
- **Known: a scrub run with other defaults than the store's program can change how
|
|
335
456
|
early rows read.** In a `.tsv` with an empty `#_defaults_#` line and rows with empty
|
|
336
457
|
cells before any defaults line, the reset line the scrub writes before the rows keeps
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TSVZ
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2
|
|
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
|
|
@@ -163,6 +163,20 @@ named like an operation then needs a path prefix (`./read`).
|
|
|
163
163
|
with a warning.
|
|
164
164
|
- **TSVZ extensions:**
|
|
165
165
|
- `--x-header H` and `--x-defaults KEY<d>D1<d>...` decode backslash escapes such as `\t`;
|
|
166
|
+
- `--x-header H` makes line 1 of a `.tsv`-family file its header for every operation
|
|
167
|
+
when line 1 is H: the same number of cells, each equal to H's once the whitespace
|
|
168
|
+
around it is taken off (a trailing empty cell counts: `id\tv\t` is not `id\tv`; a UTF-8
|
|
169
|
+
byte order mark before line 1, as Excel writes one, is skipped). It
|
|
170
|
+
is then never a key, row or count, `scrub` and `clear` keep it, and
|
|
171
|
+
`read --format table` shows it above the rows. When line 1 differs, the file has no
|
|
172
|
+
header and line 1 is a row, with 3.39's "Header mismatch" warning (`--x-strict`: an
|
|
173
|
+
error, exit 1, nothing written). A missing or empty file gets H first. Without
|
|
174
|
+
`--x-header`, line 1 is a row, as in 3.39. (The library's own header check, a
|
|
175
|
+
`header=` given to `readTabularFile`, `appendTabularFile`, `clearTabularFile`,
|
|
176
|
+
`scrubTabularFile` or `TSVZed`, is 3.39's, looser one: each cell of H within the
|
|
177
|
+
cell of line 1 under it, so `idaho\tvalue` passes for `id\tv`.) A rewrite without
|
|
178
|
+
`--x-header` can put a row equal to H on line 1
|
|
179
|
+
([limitations](docs/limitations.md#a-rewrite-without---x-header-can-put-a-row-equal-to-h-on-line-1)).
|
|
166
180
|
- `--x-strict` / `--x-force` turn 3.39's column and header checks on or off.
|
|
167
181
|
|
|
168
182
|
## Serving a store
|
|
@@ -193,7 +207,13 @@ status stay the same.
|
|
|
193
207
|
and `fsync`s everything and removes `STORE.serve`.
|
|
194
208
|
- **Bypassing it.** `--x-direct` makes a `tsvz` command use the files. Command
|
|
195
209
|
lines with `--x-header`, `--x-defaults` or `--x-strict`, or another delimiter
|
|
196
|
-
than the server's, use the files too.
|
|
210
|
+
than the server's, use the files too. A server started with `--x-header H`
|
|
211
|
+
answers every request by the `--x-header` rule above, and one started with
|
|
212
|
+
`--x-strict` too answers as a direct `--x-strict` command does: a line 1 that is
|
|
213
|
+
not H fails the request (status 1, nothing written), and reads and scrubs drop
|
|
214
|
+
the rows of another width. It does not start when line 1 is not H then. The
|
|
215
|
+
table of a routed `read --format table` shows no H line: its first row is set
|
|
216
|
+
as the table's header, as with no `--x-header`.
|
|
197
217
|
|
|
198
218
|
```python
|
|
199
219
|
import TSVZ
|
|
@@ -261,7 +281,9 @@ microseconds as a write starts is followed as a rotation instead); while that re
|
|
|
261
281
|
fails or is refused, the rows are appended to a new file there, and memory keeps all of
|
|
262
282
|
them for the rewrite that gets through. A compressed file another writer left damaged
|
|
263
283
|
is made again too (L5: every read stops at the damage, so rows
|
|
264
|
-
appended after it would be read by no one)
|
|
284
|
+
appended after it would be read by no one): what the read did not get goes, the bytes after
|
|
285
|
+
the damage and also the lines of the damaged member before it that the codec never handed
|
|
286
|
+
over (Python's gzip gives none of a first member cut short, where `zcat` shows them). While that
|
|
265
287
|
rewrite fails, the rows are appended after the damage and memory keeps them for the
|
|
266
288
|
next rewrite that gets through; while it is refused (a row changed in place to text
|
|
267
289
|
the encoding cannot hold), they are appended after it as in 4.9, and the next read
|
|
@@ -301,6 +323,106 @@ empty; the keys it sets or deletes then take its values, and the keys it left ou
|
|
|
301
323
|
memory's. An explicit `reload()` while the file is damaged replaces memory with the
|
|
302
324
|
damaged file, as asked, and warns.
|
|
303
325
|
|
|
326
|
+
## Upgrading to 5.2 from 5.1
|
|
327
|
+
|
|
328
|
+
The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
|
|
329
|
+
5.2 writes.
|
|
330
|
+
|
|
331
|
+
- **A scrub of a `.tsv` whose rows were all deleted shrinks it.** `scrubTabularFile`,
|
|
332
|
+
`tsvz scrub` (also through a `tsvz serve` handler) of a `.tsv`-family file (`.tsv`,
|
|
333
|
+
`.csv`, `.nsv`, `.psv` and their compressed forms) in which every key has been deleted
|
|
334
|
+
leaves the file as an empty store: the header line it has, and its `#_defaults_#`
|
|
335
|
+
lines as a scrub of a file with rows keeps them (an empty one after the header, the
|
|
336
|
+
last non-empty one after it, both as wide as the header, or as the column count the
|
|
337
|
+
read took, which caller defaults set when given). A file with no header line keeps its
|
|
338
|
+
empty first line, or becomes 0 bytes when its first line was a row; a first line that
|
|
339
|
+
is no row (a comment, say) becomes an empty line. A defaults line it keeps follows an
|
|
340
|
+
empty first line, which the scrub then writes. Comment lines go, as a scrub of a file
|
|
341
|
+
with rows already drops them. A scrub that is given
|
|
342
|
+
a header (and checks it, the default) keeps the file's first line as its header, an
|
|
343
|
+
empty one included, with a "Header mismatch" warning when it differs; the header it is
|
|
344
|
+
given is written only with `verifyHeader=False`, or when the file's first line cannot be
|
|
345
|
+
decoded (plain text named `.gz`); `tsvz scrub --x-header` follows the `--x-header` rule
|
|
346
|
+
below instead. A scrub of a path that is not there
|
|
347
|
+
makes no file and returns `{}`, as in 5.1, unless `createIfNotExist=True`. A compressed
|
|
348
|
+
file whose read decodes no row (plain text named `.gz`, a first member cut short) is
|
|
349
|
+
made again as an empty store, as a scrub of a damaged file with rows already was in
|
|
350
|
+
5.1: what the read did not get goes, the bytes after the damage and also the lines of the
|
|
351
|
+
damaged member before it that the codec never handed over (Python's gzip gives none of
|
|
352
|
+
a first member cut short, where `zcat` shows them) (see the damaged-stream notes above), and
|
|
353
|
+
`tsvz clear --x-header H --x-strict` (`clearTabularFile(verifyHeader=True)`) now clears
|
|
354
|
+
such a file, where 5.1 exited 1 with the codec's error.
|
|
355
|
+
5.1 and 5.0 left such a file as it was, deletion lines included, so a writer that
|
|
356
|
+
appended keys, deleted them all and scrubbed, over and over, saw the file grow without
|
|
357
|
+
limit (205, 405, 605 bytes, ... for 20 keys a cycle); now it is the same size after
|
|
358
|
+
every scrub. A file with no byte (or a compressed one with only empty streams) is
|
|
359
|
+
still left alone, and so is a file whose strict scrub (`strict=True`, `--x-strict`) read
|
|
360
|
+
dropped a line for its width and found no row: that line is not a deletion, as 5.1 left
|
|
361
|
+
it. A scrub that finds the file changed since its read still reads
|
|
362
|
+
again and writes nothing if it keeps changing, as for a file with rows.
|
|
363
|
+
- **`get_file_obj('ab')` on a compressed store writes its trailer before the lock is let
|
|
364
|
+
go.** A program that calls it on a `.tsv.gz`, `.bz2`, `.xz` or `.zst` store and writes
|
|
365
|
+
through the file now has all of it, the gzip trailer and the bz2, xz and zstd data included,
|
|
366
|
+
in the file when `release_file_obj()` unlocks it. 5.1 and earlier wrote them after the
|
|
367
|
+
unlock, where another writer could append in between and corrupt the stream.
|
|
368
|
+
- **A `TSVZed` no longer lets a file grow with every session of sets and deletes.** It
|
|
369
|
+
rewrites a `.tsv`-family file at open when the file holds a key's row and the deletion
|
|
370
|
+
of it, as 3.39 did only for a key with two values, so a program that sets keys and
|
|
371
|
+
deletes them in each session (default settings, or `rewrite_interval`) keeps the file
|
|
372
|
+
the same size (205, 405, 605 bytes, ... for 20 keys a session before). Every session
|
|
373
|
+
that opens such a file now rewrites it once. With `rewrite_on_load=False` the open
|
|
374
|
+
does not, and the next exit or periodic rewrite does it (`rewrite_on_exit=True`,
|
|
375
|
+
`rewrite_interval`). What such a rewrite does, it now does at a plain open:
|
|
376
|
+
- it cuts a row wider than the header, and the cut stays
|
|
377
|
+
([limitations](docs/limitations.md#scrubs-and-rewrites-read-rows-at-one-column-width-deferred-to-the-width-design)):
|
|
378
|
+
`id\tv\na\t1\t2\nb\t1\nb\t\n` becomes `id\tv\na\t1 \n`, and
|
|
379
|
+
`id\tv\na\t\t7\nb\t1\nb\t\n` becomes `id\tv\na\t \n`, the 7 lost;
|
|
380
|
+
- it writes over the file in place and pads with spaces (`a\t1 ` above): TSVZ readers
|
|
381
|
+
strip them, tools that read the raw bytes (`awk`, `cut`) see them;
|
|
382
|
+
- on a file it cannot write (read-only), it prints `Failed to open file … Permission
|
|
383
|
+
denied` twice and leaves the file as it is, as 5.1 did for a key with two values.
|
|
384
|
+
- **`--x-header H` makes line 1 the header for every `tsvz` operation.** On a
|
|
385
|
+
`.tsv`-family file whose line 1 is H (an exact match: the same cells, each without the
|
|
386
|
+
whitespace around it, a trailing empty cell included), line 1 is the header: `keys`,
|
|
387
|
+
`len`, `read`, `get`, `has`, `pop`, `popitem` and `delete` never list, count, print,
|
|
388
|
+
pop or delete it, and `scrub` and `clear` keep it. When line 1 differs, the file has no
|
|
389
|
+
header and line 1 is a row, with 3.39's "Header mismatch" warning, for the reads and
|
|
390
|
+
the scrub alike, so a scrub keeps the rows a read shows (but for the column-width cases
|
|
391
|
+
below), and in such a file it never writes a row that is H as line 1: it puts an empty
|
|
392
|
+
line first (`a\t1\nid\tv\na\t\n` becomes `\nid\tv\n`), so the reads still take that row
|
|
393
|
+
for a row. With `--x-strict` a line 1 that is not H is an error (exit 1, nothing
|
|
394
|
+
written) for every operation, through a handler too. A missing or empty file gets H
|
|
395
|
+
first at its first write, as in 5.1. A `tsvz serve STORE --x-header H` handler applies
|
|
396
|
+
the rule to every request it answers, routed `tsvz` commands included; with
|
|
397
|
+
`--x-strict` it is strict for them as above (5.1 ignored `--x-strict` at `serve`): such
|
|
398
|
+
a handler refuses to start on a file whose line 1 is not H, its routed `.tsv` reads and
|
|
399
|
+
scrubs drop lines of another column count, and its `.tsvz` reads are strict, as for a
|
|
400
|
+
direct `--x-strict` command. The library's own header check (`header=` to
|
|
401
|
+
`readTabularFile`, `appendTabularFile`, `clearTabularFile`, `scrubTabularFile`,
|
|
402
|
+
`TSVZed`) is unchanged, 3.39's containment check. The CLI's writes check line 1 more
|
|
403
|
+
strictly than 5.1's did: 5.1's `set`, `append`, `delete` and `clear` with `--x-header`
|
|
404
|
+
silently took a line 1 that merely held H's cells (`id\tvalue` for `id\tv`) for the
|
|
405
|
+
header and fitted rows to H's width; now they warn and write the row at its own width,
|
|
406
|
+
and with `--x-strict` exit 1. A rewrite without `--x-header` (a scrub without it,
|
|
407
|
+
`scrubTabularFile`, a `TSVZed` rewrite) can still put a row equal to H on line 1 of a
|
|
408
|
+
header-less file
|
|
409
|
+
([limitations](docs/limitations.md#a-rewrite-without---x-header-can-put-a-row-equal-to-h-on-line-1)).
|
|
410
|
+
In 5.1 only `set`, `append`, `delete` and `clear` used `--x-header`: the reads and
|
|
411
|
+
`scrub` took line 1 as a row (`tsvz keys`
|
|
412
|
+
printed `id a b` for two keys, `len` said 3, `popitem first` popped the header), and
|
|
413
|
+
so did every request to a handler. Without `--x-header` nothing changed: line 1 is a
|
|
414
|
+
row, as in 5.1 and 3.39. In a `.tsvz` store `pop`, `popitem` and `setdefault` now check
|
|
415
|
+
`--x-header` as `set` does; nothing else changed there.
|
|
416
|
+
- **Known, mostly as in 5.1: a scrub, a rewrite or a read takes one column count for every row,
|
|
417
|
+
and can lose cells or keys.** A scrub given defaults narrower than the rows, a row wider
|
|
418
|
+
than the header, and a `.tsv` that begins with a bare `#_defaults_#` line (read without
|
|
419
|
+
defaults) are examples; the entry lists each, and which is new in 5.2
|
|
420
|
+
([limitations](docs/limitations.md#scrubs-and-rewrites-read-rows-at-one-column-width-deferred-to-the-width-design)).
|
|
421
|
+
A strict scrub of a file whose read dropped a line for its width and found no row leaves it,
|
|
422
|
+
so it grows again over append, delete-all and scrub cycles
|
|
423
|
+
([limitations](docs/limitations.md#a-strict-scrub-leaves-a-file-whose-read-dropped-a-line-for-its-width)).
|
|
424
|
+
Both wait for the column-width design after 5.2.
|
|
425
|
+
|
|
304
426
|
## Upgrading to 5.1 from 5.0
|
|
305
427
|
|
|
306
428
|
The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family file
|
|
@@ -357,12 +479,11 @@ The file formats are unchanged, and a 3.39 host still reads every `.tsv`-family
|
|
|
357
479
|
file's own header line.)
|
|
358
480
|
`TSVZed`'s message for a refused rewrite lists `hardMapToFile()` among the rewrites
|
|
359
481
|
a program asks for.
|
|
360
|
-
- **Known: a scrub of a `.tsv` whose rows were all deleted
|
|
361
|
-
the scrub
|
|
362
|
-
|
|
363
|
-
right, as in 3.39 and 5.0. (`tsvz scrub` of a headed file
|
|
364
|
-
row, so it
|
|
365
|
-
([limitations](docs/limitations.md#a-scrub-of-a-tsv-whose-rows-were-all-deleted-does-not-compact-it)).
|
|
482
|
+
- **Known in 5.1, fixed in 5.2: a scrub of a `.tsv` whose rows were all deleted did not
|
|
483
|
+
compact it** when the scrub was given the file's header (`scrubTabularFile(path, header=H)`)
|
|
484
|
+
or the file had no header line: it left the file as it was, deletion lines included;
|
|
485
|
+
reads were right, as in 3.39 and 5.0. (`tsvz scrub` of a headed file read the header
|
|
486
|
+
line as a row, so it did compact it.)
|
|
366
487
|
- **Known: a scrub run with other defaults than the store's program can change how
|
|
367
488
|
early rows read.** In a `.tsv` with an empty `#_defaults_#` line and rows with empty
|
|
368
489
|
cells before any defaults line, the reset line the scrub writes before the rows keeps
|
{tsvz-5.1 → tsvz-5.2}/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.2'
|
|
46
46
|
__version__ = version
|
|
47
47
|
author = 'pan@zopyr.us'
|
|
48
|
-
COMMIT_DATE = '2026-11-
|
|
48
|
+
COMMIT_DATE = '2026-11-03'
|
|
49
49
|
|
|
50
50
|
DEFAULT_DELIMITER = '\t'
|
|
51
51
|
DEFAULTS_INDICATOR_KEY = '#_defaults_#'
|
|
@@ -1345,6 +1345,8 @@ def _processLine(line,taskDic,correctColumnNum,strict = True,delimiter = ...,def
|
|
|
1345
1345
|
correctColumnNum = len(lineCache)
|
|
1346
1346
|
# if verbose:
|
|
1347
1347
|
# __teePrintOrNot(f"detected correctColumnNum: {len(lineCache)}",teeLogger=teeLogger)
|
|
1348
|
+
if isinstance(defaults, _DefaultsList) and defaults.columns is None:
|
|
1349
|
+
defaults.columns = correctColumnNum
|
|
1348
1350
|
if len(lineCache) == 1 or not any(lineCache[1:]):
|
|
1349
1351
|
if correctColumnNum == 1:
|
|
1350
1352
|
taskDic[lineCache[0]] = lineCache if not storeOffset else offset
|
|
@@ -1364,6 +1366,8 @@ def _processLine(line,taskDic,correctColumnNum,strict = True,delimiter = ...,def
|
|
|
1364
1366
|
return correctColumnNum , []
|
|
1365
1367
|
elif len(lineCache) != correctColumnNum:
|
|
1366
1368
|
if strict and not any(defaults[1:]):
|
|
1369
|
+
if isinstance(defaults, _DefaultsList):
|
|
1370
|
+
defaults.dropped = True
|
|
1367
1371
|
if reporter is not None:
|
|
1368
1372
|
reporter.note('strict-drop', None, 'dropped a line whose column count is not {} (strict mode)'.format(correctColumnNum))
|
|
1369
1373
|
return correctColumnNum , []
|
|
@@ -1605,6 +1609,21 @@ def _headerMatches(header, line, delimiter):
|
|
|
1605
1609
|
return _headerCellsMatch(header, _formatHeader(line, delimiter=get_delimiter(delimiter)))
|
|
1606
1610
|
|
|
1607
1611
|
|
|
1612
|
+
def _isHeaderLine(header, line, delimiter):
|
|
1613
|
+
"""Whether ``line`` is the header ``header`` to the CLI's ``--x-header`` rule: the same number of cells, each equal
|
|
1614
|
+
once the whitespace around it (and the line's end, and a UTF-8 byte order mark before it, as Excel writes one) is
|
|
1615
|
+
taken off. Stricter than 3.39's ``_lineContainHeader``."""
|
|
1616
|
+
cells = line.rstrip('\r\n').lstrip('\ufeff').split(delimiter)
|
|
1617
|
+
return len(cells) == len(header) and all(a.strip() == b.strip() for a, b in zip(cells, header))
|
|
1618
|
+
|
|
1619
|
+
|
|
1620
|
+
def _headerMismatch(header, line, teeLogger, strict, delimiter):
|
|
1621
|
+
"""Print 3.39's "Header mismatch" (as ``_lineContainHeader`` prints it) for ``line``; raise as it does when strict."""
|
|
1622
|
+
__teePrintOrNot(f"Header mismatch: \n{_formatHeader(line, delimiter=delimiter)} \n!= \n{header}",teeLogger=teeLogger)
|
|
1623
|
+
if strict:
|
|
1624
|
+
raise ValueError("Data format error! Header mismatch")
|
|
1625
|
+
|
|
1626
|
+
|
|
1608
1627
|
def _lineContainHeader(header,line,verbose = False,teeLogger = None,strict = False,delimiter = ...):
|
|
1609
1628
|
"""
|
|
1610
1629
|
Verify if a line contains the header.
|
|
@@ -2063,7 +2082,10 @@ def _legacyKeptHeader(fileName, header, verifyHeader, verbose, teeLogger, encodi
|
|
|
2063
2082
|
``verifyHeader`` (the header given for a file that holds nothing), else ``header``; [] when it has no cell: the file
|
|
2064
2083
|
is then left with no header line."""
|
|
2065
2084
|
if any(header) and verifyHeader:
|
|
2066
|
-
|
|
2085
|
+
try:
|
|
2086
|
+
line = _legacyFirstLine(fileName, encoding, teeLogger)
|
|
2087
|
+
except Exception: # a first line the codec cannot decode (gzip: BadGzipFile, zlib.error; bz2, xz, zstd: their own errors): none to keep
|
|
2088
|
+
line = '' # (a scrub or clear makes the file again; an append, which calls _legacyFirstLine itself, raises)
|
|
2067
2089
|
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
2090
|
if warn and not _lineContainHeader(header,line,verbose = verbose,teeLogger = teeLogger,strict = strict,delimiter = delimiter):
|
|
2069
2091
|
__teePrintOrNot(f'Warning: Header mismatch in {fileName}. Keeping original header in file...','warning',teeLogger)
|
|
@@ -2287,6 +2309,8 @@ class _DefaultsList(list):
|
|
|
2287
2309
|
replaces its cells in place), not the caller's defaults it began with."""
|
|
2288
2310
|
found = False
|
|
2289
2311
|
reset = False # an empty defaults line was read (it stays one whatever follows it)
|
|
2312
|
+
columns = None # the column count the read took (_processLine), None before it took one
|
|
2313
|
+
dropped = False # a strict read dropped a line for its width (_processLine): a read with no row may not be all deletions
|
|
2290
2314
|
line = None # the cells of that line as the file has them (``defaults`` holds them filled and fitted to the columns)
|
|
2291
2315
|
|
|
2292
2316
|
def __setitem__(self, index, value):
|
|
@@ -2295,7 +2319,8 @@ class _DefaultsList(list):
|
|
|
2295
2319
|
|
|
2296
2320
|
|
|
2297
2321
|
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'
|
|
2322
|
+
taskDic = None,encoding = 'utf8',strict = False,delimiter = ...,defaults = ...,correctColumnNum = -1,errors = 'strict',
|
|
2323
|
+
notFirst = None):
|
|
2299
2324
|
"""Compact a legacy file in place; return ``(taskDic, done)``: the rows read, and whether the file was written (or
|
|
2300
2325
|
had nothing to write). ``done`` is False when nothing was written because the file kept changing (the shape of
|
|
2301
2326
|
``_specScrub``): it is rewritten only if it is what the read saw (spec §19.8), so a row another process appends
|
|
@@ -2305,7 +2330,15 @@ def _legacyScrub(fileName,teeLogger = None,header = '',createIfNotExist = False,
|
|
|
2305
2330
|
non-empty one after the rows (those before it keep their empty cells, those after it were filled when read), so
|
|
2306
2331
|
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
2332
|
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.
|
|
2333
|
+
written. With no header the file begins with an empty line only if the first line of the file was not a row.
|
|
2334
|
+
|
|
2335
|
+
A file whose read finds no live row (all deleted) is written the same way, with no row: the header line, the defaults
|
|
2336
|
+
lines, and the empty line for a header-less file that began with one. A reset is as wide as the header then, or, with
|
|
2337
|
+
no header, as the lines the read took its column count from. A file that holds nothing (no byte, or only empty streams) is left as it is, and so is one whose
|
|
2338
|
+
strict read dropped a line for its width: that line is not a deletion, and writing the file empty would lose it.
|
|
2339
|
+
|
|
2340
|
+
``notFirst``, a header list, for a file written with no header: when its first row would be that header
|
|
2341
|
+
(``_isHeaderLine``), the file begins with an empty line instead, so a reader given that header still reads the row."""
|
|
2309
2342
|
given = list(taskDic.items()) if taskDic else []
|
|
2310
2343
|
givenDefaults = list(defaults) if defaults not in (..., None) else []
|
|
2311
2344
|
for tries in range(_SPEC_REWRITE_ATTEMPTS):
|
|
@@ -2319,8 +2352,8 @@ def _legacyScrub(fileName,teeLogger = None,header = '',createIfNotExist = False,
|
|
|
2319
2352
|
file = readTabularFile(fileName,teeLogger = teeLogger,header = header,createIfNotExist = createIfNotExist,
|
|
2320
2353
|
verifyHeader = verifyHeader,verbose = verbose,taskDic = taskDic,
|
|
2321
2354
|
encoding = encoding,strict = strict,delimiter = delimiter,defaults=readDefaults,correctColumnNum = correctColumnNum,errors = errors)
|
|
2322
|
-
if not file:
|
|
2323
|
-
return file, True
|
|
2355
|
+
if not file and (readDefaults.dropped or not os.path.exists(fileName) or _fileHoldsNothing(fileName)):
|
|
2356
|
+
return file, True # no row, and no file (not made by a scrub), no byte (or only empty streams) to compact, or a line the strict read dropped (not a deletion; it stays): as it is
|
|
2324
2357
|
dlm = get_delimiter(delimiter,file_name=fileName)
|
|
2325
2358
|
formatted = _formatHeader(header,delimiter=dlm)
|
|
2326
2359
|
if not _verifyFileExistence(fileName,createIfNotExist = True,teeLogger = teeLogger,header = formatted,encoding = encoding,strict = False,delimiter=dlm,errors = errors):
|
|
@@ -2331,6 +2364,17 @@ def _legacyScrub(fileName,teeLogger = None,header = '',createIfNotExist = False,
|
|
|
2331
2364
|
payload, count, columns = _legacyFormatRows(fileName, file, teeLogger, formatted, verifyHeader, verbose, encoding,
|
|
2332
2365
|
strict, dlm, errors, assume=_sanitize(kept,delimiter=dlm) if kept else [])
|
|
2333
2366
|
blank = not kept and _legacyBeginsBlank(fileName, encoding, teeLogger, dlm)
|
|
2367
|
+
if not kept and not blank and notFirst and count and \
|
|
2368
|
+
_isHeaderLine(notFirst, payload.split(b'\n', 1)[0].decode(encoding or 'utf8', errors='replace'), dlm):
|
|
2369
|
+
blank = True
|
|
2370
|
+
if not count:
|
|
2371
|
+
# No row to take the width from: a reader takes it from the header line, else from the first line it reads,
|
|
2372
|
+
# which a defaults line written here would be, so it is as wide as the file's lines were.
|
|
2373
|
+
columns = len(kept) or readDefaults.columns or 0
|
|
2374
|
+
last = readDefaults.line if readDefaults.found else None
|
|
2375
|
+
last = last if last and len(last) > 1 else None
|
|
2376
|
+
if not count and not kept and ((readDefaults.reset and columns > 1) or last):
|
|
2377
|
+
blank = True # a defaults line is never the first line of a file with no header: a reader given one would take it for it
|
|
2334
2378
|
if readDefaults.reset and columns > 1:
|
|
2335
2379
|
# An empty defaults line was read: no defaults reach the rows that follow it, whoever reads them (the rows
|
|
2336
2380
|
# were read with it, and any the caller of that read gave are not written). Written before the rows, so
|
|
@@ -2343,8 +2387,9 @@ def _legacyScrub(fileName,teeLogger = None,header = '',createIfNotExist = False,
|
|
|
2343
2387
|
else:
|
|
2344
2388
|
first, newline, rest = payload.partition(b'\n')
|
|
2345
2389
|
payload = first + newline + reset + rest
|
|
2346
|
-
|
|
2347
|
-
|
|
2390
|
+
if last:
|
|
2391
|
+
if not count and columns > 1:
|
|
2392
|
+
last = (list(last) + [''] * columns)[:columns] # (the same defaults to a reader, fitted to the width of the file as it fits a line)
|
|
2348
2393
|
payload += _encodeRow(_sanitize(last,delimiter=dlm),dlm,encoding,errors,what='defaults') + b'\n'
|
|
2349
2394
|
if _legacyReplace(fileName, kept, blank, encoding, dlm, errors, payload, expect=stamp) is None:
|
|
2350
2395
|
continue # another writer changed the file after the read
|
|
@@ -4299,7 +4344,7 @@ class _TSVZedLoad(OrderedDict):
|
|
|
4299
4344
|
|
|
4300
4345
|
Its rows are shaped as ``TSVZed.__setitem__`` shapes them in memory only (keys and cells right-stripped,
|
|
4301
4346
|
the column count and the defaults applied), with a column count and defaults of its own. ``dirty`` tells
|
|
4302
|
-
that the file holds a key twice.
|
|
4347
|
+
that the file holds a key twice (two values, or a row and its deletion).
|
|
4303
4348
|
"""
|
|
4304
4349
|
|
|
4305
4350
|
def __init__(self, owner, columns, defaults):
|
|
@@ -4349,6 +4394,7 @@ class _TSVZedLoad(OrderedDict):
|
|
|
4349
4394
|
def __delitem__(self, key):
|
|
4350
4395
|
key = str(key)
|
|
4351
4396
|
OrderedDict.__delitem__(self, key if OrderedDict.__contains__(self, key) else key.rstrip())
|
|
4397
|
+
self.dirty = True # the file holds the key twice: its row, and this deletion
|
|
4352
4398
|
|
|
4353
4399
|
def replay(self, rows):
|
|
4354
4400
|
"""Apply rows queued for the file, as reading them back will: a row whose values are all empty deletes."""
|
|
@@ -6106,8 +6152,8 @@ memoryOnly:{self.memoryOnly}
|
|
|
6106
6152
|
|
|
6107
6153
|
def get_file_obj(self,modes = 'ab'):
|
|
6108
6154
|
"""The store's file opened in ``modes`` and locked, with writeLock held until release_file_obj() (3.39 API; the
|
|
6109
|
-
store's own writes do not use it). 'wb' empties it once locked
|
|
6110
|
-
codec. None when it cannot be opened or locked (reported)."""
|
|
6155
|
+
store's own writes do not use it). 'wb' empties it once locked; for a compressed file 'w' and 'a' modes give
|
|
6156
|
+
a writer through its codec over the locked file (a new member at the end for 'a'). None when it cannot be opened or locked (reported)."""
|
|
6111
6157
|
self.writeLock.acquire()
|
|
6112
6158
|
file = None
|
|
6113
6159
|
try:
|
|
@@ -6116,12 +6162,21 @@ memoryOnly:{self.memoryOnly}
|
|
|
6116
6162
|
file = _openUntruncated(self._fileName)
|
|
6117
6163
|
elif modes == 'a+b':
|
|
6118
6164
|
file = open(self._fileName, 'a+b')
|
|
6165
|
+
elif 'a' in modes and '+' not in modes and _fileCodec(self._fileName):
|
|
6166
|
+
_requireCodec(_fileCodec(self._fileName))
|
|
6167
|
+
file = open(self._fileName, 'ab') # the codec writes into this locked file, a new member at its end
|
|
6119
6168
|
else:
|
|
6120
6169
|
file = openFileAsCompressed(self._fileName, mode=modes, encoding=self.encoding, teeLogger=self.teeLogger)
|
|
6121
6170
|
_lockFile(file)
|
|
6122
6171
|
if 'w' in modes:
|
|
6123
6172
|
file.truncate(0)
|
|
6124
6173
|
file = _compressedWriter(self._fileName, file) or file
|
|
6174
|
+
elif 'a' in modes and '+' not in modes and _fileCodec(self._fileName):
|
|
6175
|
+
raw = file
|
|
6176
|
+
file = _compressedWriter(self._fileName, raw) # (release_file_obj closes it, trailer and all, before the unlock)
|
|
6177
|
+
if 'b' not in modes:
|
|
6178
|
+
file = io.TextIOWrapper(file, encoding=self.encoding)
|
|
6179
|
+
file._tsvzRaw = raw
|
|
6125
6180
|
if self.verbose:
|
|
6126
6181
|
self.__teePrintOrNot(f"File {self._fileName} locked with mode {modes}")
|
|
6127
6182
|
return file
|
|
@@ -7090,12 +7145,13 @@ class _ServeStore(object):
|
|
|
7090
7145
|
takes ``self.lock``; ``log`` is the teeLogger its messages go to.
|
|
7091
7146
|
|
|
7092
7147
|
``header`` and ``createDefaults`` (default ``defaults``) are what a missing store is created with
|
|
7093
|
-
(``create``). ``
|
|
7094
|
-
|
|
7148
|
+
(``create``). ``header`` is also the CLI's ``--x-header``: a spec store's writes and ``clear`` check it as
|
|
7149
|
+
3.39 did, and a loose file takes it by one rule for every request (``_legacyHeader``). ``lazy``: the store is
|
|
7150
|
+
read at the first request that needs it, not here.
|
|
7095
7151
|
"""
|
|
7096
7152
|
|
|
7097
7153
|
def __init__(self, fileName, delimiter=..., header='', defaults=None, strict=False, teeLogger=None,
|
|
7098
|
-
verbose=False, create=False, createDefaults=None,
|
|
7154
|
+
verbose=False, create=False, createDefaults=None, lazy=False):
|
|
7099
7155
|
self.path = fileName
|
|
7100
7156
|
self.spec = _isSpecPath(fileName)
|
|
7101
7157
|
if self.spec:
|
|
@@ -7112,7 +7168,9 @@ class _ServeStore(object):
|
|
|
7112
7168
|
self.unreadable = False
|
|
7113
7169
|
self.stopped = False
|
|
7114
7170
|
self.parts = self.state = None # not read yet
|
|
7115
|
-
self.
|
|
7171
|
+
self.given = _formatHeader(header, teeLogger=teeLogger, delimiter=self.delimiter) if header else ['']
|
|
7172
|
+
self.header = self.given # a loose file's: the header in effect, as _legacyHeader last found it
|
|
7173
|
+
self.mismatch = None # the line 1 last warned of (_legacyHeader)
|
|
7116
7174
|
if create:
|
|
7117
7175
|
self._create(header, self.defaults if createDefaults is None else createDefaults)
|
|
7118
7176
|
if not lazy:
|
|
@@ -7138,12 +7196,42 @@ class _ServeStore(object):
|
|
|
7138
7196
|
if self.spec:
|
|
7139
7197
|
self._loadSpec(reporter, log)
|
|
7140
7198
|
else:
|
|
7141
|
-
self._loadLegacy(reporter)
|
|
7199
|
+
self._loadLegacy(reporter, log)
|
|
7142
7200
|
self.unreadable = reporter.incomplete()
|
|
7143
7201
|
self.stopped = reporter.has('stopped') # a stop policy ended the read (spec §4.10)
|
|
7202
|
+
except Exception:
|
|
7203
|
+
self.parts = None # (a strict header mismatch): the next request reads again, and fails again
|
|
7204
|
+
raise
|
|
7144
7205
|
finally:
|
|
7145
7206
|
reporter.flush()
|
|
7146
7207
|
|
|
7208
|
+
def _legacyHeader(self, line, log):
|
|
7209
|
+
"""The header in effect for a loose file whose first line is ``line``: the one rule of ``--x-header``.
|
|
7210
|
+
|
|
7211
|
+
``line`` is None when the file is missing, holds nothing, or its first line cannot be read. With a header
|
|
7212
|
+
given, line 1 is the header when it is that header (``_isHeaderLine``: the same cells, each without the
|
|
7213
|
+
whitespace around it), or when there is no line 1 yet (a write puts the header first). Otherwise 3.39's
|
|
7214
|
+
"Header mismatch" is printed and strict raises (nothing is written); loose, the file has no header and line 1
|
|
7215
|
+
is a row, for every request, the scrub included. Sets and returns ``self.header``."""
|
|
7216
|
+
header = self.given
|
|
7217
|
+
if any(header) and line is not None and not _isHeaderLine(header, line, self.delimiter):
|
|
7218
|
+
if line != self.mismatch or self.strict: # (said once for the line, as 3.39 says it)
|
|
7219
|
+
self.mismatch = line
|
|
7220
|
+
_headerMismatch(header, line, log if log is not None else self.teeLogger, self.strict, self.delimiter)
|
|
7221
|
+
header = ['']
|
|
7222
|
+
self.header = header
|
|
7223
|
+
return header
|
|
7224
|
+
|
|
7225
|
+
def _legacyHeaderNow(self, log):
|
|
7226
|
+
"""``_legacyHeader`` for the file as it is now (a write, ``clear`` or ``scrub`` that does not read it)."""
|
|
7227
|
+
line = None
|
|
7228
|
+
if any(self.given) and os.path.isfile(self.path):
|
|
7229
|
+
try:
|
|
7230
|
+
line = _legacyFirstLine(self.path, 'utf8', self.teeLogger) or None
|
|
7231
|
+
except Exception: # a first line the codec cannot decode: the write or scrub that reads it reports it
|
|
7232
|
+
pass
|
|
7233
|
+
return self._legacyHeader(line, log)
|
|
7234
|
+
|
|
7147
7235
|
def _noteDirMtime(self, dirMtime):
|
|
7148
7236
|
self.dirMtime = dirMtime
|
|
7149
7237
|
self.dirRacy = time.time() - dirMtime / 1e9 < _SERVE_RACY_SECONDS
|
|
@@ -7181,19 +7269,33 @@ class _ServeStore(object):
|
|
|
7181
7269
|
self.label = os.path.basename(self.parts[-1]) + ' ' if len(self.parts) > 1 else ''
|
|
7182
7270
|
self.check = info.check if info is not None else b'' # as read: the part may be rewritten since
|
|
7183
7271
|
|
|
7184
|
-
def _loadLegacy(self, reporter):
|
|
7272
|
+
def _loadLegacy(self, reporter, log):
|
|
7185
7273
|
self.parts = [self.path] if os.path.isfile(self.path) else []
|
|
7186
7274
|
self.stamps = self._stamps(self.parts)
|
|
7187
7275
|
self.data = self.mapping()
|
|
7188
7276
|
self.legacyDefaults = list(self.defaults)
|
|
7189
7277
|
self.width, self.offset, self.lineNo, self.tailSeen = -1, 0, 0, b''
|
|
7278
|
+
self.header = self.given # (no line 1: a write puts the header first)
|
|
7190
7279
|
# A compressed file with no byte, or only empty streams, is an empty store, as readTabularFile reads it.
|
|
7191
7280
|
if self.parts and not (_isCompressedFile(self.path) and _fileHoldsNothing(self.path)):
|
|
7192
7281
|
try:
|
|
7193
7282
|
with openFileAsCompressed(self.path, mode='rb', teeLogger=self.teeLogger) as f:
|
|
7283
|
+
if any(self.given):
|
|
7284
|
+
try:
|
|
7285
|
+
raw = f.readline()
|
|
7286
|
+
except Exception as e: # L5: a stream damaged inside line 1 holds nothing more to read
|
|
7287
|
+
if not _isCompressedFile(self.path):
|
|
7288
|
+
raise
|
|
7289
|
+
reporter.note('damaged', None, 'compressed stream is damaged ({}: {}); read up to the '
|
|
7290
|
+
'damage'.format(type(e).__name__, e))
|
|
7291
|
+
return
|
|
7292
|
+
if raw and any(self._legacyHeader(raw.decode('utf8', errors='replace'), log)):
|
|
7293
|
+
self.width, self.lineNo = len(self.header), 1 # the header: not a row
|
|
7294
|
+
elif raw:
|
|
7295
|
+
self._apply(raw, reporter) # line 1 is a row
|
|
7194
7296
|
self.width, self.offset, self.lineNo = _legacyReadLoop(
|
|
7195
|
-
f, self.path, self.data,
|
|
7196
|
-
'utf8', reporter)
|
|
7297
|
+
f, self.path, self.data, self.width, self.lineNo, self.strict, self.delimiter,
|
|
7298
|
+
self.legacyDefaults, False, 'utf8', reporter)
|
|
7197
7299
|
except OSError as e:
|
|
7198
7300
|
reporter.note('unreadable', None, 'could not read {} ({})'.format(self.path, e))
|
|
7199
7301
|
self.label = ''
|
|
@@ -7236,7 +7338,8 @@ class _ServeStore(object):
|
|
|
7236
7338
|
"""Replay what was appended to the active part; False when it changed some other way."""
|
|
7237
7339
|
old = self.stamps.get(path)
|
|
7238
7340
|
if (self.stopped or _parsePartName(path).codec or old is None or stamp[0] != old[0]
|
|
7239
|
-
or stamp[1] < self.offset or stamp[1] <= old[1]):
|
|
7341
|
+
or stamp[1] < self.offset or stamp[1] <= old[1] or (not self.spec and self.lineNo <= 1 and any(self.given))):
|
|
7342
|
+
# (a loose file's line 1 is read by a reload, which tells whether it is the header, also once it is whole)
|
|
7240
7343
|
return False # an append always grows the part; anything else is a rewrite (and a stopped read is read again)
|
|
7241
7344
|
try:
|
|
7242
7345
|
with open(path, 'rb') as f:
|
|
@@ -7389,6 +7492,7 @@ class _ServeStore(object):
|
|
|
7389
7492
|
if rows and any(self.header):
|
|
7390
7493
|
_specCheckHeader(self.path, self.header, self.delimiter, self.strict, self.verbose, log)
|
|
7391
7494
|
return _specFormatRecords(rows, self.delimiter).encode('utf-8')
|
|
7495
|
+
self._legacyHeaderNow(log)
|
|
7392
7496
|
# Rows for a file that holds nothing yet are fitted to the header it is about to begin with.
|
|
7393
7497
|
assume = self.header if any(self.header) and _fileHoldsNothing(self.path) else None
|
|
7394
7498
|
return _legacyFormatPayload(self.path, [cells for cells, marker in rows], log, self.header, True, self.verbose,
|
|
@@ -7503,9 +7607,12 @@ class _ServeStore(object):
|
|
|
7503
7607
|
with self.lock:
|
|
7504
7608
|
self.writer.barrier()
|
|
7505
7609
|
try:
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7610
|
+
if self.spec:
|
|
7611
|
+
return _specClearTabularFile(self.path, teeLogger=log, header=self.header, verifyHeader=self.strict,
|
|
7612
|
+
verbose=self.verbose, delimiter=self.delimiter) is not False
|
|
7613
|
+
self._legacyHeaderNow(log) # a strict mismatch is an error; else the file is left with the header given
|
|
7614
|
+
return clearTabularFile(self.path, teeLogger=log, header=self.given, verifyHeader=False,
|
|
7615
|
+
verbose=self.verbose, delimiter=self.delimiter) is not False
|
|
7509
7616
|
finally:
|
|
7510
7617
|
self.parts = None # read again at the next request
|
|
7511
7618
|
|
|
@@ -7518,8 +7625,13 @@ class _ServeStore(object):
|
|
|
7518
7625
|
if self.spec:
|
|
7519
7626
|
return _specScrub(self.path, teeLogger=log, verifyHeader=False, verbose=self.verbose,
|
|
7520
7627
|
strict=self.strict, delimiter=self.delimiter, defaults=self.defaults)[1]
|
|
7521
|
-
|
|
7522
|
-
|
|
7628
|
+
# Line 1 is the header exactly when every read takes it for one, so the scrub keeps what they read; in a
|
|
7629
|
+
# file with no header, a row that is the header given is never written as line 1 (reads would take it
|
|
7630
|
+
# for the header).
|
|
7631
|
+
header = self._legacyHeaderNow(log)
|
|
7632
|
+
return _legacyScrub(self.path, teeLogger=log, header=header, verifyHeader=True, verbose=self.verbose,
|
|
7633
|
+
strict=self.strict, delimiter=self.delimiter, defaults=self.defaults,
|
|
7634
|
+
notFirst=self.given if any(self.given) and not any(header) else None)[1]
|
|
7523
7635
|
finally:
|
|
7524
7636
|
self.parts = None # read again at the next request
|
|
7525
7637
|
|
|
@@ -7543,9 +7655,9 @@ class _ServeStore(object):
|
|
|
7543
7655
|
else:
|
|
7544
7656
|
codec = _fileCodec(self.path)
|
|
7545
7657
|
_requireCodec(codec)
|
|
7546
|
-
header = None # a file that holds nothing gets the header first (_appendOpen)
|
|
7547
|
-
if any(self.
|
|
7548
|
-
header = functools.partial(_encodeRow, _sanitize(self.
|
|
7658
|
+
header = None # a file that holds nothing gets the header given first (_appendOpen)
|
|
7659
|
+
if any(self.given):
|
|
7660
|
+
header = functools.partial(_encodeRow, _sanitize(self.given, delimiter=self.delimiter),
|
|
7549
7661
|
self.delimiter, 'utf8', 'strict', what='header')
|
|
7550
7662
|
_lockedAppend(self.path, payload, 'newline', reporter, fsync=fsync, header=header, codec=codec)
|
|
7551
7663
|
finally:
|
|
@@ -8614,7 +8726,8 @@ options:
|
|
|
8614
8726
|
-q, --quiet print errors only
|
|
8615
8727
|
-v, --verbose print more detail
|
|
8616
8728
|
-d, --delimiter D .tsv-family delimiter: tab, comma, pipe, null or one character
|
|
8617
|
-
--x-header H header
|
|
8729
|
+
--x-header H a .tsv's header: line 1 when it is H, else a row
|
|
8730
|
+
(--x-strict: an error); a new file begins with H
|
|
8618
8731
|
--x-defaults D defaults row KEY D1 D2 ... joined by the delimiter (KEY is ignored)
|
|
8619
8732
|
--x-strict, --x-force turn the 3.39 column and header checks on or off (default: off)
|
|
8620
8733
|
--x-direct work on the files even when a server runs
|
|
@@ -8814,14 +8927,17 @@ def _cliTableCells(rows):
|
|
|
8814
8927
|
return [[_CLI_CONTROL_RE.sub(escape, str(cell)) for cell in row] for row in rows]
|
|
8815
8928
|
|
|
8816
8929
|
|
|
8817
|
-
def _cliEmitRows(rows, args, stdout, spec, delimiter, headerRow=False):
|
|
8930
|
+
def _cliEmitRows(rows, args, stdout, spec, delimiter, headerRow=False, header=None):
|
|
8818
8931
|
"""Print store rows for ``read`` / ``get`` (spec §20.4): §13 records, 3.39 records or a table.
|
|
8819
8932
|
|
|
8820
8933
|
The table shows the rows only, each padded to the widest and none truncated to the
|
|
8821
8934
|
terminal's width. ``headerRow`` keeps 3.39's ``read`` table of a loose file, whose
|
|
8822
|
-
first row is the file's header line
|
|
8935
|
+
first row is the file's header line; ``header``, a loose file's header line that is not
|
|
8936
|
+
a row (``--x-header``), is shown there instead. Records never hold it.
|
|
8823
8937
|
"""
|
|
8824
8938
|
if _cliFormat(args, stdout) == 'table':
|
|
8939
|
+
if headerRow and header:
|
|
8940
|
+
rows = [header] + rows
|
|
8825
8941
|
rows = _cliTableCells(rows)
|
|
8826
8942
|
if headerRow or not rows:
|
|
8827
8943
|
_cliWrite(stdout, pretty_format_table(rows, delimiter=delimiter) + '\n')
|
|
@@ -8952,16 +9068,16 @@ def _cliRequestLine(args):
|
|
|
8952
9068
|
return '\t'.join([args.operation] + [_serveEncode(arg) for arg in args.args])
|
|
8953
9069
|
|
|
8954
9070
|
|
|
8955
|
-
def _cliEmitServed(args, delimiter, logger, stdout, response):
|
|
9071
|
+
def _cliEmitServed(args, delimiter, logger, stdout, response, header=None):
|
|
8956
9072
|
"""Print a response (``(rows, diagnostics, status, message)``) as the operation prints on the files (§20.8);
|
|
8957
|
-
return the exit status."""
|
|
9073
|
+
return the exit status. ``header``: a loose file's header line in effect (``read``'s table shows it)."""
|
|
8958
9074
|
rows, diagnostics, status, message = response
|
|
8959
9075
|
for text in diagnostics:
|
|
8960
9076
|
logger.teelog(text, 'warning')
|
|
8961
9077
|
spec = _isSpecPath(args.store)
|
|
8962
9078
|
operation = args.operation
|
|
8963
9079
|
if operation in ('read', 'get', 'keys', 'pop', 'popitem', 'setdefault'):
|
|
8964
|
-
_cliEmitRows(rows, args, stdout, spec, delimiter, headerRow=operation == 'read' and not spec)
|
|
9080
|
+
_cliEmitRows(rows, args, stdout, spec, delimiter, headerRow=operation == 'read' and not spec, header=header)
|
|
8965
9081
|
elif operation == 'len':
|
|
8966
9082
|
_cliWrite(stdout, ''.join(row[0] + '\n' for row in rows))
|
|
8967
9083
|
elif operation == 'verify':
|
|
@@ -9026,8 +9142,7 @@ def _cliEngineOp(args, delimiter, logger, stdin, stdout):
|
|
|
9026
9142
|
store = _ServeStore(args.store, delimiter=delimiter, header=args.header, defaults=args.defaults,
|
|
9027
9143
|
strict=args.strict, teeLogger=logger, verbose=args.verbose,
|
|
9028
9144
|
create=not exists and operation != 'clear', # clear creates the store itself
|
|
9029
|
-
createDefaults=None if operation == 'setdefault' else [],
|
|
9030
|
-
checkHeader=operation in ('set', 'append', 'delete', 'clear'), lazy=True) # as 3.39 did
|
|
9145
|
+
createDefaults=None if operation == 'setdefault' else [], lazy=True)
|
|
9031
9146
|
|
|
9032
9147
|
def request(line, bulk=None):
|
|
9033
9148
|
"""The response to ``line``; the engine's messages go to stderr as they come, at their own level."""
|
|
@@ -9043,7 +9158,7 @@ def _cliEngineOp(args, delimiter, logger, stdin, stdout):
|
|
|
9043
9158
|
response = request('\t'.join(written + [_cliRequestLine(args)]), bulk)
|
|
9044
9159
|
finally:
|
|
9045
9160
|
store.close()
|
|
9046
|
-
return _cliEmitServed(args, delimiter, logger, stdout, response)
|
|
9161
|
+
return _cliEmitServed(args, delimiter, logger, stdout, response, header=store.header if any(store.header) else None)
|
|
9047
9162
|
|
|
9048
9163
|
|
|
9049
9164
|
def _cliServe(args, delimiter, logger, stdin, stdout):
|
|
@@ -9073,7 +9188,7 @@ def _cliServe(args, delimiter, logger, stdin, stdout):
|
|
|
9073
9188
|
restore = _serveSignals() # before the pointer file announces this handler
|
|
9074
9189
|
try:
|
|
9075
9190
|
store = _ServeStore(args.store, delimiter=delimiter, header=args.header, createDefaults=args.defaults,
|
|
9076
|
-
teeLogger=logger, verbose=args.verbose, create=True)
|
|
9191
|
+
strict=args.strict, teeLogger=logger, verbose=args.verbose, create=True)
|
|
9077
9192
|
handle.start(store)
|
|
9078
9193
|
logger.teelog('tsvz: serving {} at {} (pid {})'.format(args.store, handle.address, os.getpid()))
|
|
9079
9194
|
handle.serve()
|
{tsvz-5.1 → tsvz-5.2}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tsvz-5.1 → tsvz-5.2}/setup.cfg
RENAMED
|
File without changes
|
{tsvz-5.1 → tsvz-5.2}/setup.py
RENAMED
|
File without changes
|