TSVZ 4.2__tar.gz → 4.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tsvz-4.2 → tsvz-4.3}/PKG-INFO +62 -11
- {tsvz-4.2 → tsvz-4.3}/README.md +61 -8
- {tsvz-4.2 → tsvz-4.3}/TSVZ.egg-info/PKG-INFO +62 -11
- {tsvz-4.2 → tsvz-4.3}/TSVZ.egg-info/requires.txt +0 -1
- {tsvz-4.2 → tsvz-4.3}/TSVZ.py +80 -14
- {tsvz-4.2 → tsvz-4.3}/setup.py +0 -3
- {tsvz-4.2 → tsvz-4.3}/TSVZ.egg-info/SOURCES.txt +0 -0
- {tsvz-4.2 → tsvz-4.3}/TSVZ.egg-info/dependency_links.txt +0 -0
- {tsvz-4.2 → tsvz-4.3}/TSVZ.egg-info/entry_points.txt +0 -0
- {tsvz-4.2 → tsvz-4.3}/TSVZ.egg-info/top_level.txt +0 -0
- {tsvz-4.2 → tsvz-4.3}/setup.cfg +0 -0
{tsvz-4.2 → tsvz-4.3}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TSVZ
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3
|
|
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/yufei-pan/TSVZ
|
|
6
6
|
Author: Yufei Pan
|
|
@@ -12,7 +12,6 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (G
|
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Requires-Python: >=3.6
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
|
-
Requires-Dist: argparse
|
|
16
15
|
Provides-Extra: completion
|
|
17
16
|
Requires-Dist: argcomplete; extra == "completion"
|
|
18
17
|
Dynamic: author
|
|
@@ -23,7 +22,6 @@ Dynamic: description-content-type
|
|
|
23
22
|
Dynamic: home-page
|
|
24
23
|
Dynamic: license
|
|
25
24
|
Dynamic: provides-extra
|
|
26
|
-
Dynamic: requires-dist
|
|
27
25
|
Dynamic: requires-python
|
|
28
26
|
Dynamic: summary
|
|
29
27
|
|
|
@@ -36,7 +34,7 @@ key-value store in a delimiter-separated text file**. The first column of each
|
|
|
36
34
|
row is the key; the file behaves like an ordered dictionary that is persisted to
|
|
37
35
|
disk as you change it.
|
|
38
36
|
|
|
39
|
-
**Version 4.
|
|
37
|
+
**Version 4.3**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
|
|
40
38
|
next to your code and `import TSVZ`, or install it.
|
|
41
39
|
|
|
42
40
|
```bash
|
|
@@ -258,16 +256,14 @@ and `-d=,`, except the ones C5 and C9 list.
|
|
|
258
256
|
|
|
259
257
|
## Spec deviations and interpretations
|
|
260
258
|
|
|
261
|
-
|
|
259
|
+
None of these fails a conformance case: `conformance/reference-baseline.json`,
|
|
260
|
+
which lists the cases this implementation does not pass, is empty (see
|
|
261
|
+
Conformance testing).
|
|
262
|
+
|
|
262
263
|
- §17.5: a numbered part opened directly is read alone, with a warning.
|
|
263
|
-
- §4.1: a leading byte order mark is tolerated (stripped, with a warning).
|
|
264
|
-
- A leading byte order mark is not part of any `#_checksum_*_#` segment.
|
|
265
|
-
- §12.4.1: boolean marker values also accept `yes/no/on/off/1/0`; TSVZ writes `true`/`false`.
|
|
266
264
|
- §14.5: the defaults-on-missing read applies to `t[key]` only.
|
|
267
265
|
- §18.6: locking is the 3.39 exclusive `lockf` per write plus an in-process lock; readers do not lock.
|
|
268
266
|
- §19.4: a scrub whose rows would read differently under the final markers writes them with stripping and fill-empty off, then restores the final markers after the rows.
|
|
269
|
-
- Invalid UTF-8 is replaced with U+FFFD (and reported).
|
|
270
|
-
- v1 has no `<CR>` token, so a value whose last cell ends in `\r` loses it; values set through `TSVZed` are right-stripped, so this only affects rows passed raw to the append helpers.
|
|
271
267
|
- §20.7: `-q` also silences informational messages ("Created …"); only errors remain.
|
|
272
268
|
- §20.2: `scrub` of a multi-part store, and `clear` of one part of one, are refused with exit 1; `read`, `get` and `parts` of a numbered part read that part alone, with a warning.
|
|
273
269
|
- §20.2.4: on `.tsv`-family files `get` prints nothing for a missing key, `verify` checks nothing, and `parts` lists the file.
|
|
@@ -276,7 +272,6 @@ and `-d=,`, except the ones C5 and C9 list.
|
|
|
276
272
|
- §21: a served `.tsv` file with a header line shows that line as a row, as `tsvz read` does (`TSVZed` hides it when given the header).
|
|
277
273
|
- §21.8: the server notices changes with `stat()`. It re-reads the part list while the store's directory changed less than 2 s ago, and reloads a part whose size stayed the same but whose inode or modification time changed. A rewrite in place that keeps a part's size and modification time is not seen until the part changes again.
|
|
278
274
|
- §21.9: `tsvz` commands and `TSVZClient` ask the server to answer writes once they are written (`--x-written`), so a write the server cannot make fails as it would on the files. While writes fail, every answer carries a warning.
|
|
279
|
-
- §17.7: the server appends to the store's current part; it does not start a new part when it starts.
|
|
280
275
|
- §21.3: `tsvz serve` needs Unix-domain sockets. Where Python has none (Windows), it exits 1, and `tsvz` commands and `TSVZClient` work on the files.
|
|
281
276
|
- §20.7: on `.tsv`-family files a `-d` longer than one character is used, as in 3.39, with a warning; a `-d` that does not decode is a usage error.
|
|
282
277
|
|
|
@@ -293,11 +288,67 @@ python3 website/tsvz_site.py build /tmp/site # static files for any host
|
|
|
293
288
|
|
|
294
289
|
`deploy/tsvz-site.service` runs it under systemd; restart it to publish an edit.
|
|
295
290
|
|
|
291
|
+
## Conformance testing
|
|
292
|
+
|
|
293
|
+
`tsvzconform.py` tests how closely an implementation of tsvz-spec-v1, in any
|
|
294
|
+
language, follows the spec. Its cases are JSON files in `conformance/cases/`,
|
|
295
|
+
each naming the § clauses it tests, and it drives an implementation only
|
|
296
|
+
through the spec's command line (§20) and handler socket (§21).
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
python3 tsvzconform.py --impl ./tsvz --feature crc32,sha256,handler
|
|
300
|
+
python3 tsvzconform.py --impl ./tsvz --class reader --only 13
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Each failure is printed with its input, what was expected, what came back and
|
|
304
|
+
a command line that reruns it alone; the summary gives a verdict for each
|
|
305
|
+
conformance class (reader, writer, cli, handler). `conformance/README.md`
|
|
306
|
+
explains the options and the features to claim, and how to load the cases into
|
|
307
|
+
another language's tests.
|
|
308
|
+
|
|
309
|
+
## Benchmarking implementations
|
|
310
|
+
|
|
311
|
+
`tsvzbench.py` compares implementations of tsvz-spec-v1, in any language, side
|
|
312
|
+
by side on one machine. It drives each one only through the spec's command
|
|
313
|
+
line (§20) and, when it has one, its handler socket (§21), so any conformant
|
|
314
|
+
`tsvz` tool can be measured as it is.
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
python3 tsvzbench.py --impl ref='python3 TSVZ.py' --impl new='../tsvz-rs/target/release/tsvz' \
|
|
318
|
+
--suite quick --work-dir /mnt/fast-disk/tsvzbench
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
- `--impl NAME=COMMAND`, repeatable: the first is the baseline, and every other
|
|
322
|
+
implementation gets a `×` ratio against it (below 1 is faster). A relative
|
|
323
|
+
COMMAND runs from the directory you start tsvzbench in.
|
|
324
|
+
- `--suite smoke|quick|standard|large`: `smoke` checks a setup in seconds,
|
|
325
|
+
`quick` takes about a minute, `standard` (the default, up to 1M rows) about
|
|
326
|
+
five, and `large` (up to 10M rows) twenty or more and ~5 GB of memory for the
|
|
327
|
+
harness itself.
|
|
328
|
+
- `--only cold,bulk,perop,maint` picks families: one-shot replays, `set -` and
|
|
329
|
+
`delete -` batches, single operations (CLI and handler), and scrub/clear.
|
|
330
|
+
- `--work-dir DIR` keeps the generated fixtures between runs. Put it on the disk
|
|
331
|
+
you mean to measure: not a tmpfs, and not a FUSE file system such as mergerfs
|
|
332
|
+
(the harness warns about those). The default is a `tsvzbench-*` directory here.
|
|
333
|
+
- `--json PATH` also writes every raw sample.
|
|
334
|
+
|
|
335
|
+
The harness writes its own input stores, so every implementation reads the same
|
|
336
|
+
bytes, and checks every result against its own replay model: `✓` right, `✗`
|
|
337
|
+
wrong output or files, `! N` an unexpected exit status, `⏱` timed out, `refused`
|
|
338
|
+
an allowed refusal. It exits 1 when any cell failed. Each command runs under a
|
|
339
|
+
small launcher that measures it with `wait4`; a peak RSS shown as `≤8.0 MiB` is
|
|
340
|
+
the launcher's own size, so the true peak is no higher. Compare an
|
|
341
|
+
implementation with itself to see the noise: a few percent per cell, but the
|
|
342
|
+
socket rows, whose latencies are tens of microseconds, can move 10–20% between
|
|
343
|
+
runs, because a single run can land in a slower mode as a whole.
|
|
344
|
+
|
|
296
345
|
## Tests
|
|
297
346
|
|
|
298
347
|
```bash
|
|
299
348
|
python3 -m pytest TSVZ_test.py -q # everything, incl. 3.39 differential tests
|
|
300
349
|
python3 -m pytest website/tsvz_site_test.py -q # the tsvz.org site
|
|
350
|
+
python3 -m pytest tsvzbench_test.py -q # the benchmark harness
|
|
351
|
+
python3 -m pytest tsvzconform_test.py -q # the conformance runner and cases, against the reference
|
|
301
352
|
python3 benchTSVZ.py /tmp/b.tsv -n 100000 --compare-old # benchmark (not a test)
|
|
302
353
|
```
|
|
303
354
|
|
{tsvz-4.2 → tsvz-4.3}/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 4.
|
|
10
|
+
**Version 4.3**, 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
|
|
@@ -229,16 +229,14 @@ and `-d=,`, except the ones C5 and C9 list.
|
|
|
229
229
|
|
|
230
230
|
## Spec deviations and interpretations
|
|
231
231
|
|
|
232
|
-
|
|
232
|
+
None of these fails a conformance case: `conformance/reference-baseline.json`,
|
|
233
|
+
which lists the cases this implementation does not pass, is empty (see
|
|
234
|
+
Conformance testing).
|
|
235
|
+
|
|
233
236
|
- §17.5: a numbered part opened directly is read alone, with a warning.
|
|
234
|
-
- §4.1: a leading byte order mark is tolerated (stripped, with a warning).
|
|
235
|
-
- A leading byte order mark is not part of any `#_checksum_*_#` segment.
|
|
236
|
-
- §12.4.1: boolean marker values also accept `yes/no/on/off/1/0`; TSVZ writes `true`/`false`.
|
|
237
237
|
- §14.5: the defaults-on-missing read applies to `t[key]` only.
|
|
238
238
|
- §18.6: locking is the 3.39 exclusive `lockf` per write plus an in-process lock; readers do not lock.
|
|
239
239
|
- §19.4: a scrub whose rows would read differently under the final markers writes them with stripping and fill-empty off, then restores the final markers after the rows.
|
|
240
|
-
- Invalid UTF-8 is replaced with U+FFFD (and reported).
|
|
241
|
-
- v1 has no `<CR>` token, so a value whose last cell ends in `\r` loses it; values set through `TSVZed` are right-stripped, so this only affects rows passed raw to the append helpers.
|
|
242
240
|
- §20.7: `-q` also silences informational messages ("Created …"); only errors remain.
|
|
243
241
|
- §20.2: `scrub` of a multi-part store, and `clear` of one part of one, are refused with exit 1; `read`, `get` and `parts` of a numbered part read that part alone, with a warning.
|
|
244
242
|
- §20.2.4: on `.tsv`-family files `get` prints nothing for a missing key, `verify` checks nothing, and `parts` lists the file.
|
|
@@ -247,7 +245,6 @@ and `-d=,`, except the ones C5 and C9 list.
|
|
|
247
245
|
- §21: a served `.tsv` file with a header line shows that line as a row, as `tsvz read` does (`TSVZed` hides it when given the header).
|
|
248
246
|
- §21.8: the server notices changes with `stat()`. It re-reads the part list while the store's directory changed less than 2 s ago, and reloads a part whose size stayed the same but whose inode or modification time changed. A rewrite in place that keeps a part's size and modification time is not seen until the part changes again.
|
|
249
247
|
- §21.9: `tsvz` commands and `TSVZClient` ask the server to answer writes once they are written (`--x-written`), so a write the server cannot make fails as it would on the files. While writes fail, every answer carries a warning.
|
|
250
|
-
- §17.7: the server appends to the store's current part; it does not start a new part when it starts.
|
|
251
248
|
- §21.3: `tsvz serve` needs Unix-domain sockets. Where Python has none (Windows), it exits 1, and `tsvz` commands and `TSVZClient` work on the files.
|
|
252
249
|
- §20.7: on `.tsv`-family files a `-d` longer than one character is used, as in 3.39, with a warning; a `-d` that does not decode is a usage error.
|
|
253
250
|
|
|
@@ -264,11 +261,67 @@ python3 website/tsvz_site.py build /tmp/site # static files for any host
|
|
|
264
261
|
|
|
265
262
|
`deploy/tsvz-site.service` runs it under systemd; restart it to publish an edit.
|
|
266
263
|
|
|
264
|
+
## Conformance testing
|
|
265
|
+
|
|
266
|
+
`tsvzconform.py` tests how closely an implementation of tsvz-spec-v1, in any
|
|
267
|
+
language, follows the spec. Its cases are JSON files in `conformance/cases/`,
|
|
268
|
+
each naming the § clauses it tests, and it drives an implementation only
|
|
269
|
+
through the spec's command line (§20) and handler socket (§21).
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
python3 tsvzconform.py --impl ./tsvz --feature crc32,sha256,handler
|
|
273
|
+
python3 tsvzconform.py --impl ./tsvz --class reader --only 13
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Each failure is printed with its input, what was expected, what came back and
|
|
277
|
+
a command line that reruns it alone; the summary gives a verdict for each
|
|
278
|
+
conformance class (reader, writer, cli, handler). `conformance/README.md`
|
|
279
|
+
explains the options and the features to claim, and how to load the cases into
|
|
280
|
+
another language's tests.
|
|
281
|
+
|
|
282
|
+
## Benchmarking implementations
|
|
283
|
+
|
|
284
|
+
`tsvzbench.py` compares implementations of tsvz-spec-v1, in any language, side
|
|
285
|
+
by side on one machine. It drives each one only through the spec's command
|
|
286
|
+
line (§20) and, when it has one, its handler socket (§21), so any conformant
|
|
287
|
+
`tsvz` tool can be measured as it is.
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
python3 tsvzbench.py --impl ref='python3 TSVZ.py' --impl new='../tsvz-rs/target/release/tsvz' \
|
|
291
|
+
--suite quick --work-dir /mnt/fast-disk/tsvzbench
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
- `--impl NAME=COMMAND`, repeatable: the first is the baseline, and every other
|
|
295
|
+
implementation gets a `×` ratio against it (below 1 is faster). A relative
|
|
296
|
+
COMMAND runs from the directory you start tsvzbench in.
|
|
297
|
+
- `--suite smoke|quick|standard|large`: `smoke` checks a setup in seconds,
|
|
298
|
+
`quick` takes about a minute, `standard` (the default, up to 1M rows) about
|
|
299
|
+
five, and `large` (up to 10M rows) twenty or more and ~5 GB of memory for the
|
|
300
|
+
harness itself.
|
|
301
|
+
- `--only cold,bulk,perop,maint` picks families: one-shot replays, `set -` and
|
|
302
|
+
`delete -` batches, single operations (CLI and handler), and scrub/clear.
|
|
303
|
+
- `--work-dir DIR` keeps the generated fixtures between runs. Put it on the disk
|
|
304
|
+
you mean to measure: not a tmpfs, and not a FUSE file system such as mergerfs
|
|
305
|
+
(the harness warns about those). The default is a `tsvzbench-*` directory here.
|
|
306
|
+
- `--json PATH` also writes every raw sample.
|
|
307
|
+
|
|
308
|
+
The harness writes its own input stores, so every implementation reads the same
|
|
309
|
+
bytes, and checks every result against its own replay model: `✓` right, `✗`
|
|
310
|
+
wrong output or files, `! N` an unexpected exit status, `⏱` timed out, `refused`
|
|
311
|
+
an allowed refusal. It exits 1 when any cell failed. Each command runs under a
|
|
312
|
+
small launcher that measures it with `wait4`; a peak RSS shown as `≤8.0 MiB` is
|
|
313
|
+
the launcher's own size, so the true peak is no higher. Compare an
|
|
314
|
+
implementation with itself to see the noise: a few percent per cell, but the
|
|
315
|
+
socket rows, whose latencies are tens of microseconds, can move 10–20% between
|
|
316
|
+
runs, because a single run can land in a slower mode as a whole.
|
|
317
|
+
|
|
267
318
|
## Tests
|
|
268
319
|
|
|
269
320
|
```bash
|
|
270
321
|
python3 -m pytest TSVZ_test.py -q # everything, incl. 3.39 differential tests
|
|
271
322
|
python3 -m pytest website/tsvz_site_test.py -q # the tsvz.org site
|
|
323
|
+
python3 -m pytest tsvzbench_test.py -q # the benchmark harness
|
|
324
|
+
python3 -m pytest tsvzconform_test.py -q # the conformance runner and cases, against the reference
|
|
272
325
|
python3 benchTSVZ.py /tmp/b.tsv -n 100000 --compare-old # benchmark (not a test)
|
|
273
326
|
```
|
|
274
327
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TSVZ
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3
|
|
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/yufei-pan/TSVZ
|
|
6
6
|
Author: Yufei Pan
|
|
@@ -12,7 +12,6 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (G
|
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Requires-Python: >=3.6
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
|
-
Requires-Dist: argparse
|
|
16
15
|
Provides-Extra: completion
|
|
17
16
|
Requires-Dist: argcomplete; extra == "completion"
|
|
18
17
|
Dynamic: author
|
|
@@ -23,7 +22,6 @@ Dynamic: description-content-type
|
|
|
23
22
|
Dynamic: home-page
|
|
24
23
|
Dynamic: license
|
|
25
24
|
Dynamic: provides-extra
|
|
26
|
-
Dynamic: requires-dist
|
|
27
25
|
Dynamic: requires-python
|
|
28
26
|
Dynamic: summary
|
|
29
27
|
|
|
@@ -36,7 +34,7 @@ key-value store in a delimiter-separated text file**. The first column of each
|
|
|
36
34
|
row is the key; the file behaves like an ordered dictionary that is persisted to
|
|
37
35
|
disk as you change it.
|
|
38
36
|
|
|
39
|
-
**Version 4.
|
|
37
|
+
**Version 4.3**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
|
|
40
38
|
next to your code and `import TSVZ`, or install it.
|
|
41
39
|
|
|
42
40
|
```bash
|
|
@@ -258,16 +256,14 @@ and `-d=,`, except the ones C5 and C9 list.
|
|
|
258
256
|
|
|
259
257
|
## Spec deviations and interpretations
|
|
260
258
|
|
|
261
|
-
|
|
259
|
+
None of these fails a conformance case: `conformance/reference-baseline.json`,
|
|
260
|
+
which lists the cases this implementation does not pass, is empty (see
|
|
261
|
+
Conformance testing).
|
|
262
|
+
|
|
262
263
|
- §17.5: a numbered part opened directly is read alone, with a warning.
|
|
263
|
-
- §4.1: a leading byte order mark is tolerated (stripped, with a warning).
|
|
264
|
-
- A leading byte order mark is not part of any `#_checksum_*_#` segment.
|
|
265
|
-
- §12.4.1: boolean marker values also accept `yes/no/on/off/1/0`; TSVZ writes `true`/`false`.
|
|
266
264
|
- §14.5: the defaults-on-missing read applies to `t[key]` only.
|
|
267
265
|
- §18.6: locking is the 3.39 exclusive `lockf` per write plus an in-process lock; readers do not lock.
|
|
268
266
|
- §19.4: a scrub whose rows would read differently under the final markers writes them with stripping and fill-empty off, then restores the final markers after the rows.
|
|
269
|
-
- Invalid UTF-8 is replaced with U+FFFD (and reported).
|
|
270
|
-
- v1 has no `<CR>` token, so a value whose last cell ends in `\r` loses it; values set through `TSVZed` are right-stripped, so this only affects rows passed raw to the append helpers.
|
|
271
267
|
- §20.7: `-q` also silences informational messages ("Created …"); only errors remain.
|
|
272
268
|
- §20.2: `scrub` of a multi-part store, and `clear` of one part of one, are refused with exit 1; `read`, `get` and `parts` of a numbered part read that part alone, with a warning.
|
|
273
269
|
- §20.2.4: on `.tsv`-family files `get` prints nothing for a missing key, `verify` checks nothing, and `parts` lists the file.
|
|
@@ -276,7 +272,6 @@ and `-d=,`, except the ones C5 and C9 list.
|
|
|
276
272
|
- §21: a served `.tsv` file with a header line shows that line as a row, as `tsvz read` does (`TSVZed` hides it when given the header).
|
|
277
273
|
- §21.8: the server notices changes with `stat()`. It re-reads the part list while the store's directory changed less than 2 s ago, and reloads a part whose size stayed the same but whose inode or modification time changed. A rewrite in place that keeps a part's size and modification time is not seen until the part changes again.
|
|
278
274
|
- §21.9: `tsvz` commands and `TSVZClient` ask the server to answer writes once they are written (`--x-written`), so a write the server cannot make fails as it would on the files. While writes fail, every answer carries a warning.
|
|
279
|
-
- §17.7: the server appends to the store's current part; it does not start a new part when it starts.
|
|
280
275
|
- §21.3: `tsvz serve` needs Unix-domain sockets. Where Python has none (Windows), it exits 1, and `tsvz` commands and `TSVZClient` work on the files.
|
|
281
276
|
- §20.7: on `.tsv`-family files a `-d` longer than one character is used, as in 3.39, with a warning; a `-d` that does not decode is a usage error.
|
|
282
277
|
|
|
@@ -293,11 +288,67 @@ python3 website/tsvz_site.py build /tmp/site # static files for any host
|
|
|
293
288
|
|
|
294
289
|
`deploy/tsvz-site.service` runs it under systemd; restart it to publish an edit.
|
|
295
290
|
|
|
291
|
+
## Conformance testing
|
|
292
|
+
|
|
293
|
+
`tsvzconform.py` tests how closely an implementation of tsvz-spec-v1, in any
|
|
294
|
+
language, follows the spec. Its cases are JSON files in `conformance/cases/`,
|
|
295
|
+
each naming the § clauses it tests, and it drives an implementation only
|
|
296
|
+
through the spec's command line (§20) and handler socket (§21).
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
python3 tsvzconform.py --impl ./tsvz --feature crc32,sha256,handler
|
|
300
|
+
python3 tsvzconform.py --impl ./tsvz --class reader --only 13
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Each failure is printed with its input, what was expected, what came back and
|
|
304
|
+
a command line that reruns it alone; the summary gives a verdict for each
|
|
305
|
+
conformance class (reader, writer, cli, handler). `conformance/README.md`
|
|
306
|
+
explains the options and the features to claim, and how to load the cases into
|
|
307
|
+
another language's tests.
|
|
308
|
+
|
|
309
|
+
## Benchmarking implementations
|
|
310
|
+
|
|
311
|
+
`tsvzbench.py` compares implementations of tsvz-spec-v1, in any language, side
|
|
312
|
+
by side on one machine. It drives each one only through the spec's command
|
|
313
|
+
line (§20) and, when it has one, its handler socket (§21), so any conformant
|
|
314
|
+
`tsvz` tool can be measured as it is.
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
python3 tsvzbench.py --impl ref='python3 TSVZ.py' --impl new='../tsvz-rs/target/release/tsvz' \
|
|
318
|
+
--suite quick --work-dir /mnt/fast-disk/tsvzbench
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
- `--impl NAME=COMMAND`, repeatable: the first is the baseline, and every other
|
|
322
|
+
implementation gets a `×` ratio against it (below 1 is faster). A relative
|
|
323
|
+
COMMAND runs from the directory you start tsvzbench in.
|
|
324
|
+
- `--suite smoke|quick|standard|large`: `smoke` checks a setup in seconds,
|
|
325
|
+
`quick` takes about a minute, `standard` (the default, up to 1M rows) about
|
|
326
|
+
five, and `large` (up to 10M rows) twenty or more and ~5 GB of memory for the
|
|
327
|
+
harness itself.
|
|
328
|
+
- `--only cold,bulk,perop,maint` picks families: one-shot replays, `set -` and
|
|
329
|
+
`delete -` batches, single operations (CLI and handler), and scrub/clear.
|
|
330
|
+
- `--work-dir DIR` keeps the generated fixtures between runs. Put it on the disk
|
|
331
|
+
you mean to measure: not a tmpfs, and not a FUSE file system such as mergerfs
|
|
332
|
+
(the harness warns about those). The default is a `tsvzbench-*` directory here.
|
|
333
|
+
- `--json PATH` also writes every raw sample.
|
|
334
|
+
|
|
335
|
+
The harness writes its own input stores, so every implementation reads the same
|
|
336
|
+
bytes, and checks every result against its own replay model: `✓` right, `✗`
|
|
337
|
+
wrong output or files, `! N` an unexpected exit status, `⏱` timed out, `refused`
|
|
338
|
+
an allowed refusal. It exits 1 when any cell failed. Each command runs under a
|
|
339
|
+
small launcher that measures it with `wait4`; a peak RSS shown as `≤8.0 MiB` is
|
|
340
|
+
the launcher's own size, so the true peak is no higher. Compare an
|
|
341
|
+
implementation with itself to see the noise: a few percent per cell, but the
|
|
342
|
+
socket rows, whose latencies are tens of microseconds, can move 10–20% between
|
|
343
|
+
runs, because a single run can land in a slower mode as a whole.
|
|
344
|
+
|
|
296
345
|
## Tests
|
|
297
346
|
|
|
298
347
|
```bash
|
|
299
348
|
python3 -m pytest TSVZ_test.py -q # everything, incl. 3.39 differential tests
|
|
300
349
|
python3 -m pytest website/tsvz_site_test.py -q # the tsvz.org site
|
|
350
|
+
python3 -m pytest tsvzbench_test.py -q # the benchmark harness
|
|
351
|
+
python3 -m pytest tsvzconform_test.py -q # the conformance runner and cases, against the reference
|
|
301
352
|
python3 benchTSVZ.py /tmp/b.tsv -n 100000 --compare-old # benchmark (not a test)
|
|
302
353
|
```
|
|
303
354
|
|
{tsvz-4.2 → tsvz-4.3}/TSVZ.py
RENAMED
|
@@ -40,10 +40,10 @@ if os.name == 'nt':
|
|
|
40
40
|
elif os.name == 'posix':
|
|
41
41
|
import fcntl
|
|
42
42
|
|
|
43
|
-
version = '4.
|
|
43
|
+
version = '4.3'
|
|
44
44
|
__version__ = version
|
|
45
45
|
author = 'pan@zopyr.us'
|
|
46
|
-
COMMIT_DATE = '2026-10-
|
|
46
|
+
COMMIT_DATE = '2026-10-11'
|
|
47
47
|
|
|
48
48
|
DEFAULT_DELIMITER = '\t'
|
|
49
49
|
DEFAULTS_INDICATOR_KEY = '#_defaults_#'
|
|
@@ -121,6 +121,11 @@ def eprint(*args, **kwargs):
|
|
|
121
121
|
|
|
122
122
|
def openFileAsCompressed(fileName,mode = 'rb',encoding = 'utf8',teeLogger = None,compressLevel = 1):
|
|
123
123
|
lowerFileName = fileName.lower()
|
|
124
|
+
if any(c in mode for c in 'wax+'):
|
|
125
|
+
# Writing plain bytes into a compressed file destroys it: refuse when this Python lacks the codec.
|
|
126
|
+
suffix = lowerFileName.rpartition('.')[2]
|
|
127
|
+
if suffix in COMPRESSED_FILE_EXTENSIONS:
|
|
128
|
+
_requireCodec(suffix)
|
|
124
129
|
if 'b' not in mode:
|
|
125
130
|
mode += 't'
|
|
126
131
|
kwargs = {}
|
|
@@ -683,6 +688,29 @@ def _lockedAppend(path, payload, tailPolicy, reporter, fsync=False):
|
|
|
683
688
|
_unlockFile(f)
|
|
684
689
|
|
|
685
690
|
|
|
691
|
+
#: Compression suffix -> codec name (spec §16.2), as ``parts`` and error messages say it.
|
|
692
|
+
_CODEC_NAMES = {'gz': 'gzip', 'gzip': 'gzip', 'bz2': 'bzip2', 'bzip2': 'bzip2', 'xz': 'xz', 'lzma': 'xz', 'zst': 'zstd', 'zstd': 'zstd'}
|
|
693
|
+
_CODEC_SUPPORT = {}
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _codecSupported(codec):
|
|
697
|
+
"""True when this Python has ``codec`` (zstd needs Python 3.14; a minimal build may lack lzma or bz2). Checked once per codec."""
|
|
698
|
+
name = _CODEC_NAMES.get(codec, codec)
|
|
699
|
+
if name not in _CODEC_SUPPORT:
|
|
700
|
+
try:
|
|
701
|
+
_newDecompressor(codec)
|
|
702
|
+
_CODEC_SUPPORT[name] = True
|
|
703
|
+
except ImportError:
|
|
704
|
+
_CODEC_SUPPORT[name] = False
|
|
705
|
+
return _CODEC_SUPPORT[name]
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def _requireCodec(codec):
|
|
709
|
+
"""Raise ImportError before any write to a part in a codec this Python lacks (spec §16.5): nothing is written."""
|
|
710
|
+
if codec and not _codecSupported(codec):
|
|
711
|
+
raise ImportError('no {} support in this Python'.format(_CODEC_NAMES.get(codec, codec)))
|
|
712
|
+
|
|
713
|
+
|
|
686
714
|
def _compressBytes(codec, data, level=1):
|
|
687
715
|
"""Compress ``data`` as one complete member / stream for ``codec``."""
|
|
688
716
|
if codec in ('gz', 'gzip'):
|
|
@@ -694,6 +722,7 @@ def _compressBytes(codec, data, level=1):
|
|
|
694
722
|
if codec in ('xz', 'lzma'):
|
|
695
723
|
import lzma
|
|
696
724
|
return lzma.compress(data, preset=level)
|
|
725
|
+
_requireCodec(codec)
|
|
697
726
|
from compression import zstd # Python 3.14+
|
|
698
727
|
return zstd.compress(data, level=level)
|
|
699
728
|
|
|
@@ -1488,8 +1517,8 @@ MAX_SPEC_VERSION = 1
|
|
|
1488
1517
|
_MARKER_RE = re.compile(r'^#_[A-Za-z0-9_-]+_#$')
|
|
1489
1518
|
_CHECKSUM_RE = re.compile(r'^#_checksum_([A-Za-z0-9_-]+)_#$', re.IGNORECASE)
|
|
1490
1519
|
_ASCII_DIGITS_RE = re.compile(r'^[0-9]+$')
|
|
1491
|
-
_SPEC_DECODE_RE = re.compile(r'<(?:sep|LF|lt|#)>')
|
|
1492
|
-
_SPEC_TOKENS = {'<LF>': '\n', '<lt>': '<', '<#>': '#'}
|
|
1520
|
+
_SPEC_DECODE_RE = re.compile(r'<(?:sep|LF|CR|lt|#)>')
|
|
1521
|
+
_SPEC_TOKENS = {'<LF>': '\n', '<CR>': '\r', '<lt>': '<', '<#>': '#'}
|
|
1493
1522
|
_RowState = namedtuple('_RowState', 'defaults strip fillEmpty')
|
|
1494
1523
|
_BOOL_VALUES = {'true': True, 'yes': True, 'on': True, '1': True,
|
|
1495
1524
|
'false': False, 'no': False, 'off': False, '0': False}
|
|
@@ -1518,6 +1547,8 @@ def _specEncodeField(value, delimiter, isKey=False):
|
|
|
1518
1547
|
value = value.replace(delimiter, '<sep>')
|
|
1519
1548
|
if '\n' in value:
|
|
1520
1549
|
value = value.replace('\n', '<LF>')
|
|
1550
|
+
if '\r' in value:
|
|
1551
|
+
value = value.replace('\r', '<CR>')
|
|
1521
1552
|
if isKey and value.startswith('#'):
|
|
1522
1553
|
value = '<#>' + value[1:]
|
|
1523
1554
|
return value
|
|
@@ -1563,7 +1594,7 @@ class _SpecState(object):
|
|
|
1563
1594
|
|
|
1564
1595
|
def _specApplyMarker(state, keyLower, values, reporter, where):
|
|
1565
1596
|
"""Apply an official marker (spec §12.4). Returns False when its value was rejected."""
|
|
1566
|
-
value = values[0]
|
|
1597
|
+
value = values[0] if values else '' # marker lines are not stripped (spec §10.4)
|
|
1567
1598
|
if keyLower == DEFAULTS_INDICATOR_KEY:
|
|
1568
1599
|
state.defaults = [DEFAULTS_INDICATOR_KEY] + list(values)
|
|
1569
1600
|
state.sawDefaults = True
|
|
@@ -1741,11 +1772,7 @@ def _decompressChunks(f, info):
|
|
|
1741
1772
|
(``'corrupt (...)'``). ``info.goodRawEnd`` is the raw offset after the last
|
|
1742
1773
|
complete member.
|
|
1743
1774
|
"""
|
|
1744
|
-
|
|
1745
|
-
decompressor = _newDecompressor(info.codec)
|
|
1746
|
-
except ImportError as e:
|
|
1747
|
-
info.damaged = 'cannot decompress ({})'.format(e)
|
|
1748
|
-
return
|
|
1775
|
+
decompressor = _newDecompressor(info.codec)
|
|
1749
1776
|
isGzip = info.codec in ('gz', 'gzip')
|
|
1750
1777
|
rawPos = 0
|
|
1751
1778
|
inMember = False
|
|
@@ -1792,6 +1819,11 @@ def _iterPartLines(info, reporter):
|
|
|
1792
1819
|
except OSError as e:
|
|
1793
1820
|
reporter.note('unreadable', None, 'skipped unreadable part {} ({})'.format(info.path, e))
|
|
1794
1821
|
return
|
|
1822
|
+
if info.codec and not _codecSupported(info.codec):
|
|
1823
|
+
# Spec §16.5: a codec this Python lacks makes the part unreadable; it is not damage to repair.
|
|
1824
|
+
f.close()
|
|
1825
|
+
reporter.note('unreadable', None, 'cannot read part {}: no {} support in this Python'.format(info.path, _CODEC_NAMES.get(info.codec, info.codec)))
|
|
1826
|
+
return
|
|
1795
1827
|
rest = b''
|
|
1796
1828
|
with f:
|
|
1797
1829
|
chunks = _decompressChunks(f, info) if info.codec else iter(lambda: f.read(_CHUNK), b'')
|
|
@@ -1842,7 +1874,7 @@ def _specChecksum(state, algo, text, delimiter, reporter, where, position=None):
|
|
|
1842
1874
|
"""
|
|
1843
1875
|
if algo in state.digests:
|
|
1844
1876
|
fields = text.split(delimiter)
|
|
1845
|
-
expected = fields[1].
|
|
1877
|
+
expected = fields[1].lower() if len(fields) > 1 else '' # not stripped (spec §10.4)
|
|
1846
1878
|
if expected:
|
|
1847
1879
|
actual = state.digests[algo].hexdigest()
|
|
1848
1880
|
if actual != expected:
|
|
@@ -2109,6 +2141,13 @@ def _specEnsureStore(fileName, createIfNotExist, header, defaults, strict, teeLo
|
|
|
2109
2141
|
lines.append(_specFormatRecord(defaults, delimiter, marker=True))
|
|
2110
2142
|
content = ''.join(line + '\n' for line in lines).encode('utf-8')
|
|
2111
2143
|
codec = _parsePartName(fileName).codec
|
|
2144
|
+
try:
|
|
2145
|
+
_requireCodec(codec) # not even an empty file in a codec this Python lacks
|
|
2146
|
+
except ImportError as e:
|
|
2147
|
+
__teePrintOrNot('Failed to create {}: {}'.format(fileName, e),'error',teeLogger=teeLogger)
|
|
2148
|
+
if strict:
|
|
2149
|
+
raise
|
|
2150
|
+
return False
|
|
2112
2151
|
try:
|
|
2113
2152
|
with open(fileName, 'xb') as f:
|
|
2114
2153
|
f.write(_compressBytes(codec, content) if codec and content else content)
|
|
@@ -2163,6 +2202,7 @@ def _specRepairCompressed(path, reporter):
|
|
|
2163
2202
|
intact.
|
|
2164
2203
|
"""
|
|
2165
2204
|
info = _PartInfo(path)
|
|
2205
|
+
_requireCodec(info.codec) # a part this Python cannot decode is not damaged; it is left alone
|
|
2166
2206
|
with _pathLock(path):
|
|
2167
2207
|
with open(path, 'r+b') as f:
|
|
2168
2208
|
_lockFile(f)
|
|
@@ -2208,6 +2248,7 @@ def _specAppendPayload(path, payload, reporter, repair=False, fsync=False):
|
|
|
2208
2248
|
if not codec:
|
|
2209
2249
|
_lockedAppend(path, payload, 'truncate', reporter, fsync=fsync)
|
|
2210
2250
|
return
|
|
2251
|
+
_requireCodec(codec)
|
|
2211
2252
|
if repair and os.path.exists(path):
|
|
2212
2253
|
_specRepairCompressed(path, reporter)
|
|
2213
2254
|
with _pathLock(path):
|
|
@@ -2252,6 +2293,7 @@ def _specRewriteInPlace(path, content, expect=None):
|
|
|
2252
2293
|
writer committed in between, so nothing is written and None is returned.
|
|
2253
2294
|
"""
|
|
2254
2295
|
codec = _parsePartName(path).codec
|
|
2296
|
+
_requireCodec(codec)
|
|
2255
2297
|
raw = _compressBytes(codec, content) if codec and content else content
|
|
2256
2298
|
with _pathLock(path):
|
|
2257
2299
|
with open(path, 'r+b') as f:
|
|
@@ -2398,6 +2440,11 @@ def _specClearTabularFile(fileName, teeLogger=None, header='', verifyHeader=Fals
|
|
|
2398
2440
|
parts, stamp = _specSnapshot(fileName)
|
|
2399
2441
|
load = _specLoad(fileName, delimiter, header=header, verifyHeader=False, strict=False,
|
|
2400
2442
|
reporter=attempt, teeLogger=teeLogger, verbose=verbose)
|
|
2443
|
+
if attempt.has('unreadable'):
|
|
2444
|
+
if _storeParts(fileName)[0] != parts:
|
|
2445
|
+
continue # a part went away during the read (a rotation): read again
|
|
2446
|
+
attempt.note('refused', None, 'clear skipped: a part of the store could not be read; nothing was written')
|
|
2447
|
+
return False
|
|
2401
2448
|
if len(load.parts) > 1:
|
|
2402
2449
|
tombstones = ''.join(_specFormatRecord([key], delimiter) + '\n' for key in load.data)
|
|
2403
2450
|
if tombstones:
|
|
@@ -2513,6 +2560,11 @@ def _specScrub(fileName, teeLogger=None, header='', createIfNotExist=False, veri
|
|
|
2513
2560
|
load = _specLoad(fileName, delimiter, header=header, verifyHeader=verifyHeader, strict=strict,
|
|
2514
2561
|
defaults=initial, correctColumnNum=correctColumnNum, taskDic=taskDic,
|
|
2515
2562
|
reporter=attempt, teeLogger=teeLogger, verbose=verbose)
|
|
2563
|
+
if attempt.has('unreadable'):
|
|
2564
|
+
if _storeParts(fileName)[0] != parts:
|
|
2565
|
+
continue # a part went away during the read (a rotation): read again
|
|
2566
|
+
attempt.note('refused', None, 'scrub skipped: a part of the store could not be read; nothing was written')
|
|
2567
|
+
return taskDic, False
|
|
2516
2568
|
if len(load.parts) != 1 or name.ordinal is not None or name.rotated:
|
|
2517
2569
|
attempt.note('multipart', None, 'scrub skipped: TSVZ does not compact multi-part stores or single parts of them; nothing was written')
|
|
2518
2570
|
return taskDic, False
|
|
@@ -2826,6 +2878,7 @@ class TSVZed(OrderedDict):
|
|
|
2826
2878
|
load = _specLoad(self._fileName, self.delimiter, header=self.header, verifyHeader=self.verifyHeader,
|
|
2827
2879
|
strict=self.strict, defaults=self._constructorDefaults, taskDic=OrderedDict(),
|
|
2828
2880
|
reporter=reporter, teeLogger=self.teeLogger, verbose=self.verbose)
|
|
2881
|
+
unreadable = reporter.has('unreadable')
|
|
2829
2882
|
finally:
|
|
2830
2883
|
reporter.flush()
|
|
2831
2884
|
OrderedDict.clear(self)
|
|
@@ -2849,7 +2902,7 @@ class TSVZed(OrderedDict):
|
|
|
2849
2902
|
self.lastUpdateTime = self.externalFileUpdateTime
|
|
2850
2903
|
self._dirMtimeNs = _dirMtimeNs(self._fileName)
|
|
2851
2904
|
if (load is not None and not load.state.sawDefaults and any(self._constructorDefaults[1:])
|
|
2852
|
-
and not self.memoryOnly):
|
|
2905
|
+
and not self.memoryOnly and not unreadable): # the store's own defaults may be in the unread part
|
|
2853
2906
|
# Design §5.3: persist constructor defaults by appending, as 3.39 did by rewriting.
|
|
2854
2907
|
self.appendQueue.append(_specFormatRecord(self._constructorDefaults, self.delimiter, marker=True))
|
|
2855
2908
|
self._specState.sawDefaults = True
|
|
@@ -3865,6 +3918,7 @@ class TSVZedLite(MutableMapping):
|
|
|
3865
3918
|
strict=self.strict, defaults=self._constructorDefaults,
|
|
3866
3919
|
correctColumnNum=self.correctColumnNum, storeOffset=True, taskDic=target,
|
|
3867
3920
|
reporter=reporter, verbose=self.verbose)
|
|
3921
|
+
unreadable = reporter.has('unreadable')
|
|
3868
3922
|
finally:
|
|
3869
3923
|
reporter.flush()
|
|
3870
3924
|
self._specState = load.state
|
|
@@ -3881,7 +3935,7 @@ class TSVZedLite(MutableMapping):
|
|
|
3881
3935
|
held = sum(1 for value in target.values() if isinstance(value, list))
|
|
3882
3936
|
if held:
|
|
3883
3937
|
_warnOnce(self, 'held', self._fileName, f'{held} keys from older or compressed parts are held in memory (offsets cannot address them)')
|
|
3884
|
-
if not load.state.sawDefaults and any(self._constructorDefaults[1:]):
|
|
3938
|
+
if not load.state.sawDefaults and any(self._constructorDefaults[1:]) and not unreadable:
|
|
3885
3939
|
self._pendingDefaults = True
|
|
3886
3940
|
return self
|
|
3887
3941
|
|
|
@@ -4474,7 +4528,7 @@ def _partRows(store, reporter):
|
|
|
4474
4528
|
if name.codec:
|
|
4475
4529
|
base = base[:-len(name.codec) - 1]
|
|
4476
4530
|
ordinal = base.rpartition('.')[2]
|
|
4477
|
-
flags = (['active'] if path == active else []) + ([name.codec] if name.codec else [])
|
|
4531
|
+
flags = (['active'] if path == active else []) + ([_CODEC_NAMES.get(name.codec, name.codec)] if name.codec else [])
|
|
4478
4532
|
rows.append([str(index), ordinal, path, ','.join(flags)])
|
|
4479
4533
|
return rows
|
|
4480
4534
|
|
|
@@ -4890,6 +4944,8 @@ class _ServeStore(object):
|
|
|
4890
4944
|
"""Atomically return the row of ``key`` and append its tombstone; None when it is missing."""
|
|
4891
4945
|
with self.lock:
|
|
4892
4946
|
self._fresh(log)
|
|
4947
|
+
if self.unreadable:
|
|
4948
|
+
return _SERVE_PARTIAL
|
|
4893
4949
|
key = self.resolve(key)
|
|
4894
4950
|
row = self.data.get(key)
|
|
4895
4951
|
if row is None:
|
|
@@ -4903,6 +4959,8 @@ class _ServeStore(object):
|
|
|
4903
4959
|
"""Atomically return the row of the last (or first) live key and delete it; None when empty."""
|
|
4904
4960
|
with self.lock:
|
|
4905
4961
|
self._fresh(log)
|
|
4962
|
+
if self.unreadable:
|
|
4963
|
+
return _SERVE_PARTIAL
|
|
4906
4964
|
if not self.data:
|
|
4907
4965
|
return None
|
|
4908
4966
|
key = next(reversed(self.data)) if last else next(iter(self.data))
|
|
@@ -4915,6 +4973,8 @@ class _ServeStore(object):
|
|
|
4915
4973
|
"""Atomically return the row of ``cells[0]``, appending ``cells`` first when the key is missing."""
|
|
4916
4974
|
with self.lock:
|
|
4917
4975
|
self._fresh(log)
|
|
4976
|
+
if self.unreadable:
|
|
4977
|
+
return _SERVE_PARTIAL
|
|
4918
4978
|
key = self.resolve(cells[0])
|
|
4919
4979
|
row = self.data.get(key)
|
|
4920
4980
|
if row is not None:
|
|
@@ -4999,6 +5059,8 @@ _SERVE_ARITY = {
|
|
|
4999
5059
|
'delete': (1, None), 'has': (1, 1), 'pop': (1, 1), 'popitem': (0, 1), 'setdefault': (2, None),
|
|
5000
5060
|
}
|
|
5001
5061
|
_SERVE_UNREADABLE = 'could not read every part of the store'
|
|
5062
|
+
#: What pop, popitem and setdefault return instead of acting on a store they could not read whole.
|
|
5063
|
+
_SERVE_PARTIAL = object()
|
|
5002
5064
|
#: Operations that fail on a missing store (spec §20.2.2).
|
|
5003
5065
|
_SERVE_NEED_STORE = ('read', 'get', 'has', 'len', 'keys', 'pop', 'popitem', 'scrub', 'verify', 'parts')
|
|
5004
5066
|
#: A pointer file larger than this is not one (spec §21.2).
|
|
@@ -5122,11 +5184,15 @@ def _serveDispatch(store, line, bulk, log):
|
|
|
5122
5184
|
if args and args[0] not in ('first', 'last'):
|
|
5123
5185
|
raise _ServeUsage('popitem takes first or last')
|
|
5124
5186
|
row = store.popitem(not args or args[0] == 'last', sync, log)
|
|
5187
|
+
if row is _SERVE_PARTIAL:
|
|
5188
|
+
return 1, [], _SERVE_UNREADABLE
|
|
5125
5189
|
return (3, [], '') if row is None else (0, [_specFormatRecord(row, '\t')], '')
|
|
5126
5190
|
if operation == 'setdefault':
|
|
5127
5191
|
if _MARKER_RE.match(raw[0]):
|
|
5128
5192
|
raise _ServeUsage('setdefault takes a data KEY, not a marker')
|
|
5129
5193
|
row = store.setdefault(args, sync, log)
|
|
5194
|
+
if row is _SERVE_PARTIAL:
|
|
5195
|
+
return 1, [], _SERVE_UNREADABLE
|
|
5130
5196
|
return (3, [], '') if row is None else (0, [_specFormatRecord(row, '\t')], '')
|
|
5131
5197
|
if operation in ('clear', 'scrub'):
|
|
5132
5198
|
done = store.clear(log) if operation == 'clear' else store.scrub(log)
|
{tsvz-4.2 → tsvz-4.3}/setup.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tsvz-4.2 → tsvz-4.3}/setup.cfg
RENAMED
|
File without changes
|