TSVZ 3.38__tar.gz → 4.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-4.2/PKG-INFO ADDED
@@ -0,0 +1,309 @@
1
+ Metadata-Version: 2.4
2
+ Name: TSVZ
3
+ Version: 4.2
4
+ Summary: Key-value store kept in a TSV / CSV / NSV / PSV file, with tsvz-spec-v1 support
5
+ Home-page: https://github.com/yufei-pan/TSVZ
6
+ Author: Yufei Pan
7
+ Author-email: pan@zopyr.us
8
+ License: GPLv3+
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.6
11
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.6
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: argparse
16
+ Provides-Extra: completion
17
+ Requires-Dist: argcomplete; extra == "completion"
18
+ Dynamic: author
19
+ Dynamic: author-email
20
+ Dynamic: classifier
21
+ Dynamic: description
22
+ Dynamic: description-content-type
23
+ Dynamic: home-page
24
+ Dynamic: license
25
+ Dynamic: provides-extra
26
+ Dynamic: requires-dist
27
+ Dynamic: requires-python
28
+ Dynamic: summary
29
+
30
+ # TSVZ
31
+
32
+ **[tsvz.org](https://tsvz.org)**: the format, its specification and its implementations.
33
+
34
+ TSVZ is a small, dependency-free library and CLI that keeps an **ordered
35
+ key-value store in a delimiter-separated text file**. The first column of each
36
+ row is the key; the file behaves like an ordered dictionary that is persisted to
37
+ disk as you change it.
38
+
39
+ **Version 4.2**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
40
+ next to your code and `import TSVZ`, or install it.
41
+
42
+ ```bash
43
+ pip install tsvz # from PyPI
44
+ pip install -e . # editable, from this directory
45
+ ```
46
+
47
+ ## Two file dialects, one API
48
+
49
+ | Extension | Rules |
50
+ |---|---|
51
+ | `.tsv` `.csv` `.nsv` `.psv` (and any other) | The TSVZ **3.39** format, unchanged. A 3.39 host can read every file 4.x writes. |
52
+ | `.tsvz` `.csvz` `.nsvz` `.psvz` | The **[tsvz-spec-v1](tsvz-spec-v1.md)** append-only key-value log. |
53
+
54
+ The delimiter follows the extension: tab, comma, NUL, pipe. A trailing
55
+ compression suffix (`.gz`, `.bz2`, `.xz`, `.zst`) is allowed on both; `.zst`
56
+ needs Python 3.14.
57
+
58
+ `TSVZed`, `TSVZedLite`, `readTabularFile`, `appendTabularFile`,
59
+ `appendLinesTabularFile`, `clearTabularFile`, `scrubTabularFile` (and the
60
+ `*TSV` aliases) work on both dialects. `t.dialect` tells you which one a store
61
+ uses.
62
+
63
+ ## Quick start
64
+
65
+ ```python
66
+ import TSVZ
67
+
68
+ t = TSVZ.TSVZed('people.tsvz', header='id\tname\tscore')
69
+ t['alice'] = ['alice', 'Alice', '10']
70
+ t['bob'] = 'bob\tBob\t20'
71
+ t['alice'] = ['alice', 'Alice', '11'] # last write wins
72
+ del t['bob'] # appends a tombstone
73
+ print(t['alice']) # ['alice', 'Alice', '11']
74
+ t.close()
75
+ ```
76
+
77
+ ```bash
78
+ tsvz set people.tsvz carol Carol 5 # add or update a row (append is the same)
79
+ tsvz get people.tsvz carol # one row; exit 3 if carol is missing
80
+ tsvz read people.tsvz # every row: a table on a terminal, records in a pipe
81
+ tsvz delete people.tsvz carol # tombstone
82
+ tsvz read people.tsvz | tsvz set copy.tsvz - # copy every row
83
+ tsvz clear people.tsvz # empty it, keeping the header and markers
84
+ tsvz scrub people.tsvz # compact it (for .tsvz: archival maintenance)
85
+ tsvz verify people.tsvz # check #_checksum_*_# segments; exit 4 on a mismatch
86
+ tsvz parts events.tsvz # list the parts of a multi-part store
87
+ tsvz has people.tsvz carol # exit 0 if carol is live, 3 if not
88
+ tsvz pop people.tsvz carol # print carol's row and delete it
89
+ tsvz serve people.tsvz & # keep the store loaded; tsvz commands then use it
90
+ tsvz stop people.tsvz
91
+ tsvz people.tsvz append carol Carol 5 # the 3.39 form still works
92
+ tsvz -V
93
+ ```
94
+
95
+ ## What TSVZ 4 does with `.tsvz` files
96
+
97
+ - **Reads** everything a conformant writer can produce: markers
98
+ (`#_defaults_#`, `#_strip_trailing_whites_#`, `#_fill_empty_with_default_#`,
99
+ `#_return_defaults_when_missing_#`, ...), escapes (`<sep>`, `<LF>`, `<lt>`,
100
+ `<#>`), tombstones, `#_checksum_<algo>_#` segments (crc32 and every
101
+ fixed-length hashlib algorithm), compressed parts, and **multi-part stores**
102
+ (`store.tsvz.<hex>`, part 0 first, `.rotated` parts skipped).
103
+ - **Writes** one file: the named file, or the highest-numbered part of a
104
+ multi-part store. Rows are written as given (no padding); a lone key is a
105
+ tombstone; a row of empty cells (`k\t\t`) is a live row.
106
+ - **Compaction is manual.** `scrubTabularFile` / `tsvz scrub f.tsvz` rewrites a
107
+ single-part store in place (same inode). If another writer commits while it
108
+ runs, it reads the store again; after three tries it writes nothing, with a
109
+ warning (`clearTabularFile` does the same). The automatic rewrite options of
110
+ 3.39 (`rewrite_on_load`, `rewrite_on_exit`, `rewrite_interval`, `rewrite()`,
111
+ `mapToFile()`, `hardMapToFile()`) are ignored for `.tsvz`, with a warning.
112
+ - **Missing keys:** `t[key]` returns the defaults row while
113
+ `#_return_defaults_when_missing_#` is true (its default); `in`, `get`,
114
+ `setdefault` and `pop` behave like a dict.
115
+ - **`#` keys** are stored (`<#>key`). Keys of the form `#_name_#` are marker
116
+ writes: `t['#_defaults_#'] = [...]` sets the defaults, and so does
117
+ `setDefaults([...])` once the object is constructed.
118
+
119
+ ## Command line
120
+
121
+ `tsvz` follows [tsvz-spec-v1 §20](tsvz-spec-v1.md#20-command-line-interface):
122
+ `tsvz [OPTION ...] OPERATION STORE [ARG ...]`, with options anywhere before a
123
+ lone `--`. The 3.39 form `tsvz STORE [OPERATION] [ARG ...]` works too; a store
124
+ named like an operation then needs a path prefix (`./read`).
125
+
126
+ - **stdout carries only rows.** It gets a table on a terminal and records
127
+ otherwise; `--format table|records` overrides. Records use the store's
128
+ delimiter and escaping, so `tsvz read A | tsvz set B -` copies a store of
129
+ the same variant.
130
+ - **stderr carries every message.** `-q` prints errors only; `-v` adds detail.
131
+ - **Exit status:**
132
+ - 0: done.
133
+ - 1: failed or refused. Also a missing store for `read`, `get`, `scrub`, `verify` and `parts`,
134
+ a store that `set`, `append` or `delete` could not create, and a `.tsvz` part that could not
135
+ be read (the rows of the other parts are still printed).
136
+ - 2: usage error.
137
+ - 3: `get` found a missing key.
138
+ - 4: `verify` found a checksum mismatch.
139
+ - **Arguments are literal.** `tsvz set s.tsvz k 'a<b'` stores `a<b`. Values may
140
+ begin with `-` (`-5`); after `--` every argument is positional. `set KEY`
141
+ alone deletes KEY, as in 3.39. `set KEY ''` stores an empty cell in a `.tsvz`
142
+ store; in a `.tsv` file it deletes KEY, as in 3.39.
143
+ - **Bulk input.** `set STORE -` and `delete STORE -` read records or keys from
144
+ stdin and append them in one write. An unterminated last line is ignored,
145
+ with a warning.
146
+ - **TSVZ extensions:**
147
+ - `--x-header H` and `--x-defaults KEY<d>D1<d>...` decode backslash escapes such as `\t`;
148
+ - `--x-strict` / `--x-force` turn 3.39's column and header checks on or off.
149
+
150
+ ## Serving a store
151
+
152
+ `tsvz serve STORE` keeps a store loaded in one process and answers requests on a
153
+ local socket ([tsvz-spec-v1 §21](tsvz-spec-v1.md#21-write-handler-protocol)).
154
+ `tsvz` commands and `TSVZ.TSVZClient` find it through the pointer file
155
+ `STORE.serve` and use it instead of re-reading the store. Their output and exit
156
+ status stay the same.
157
+
158
+ - **One appender.** The server batches the writes it receives into single appends.
159
+ Other writers may still append to the files; the server reads what they add
160
+ before it answers.
161
+ - **Acknowledgement.** A write is acknowledged once it is queued (`#_write_ack_#`
162
+ `memory`, the default), or after `fsync` (`#_write_ack_#` `disk`, or
163
+ `TSVZClient(..., sync=True)`). Reads always see acknowledged writes.
164
+ - **Access.** Only the user who started the server can connect (socket mode 0600).
165
+ `--x-group G` or `--x-mode 0660` opens it on purpose. The server listens on a
166
+ Unix-domain socket only; reaching it from other hosts is left to a separate
167
+ adapter (for example TCP with TLS) that would relay to the socket.
168
+ - **Trust.** Clients use `STORE.serve` only when it is a regular file owned by
169
+ them or by the store's owner, not writable by anyone else, naming a socket that
170
+ belongs to that owner. Otherwise they warn and use the
171
+ files. `serve` never writes through a symbolic link or into a file it did not
172
+ create.
173
+ - **Lifetime.** It runs in the foreground until `tsvz stop STORE`, SIGINT or
174
+ SIGTERM, or `--x-idle-timeout S` seconds without connections. It then writes
175
+ and `fsync`s everything and removes `STORE.serve`.
176
+ - **Bypassing it.** `--x-direct` makes a `tsvz` command use the files. Command
177
+ lines with `--x-header`, `--x-defaults` or `--x-strict`, or another delimiter
178
+ than the server's, use the files too.
179
+
180
+ ```python
181
+ import TSVZ
182
+
183
+ with TSVZ.TSVZClient('people.tsvz') as people: # served, or in this process when no server runs
184
+ people['dave'] = ['dave', 'Dave', '41']
185
+ print(people['dave'], len(people), 'alice' in people)
186
+ print(people.pop('dave'))
187
+ ```
188
+
189
+ `TSVZClient` has `TSVZed`'s semantics for reading and setting keys, `del`, `in`,
190
+ `len`, iteration, `pop`, `popitem`, `setdefault`, `update`, `clear` and
191
+ `setDefaults`. Its writes return once they are written (raising `OSError` if they
192
+ cannot be), one client may be shared between threads, and without a server it
193
+ warns once and works on the files in-process, finishing its writes when the
194
+ program exits.
195
+
196
+ ## Fault tolerance
197
+
198
+ TSVZ keeps your data and keeps going when a file is not what it expects, and
199
+ says so on stderr (or through `teeLogger`) with one summarised line per problem:
200
+
201
+ ```
202
+ TSVZ warning: data.tsvz: invalid UTF-8 replaced with U+FFFD (3 occurrences, first at line 17)
203
+ ```
204
+
205
+ Examples: bytes after the last newline (ignored on read; truncated before the
206
+ next append, and quoted), invalid UTF-8, a byte order mark, a newer
207
+ `#_version_#`, a marker with a bad value, a checksum mismatch, a damaged
208
+ compressed part (read up to the damage; repaired in place before the next
209
+ append, with the original saved as `<part>.damaged-<time>`), an unreadable
210
+ part, and a delimiter or encoding argument that conflicts with the extension.
211
+
212
+ ## Breaking changes from 3.39
213
+
214
+ ### `.tsv`, `.csv`, `.nsv`, `.psv`: bug fixes only
215
+
216
+ | # | Change |
217
+ |---|---|
218
+ | L1 | Appending after an unterminated last line writes `\n` first; 3.39 merged the two lines. |
219
+ | L2 | Setting a row whose values are all empty deletes it in memory too (it already meant delete on disk), with a warning. |
220
+ | L3 | `TSVZedLite` on a compressed file keeps rows in memory; 3.39 read compressed bytes as text. Rows held in memory (also `#` keys) are returned directly. |
221
+ | L4 | New warnings on stderr (invalid encoding, lines dropped by `strict`); data unchanged. |
222
+ | L5 | A damaged compressed file is read up to the damage instead of raising. |
223
+ | L6 | `mapToFile` no longer glues a row it adds at the end of the file to the next one. |
224
+
225
+ ### `.tsvz`, `.csvz`, `.nsvz`, `.psvz`: now tsvz-spec-v1
226
+
227
+ | # | Change |
228
+ |---|---|
229
+ | S1 | `.csvz/.nsvz/.psvz` use `, \0 \|` (3.39: tab). A conflicting `delimiter=` or `encoding=` is overridden, with a warning. |
230
+ | S2 | A file written by 3.39 under one of these names reads differently (header line becomes data, `key\t\t` deletes come back, `</sep/>` stays literal, an unterminated last line is dropped, single-column files read as empty, empty cells are not defaulted, NULs are not stripped). **Rename such a file to `.tsv`.** |
231
+ | S3 | `t[missing]` returns the defaults row instead of raising `KeyError` (see above). |
232
+ | S4 | `#` keys are stored; `#_name_#` keys are marker writes. |
233
+ | S5 | Automatic rewrites are ignored with a warning; `rewrite_on_load` effectively defaults to `False`; `move_to_end` only reorders memory. |
234
+ | S6 | Rows are never trimmed or dropped for their width; `strict` no longer drops rows; `appendLinesTabularFile` does not pad. |
235
+ | S7 | The header is written as a `#` comment; a new file without a header is empty. |
236
+ | S8 | An unterminated tail is ignored on read and truncated on append. |
237
+ | S9 | `clear` keeps the active markers; scrub keeps the header comment and drops other comments, custom markers and checksums. |
238
+ | S10 | Files named `x.tsvz.<hex>` beside `x.tsvz` are read as parts of the store. |
239
+ | S11 | `lastLineOnly` reads forward (slower on large files). |
240
+
241
+ ### The `tsvz` command
242
+
243
+ 3.39 command lines still parse, including abbreviated long options (`--delim`)
244
+ and `-d=,`, except the ones C5 and C9 list.
245
+
246
+ | # | Change |
247
+ |---|---|
248
+ | C1 | `read` into a pipe or a file prints records instead of the table; `--format table` gives the table. |
249
+ | C2 | Every message goes to stderr: "Created …", "File not found", header mismatches, warnings. |
250
+ | C3 | `read` and `scrub` of a missing store exit 1 with a message (3.39: exit 0, and `read` printed "File not found" on stdout). |
251
+ | C4 | `delete` deletes every KEY it is given (3.39: only the first). |
252
+ | C5 | `append` or `delete` without a KEY, or with an empty KEY, is a usage error with exit 2 (3.39 wrote an empty line). |
253
+ | C6 | A `clear` or `scrub` that writes nothing (one part of a multi-part store, a store that kept changing) exits 1. |
254
+ | C7 | `-c/--header`, `--defaults`, `-s/--strict` and `-f/--force` are undocumented aliases of the `--x-` options. `--x-header` and `--x-defaults` keep non-ASCII text. |
255
+ | C8 | Usage errors print tsvz's own message, exit 2; the `-h` text is new. |
256
+ | C9 | Arguments after STORE for `read`, `clear` and `scrub` are a usage error, exit 2 (3.39 ignored them, so `clear junk` cleared the file). |
257
+ | C10 | The table (a terminal, or `--format table`) of `get`, `keys`, `pop`, `popitem`, `setdefault`, and of `read` on a `.tsvz`-family store, shows the rows only: no row is drawn as a header, and no row is cut to the first row's or the terminal's width. `read` of a `.tsv`-family file keeps 3.39's table, whose first row is the file's header line. |
258
+
259
+ ## Spec deviations and interpretations
260
+
261
+ - §19.2.3c: scrub keeps the header comment (3.39 header verification needs it).
262
+ - §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
+ - §14.5: the defaults-on-missing read applies to `t[key]` only.
267
+ - §18.6: locking is the 3.39 exclusive `lockf` per write plus an in-process lock; readers do not lock.
268
+ - §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
+ - §20.7: `-q` also silences informational messages ("Created …"); only errors remain.
272
+ - §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
+ - §20.2.4: on `.tsv`-family files `get` prints nothing for a missing key, `verify` checks nothing, and `parts` lists the file.
274
+ - §20.3: a bulk input line with an empty key is skipped, with a warning.
275
+ - §20.6: a write whose store cannot be created, and a `read`, `get` or `verify` that cannot read every part, exit 1 with an error that `-q` keeps.
276
+ - §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
+ - §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
+ - §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
+ - §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
+ - §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
+
283
+ ## Website
284
+
285
+ `website/` holds the tsvz.org site. `website/tsvz_site.py` (standard library
286
+ only, Python 3.8+) renders `website/index.md`, `website/spec-glance.md` and
287
+ `tsvz-spec-v1.md`, and serves HTML to browsers and markdown to agents.
288
+
289
+ ```bash
290
+ python3 website/tsvz_site.py serve -p 8765 # http://127.0.0.1:8765/
291
+ python3 website/tsvz_site.py build /tmp/site # static files for any host
292
+ ```
293
+
294
+ `deploy/tsvz-site.service` runs it under systemd; restart it to publish an edit.
295
+
296
+ ## Tests
297
+
298
+ ```bash
299
+ python3 -m pytest TSVZ_test.py -q # everything, incl. 3.39 differential tests
300
+ python3 -m pytest website/tsvz_site_test.py -q # the tsvz.org site
301
+ python3 benchTSVZ.py /tmp/b.tsv -n 100000 --compare-old # benchmark (not a test)
302
+ ```
303
+
304
+ `TSVZ_old.py` is the frozen 3.39 module the differential tests compare against;
305
+ it is not installed.
306
+
307
+ ## License
308
+
309
+ GPL-3.0-or-later © Yufei Pan (pan@zopyr.us)
tsvz-4.2/README.md ADDED
@@ -0,0 +1,280 @@
1
+ # TSVZ
2
+
3
+ **[tsvz.org](https://tsvz.org)**: the format, its specification and its implementations.
4
+
5
+ TSVZ is a small, dependency-free library and CLI that keeps an **ordered
6
+ key-value store in a delimiter-separated text file**. The first column of each
7
+ row is the key; the file behaves like an ordered dictionary that is persisted to
8
+ disk as you change it.
9
+
10
+ **Version 4.2**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
11
+ next to your code and `import TSVZ`, or install it.
12
+
13
+ ```bash
14
+ pip install tsvz # from PyPI
15
+ pip install -e . # editable, from this directory
16
+ ```
17
+
18
+ ## Two file dialects, one API
19
+
20
+ | Extension | Rules |
21
+ |---|---|
22
+ | `.tsv` `.csv` `.nsv` `.psv` (and any other) | The TSVZ **3.39** format, unchanged. A 3.39 host can read every file 4.x writes. |
23
+ | `.tsvz` `.csvz` `.nsvz` `.psvz` | The **[tsvz-spec-v1](tsvz-spec-v1.md)** append-only key-value log. |
24
+
25
+ The delimiter follows the extension: tab, comma, NUL, pipe. A trailing
26
+ compression suffix (`.gz`, `.bz2`, `.xz`, `.zst`) is allowed on both; `.zst`
27
+ needs Python 3.14.
28
+
29
+ `TSVZed`, `TSVZedLite`, `readTabularFile`, `appendTabularFile`,
30
+ `appendLinesTabularFile`, `clearTabularFile`, `scrubTabularFile` (and the
31
+ `*TSV` aliases) work on both dialects. `t.dialect` tells you which one a store
32
+ uses.
33
+
34
+ ## Quick start
35
+
36
+ ```python
37
+ import TSVZ
38
+
39
+ t = TSVZ.TSVZed('people.tsvz', header='id\tname\tscore')
40
+ t['alice'] = ['alice', 'Alice', '10']
41
+ t['bob'] = 'bob\tBob\t20'
42
+ t['alice'] = ['alice', 'Alice', '11'] # last write wins
43
+ del t['bob'] # appends a tombstone
44
+ print(t['alice']) # ['alice', 'Alice', '11']
45
+ t.close()
46
+ ```
47
+
48
+ ```bash
49
+ tsvz set people.tsvz carol Carol 5 # add or update a row (append is the same)
50
+ tsvz get people.tsvz carol # one row; exit 3 if carol is missing
51
+ tsvz read people.tsvz # every row: a table on a terminal, records in a pipe
52
+ tsvz delete people.tsvz carol # tombstone
53
+ tsvz read people.tsvz | tsvz set copy.tsvz - # copy every row
54
+ tsvz clear people.tsvz # empty it, keeping the header and markers
55
+ tsvz scrub people.tsvz # compact it (for .tsvz: archival maintenance)
56
+ tsvz verify people.tsvz # check #_checksum_*_# segments; exit 4 on a mismatch
57
+ tsvz parts events.tsvz # list the parts of a multi-part store
58
+ tsvz has people.tsvz carol # exit 0 if carol is live, 3 if not
59
+ tsvz pop people.tsvz carol # print carol's row and delete it
60
+ tsvz serve people.tsvz & # keep the store loaded; tsvz commands then use it
61
+ tsvz stop people.tsvz
62
+ tsvz people.tsvz append carol Carol 5 # the 3.39 form still works
63
+ tsvz -V
64
+ ```
65
+
66
+ ## What TSVZ 4 does with `.tsvz` files
67
+
68
+ - **Reads** everything a conformant writer can produce: markers
69
+ (`#_defaults_#`, `#_strip_trailing_whites_#`, `#_fill_empty_with_default_#`,
70
+ `#_return_defaults_when_missing_#`, ...), escapes (`<sep>`, `<LF>`, `<lt>`,
71
+ `<#>`), tombstones, `#_checksum_<algo>_#` segments (crc32 and every
72
+ fixed-length hashlib algorithm), compressed parts, and **multi-part stores**
73
+ (`store.tsvz.<hex>`, part 0 first, `.rotated` parts skipped).
74
+ - **Writes** one file: the named file, or the highest-numbered part of a
75
+ multi-part store. Rows are written as given (no padding); a lone key is a
76
+ tombstone; a row of empty cells (`k\t\t`) is a live row.
77
+ - **Compaction is manual.** `scrubTabularFile` / `tsvz scrub f.tsvz` rewrites a
78
+ single-part store in place (same inode). If another writer commits while it
79
+ runs, it reads the store again; after three tries it writes nothing, with a
80
+ warning (`clearTabularFile` does the same). The automatic rewrite options of
81
+ 3.39 (`rewrite_on_load`, `rewrite_on_exit`, `rewrite_interval`, `rewrite()`,
82
+ `mapToFile()`, `hardMapToFile()`) are ignored for `.tsvz`, with a warning.
83
+ - **Missing keys:** `t[key]` returns the defaults row while
84
+ `#_return_defaults_when_missing_#` is true (its default); `in`, `get`,
85
+ `setdefault` and `pop` behave like a dict.
86
+ - **`#` keys** are stored (`<#>key`). Keys of the form `#_name_#` are marker
87
+ writes: `t['#_defaults_#'] = [...]` sets the defaults, and so does
88
+ `setDefaults([...])` once the object is constructed.
89
+
90
+ ## Command line
91
+
92
+ `tsvz` follows [tsvz-spec-v1 §20](tsvz-spec-v1.md#20-command-line-interface):
93
+ `tsvz [OPTION ...] OPERATION STORE [ARG ...]`, with options anywhere before a
94
+ lone `--`. The 3.39 form `tsvz STORE [OPERATION] [ARG ...]` works too; a store
95
+ named like an operation then needs a path prefix (`./read`).
96
+
97
+ - **stdout carries only rows.** It gets a table on a terminal and records
98
+ otherwise; `--format table|records` overrides. Records use the store's
99
+ delimiter and escaping, so `tsvz read A | tsvz set B -` copies a store of
100
+ the same variant.
101
+ - **stderr carries every message.** `-q` prints errors only; `-v` adds detail.
102
+ - **Exit status:**
103
+ - 0: done.
104
+ - 1: failed or refused. Also a missing store for `read`, `get`, `scrub`, `verify` and `parts`,
105
+ a store that `set`, `append` or `delete` could not create, and a `.tsvz` part that could not
106
+ be read (the rows of the other parts are still printed).
107
+ - 2: usage error.
108
+ - 3: `get` found a missing key.
109
+ - 4: `verify` found a checksum mismatch.
110
+ - **Arguments are literal.** `tsvz set s.tsvz k 'a<b'` stores `a<b`. Values may
111
+ begin with `-` (`-5`); after `--` every argument is positional. `set KEY`
112
+ alone deletes KEY, as in 3.39. `set KEY ''` stores an empty cell in a `.tsvz`
113
+ store; in a `.tsv` file it deletes KEY, as in 3.39.
114
+ - **Bulk input.** `set STORE -` and `delete STORE -` read records or keys from
115
+ stdin and append them in one write. An unterminated last line is ignored,
116
+ with a warning.
117
+ - **TSVZ extensions:**
118
+ - `--x-header H` and `--x-defaults KEY<d>D1<d>...` decode backslash escapes such as `\t`;
119
+ - `--x-strict` / `--x-force` turn 3.39's column and header checks on or off.
120
+
121
+ ## Serving a store
122
+
123
+ `tsvz serve STORE` keeps a store loaded in one process and answers requests on a
124
+ local socket ([tsvz-spec-v1 §21](tsvz-spec-v1.md#21-write-handler-protocol)).
125
+ `tsvz` commands and `TSVZ.TSVZClient` find it through the pointer file
126
+ `STORE.serve` and use it instead of re-reading the store. Their output and exit
127
+ status stay the same.
128
+
129
+ - **One appender.** The server batches the writes it receives into single appends.
130
+ Other writers may still append to the files; the server reads what they add
131
+ before it answers.
132
+ - **Acknowledgement.** A write is acknowledged once it is queued (`#_write_ack_#`
133
+ `memory`, the default), or after `fsync` (`#_write_ack_#` `disk`, or
134
+ `TSVZClient(..., sync=True)`). Reads always see acknowledged writes.
135
+ - **Access.** Only the user who started the server can connect (socket mode 0600).
136
+ `--x-group G` or `--x-mode 0660` opens it on purpose. The server listens on a
137
+ Unix-domain socket only; reaching it from other hosts is left to a separate
138
+ adapter (for example TCP with TLS) that would relay to the socket.
139
+ - **Trust.** Clients use `STORE.serve` only when it is a regular file owned by
140
+ them or by the store's owner, not writable by anyone else, naming a socket that
141
+ belongs to that owner. Otherwise they warn and use the
142
+ files. `serve` never writes through a symbolic link or into a file it did not
143
+ create.
144
+ - **Lifetime.** It runs in the foreground until `tsvz stop STORE`, SIGINT or
145
+ SIGTERM, or `--x-idle-timeout S` seconds without connections. It then writes
146
+ and `fsync`s everything and removes `STORE.serve`.
147
+ - **Bypassing it.** `--x-direct` makes a `tsvz` command use the files. Command
148
+ lines with `--x-header`, `--x-defaults` or `--x-strict`, or another delimiter
149
+ than the server's, use the files too.
150
+
151
+ ```python
152
+ import TSVZ
153
+
154
+ with TSVZ.TSVZClient('people.tsvz') as people: # served, or in this process when no server runs
155
+ people['dave'] = ['dave', 'Dave', '41']
156
+ print(people['dave'], len(people), 'alice' in people)
157
+ print(people.pop('dave'))
158
+ ```
159
+
160
+ `TSVZClient` has `TSVZed`'s semantics for reading and setting keys, `del`, `in`,
161
+ `len`, iteration, `pop`, `popitem`, `setdefault`, `update`, `clear` and
162
+ `setDefaults`. Its writes return once they are written (raising `OSError` if they
163
+ cannot be), one client may be shared between threads, and without a server it
164
+ warns once and works on the files in-process, finishing its writes when the
165
+ program exits.
166
+
167
+ ## Fault tolerance
168
+
169
+ TSVZ keeps your data and keeps going when a file is not what it expects, and
170
+ says so on stderr (or through `teeLogger`) with one summarised line per problem:
171
+
172
+ ```
173
+ TSVZ warning: data.tsvz: invalid UTF-8 replaced with U+FFFD (3 occurrences, first at line 17)
174
+ ```
175
+
176
+ Examples: bytes after the last newline (ignored on read; truncated before the
177
+ next append, and quoted), invalid UTF-8, a byte order mark, a newer
178
+ `#_version_#`, a marker with a bad value, a checksum mismatch, a damaged
179
+ compressed part (read up to the damage; repaired in place before the next
180
+ append, with the original saved as `<part>.damaged-<time>`), an unreadable
181
+ part, and a delimiter or encoding argument that conflicts with the extension.
182
+
183
+ ## Breaking changes from 3.39
184
+
185
+ ### `.tsv`, `.csv`, `.nsv`, `.psv`: bug fixes only
186
+
187
+ | # | Change |
188
+ |---|---|
189
+ | L1 | Appending after an unterminated last line writes `\n` first; 3.39 merged the two lines. |
190
+ | L2 | Setting a row whose values are all empty deletes it in memory too (it already meant delete on disk), with a warning. |
191
+ | L3 | `TSVZedLite` on a compressed file keeps rows in memory; 3.39 read compressed bytes as text. Rows held in memory (also `#` keys) are returned directly. |
192
+ | L4 | New warnings on stderr (invalid encoding, lines dropped by `strict`); data unchanged. |
193
+ | L5 | A damaged compressed file is read up to the damage instead of raising. |
194
+ | L6 | `mapToFile` no longer glues a row it adds at the end of the file to the next one. |
195
+
196
+ ### `.tsvz`, `.csvz`, `.nsvz`, `.psvz`: now tsvz-spec-v1
197
+
198
+ | # | Change |
199
+ |---|---|
200
+ | S1 | `.csvz/.nsvz/.psvz` use `, \0 \|` (3.39: tab). A conflicting `delimiter=` or `encoding=` is overridden, with a warning. |
201
+ | S2 | A file written by 3.39 under one of these names reads differently (header line becomes data, `key\t\t` deletes come back, `</sep/>` stays literal, an unterminated last line is dropped, single-column files read as empty, empty cells are not defaulted, NULs are not stripped). **Rename such a file to `.tsv`.** |
202
+ | S3 | `t[missing]` returns the defaults row instead of raising `KeyError` (see above). |
203
+ | S4 | `#` keys are stored; `#_name_#` keys are marker writes. |
204
+ | S5 | Automatic rewrites are ignored with a warning; `rewrite_on_load` effectively defaults to `False`; `move_to_end` only reorders memory. |
205
+ | S6 | Rows are never trimmed or dropped for their width; `strict` no longer drops rows; `appendLinesTabularFile` does not pad. |
206
+ | S7 | The header is written as a `#` comment; a new file without a header is empty. |
207
+ | S8 | An unterminated tail is ignored on read and truncated on append. |
208
+ | S9 | `clear` keeps the active markers; scrub keeps the header comment and drops other comments, custom markers and checksums. |
209
+ | S10 | Files named `x.tsvz.<hex>` beside `x.tsvz` are read as parts of the store. |
210
+ | S11 | `lastLineOnly` reads forward (slower on large files). |
211
+
212
+ ### The `tsvz` command
213
+
214
+ 3.39 command lines still parse, including abbreviated long options (`--delim`)
215
+ and `-d=,`, except the ones C5 and C9 list.
216
+
217
+ | # | Change |
218
+ |---|---|
219
+ | C1 | `read` into a pipe or a file prints records instead of the table; `--format table` gives the table. |
220
+ | C2 | Every message goes to stderr: "Created …", "File not found", header mismatches, warnings. |
221
+ | C3 | `read` and `scrub` of a missing store exit 1 with a message (3.39: exit 0, and `read` printed "File not found" on stdout). |
222
+ | C4 | `delete` deletes every KEY it is given (3.39: only the first). |
223
+ | C5 | `append` or `delete` without a KEY, or with an empty KEY, is a usage error with exit 2 (3.39 wrote an empty line). |
224
+ | C6 | A `clear` or `scrub` that writes nothing (one part of a multi-part store, a store that kept changing) exits 1. |
225
+ | C7 | `-c/--header`, `--defaults`, `-s/--strict` and `-f/--force` are undocumented aliases of the `--x-` options. `--x-header` and `--x-defaults` keep non-ASCII text. |
226
+ | C8 | Usage errors print tsvz's own message, exit 2; the `-h` text is new. |
227
+ | C9 | Arguments after STORE for `read`, `clear` and `scrub` are a usage error, exit 2 (3.39 ignored them, so `clear junk` cleared the file). |
228
+ | C10 | The table (a terminal, or `--format table`) of `get`, `keys`, `pop`, `popitem`, `setdefault`, and of `read` on a `.tsvz`-family store, shows the rows only: no row is drawn as a header, and no row is cut to the first row's or the terminal's width. `read` of a `.tsv`-family file keeps 3.39's table, whose first row is the file's header line. |
229
+
230
+ ## Spec deviations and interpretations
231
+
232
+ - §19.2.3c: scrub keeps the header comment (3.39 header verification needs it).
233
+ - §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
+ - §14.5: the defaults-on-missing read applies to `t[key]` only.
238
+ - §18.6: locking is the 3.39 exclusive `lockf` per write plus an in-process lock; readers do not lock.
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
+ - §20.7: `-q` also silences informational messages ("Created …"); only errors remain.
243
+ - §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
+ - §20.2.4: on `.tsv`-family files `get` prints nothing for a missing key, `verify` checks nothing, and `parts` lists the file.
245
+ - §20.3: a bulk input line with an empty key is skipped, with a warning.
246
+ - §20.6: a write whose store cannot be created, and a `read`, `get` or `verify` that cannot read every part, exit 1 with an error that `-q` keeps.
247
+ - §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
+ - §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
+ - §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
+ - §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
+ - §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
+
254
+ ## Website
255
+
256
+ `website/` holds the tsvz.org site. `website/tsvz_site.py` (standard library
257
+ only, Python 3.8+) renders `website/index.md`, `website/spec-glance.md` and
258
+ `tsvz-spec-v1.md`, and serves HTML to browsers and markdown to agents.
259
+
260
+ ```bash
261
+ python3 website/tsvz_site.py serve -p 8765 # http://127.0.0.1:8765/
262
+ python3 website/tsvz_site.py build /tmp/site # static files for any host
263
+ ```
264
+
265
+ `deploy/tsvz-site.service` runs it under systemd; restart it to publish an edit.
266
+
267
+ ## Tests
268
+
269
+ ```bash
270
+ python3 -m pytest TSVZ_test.py -q # everything, incl. 3.39 differential tests
271
+ python3 -m pytest website/tsvz_site_test.py -q # the tsvz.org site
272
+ python3 benchTSVZ.py /tmp/b.tsv -n 100000 --compare-old # benchmark (not a test)
273
+ ```
274
+
275
+ `TSVZ_old.py` is the frozen 3.39 module the differential tests compare against;
276
+ it is not installed.
277
+
278
+ ## License
279
+
280
+ GPL-3.0-or-later © Yufei Pan (pan@zopyr.us)