TSVZ 3.37__tar.gz → 3.39__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-3.39/PKG-INFO ADDED
@@ -0,0 +1,396 @@
1
+ Metadata-Version: 2.4
2
+ Name: TSVZ
3
+ Version: 3.39
4
+ Summary: An simple in memory wrapper around a TSV file to function as a database
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: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.6
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: argparse
15
+ Provides-Extra: completion
16
+ Requires-Dist: argcomplete; extra == "completion"
17
+ Dynamic: author
18
+ Dynamic: author-email
19
+ Dynamic: classifier
20
+ Dynamic: description
21
+ Dynamic: description-content-type
22
+ Dynamic: home-page
23
+ Dynamic: license
24
+ Dynamic: provides-extra
25
+ Dynamic: requires-dist
26
+ Dynamic: requires-python
27
+ Dynamic: summary
28
+
29
+ # TSVZ
30
+
31
+ TSVZ is a tiny, dependency-free library and CLI for treating a **tab-separated
32
+ values file as a key-value store**. The first column of every row is a unique
33
+ key; the file behaves like an ordered dictionary that is transparently
34
+ persisted to disk.
35
+
36
+ Two front-ends are provided:
37
+
38
+ - **`TSVZed`** — an in-memory `OrderedDict` backed by the file, with a
39
+ background worker that does **non-blocking, append-only** writes, optional
40
+ periodic / on-exit rewrites, file locking for multi-process access, and
41
+ transparent (de)compression.
42
+ - **`TSVZedLite`** — a `MutableMapping` that keeps only a **key → byte-offset
43
+ index** in memory and reads values from disk on demand. Minimal footprint,
44
+ single-process, append-only.
45
+
46
+ It is a single self-contained module (`TSVZ.py`) — drop it next to your code and
47
+ `import TSVZ`, or install it.
48
+
49
+ ```bash
50
+ pip install tsvz # from PyPI
51
+ pip install -e . # editable, from this directory
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Quick start
57
+
58
+ ### As a library
59
+
60
+ ```python
61
+ import TSVZ
62
+
63
+ # Opens (creating if needed), loads into memory, starts the async writer.
64
+ db = TSVZ.TSVZed('data.tsv', header='id\tname\tscore')
65
+
66
+ db['alice'] = ['alice', 'Alice', '10'] # value as a list
67
+ db['bob'] = 'bob\tBob\t20' # ...or a delimited string
68
+ db['alice'] = ['alice', 'Alice', '11'] # last write wins
69
+ del db['bob'] # deletes (and persists the deletion)
70
+
71
+ print(db['alice']) # ['alice', 'Alice', '11']
72
+ db.close() # flush the queue and stop the worker (also runs atexit)
73
+ ```
74
+
75
+ There are also stateless helper functions if you just want to touch a file once:
76
+ `readTabularFile`, `appendTabularFile`, `appendLinesTabularFile`,
77
+ `clearTabularFile`, `scrubTabularFile` (and `readTSV` / `appendTSV` / … aliases).
78
+
79
+ ### From the command line
80
+
81
+ ```bash
82
+ tsvz data.tsv # read + pretty-print
83
+ tsvz data.tsv append alice Alice 10 # append/update a row keyed by "alice"
84
+ tsvz data.tsv delete alice # delete the row keyed by "alice"
85
+ tsvz data.tsv clear # truncate to just the header
86
+ tsvz data.tsv scrub # rewrite compactly (drops comments, applies last-wins)
87
+
88
+ tsvz data.csv -d comma append k v1 v2 # pick a delimiter explicitly
89
+ tsvz -h
90
+ ```
91
+
92
+ ---
93
+
94
+ ## The TSVZ file format
95
+
96
+ > **Status: draft / evolving spec.** The reference code in this repo does not
97
+ > fully implement it yet (see *Implementation notes*).
98
+
99
+ `.tsvz` / `.csvz` / `.nsvz` / `.psvz` are a **format in their own right** — not
100
+ merely a `.tsv` that TSVZ happens to manage. They define a **strict,
101
+ append-only, write-ahead-log (WAL) style key-value store** that is *also*
102
+ readable by ordinary CSV/TSV tools (which see the rows fine but may
103
+ misinterpret escaped values; see *Escaping*).
104
+
105
+ The plain, un-suffixed `.tsv` / `.csv` / `.nsv` / `.psv` extensions are **not**
106
+ this format: they remain loose delimited tables with no semantic guarantees. The
107
+ same delimiter mapping carries over to both; only the `z` forms carry the
108
+ stricter contract below.
109
+
110
+ The format is defined by the rules below; they are written to be implementable by
111
+ any reader/writer, not just this library.
112
+
113
+ ### 0. Model — the file *is* a write-ahead log
114
+
115
+ - **Every mutation is an append.** There is no in-place update during normal
116
+ operation. Current state is reconstructed by **replaying the file forward**,
117
+ **last-wins**.
118
+ - **Key-value, not relational.** First column is the globally-unique key; the
119
+ remaining columns are the value (variable width; a virtual infinite tail of
120
+ empty columns; a defaults line as per-column fallback). No schema, no header
121
+ semantics beyond "a `#` comment."
122
+ - **Delete is a tombstone** (a lone key / all-empty values), never a physical
123
+ removal — consistent with a log.
124
+ - **Minimal overhead vs. an embedded DB.** Plain UTF-8 text, line-oriented,
125
+ human-readable and greppable; no page cache, b-tree, or separate journal. You
126
+ trade random-update efficiency and bounded file size for near-zero write
127
+ overhead and trivial crash recovery (truncate the last partial line). Fits
128
+ logging / event streams / transactional state where overwrites are rare, write
129
+ latency matters, or the full history is worth keeping.
130
+ - **Compaction is occasional maintenance, not normal operation** — the one place
131
+ the append-only rule is deliberately, explicitly broken. See *Compaction*.
132
+
133
+ ### 1. Encoding & lines
134
+
135
+ 1. The file is **UTF-8** encoded (no BOM).
136
+ 2. Records are separated by a line feed (`\n`). A reader **must** tolerate a
137
+ trailing `\r` (i.e. read CRLF files); writers **should** emit bare `\n`.
138
+ 3. The file **should** end with a trailing newline. A reader **must** tolerate a
139
+ missing final newline.
140
+ 4. An empty file (zero bytes) — or a file containing only comments — is a valid,
141
+ empty TSVZ.
142
+
143
+ ### 2. Delimiter & variants
144
+
145
+ 5. The variants differ only in the field delimiter. The `z` extension marks the
146
+ strict WAL format; the plain extension is the loose tabular fallback:
147
+
148
+ | Variant | Format ext. | Plain ext. | Delimiter |
149
+ |---------|-------------|------------|-----------|
150
+ | TSVZ | `.tsvz` | `.tsv` | tab `\t` |
151
+ | CSVZ | `.csvz` | `.csv` | comma `,` |
152
+ | NSVZ | `.nsvz` | `.nsv` | NUL `\0` |
153
+ | PSVZ | `.psvz` | `.psv` | pipe `\|` |
154
+
155
+ 6. Each line is a list of fields joined by the delimiter. **Column count is not
156
+ fixed** — a reader must accept arbitrarily short or long lines. Conceptually
157
+ every row is its values followed by an infinite tail of empty-string columns,
158
+ so reading a field beyond a row's length yields `''` (subject to the defaults
159
+ rule below). A returned row is therefore **not guaranteed to be a fixed
160
+ length**; a reader is free to additionally validate or pad/trim if it wants.
161
+
162
+ ### 3. Keys & values
163
+
164
+ 7. The **first column is the key** and is **globally unique** within the file.
165
+ 8. **Last-wins:** when the same key appears on multiple lines, the **last**
166
+ occurrence determines the current value. (This is what makes append-only
167
+ updates work.)
168
+ 9. A key's **logical position** is the position of its *first* appearance; later
169
+ updates change its value but not its order.
170
+ 10. **Append-only:** during normal operation a TSVZ file is only ever appended
171
+ to — an update or a delete is a new line at the end of the file, never an
172
+ in-place edit. (Compaction is a separate, explicit operation; see *scrub*.)
173
+
174
+ ### 4. Deletion (tombstones)
175
+
176
+ 11. A line consisting of **a lone key with no following values, or whose
177
+ following values are all empty**, is a **tombstone**: it removes that key if
178
+ it currently exists, and is tolerated (no-op) if it does not.
179
+ - e.g. `mykey\n` or `mykey\t\t\n` both delete `mykey`.
180
+ - Edge case: in a strictly single-column file a lone key *is* the value, so
181
+ deletion cannot be represented; such files cannot tombstone.
182
+
183
+ ### 5. Whitespace
184
+
185
+ 12. **Trailing whitespace is stripped from every field on read** (including the
186
+ key) — trailing whitespace is treated as visual padding and is not
187
+ significant. Leading whitespace **is** significant and preserved. (This does
188
+ not apply to comment lines, which are not parsed.)
189
+
190
+ ### 6. Comments & header
191
+
192
+ 13. Any line whose first field starts with `#` is a **comment** and is discarded
193
+ by the reader (with the exception of the reserved `#_..._#` keys below).
194
+ 14. There is **no formal header**. A header row, if present, is simply a comment
195
+ (a line starting with `#`, e.g. `#id\tname\tscore`) and carries no semantics
196
+ for the data — it is purely human documentation.
197
+
198
+ ### 7. Defaults line
199
+
200
+ 15. A line whose key is **`#_defaults_#`** (case-insensitive — `#_DEFAULTS_#` is
201
+ equally valid) is a **defaults line**. It is not data; it supplies fallback
202
+ values per column index.
203
+ 16. Defaults have an **unbounded** number of columns and are applied **forward
204
+ only**: as a reader streams the file, a field that is empty (or absent) in a
205
+ data row takes the value at the same column index from the **currently
206
+ active** defaults line.
207
+ - Rows that appear *before* a defaults line are unaffected by it (the reader
208
+ does not backtrack).
209
+ - Example: rows have 5 fields and there is no defaults line ⇒ reading field 6
210
+ yields `''`. Then a defaults line with 10 fields appears ⇒ for *subsequent*
211
+ 5-field rows, reading field 6 yields the defaults line's field 6.
212
+ 17. **Multiple defaults lines are allowed.** A later defaults line **completely
213
+ replaces** the previous one — if the later line is shorter, the extra columns
214
+ of the previous line are discarded, not merged.
215
+ 18. A `#_defaults_#` line with **no values** clears the active defaults.
216
+
217
+ ### 8. Reserved namespace
218
+
219
+ 19. Keys matching **`#_<ascii letters/digits>_#`** are **reserved** for current
220
+ and future internal TSVZ semantics (`#_defaults_#` is the first). Don't use
221
+ that pattern for your own comments. (Conformance is only enforced where a
222
+ given reserved key is actually defined; unknown `#_..._#` keys are treated as
223
+ ordinary comments today.)
224
+
225
+ ### 9. Escaping / sanitization
226
+
227
+ The delimiter and newline can't appear literally inside a field, so they are
228
+ encoded with reversible tokens. Reading inverts writing:
229
+
230
+ | In a field (in memory) | Stored on disk | Read back as |
231
+ |---|---|---|
232
+ | the delimiter char (e.g. `\t`) | `<sep>` | the delimiter char |
233
+ | line feed `\n` | `<LF>` | line feed `\n` |
234
+ | literal `<sep>` | `</sep/>` | literal `<sep>` |
235
+ | literal `<LF>` | `</LF/>` | literal `<LF>` |
236
+
237
+ 20. Consequently `<sep>` / `<LF>` appearing **in the file** are interpreted as a
238
+ delimiter / line feed on read. The double-wrapped forms `</sep/>` / `</LF/>`
239
+ exist so that the literal strings `<sep>` / `<LF>` (e.g. HTML-ish tags) can
240
+ round-trip.
241
+ 21. To keep the mapping finite, the literal strings **`</sep/>` and `</LF/>` are
242
+ invalid as field data**: if they occur in a file they are read back as the
243
+ literal `<sep>` / `<LF>` (i.e. they have no escape of their own and are
244
+ *lossy* — they collapse one level). Writers should avoid emitting them as
245
+ real data.
246
+
247
+ ### 10. Compression
248
+
249
+ 22. Compression is an **orthogonal stream filter**, not part of the format
250
+ semantics: any TSVZ file (or any single part of a multi-part set, below) may
251
+ be wrapped by appending a compression extension — `.gz`/`.gzip`,
252
+ `.bz2`/`.bzip2`, `.xz`/`.lzma`, `.zst`/`.zstd` (e.g. `log.tsvz.zst`).
253
+ Delimiter/format inference ignores the compression suffix. A compressed part
254
+ is rewritten as a whole, so the append-only optimization applies only to
255
+ uncompressed parts.
256
+
257
+ ### 11. Multi-part files
258
+
259
+ 23. A logical TSVZ store may be **split across multiple parts**. Parts share the
260
+ same path up to the format extension and add a `.<hex>` ordinal suffix, e.g.
261
+ `events.tsvz.0`, `events.tsvz.1`, … or with timestamps
262
+ `events.tsvz.6650a3c0`, `events.tsvz.66518f10`, ….
263
+ 24. Parts are ordered by interpreting the suffix as a **hexadecimal (and hence
264
+ also plain-integer) number, ascending** — smaller = earlier in the log. The
265
+ logical store is the concatenation of the parts in that order; replay forward
266
+ / last-wins simply spans part boundaries.
267
+ 25. **Each part is itself a complete, valid TSVZ file** (comments, defaults, etc.
268
+ all legal per part). Multi-part is purely an addressing convention layered on
269
+ top; a single-file store is just the one-part case.
270
+ 26. **Recommended suffix:** the hex (or integer) Unix timestamp of when the part
271
+ was created — naturally chronological and collision-resistant.
272
+
273
+ Operational recommendations for multi-part writers:
274
+
275
+ - On startup, **open a fresh part** named with the current timestamp and append
276
+ new records there; leave older parts untouched (they become immutable).
277
+ - **Do not compress the current (active) part** — it is being appended to.
278
+ - Provide a processor/flag that, on the next launch, **compresses prior parts**
279
+ that aren't already compressed (now immutable, so it's safe and frees space).
280
+ Different parts may use different codecs.
281
+
282
+ ### Additional ideas worth considering (not yet normative)
283
+
284
+ These came up while writing the spec; flagging them for your call:
285
+
286
+ - **Format-version marker.** Reserve a `#_tsvz1_#`-style first-line comment so
287
+ future readers can detect the format/version explicitly. Cheap insurance.
288
+ - **Key constraints.** State explicitly that a *data* key must be non-empty
289
+ (after trailing-whitespace strip) and must not start with `#` (that prefix is
290
+ reserved for comments/internal keys).
291
+ - **Append atomicity.** Recommend that a conformant appender write each record
292
+ as a *single* `write()` of a complete, newline-terminated line (open with
293
+ `O_APPEND`) so concurrent appenders never interleave partial lines. This is
294
+ what makes multi-writer last-wins safe without a global rewrite.
295
+ - **Compaction definition.** Define "scrub/compact" formally: produce an
296
+ equivalent file containing, for each live key, exactly one line in
297
+ first-appearance order, dropping comments and tombstones, optionally
298
+ re-emitting a single `#_defaults_#` line. (This is lossy w.r.t. comments — say
299
+ so.)
300
+ - **NUL caveat.** Note that NSVZ (`\0` delimiter) is not friendly to most text
301
+ tools; it's meant for programmatic use.
302
+
303
+ ---
304
+
305
+ ## API
306
+
307
+ ### `TSVZed(fileName, …)`
308
+
309
+ An `OrderedDict` subclass that auto-syncs to `fileName`. Selected options:
310
+
311
+ | Option | Default | Meaning |
312
+ |---|---|---|
313
+ | `header` | `''` | Header line (string or list) for creation/validation. |
314
+ | `createIfNotExist` | `True` | Create the file if missing. |
315
+ | `verifyHeader` | `True` | Check the file's first line against `header`. |
316
+ | `delimiter` | auto | Delimiter; inferred from the extension if omitted. |
317
+ | `defaults` | `None` | Per-column default values. |
318
+ | `strict` | `False` | Enforce column counts / header; reject malformed rows. |
319
+ | `rewrite_on_load` | `True` | Compact the file once when opened. |
320
+ | `rewrite_on_exit` | `False` | Compact the file when closed. |
321
+ | `rewrite_interval` | `0` | Min seconds between periodic compactions (`0` = never). |
322
+ | `monitor_external_changes` | `True` | Detect / merge other processes' writes. |
323
+ | `encoding` | `'utf8'` | File encoding. |
324
+
325
+ Writes are queued and flushed by a background thread; call `close()` (or use it
326
+ as a context manager) to flush and stop cleanly. Keys starting with `#` are kept
327
+ in memory only (handy for scratch state) and are never written.
328
+
329
+ ### `TSVZedLite(fileName, …)`
330
+
331
+ A `MutableMapping` that stores only a key→offset index in RAM and seeks into the
332
+ file for each read. Append-only; single-process; no compression, no background
333
+ thread, no external-change monitoring. Good for very large, mostly-write,
334
+ seek-cheap (SSD) workloads. An external index dict can be supplied to skip the
335
+ initial full scan (turning it into a small key-value store).
336
+
337
+ ### Module functions
338
+
339
+ `readTabularFile`, `appendTabularFile`, `appendLinesTabularFile`,
340
+ `clearTabularFile`, `scrubTabularFile`, `get_delimiter`, `pretty_format_table`,
341
+ plus the legacy `readTSV` / `appendTSV` / `clearTSV` / `scrubTSV` aliases.
342
+
343
+ ---
344
+
345
+ ## Implementation notes (reference behavior vs. the format spec)
346
+
347
+ The format above is the target definition. A few places where the current
348
+ `TSVZed` / `TSVZedLite` implementation differs or only partially implements it —
349
+ useful to know, and candidates for a future alignment pass:
350
+
351
+ - **Column normalization.** Rather than treating rows as variable-length with an
352
+ infinite empty tail (rule 6), the classes detect a column count (from the
353
+ header or first row) and, in non-strict mode, pad/trim rows to it; `strict`
354
+ mode rejects mismatches. Reads therefore return fixed-width rows in practice.
355
+ - **Defaults application.** Empty cells *present* in a row are filled from the
356
+ active defaults at parse time, but rows are not virtually extended, so "read a
357
+ field beyond the row length" (rule 16) isn't exposed as an API yet.
358
+ - **Header.** The reference classes still use a *non*-`#` first line as the
359
+ header (validated via `verifyHeader`), whereas rule 14 defines the header as an
360
+ ordinary `#` comment. Existing files written by older versions use the non-`#`
361
+ header.
362
+ - **`#_DEFAULTS_#` alias.** Only the lowercase `#_defaults_#` key is currently
363
+ recognized by the parser.
364
+ - **No `z` vs. plain distinction, no multi-part.** `get_delimiter` infers from
365
+ `.tsv`/`.csv`/`.nsv`/`.psv` only, so the `z` extensions aren't recognized for
366
+ delimiter inference yet (`.csvz`/`.nsvz`/`.psvz` currently fall back to tab;
367
+ `.tsvz` happens to land on tab correctly). The code does not yet enforce the
368
+ strict append-only WAL contract differently for the `z` forms, and there is no
369
+ multi-part read/write support. These are spec items, not implemented.
370
+
371
+ ### Open design questions
372
+
373
+ Raised by the spec clarification, not yet decided:
374
+
375
+ 1. **Where does the strict contract live?** Should `TSVZed`/`TSVZedLite` key the
376
+ append-only WAL behavior off the `z` extension (`.tsv` = lenient table,
377
+ `.tsvz` = this format), or treat all forms the same and let the suffix be
378
+ purely documentary?
379
+ 2. **Append-only vs. the existing rewrite machinery.** A strict `.tsvz` arguably
380
+ forbids in-place rewrite (`rewrite_on_load` / `mapToFile`) entirely except via
381
+ explicit compaction — which would simplify the class considerably.
382
+ 3. **Compaction for history-retention users.** If "keep the full log" is a goal,
383
+ compaction must be an explicit, separate action — and perhaps emit a `.tsv`
384
+ snapshot rather than mutating the `.tsvz` log in place.
385
+
386
+ ## Tests
387
+
388
+ ```bash
389
+ python -m pytest test_TSVZ.py # regression suite
390
+ python test_TSVZ.py # or run directly
391
+ python benchTSVZ.py data.tsv -n 100000 # throughput benchmark (not a test)
392
+ ```
393
+
394
+ ## License
395
+
396
+ GPL-3.0-or-later © Yufei Pan (pan@zopyr.us)