TSVZ 4.5__tar.gz → 4.7__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.5 → tsvz-4.7}/PKG-INFO +5 -2
- {tsvz-4.5 → tsvz-4.7}/README.md +4 -1
- {tsvz-4.5 → tsvz-4.7}/TSVZ.egg-info/PKG-INFO +5 -2
- {tsvz-4.5 → tsvz-4.7}/TSVZ.py +328 -57
- {tsvz-4.5 → tsvz-4.7}/TSVZ.egg-info/SOURCES.txt +0 -0
- {tsvz-4.5 → tsvz-4.7}/TSVZ.egg-info/dependency_links.txt +0 -0
- {tsvz-4.5 → tsvz-4.7}/TSVZ.egg-info/entry_points.txt +0 -0
- {tsvz-4.5 → tsvz-4.7}/TSVZ.egg-info/requires.txt +0 -0
- {tsvz-4.5 → tsvz-4.7}/TSVZ.egg-info/top_level.txt +0 -0
- {tsvz-4.5 → tsvz-4.7}/setup.cfg +0 -0
- {tsvz-4.5 → tsvz-4.7}/setup.py +0 -0
{tsvz-4.5 → tsvz-4.7}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TSVZ
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.7
|
|
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
|
|
@@ -34,7 +34,7 @@ key-value store in a delimiter-separated text file**. The first column of each
|
|
|
34
34
|
row is the key; the file behaves like an ordered dictionary that is persisted to
|
|
35
35
|
disk as you change it.
|
|
36
36
|
|
|
37
|
-
**Version 4.
|
|
37
|
+
**Version 4.7**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
|
|
38
38
|
next to your code and `import TSVZ`, or install it.
|
|
39
39
|
|
|
40
40
|
```bash
|
|
@@ -224,6 +224,7 @@ delimiter or encoding argument that conflicts with the extension.
|
|
|
224
224
|
| L4 | New warnings on stderr (invalid encoding, lines dropped by `strict`); data unchanged. |
|
|
225
225
|
| L5 | A damaged compressed file is read up to the damage instead of raising. |
|
|
226
226
|
| L6 | `mapToFile` no longer glues a row it adds at the end of the file to the next one. |
|
|
227
|
+
| L7 | `TSVZed`'s `in` and `get` find a key as `t[key]` does: the live key equal to it, else the key right-stripped. 3.39 matched them exactly, so `'x ' in t` was false while `t['x ']` read `x`, and `t.setdefault('x ', …)` overwrote `x`. |
|
|
227
228
|
|
|
228
229
|
### `.tsvz`, `.csvz`, `.nsvz`, `.psvz`: now tsvz-spec-v1
|
|
229
230
|
|
|
@@ -243,6 +244,7 @@ delimiter or encoding argument that conflicts with the extension.
|
|
|
243
244
|
| S12 | `TSVZedLite.clear()` and `clear_file()` raise `OSError` when the clear is refused (a part could not be read, the path names one part of a store, or the store kept changing). Memory is reloaded from the unchanged store first. |
|
|
244
245
|
| S13 | Compressed parts are written in members of at most 1 MiB of records (`#_member_size_#`), Zstandard frames with a checksum; an append never reads or repairs the part. |
|
|
245
246
|
| S14 | A zero byte in a value is written `<NUL>`, and a raw zero byte in a record is read as zero-fill damage (spec §4.9): under the default `recover` only the bytes after it are read. **A value that 4.2 or 4.3 wrote with a raw zero byte therefore reads differently in 4.4.** Such values are rare; before upgrading, `grep -c -P '\x00' store.tsvz` (any `.tsvz`, `.csvz` or `.psvz` part) finds them. Stop 4.2 and 4.3 handlers before upgrading: they read `<NUL>` as text, as they read `<CR>`. |
|
|
247
|
+
| S15 | A lookup (`t[key]`, `in`, `get`, `del`, `pop`, `setdefault`) takes the live key equal to the one given before the right-stripped one, so a key ending in whitespace (written while `#_strip_trailing_whites_#` was false) can be read and deleted by name (spec §20.2.7). Setting a key still right-strips it. |
|
|
246
248
|
|
|
247
249
|
### The `tsvz` command
|
|
248
250
|
|
|
@@ -262,6 +264,7 @@ and `-d=,`, except the ones C5 and C9 list.
|
|
|
262
264
|
| 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). |
|
|
263
265
|
| 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. |
|
|
264
266
|
| C11 | The table shows control characters, bidi controls and line separators as escapes (`\r`, `\n`, `\t`, `\xNN`, `\uNNNN`), so no value can move the cursor, reorder the line or restyle the terminal. It sizes columns in terminal columns, so CJK text (two columns a character) lines up. The table is for reading: a backslash in a value is shown as is, so use records for exact values. |
|
|
267
|
+
| C12 | While stripping is on, `get`, `has`, `pop`, `delete` and `setdefault` take the live key equal to a KEY ending in a space or tab before the stripped one (spec §20.2.7), so `tsvz keys s \| tsvz delete s -` deletes every key. `delete` of such a KEY reads the store first, and exits 1 without writing when the store cannot be read whole. |
|
|
265
268
|
|
|
266
269
|
## Spec deviations and interpretations
|
|
267
270
|
|
{tsvz-4.5 → tsvz-4.7}/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.7**, 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
|
|
@@ -197,6 +197,7 @@ delimiter or encoding argument that conflicts with the extension.
|
|
|
197
197
|
| L4 | New warnings on stderr (invalid encoding, lines dropped by `strict`); data unchanged. |
|
|
198
198
|
| L5 | A damaged compressed file is read up to the damage instead of raising. |
|
|
199
199
|
| L6 | `mapToFile` no longer glues a row it adds at the end of the file to the next one. |
|
|
200
|
+
| L7 | `TSVZed`'s `in` and `get` find a key as `t[key]` does: the live key equal to it, else the key right-stripped. 3.39 matched them exactly, so `'x ' in t` was false while `t['x ']` read `x`, and `t.setdefault('x ', …)` overwrote `x`. |
|
|
200
201
|
|
|
201
202
|
### `.tsvz`, `.csvz`, `.nsvz`, `.psvz`: now tsvz-spec-v1
|
|
202
203
|
|
|
@@ -216,6 +217,7 @@ delimiter or encoding argument that conflicts with the extension.
|
|
|
216
217
|
| S12 | `TSVZedLite.clear()` and `clear_file()` raise `OSError` when the clear is refused (a part could not be read, the path names one part of a store, or the store kept changing). Memory is reloaded from the unchanged store first. |
|
|
217
218
|
| S13 | Compressed parts are written in members of at most 1 MiB of records (`#_member_size_#`), Zstandard frames with a checksum; an append never reads or repairs the part. |
|
|
218
219
|
| S14 | A zero byte in a value is written `<NUL>`, and a raw zero byte in a record is read as zero-fill damage (spec §4.9): under the default `recover` only the bytes after it are read. **A value that 4.2 or 4.3 wrote with a raw zero byte therefore reads differently in 4.4.** Such values are rare; before upgrading, `grep -c -P '\x00' store.tsvz` (any `.tsvz`, `.csvz` or `.psvz` part) finds them. Stop 4.2 and 4.3 handlers before upgrading: they read `<NUL>` as text, as they read `<CR>`. |
|
|
220
|
+
| S15 | A lookup (`t[key]`, `in`, `get`, `del`, `pop`, `setdefault`) takes the live key equal to the one given before the right-stripped one, so a key ending in whitespace (written while `#_strip_trailing_whites_#` was false) can be read and deleted by name (spec §20.2.7). Setting a key still right-strips it. |
|
|
219
221
|
|
|
220
222
|
### The `tsvz` command
|
|
221
223
|
|
|
@@ -235,6 +237,7 @@ and `-d=,`, except the ones C5 and C9 list.
|
|
|
235
237
|
| 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). |
|
|
236
238
|
| 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. |
|
|
237
239
|
| C11 | The table shows control characters, bidi controls and line separators as escapes (`\r`, `\n`, `\t`, `\xNN`, `\uNNNN`), so no value can move the cursor, reorder the line or restyle the terminal. It sizes columns in terminal columns, so CJK text (two columns a character) lines up. The table is for reading: a backslash in a value is shown as is, so use records for exact values. |
|
|
240
|
+
| C12 | While stripping is on, `get`, `has`, `pop`, `delete` and `setdefault` take the live key equal to a KEY ending in a space or tab before the stripped one (spec §20.2.7), so `tsvz keys s \| tsvz delete s -` deletes every key. `delete` of such a KEY reads the store first, and exits 1 without writing when the store cannot be read whole. |
|
|
238
241
|
|
|
239
242
|
## Spec deviations and interpretations
|
|
240
243
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TSVZ
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.7
|
|
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
|
|
@@ -34,7 +34,7 @@ key-value store in a delimiter-separated text file**. The first column of each
|
|
|
34
34
|
row is the key; the file behaves like an ordered dictionary that is persisted to
|
|
35
35
|
disk as you change it.
|
|
36
36
|
|
|
37
|
-
**Version 4.
|
|
37
|
+
**Version 4.7**, Python **3.6+**, one self-contained module (`TSVZ.py`): drop it
|
|
38
38
|
next to your code and `import TSVZ`, or install it.
|
|
39
39
|
|
|
40
40
|
```bash
|
|
@@ -224,6 +224,7 @@ delimiter or encoding argument that conflicts with the extension.
|
|
|
224
224
|
| L4 | New warnings on stderr (invalid encoding, lines dropped by `strict`); data unchanged. |
|
|
225
225
|
| L5 | A damaged compressed file is read up to the damage instead of raising. |
|
|
226
226
|
| L6 | `mapToFile` no longer glues a row it adds at the end of the file to the next one. |
|
|
227
|
+
| L7 | `TSVZed`'s `in` and `get` find a key as `t[key]` does: the live key equal to it, else the key right-stripped. 3.39 matched them exactly, so `'x ' in t` was false while `t['x ']` read `x`, and `t.setdefault('x ', …)` overwrote `x`. |
|
|
227
228
|
|
|
228
229
|
### `.tsvz`, `.csvz`, `.nsvz`, `.psvz`: now tsvz-spec-v1
|
|
229
230
|
|
|
@@ -243,6 +244,7 @@ delimiter or encoding argument that conflicts with the extension.
|
|
|
243
244
|
| S12 | `TSVZedLite.clear()` and `clear_file()` raise `OSError` when the clear is refused (a part could not be read, the path names one part of a store, or the store kept changing). Memory is reloaded from the unchanged store first. |
|
|
244
245
|
| S13 | Compressed parts are written in members of at most 1 MiB of records (`#_member_size_#`), Zstandard frames with a checksum; an append never reads or repairs the part. |
|
|
245
246
|
| S14 | A zero byte in a value is written `<NUL>`, and a raw zero byte in a record is read as zero-fill damage (spec §4.9): under the default `recover` only the bytes after it are read. **A value that 4.2 or 4.3 wrote with a raw zero byte therefore reads differently in 4.4.** Such values are rare; before upgrading, `grep -c -P '\x00' store.tsvz` (any `.tsvz`, `.csvz` or `.psvz` part) finds them. Stop 4.2 and 4.3 handlers before upgrading: they read `<NUL>` as text, as they read `<CR>`. |
|
|
247
|
+
| S15 | A lookup (`t[key]`, `in`, `get`, `del`, `pop`, `setdefault`) takes the live key equal to the one given before the right-stripped one, so a key ending in whitespace (written while `#_strip_trailing_whites_#` was false) can be read and deleted by name (spec §20.2.7). Setting a key still right-strips it. |
|
|
246
248
|
|
|
247
249
|
### The `tsvz` command
|
|
248
250
|
|
|
@@ -262,6 +264,7 @@ and `-d=,`, except the ones C5 and C9 list.
|
|
|
262
264
|
| 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). |
|
|
263
265
|
| 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. |
|
|
264
266
|
| C11 | The table shows control characters, bidi controls and line separators as escapes (`\r`, `\n`, `\t`, `\xNN`, `\uNNNN`), so no value can move the cursor, reorder the line or restyle the terminal. It sizes columns in terminal columns, so CJK text (two columns a character) lines up. The table is for reading: a backslash in a value is shown as is, so use records for exact values. |
|
|
267
|
+
| C12 | While stripping is on, `get`, `has`, `pop`, `delete` and `setdefault` take the live key equal to a KEY ending in a space or tab before the stripped one (spec §20.2.7), so `tsvz keys s \| tsvz delete s -` deletes every key. `delete` of such a KEY reads the store first, and exits 1 without writing when the store cannot be read whole. |
|
|
265
268
|
|
|
266
269
|
## Spec deviations and interpretations
|
|
267
270
|
|
{tsvz-4.5 → tsvz-4.7}/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.7'
|
|
44
44
|
__version__ = version
|
|
45
45
|
author = 'pan@zopyr.us'
|
|
46
|
-
COMMIT_DATE = '2026-10-
|
|
46
|
+
COMMIT_DATE = '2026-10-13'
|
|
47
47
|
|
|
48
48
|
DEFAULT_DELIMITER = '\t'
|
|
49
49
|
DEFAULTS_INDICATOR_KEY = '#_defaults_#'
|
|
@@ -179,13 +179,18 @@ def get_terminal_size():
|
|
|
179
179
|
try:
|
|
180
180
|
import os
|
|
181
181
|
_tsize = os.get_terminal_size()
|
|
182
|
+
if not (_tsize[0] and _tsize[1]):
|
|
183
|
+
raise OSError('the terminal has no size set')
|
|
182
184
|
except Exception:
|
|
183
185
|
try:
|
|
184
186
|
import fcntl
|
|
185
187
|
import struct
|
|
186
188
|
import termios
|
|
187
189
|
packed = fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))
|
|
188
|
-
|
|
190
|
+
rows, columns = struct.unpack('HHHH', packed)[:2]
|
|
191
|
+
if not (rows and columns):
|
|
192
|
+
raise OSError('the terminal has no size set')
|
|
193
|
+
_tsize = (columns, rows)
|
|
189
194
|
except Exception:
|
|
190
195
|
import shutil
|
|
191
196
|
_tsize = shutil.get_terminal_size(fallback=(240, 50))
|
|
@@ -1600,6 +1605,8 @@ def scrubTabularFile(fileName,teeLogger = None,header = '',createIfNotExist = Fa
|
|
|
1600
1605
|
# ===========================================================================
|
|
1601
1606
|
MAX_SPEC_VERSION = 1
|
|
1602
1607
|
_MARKER_RE = re.compile(r'^#_[A-Za-z0-9_-]+_#$')
|
|
1608
|
+
#: What a lookup gets for a key that is not live (spec §20.2.7: then the stripped key is tried).
|
|
1609
|
+
_NO_ROW = object()
|
|
1603
1610
|
_CHECKSUM_RE = re.compile(r'^#_checksum_([A-Za-z0-9_-]+)_#$', re.IGNORECASE)
|
|
1604
1611
|
_ASCII_DIGITS_RE = re.compile(r'^[0-9]+$')
|
|
1605
1612
|
_SPEC_DECODE_RE = re.compile(r'<(?:sep|LF|CR|NUL|lt|#)>')
|
|
@@ -1654,6 +1661,82 @@ def _specFormatRecord(cells, delimiter, marker=False):
|
|
|
1654
1661
|
return delimiter.join([first] + [_specEncodeField(cell, delimiter) for cell in cells[1:]])
|
|
1655
1662
|
|
|
1656
1663
|
|
|
1664
|
+
def _specTombstones(keys, delimiter, strip):
|
|
1665
|
+
"""``(cells, marker)`` records that delete ``keys`` (spec §9.2) from a store whose stripping is ``strip``.
|
|
1666
|
+
|
|
1667
|
+
While stripping is on a reader strips a tombstone's key too (spec §9.1): the
|
|
1668
|
+
tombstone of a key whose encoded form ends in a space or tab, stored while
|
|
1669
|
+
stripping was off, would delete the stripped key instead. A batch holding
|
|
1670
|
+
one is written with stripping off, and stripping is turned back on after it
|
|
1671
|
+
(spec §9.6).
|
|
1672
|
+
"""
|
|
1673
|
+
rows = [([key], False) for key in keys]
|
|
1674
|
+
if strip and any(_specStrippable(key, delimiter) for key in keys):
|
|
1675
|
+
rows.insert(0, (['#_strip_trailing_whites_#', 'false'], True))
|
|
1676
|
+
rows.append((['#_strip_trailing_whites_#', 'true'], True))
|
|
1677
|
+
return rows
|
|
1678
|
+
|
|
1679
|
+
|
|
1680
|
+
def _specFormatRecords(rows, delimiter):
|
|
1681
|
+
"""``rows`` (``[(cells, marker), ...]``) as record lines, each with its '\\n'."""
|
|
1682
|
+
return ''.join(_specFormatRecord(cells, delimiter, marker=marker) + '\n' for cells, marker in rows)
|
|
1683
|
+
|
|
1684
|
+
|
|
1685
|
+
def _specTombstoneLines(key, delimiter, strip):
|
|
1686
|
+
"""The tombstone of ``key`` (spec §9.6) as lines joined by '\\n', without a last '\\n'."""
|
|
1687
|
+
return '\n'.join(_specFormatRecord(cells, delimiter, marker=marker)
|
|
1688
|
+
for cells, marker in _specTombstones([key], delimiter, strip))
|
|
1689
|
+
|
|
1690
|
+
|
|
1691
|
+
def _specStrippable(key, delimiter):
|
|
1692
|
+
"""True when a reader that strips (spec §7.7) would change ``key``: its encoded form ends in a space or tab."""
|
|
1693
|
+
return key.endswith((' ', '\t')) and _specEncodeField(key, delimiter, isKey=True).endswith((' ', '\t'))
|
|
1694
|
+
|
|
1695
|
+
|
|
1696
|
+
def _specNameKey(key, delimiter, strip, live):
|
|
1697
|
+
"""The key that a requested ``key`` names (spec §20.2.7) in a store whose stripping is ``strip``.
|
|
1698
|
+
|
|
1699
|
+
While stripping is on, a key a reader would strip names the live key equal
|
|
1700
|
+
to it (``live`` holds the live keys), else the key a reader resolves it to.
|
|
1701
|
+
"""
|
|
1702
|
+
if not strip or key in live or not _specStrippable(key, delimiter):
|
|
1703
|
+
return key
|
|
1704
|
+
return _specDecodeField(_specEncodeField(key, delimiter, isKey=True).rstrip(' \t'), delimiter)
|
|
1705
|
+
|
|
1706
|
+
|
|
1707
|
+
def _specNamesKeys(rows, delimiter):
|
|
1708
|
+
"""True when naming the keys of ``delete`` rows (``[(cells, marker), ...]``) needs the store (spec §20.2.7)."""
|
|
1709
|
+
return any(not marker and _specStrippable(cells[0], delimiter) for cells, marker in rows)
|
|
1710
|
+
|
|
1711
|
+
|
|
1712
|
+
def _specDeleteRows(rows, delimiter, strip, live):
|
|
1713
|
+
"""The batch ``delete`` appends for ``rows`` (``[(cells, marker), ...]``, spec §20.2, §20.3).
|
|
1714
|
+
|
|
1715
|
+
Every key names a key of the store as it was before the batch, whose
|
|
1716
|
+
stripping was ``strip`` and whose live keys are ``live`` (§20.2.7). The
|
|
1717
|
+
tombstone of a key a reader would strip is written with stripping off
|
|
1718
|
+
(§9.6). A reset of ``#_strip_trailing_whites_#`` in the batch turns
|
|
1719
|
+
stripping back on (§10.3, §12.1).
|
|
1720
|
+
"""
|
|
1721
|
+
batch = []
|
|
1722
|
+
named = strip
|
|
1723
|
+
guarded = False
|
|
1724
|
+
for cells, marker in rows:
|
|
1725
|
+
if marker:
|
|
1726
|
+
if cells[0].lower() == '#_strip_trailing_whites_#':
|
|
1727
|
+
strip, guarded = True, False
|
|
1728
|
+
batch.append((cells, marker))
|
|
1729
|
+
continue
|
|
1730
|
+
key = _specNameKey(cells[0], delimiter, named, live)
|
|
1731
|
+
if strip and not guarded and _specStrippable(key, delimiter):
|
|
1732
|
+
batch.append((['#_strip_trailing_whites_#', 'false'], True))
|
|
1733
|
+
guarded = True
|
|
1734
|
+
batch.append(([key], False))
|
|
1735
|
+
if guarded:
|
|
1736
|
+
batch.append((['#_strip_trailing_whites_#', 'true'], True))
|
|
1737
|
+
return batch
|
|
1738
|
+
|
|
1739
|
+
|
|
1657
1740
|
class _SpecState(object):
|
|
1658
1741
|
"""Reader state of spec §12, carried across records and parts."""
|
|
1659
1742
|
|
|
@@ -2604,16 +2687,23 @@ def _specBackupPart(path, reporter):
|
|
|
2604
2687
|
_lockFile(f)
|
|
2605
2688
|
try:
|
|
2606
2689
|
first = backup = '{}.damaged-{}'.format(path, time.strftime('%Y%m%dT%H%M%S'))
|
|
2690
|
+
# Readable by whoever could read the part, writable by its owner only, never setuid,
|
|
2691
|
+
# setgid, sticky or executable (the umask still applies on top).
|
|
2692
|
+
st = os.fstat(f.fileno())
|
|
2693
|
+
mode = st.st_mode & 0o644
|
|
2607
2694
|
suffix = 0
|
|
2608
2695
|
while True:
|
|
2609
2696
|
try:
|
|
2610
|
-
|
|
2697
|
+
# never overwrite an earlier backup; O_BINARY: no newline translation on Windows
|
|
2698
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, 'O_BINARY', 0)
|
|
2699
|
+
copy = os.fdopen(os.open(backup, flags, mode), 'wb')
|
|
2611
2700
|
break
|
|
2612
2701
|
except FileExistsError:
|
|
2613
2702
|
suffix += 1
|
|
2614
2703
|
backup = '{}.{}'.format(first, suffix)
|
|
2615
2704
|
try:
|
|
2616
2705
|
with copy:
|
|
2706
|
+
_matchOwner(copy.fileno(), st)
|
|
2617
2707
|
shutil.copyfileobj(f, copy)
|
|
2618
2708
|
copy.flush()
|
|
2619
2709
|
os.fsync(copy.fileno())
|
|
@@ -2672,6 +2762,22 @@ def _specSnapshot(fileName):
|
|
|
2672
2762
|
return parts, None
|
|
2673
2763
|
|
|
2674
2764
|
|
|
2765
|
+
def _matchOwner(fd, st):
|
|
2766
|
+
"""Give a new copy the owner and group of the file ``st`` describes. Where that is not allowed,
|
|
2767
|
+
a copy left in another group loses its group bits, so no one reads it who could not read the file."""
|
|
2768
|
+
if not hasattr(os, 'fchown'):
|
|
2769
|
+
return
|
|
2770
|
+
for uid in (st.st_uid, -1):
|
|
2771
|
+
try:
|
|
2772
|
+
os.fchown(fd, uid, st.st_gid)
|
|
2773
|
+
return
|
|
2774
|
+
except OSError:
|
|
2775
|
+
pass
|
|
2776
|
+
created = os.fstat(fd)
|
|
2777
|
+
if created.st_gid != st.st_gid:
|
|
2778
|
+
os.fchmod(fd, created.st_mode & 0o7707) # what the umask left, without the group bits
|
|
2779
|
+
|
|
2780
|
+
|
|
2675
2781
|
def _specRewriteInPlace(path, content, expect=None, memberSize=None):
|
|
2676
2782
|
"""Replace a part's bytes in place (same inode, spec §18.7 / §19.8).
|
|
2677
2783
|
|
|
@@ -2691,18 +2797,73 @@ def _specRewriteInPlace(path, content, expect=None, memberSize=None):
|
|
|
2691
2797
|
st = os.fstat(f.fileno())
|
|
2692
2798
|
if expect is not None and _specStamp(st) != expect:
|
|
2693
2799
|
return None
|
|
2694
|
-
|
|
2800
|
+
fd = f.fileno()
|
|
2801
|
+
# The whole part, for a failed rewrite to put back whenever it fails. Read unbuffered, as
|
|
2802
|
+
# the file is written below: a read-ahead in f would leave it a stale position to close at.
|
|
2803
|
+
old = _readFrom0(fd, st.st_size)
|
|
2804
|
+
if old == raw:
|
|
2695
2805
|
return False
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2806
|
+
try:
|
|
2807
|
+
_reserveSpace(fd, len(raw)) # spec §19.8: a full disk refuses the rewrite before any byte changes
|
|
2808
|
+
_writeFrom0(fd, raw)
|
|
2809
|
+
except BaseException as e:
|
|
2810
|
+
# Put the old bytes back rather than leave a half-rewritten part (or one a failed
|
|
2811
|
+
# reservation lengthened).
|
|
2812
|
+
try:
|
|
2813
|
+
_writeFrom0(fd, old)
|
|
2814
|
+
os.fsync(fd)
|
|
2815
|
+
except OSError as undo:
|
|
2816
|
+
damaged = '{}: the rewrite failed ({!r}) and the old bytes could not be put back ({}): the part is damaged'.format(
|
|
2817
|
+
path, e, undo)
|
|
2818
|
+
if isinstance(e, OSError):
|
|
2819
|
+
raise OSError(e.errno, damaged)
|
|
2820
|
+
_warn('TSVZ warning: ' + damaged) # an interrupt stays an interrupt
|
|
2821
|
+
raise
|
|
2822
|
+
os.fsync(fd)
|
|
2701
2823
|
return True
|
|
2702
2824
|
finally:
|
|
2825
|
+
try:
|
|
2826
|
+
_seekRawStart(f) # msvcrt unlocks from the raw position: the range it locked
|
|
2827
|
+
except OSError:
|
|
2828
|
+
pass
|
|
2703
2829
|
_unlockFile(f)
|
|
2704
2830
|
|
|
2705
2831
|
|
|
2832
|
+
def _reserveSpace(fd, length):
|
|
2833
|
+
"""Reserve ``length`` bytes from the start of the file before it is rewritten in place (spec
|
|
2834
|
+
§19.8). Where the platform or the file system cannot reserve space, nothing is reserved."""
|
|
2835
|
+
fallocate = getattr(os, 'posix_fallocate', None)
|
|
2836
|
+
if fallocate is None or length <= 0:
|
|
2837
|
+
return
|
|
2838
|
+
try:
|
|
2839
|
+
fallocate(fd, 0, length)
|
|
2840
|
+
except OSError as e:
|
|
2841
|
+
if e.errno not in (errno.EOPNOTSUPP, getattr(errno, 'ENOTSUP', errno.EOPNOTSUPP), errno.ENOSYS, errno.EINVAL):
|
|
2842
|
+
raise
|
|
2843
|
+
|
|
2844
|
+
|
|
2845
|
+
def _readFrom0(fd, length):
|
|
2846
|
+
"""Up to ``length`` bytes from the start of the file (unbuffered)."""
|
|
2847
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
2848
|
+
chunks = []
|
|
2849
|
+
while length > 0:
|
|
2850
|
+
chunk = os.read(fd, length)
|
|
2851
|
+
if not chunk:
|
|
2852
|
+
break
|
|
2853
|
+
chunks.append(chunk)
|
|
2854
|
+
length -= len(chunk)
|
|
2855
|
+
return b''.join(chunks)
|
|
2856
|
+
|
|
2857
|
+
|
|
2858
|
+
def _writeFrom0(fd, data):
|
|
2859
|
+
"""Write ``data`` over the file from offset 0 and cut the file there (unbuffered)."""
|
|
2860
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
2861
|
+
view = memoryview(data)
|
|
2862
|
+
while view:
|
|
2863
|
+
view = view[os.write(fd, view):]
|
|
2864
|
+
os.ftruncate(fd, len(data))
|
|
2865
|
+
|
|
2866
|
+
|
|
2706
2867
|
def _specPreamble(state, delimiter, compensate=False, version=True):
|
|
2707
2868
|
"""Marker lines that re-create ``state`` (spec §19.2.3a): version, non-default markers, defaults last.
|
|
2708
2869
|
|
|
@@ -2905,7 +3066,7 @@ def _specClearTabularFile(fileName, teeLogger=None, header='', verifyHeader=Fals
|
|
|
2905
3066
|
attempt.note('refused', None, 'clear skipped: a part of the store could not be read; nothing was written')
|
|
2906
3067
|
return False
|
|
2907
3068
|
if len(load.parts) > 1:
|
|
2908
|
-
tombstones =
|
|
3069
|
+
tombstones = _specFormatRecords(_specTombstones(load.data, delimiter, load.state.strip), delimiter)
|
|
2909
3070
|
if tombstones:
|
|
2910
3071
|
_specAppendPayload(load.active, tombstones.encode('utf-8'), attempt, memberSize=load.state.memberSize)
|
|
2911
3072
|
attempt.note('multipart', None, 'cleared a multi-part store by appending tombstones; older parts are not compacted')
|
|
@@ -3517,14 +3678,37 @@ class TSVZed(OrderedDict):
|
|
|
3517
3678
|
# else:
|
|
3518
3679
|
# self.appendEvent.set()
|
|
3519
3680
|
|
|
3681
|
+
def _nameKey(self, key):
|
|
3682
|
+
"""The key a requested ``key`` names: the live key equal to it, else ``key`` right-stripped (spec §20.2.7).
|
|
3683
|
+
|
|
3684
|
+
The fallback strips every trailing whitespace whatever the stripping
|
|
3685
|
+
marker says, as ``__setitem__`` strips every key it writes.
|
|
3686
|
+
"""
|
|
3687
|
+
key = str(key)
|
|
3688
|
+
return key if OrderedDict.__contains__(self, key) else key.rstrip()
|
|
3689
|
+
|
|
3520
3690
|
def __getitem__(self, key):
|
|
3521
|
-
|
|
3522
|
-
|
|
3691
|
+
key = str(key)
|
|
3692
|
+
row = dict.get(self, key, _NO_ROW) # one probe for the usual hit
|
|
3693
|
+
return row if row is not _NO_ROW else OrderedDict.__getitem__(self, key.rstrip())
|
|
3694
|
+
|
|
3695
|
+
def __contains__(self, key):
|
|
3696
|
+
# OrderedDict.setdefault asks this before it reads or writes, so it names keys as t[key] does.
|
|
3697
|
+
key = str(key)
|
|
3698
|
+
return dict.__contains__(self, key) or dict.__contains__(self, key.rstrip())
|
|
3699
|
+
|
|
3700
|
+
def get(self, key, default=None):
|
|
3701
|
+
key = str(key)
|
|
3702
|
+
row = dict.get(self, key, _NO_ROW)
|
|
3703
|
+
if row is _NO_ROW:
|
|
3704
|
+
row = dict.get(self, key.rstrip(), _NO_ROW)
|
|
3705
|
+
return default if row is _NO_ROW else row
|
|
3706
|
+
|
|
3523
3707
|
|
|
3524
3708
|
def __delitem__(self,key):
|
|
3525
3709
|
if self._spec:
|
|
3526
3710
|
return self._specDelItem(key)
|
|
3527
|
-
key =
|
|
3711
|
+
key = self._nameKey(key)
|
|
3528
3712
|
if key == DEFAULTS_INDICATOR_KEY:
|
|
3529
3713
|
self.defaults = [DEFAULTS_INDICATOR_KEY]
|
|
3530
3714
|
if self.verbose:
|
|
@@ -3558,8 +3742,8 @@ class TSVZed(OrderedDict):
|
|
|
3558
3742
|
# the mapping by the caller, so self[key] must not be accessed.
|
|
3559
3743
|
self.dirty = True
|
|
3560
3744
|
if self._spec:
|
|
3561
|
-
# Spec §9.2: a tombstone is the lone key, never padded.
|
|
3562
|
-
self.appendQueue.append(
|
|
3745
|
+
# Spec §9.2: a tombstone is the lone key, never padded. One queue item, so it is committed whole.
|
|
3746
|
+
self.appendQueue.append(_specTombstoneLines(key, self.delimiter, self._specState.strip))
|
|
3563
3747
|
return self
|
|
3564
3748
|
if self.correctColumnNum > 1:
|
|
3565
3749
|
emptyLine = [key]+['']*(self.correctColumnNum-1)
|
|
@@ -3628,7 +3812,7 @@ class TSVZed(OrderedDict):
|
|
|
3628
3812
|
self.lastUpdateTime = get_time_ns()
|
|
3629
3813
|
|
|
3630
3814
|
def _specDelItem(self, key):
|
|
3631
|
-
key =
|
|
3815
|
+
key = self._nameKey(key)
|
|
3632
3816
|
if _MARKER_RE.match(key):
|
|
3633
3817
|
# A lone marker key resets that marker (spec §12.1).
|
|
3634
3818
|
self._specSetMarker(key, [key])
|
|
@@ -3781,7 +3965,15 @@ memoryOnly:{self.memoryOnly}
|
|
|
3781
3965
|
return self.close()
|
|
3782
3966
|
|
|
3783
3967
|
def popitem(self, last=True):
|
|
3784
|
-
|
|
3968
|
+
if sys.version_info >= (3, 11):
|
|
3969
|
+
key, value = OrderedDict.popitem(self, last)
|
|
3970
|
+
else:
|
|
3971
|
+
# Before Python 3.11 OrderedDict.popitem goes through self[key] and del self[key], which strip the key.
|
|
3972
|
+
if not OrderedDict.__len__(self):
|
|
3973
|
+
raise KeyError('dictionary is empty')
|
|
3974
|
+
key = next(OrderedDict.__reversed__(self)) if last else next(OrderedDict.__iter__(self))
|
|
3975
|
+
value = OrderedDict.__getitem__(self, key)
|
|
3976
|
+
OrderedDict.__delitem__(self, key)
|
|
3785
3977
|
if not self.memoryOnly:
|
|
3786
3978
|
self.__appendEmptyLine(key)
|
|
3787
3979
|
self.lastUpdateTime = get_time_ns()
|
|
@@ -3795,12 +3987,14 @@ memoryOnly:{self.memoryOnly}
|
|
|
3795
3987
|
is raised.
|
|
3796
3988
|
|
|
3797
3989
|
'''
|
|
3798
|
-
key =
|
|
3799
|
-
if
|
|
3990
|
+
key = self._nameKey(key)
|
|
3991
|
+
if not OrderedDict.__contains__(self, key):
|
|
3800
3992
|
if default is self.__marker:
|
|
3801
3993
|
raise KeyError(key)
|
|
3802
3994
|
return default
|
|
3803
|
-
|
|
3995
|
+
# Not OrderedDict.pop: before Python 3.11 it goes through del self[key], which writes a tombstone too.
|
|
3996
|
+
value = OrderedDict.__getitem__(self, key)
|
|
3997
|
+
OrderedDict.__delitem__(self, key)
|
|
3804
3998
|
if not self.memoryOnly:
|
|
3805
3999
|
self.__appendEmptyLine(key)
|
|
3806
4000
|
self.lastUpdateTime = get_time_ns()
|
|
@@ -3810,7 +4004,7 @@ memoryOnly:{self.memoryOnly}
|
|
|
3810
4004
|
'''Move an existing element to the end (or beginning if last is false).
|
|
3811
4005
|
Raise KeyError if the element does not exist.
|
|
3812
4006
|
'''
|
|
3813
|
-
key =
|
|
4007
|
+
key = self._nameKey(key)
|
|
3814
4008
|
super().move_to_end(key, last)
|
|
3815
4009
|
if self._spec:
|
|
3816
4010
|
_warnOnce(self, 'move_to_end', self._fileName,
|
|
@@ -4333,9 +4527,18 @@ class TSVZedLite(MutableMapping):
|
|
|
4333
4527
|
"""'tsvz' for tsvz-spec-v1 files (.tsvz/.csvz/.nsvz/.psvz), 'tsv' for 3.39-format files."""
|
|
4334
4528
|
return 'tsvz' if self._spec else 'tsv'
|
|
4335
4529
|
|
|
4530
|
+
def _nameKey(self, key):
|
|
4531
|
+
"""The key a requested ``key`` names: the live key equal to it, else ``key`` right-stripped (spec §20.2.7).
|
|
4532
|
+
|
|
4533
|
+
The fallback strips every trailing whitespace whatever the stripping
|
|
4534
|
+
marker says, as ``__setitem__`` strips every key it writes.
|
|
4535
|
+
"""
|
|
4536
|
+
key = str(key)
|
|
4537
|
+
return key if key in self.indexes else key.rstrip()
|
|
4538
|
+
|
|
4336
4539
|
def __contains__(self, key):
|
|
4337
4540
|
if self._spec:
|
|
4338
|
-
return
|
|
4541
|
+
return self._nameKey(key) in self.indexes
|
|
4339
4542
|
return MutableMapping.__contains__(self, key)
|
|
4340
4543
|
|
|
4341
4544
|
def get(self, key, default=None):
|
|
@@ -4486,7 +4689,7 @@ class TSVZedLite(MutableMapping):
|
|
|
4486
4689
|
self._specWrite(_specFormatRecord(value, self.delimiter, marker=True))
|
|
4487
4690
|
|
|
4488
4691
|
def _specWrite(self, line):
|
|
4489
|
-
"""Append one record to the active part; return its offset, or None for a compressed part."""
|
|
4692
|
+
"""Append one record (or lines written together) to the active part; return its offset, or None for a compressed part."""
|
|
4490
4693
|
data = (line + '\n').encode('utf-8', errors='replace')
|
|
4491
4694
|
if self.fileObj is None:
|
|
4492
4695
|
reporter = _Reporter(self._activePath)
|
|
@@ -4659,7 +4862,7 @@ class TSVZedLite(MutableMapping):
|
|
|
4659
4862
|
|
|
4660
4863
|
def __mapDeleteToFile(self,key):
|
|
4661
4864
|
if self._spec:
|
|
4662
|
-
self._specWrite(
|
|
4865
|
+
self._specWrite(_specTombstoneLines(key, self.delimiter, self._specState.strip))
|
|
4663
4866
|
return
|
|
4664
4867
|
# Persist a deletion by appending a tombstone (a lone key with no
|
|
4665
4868
|
# values); on read such a row deletes the key (see _processLine). The
|
|
@@ -4713,7 +4916,7 @@ class TSVZedLite(MutableMapping):
|
|
|
4713
4916
|
|
|
4714
4917
|
# Implement basic __getitem__, __setitem__, __delitem__, __iter__, and __len__. needed for MutableMapping
|
|
4715
4918
|
def __getitem__(self,key):
|
|
4716
|
-
key =
|
|
4919
|
+
key = self._nameKey(key)
|
|
4717
4920
|
if key not in self.indexes:
|
|
4718
4921
|
if key == DEFAULTS_INDICATOR_KEY:
|
|
4719
4922
|
return self.defaults
|
|
@@ -4792,7 +4995,7 @@ class TSVZedLite(MutableMapping):
|
|
|
4792
4995
|
self.indexes[key] = self.__writeValues(value)
|
|
4793
4996
|
|
|
4794
4997
|
def __delitem__(self,key):
|
|
4795
|
-
key =
|
|
4998
|
+
key = self._nameKey(key)
|
|
4796
4999
|
if self._spec:
|
|
4797
5000
|
if _MARKER_RE.match(key):
|
|
4798
5001
|
# A lone marker key resets that marker (spec §12.1).
|
|
@@ -4803,7 +5006,7 @@ class TSVZedLite(MutableMapping):
|
|
|
4803
5006
|
eprint(f"Key {key} not found")
|
|
4804
5007
|
return
|
|
4805
5008
|
self.indexes.pop(key, None)
|
|
4806
|
-
self.
|
|
5009
|
+
self.__mapDeleteToFile(key)
|
|
4807
5010
|
return
|
|
4808
5011
|
if key == DEFAULTS_INDICATOR_KEY:
|
|
4809
5012
|
self.__mapDeleteToFile(key)
|
|
@@ -4850,7 +5053,7 @@ class TSVZedLite(MutableMapping):
|
|
|
4850
5053
|
|
|
4851
5054
|
__marker = object()
|
|
4852
5055
|
def pop(self, key, default=__marker, return_pos = False):
|
|
4853
|
-
key =
|
|
5056
|
+
key = self._nameKey(key)
|
|
4854
5057
|
try:
|
|
4855
5058
|
pos = self.indexes.pop(key)
|
|
4856
5059
|
except KeyError:
|
|
@@ -5311,9 +5514,9 @@ class _ServeStore(object):
|
|
|
5311
5514
|
return not self.parts
|
|
5312
5515
|
|
|
5313
5516
|
def resolve(self, key):
|
|
5314
|
-
"""
|
|
5517
|
+
"""The key a requested key names (spec §20.2.7; 3.39 rstrip for loose files)."""
|
|
5315
5518
|
if self.spec:
|
|
5316
|
-
return key.
|
|
5519
|
+
return _specNameKey(key, self.delimiter, self.state.strip, self.data)
|
|
5317
5520
|
return key.rstrip()
|
|
5318
5521
|
|
|
5319
5522
|
def read(self, log):
|
|
@@ -5380,11 +5583,14 @@ class _ServeStore(object):
|
|
|
5380
5583
|
def _payload(self, rows, log):
|
|
5381
5584
|
"""Bytes for ``rows`` (``[(cells, marker), ...]``), formatted as the direct writers format them."""
|
|
5382
5585
|
if self.spec:
|
|
5383
|
-
return
|
|
5384
|
-
for cells, marker in rows).encode('utf-8')
|
|
5586
|
+
return _specFormatRecords(rows, self.delimiter).encode('utf-8')
|
|
5385
5587
|
return _legacyFormatPayload(self.path, [cells for cells, marker in rows], log, [''], True, self.verbose,
|
|
5386
5588
|
'utf8', self.strict, self.delimiter)[0]
|
|
5387
5589
|
|
|
5590
|
+
def _tombstone(self, key):
|
|
5591
|
+
"""Rows that delete ``key``: written with stripping off when it would strip ``key`` (spec §9.6)."""
|
|
5592
|
+
return _specTombstones([key], self.delimiter, self.state.strip) if self.spec else [([key], False)]
|
|
5593
|
+
|
|
5388
5594
|
def _durable(self, sync):
|
|
5389
5595
|
return sync or (self.spec and self.state.writeAck == 'disk')
|
|
5390
5596
|
|
|
@@ -5406,10 +5612,27 @@ class _ServeStore(object):
|
|
|
5406
5612
|
self.writer.wait(queued[0])
|
|
5407
5613
|
|
|
5408
5614
|
def write(self, rows, sync, log, written=False):
|
|
5409
|
-
"""Append ``rows`` (``[(cells, marker), ...]``) as one batch: ``set
|
|
5615
|
+
"""Append ``rows`` (``[(cells, marker), ...]``) as one batch: ``set`` and its bulk input."""
|
|
5616
|
+
with self.lock:
|
|
5617
|
+
queued = self._submit(rows, sync, log)
|
|
5618
|
+
self._ack(queued, written)
|
|
5619
|
+
|
|
5620
|
+
def delete(self, rows, sync, log, written=False):
|
|
5621
|
+
"""Append the tombstones of ``delete`` (``[(cells, marker), ...]``) as one batch (spec §20.2.7).
|
|
5622
|
+
|
|
5623
|
+
Naming a key a reader would strip needs the store as it is now, read
|
|
5624
|
+
under the lock, so that delete is atomic (spec §21.8.2). Returns False,
|
|
5625
|
+
having written nothing, when that read could not be completed.
|
|
5626
|
+
"""
|
|
5410
5627
|
with self.lock:
|
|
5628
|
+
if self.spec and _specNamesKeys(rows, self.delimiter):
|
|
5629
|
+
self._fresh(log)
|
|
5630
|
+
if self.unreadable:
|
|
5631
|
+
return False
|
|
5632
|
+
rows = _specDeleteRows(rows, self.delimiter, self.state.strip, self.data)
|
|
5411
5633
|
queued = self._submit(rows, sync, log)
|
|
5412
5634
|
self._ack(queued, written)
|
|
5635
|
+
return True
|
|
5413
5636
|
|
|
5414
5637
|
def pop(self, key, sync, log):
|
|
5415
5638
|
"""Atomically return the row of ``key`` and append its tombstone; None when it is missing."""
|
|
@@ -5422,7 +5645,7 @@ class _ServeStore(object):
|
|
|
5422
5645
|
if row is None:
|
|
5423
5646
|
return None
|
|
5424
5647
|
row = list(row)
|
|
5425
|
-
queued = self._submit(
|
|
5648
|
+
queued = self._submit(self._tombstone(key), sync, log)
|
|
5426
5649
|
self._ack(queued, written=True) # the row is only popped once its tombstone is written
|
|
5427
5650
|
return row
|
|
5428
5651
|
|
|
@@ -5436,7 +5659,7 @@ class _ServeStore(object):
|
|
|
5436
5659
|
return None
|
|
5437
5660
|
key = next(reversed(self.data)) if last else next(iter(self.data))
|
|
5438
5661
|
row = list(self.data[key])
|
|
5439
|
-
queued = self._submit(
|
|
5662
|
+
queued = self._submit(self._tombstone(key), sync, log)
|
|
5440
5663
|
self._ack(queued, written=True)
|
|
5441
5664
|
return row
|
|
5442
5665
|
|
|
@@ -5621,21 +5844,32 @@ def _serveDispatch(store, line, bulk, log):
|
|
|
5621
5844
|
if operation in _SERVE_NEED_STORE and store.missing(log):
|
|
5622
5845
|
return 1, [], 'no such store' # spec §20.2.2
|
|
5623
5846
|
unreadable = (1, _SERVE_UNREADABLE)
|
|
5847
|
+
# Each read takes its status under the store lock it read under: another request's reload may
|
|
5848
|
+
# change ``unreadable`` once the lock is free.
|
|
5624
5849
|
if operation == 'read':
|
|
5625
|
-
|
|
5626
|
-
|
|
5850
|
+
with store.lock:
|
|
5851
|
+
rows = store.read(log)
|
|
5852
|
+
status = unreadable if store.unreadable else (0, '')
|
|
5627
5853
|
return status[0], [_specFormatRecord(row, '\t') for row in rows], status[1]
|
|
5628
5854
|
if operation == 'get':
|
|
5629
|
-
|
|
5630
|
-
|
|
5855
|
+
with store.lock:
|
|
5856
|
+
rows, missing = store.get(args, log)
|
|
5857
|
+
status = unreadable if store.unreadable else (3 if missing else 0, '')
|
|
5631
5858
|
return status[0], [_specFormatRecord(row, '\t') for row in rows], status[1]
|
|
5632
5859
|
if operation == 'has':
|
|
5633
|
-
|
|
5634
|
-
|
|
5860
|
+
with store.lock:
|
|
5861
|
+
found = store.has(args[0], log)
|
|
5862
|
+
return (1, [], _SERVE_UNREADABLE) if store.unreadable else (0 if found else 3, [], '')
|
|
5635
5863
|
if operation == 'len':
|
|
5636
|
-
|
|
5864
|
+
with store.lock:
|
|
5865
|
+
count = store.length(log)
|
|
5866
|
+
status = unreadable if store.unreadable else (0, '') # spec §4.10, §20.6: a stopped read failed
|
|
5867
|
+
return status[0], [str(count)], status[1]
|
|
5637
5868
|
if operation == 'keys':
|
|
5638
|
-
|
|
5869
|
+
with store.lock:
|
|
5870
|
+
keys = store.keys(log)
|
|
5871
|
+
status = unreadable if store.unreadable else (0, '')
|
|
5872
|
+
return status[0], [_specEncodeField(key, '\t', isKey=True) for key in keys], status[1]
|
|
5639
5873
|
if operation in ('set', 'append', 'delete'):
|
|
5640
5874
|
keysOnly = operation == 'delete'
|
|
5641
5875
|
if isBulk:
|
|
@@ -5648,7 +5882,10 @@ def _serveDispatch(store, line, bulk, log):
|
|
|
5648
5882
|
rows = [([key], bool(_MARKER_RE.match(field))) for key, field in zip(args, raw)]
|
|
5649
5883
|
else:
|
|
5650
5884
|
rows = [(args, bool(_MARKER_RE.match(raw[0])))]
|
|
5651
|
-
|
|
5885
|
+
if not keysOnly:
|
|
5886
|
+
store.write(rows, sync, log, written)
|
|
5887
|
+
elif not store.delete(rows, sync, log, written):
|
|
5888
|
+
return 1, [], _SERVE_UNREADABLE + '; nothing was written'
|
|
5652
5889
|
return 0, [], ''
|
|
5653
5890
|
if operation in ('pop', 'popitem'):
|
|
5654
5891
|
if operation == 'pop':
|
|
@@ -6349,7 +6586,10 @@ class TSVZClient(MutableMapping):
|
|
|
6349
6586
|
|
|
6350
6587
|
def __delitem__(self, key):
|
|
6351
6588
|
"""Delete ``key``; a missing key is not an error (as in ``TSVZed``)."""
|
|
6352
|
-
|
|
6589
|
+
key = str(key)
|
|
6590
|
+
# Resending the delete of a key ending in whitespace could name, and delete, the stripped key (spec §20.2.7).
|
|
6591
|
+
retry = not (self._spec and _specStrippable(key, self.delimiter))
|
|
6592
|
+
self._request(['delete', _serveEncode(key)], write=True, retry=retry)
|
|
6353
6593
|
|
|
6354
6594
|
_marker = object()
|
|
6355
6595
|
|
|
@@ -6375,6 +6615,11 @@ class TSVZClient(MutableMapping):
|
|
|
6375
6615
|
cells = self._cells(key, default)
|
|
6376
6616
|
if len(cells) < 2:
|
|
6377
6617
|
return self.get(key)
|
|
6618
|
+
if str(key) != cells[0]:
|
|
6619
|
+
# cells[0] is stripped as set strips it; a live key equal to ``key`` wins first (spec §20.2.7).
|
|
6620
|
+
row = self.get(key)
|
|
6621
|
+
if row is not None:
|
|
6622
|
+
return row
|
|
6378
6623
|
lines = self._request(['setdefault'] + self._fields(cells), write=True, retry=False)[0]
|
|
6379
6624
|
return _serveDecodeLine(lines[0]) if lines else None
|
|
6380
6625
|
|
|
@@ -6820,7 +7065,7 @@ def _cliRead(args, delimiter, logger, stdin, stdout):
|
|
|
6820
7065
|
def _cliGet(args, delimiter, logger, stdin, stdout):
|
|
6821
7066
|
"""``get STORE KEY ...`` (spec §20.2): each KEY's row in argument order; exit 3 when one is missing.
|
|
6822
7067
|
|
|
6823
|
-
|
|
7068
|
+
Each KEY names a key as §20.2.7 says. For a missing key of a
|
|
6824
7069
|
spec store this prints what ``TSVZed`` returns for it (§14.5): the key and
|
|
6825
7070
|
the active defaults while ``#_return_defaults_when_missing_#`` is true,
|
|
6826
7071
|
else nothing. A missing key of a loose file prints nothing.
|
|
@@ -6839,7 +7084,7 @@ def _cliGet(args, delimiter, logger, stdin, stdout):
|
|
|
6839
7084
|
missing = False
|
|
6840
7085
|
for key in args.args:
|
|
6841
7086
|
if spec:
|
|
6842
|
-
key = key
|
|
7087
|
+
key = _specNameKey(key, delimiter, state.strip, data)
|
|
6843
7088
|
else:
|
|
6844
7089
|
key = key.rstrip()
|
|
6845
7090
|
row = data.get(key)
|
|
@@ -6885,15 +7130,20 @@ def _cliStdinBatch(args, delimiter, logger, stdin, keysOnly):
|
|
|
6885
7130
|
so is a line with an empty key (reported). ``keysOnly`` keeps the first
|
|
6886
7131
|
field only: a tombstone, or a marker reset.
|
|
6887
7132
|
"""
|
|
6888
|
-
|
|
7133
|
+
rows = _cliStdinRows(args, delimiter, logger, stdin, keysOnly)
|
|
7134
|
+
if not _isSpecPath(args.store):
|
|
7135
|
+
return [cells for cells, marker in rows]
|
|
7136
|
+
return [_specFormatRecord(cells, delimiter, marker=marker) for cells, marker in rows]
|
|
7137
|
+
|
|
7138
|
+
|
|
7139
|
+
def _cliStdinRows(args, delimiter, logger, stdin, keysOnly):
|
|
7140
|
+
"""Standard input for ``set STORE -`` / ``delete STORE -`` as ``[(cells, marker), ...]`` (spec §20.3)."""
|
|
6889
7141
|
source = _Reporter('<stdin>', logger)
|
|
6890
7142
|
try:
|
|
6891
|
-
|
|
7143
|
+
return _parseRecordLines(_cliStdinLines(stdin, source), delimiter, _isSpecPath(args.store), keysOnly,
|
|
7144
|
+
source)
|
|
6892
7145
|
finally:
|
|
6893
7146
|
source.flush()
|
|
6894
|
-
if not spec:
|
|
6895
|
-
return [cells for cells, marker in rows]
|
|
6896
|
-
return [_specFormatRecord(cells, delimiter, marker=marker) for cells, marker in rows]
|
|
6897
7147
|
|
|
6898
7148
|
|
|
6899
7149
|
def _parseRecordLines(texts, delimiter, spec, keysOnly, reporter):
|
|
@@ -6958,12 +7208,33 @@ def _cliDelete(args, delimiter, logger, stdin, stdout):
|
|
|
6958
7208
|
"""``delete STORE KEY [KEY ...]`` (spec §20.2): one tombstone per KEY, appended in one write.
|
|
6959
7209
|
|
|
6960
7210
|
``delete STORE -`` deletes the first field of every line on standard input (§20.3).
|
|
7211
|
+
A key that a reader would strip names a key of the store (§20.2.7), so the
|
|
7212
|
+
store is read first; when it cannot be read whole nothing is written and
|
|
7213
|
+
the exit status is 1.
|
|
6961
7214
|
"""
|
|
6962
|
-
if args.
|
|
6963
|
-
|
|
7215
|
+
if not _isSpecPath(args.store):
|
|
7216
|
+
if args.args == ['-']:
|
|
7217
|
+
_cliAppendBatch(args, delimiter, logger, _cliStdinBatch(args, delimiter, logger, stdin, keysOnly=True))
|
|
7218
|
+
else:
|
|
7219
|
+
appendLinesTabularFile(args.store, [[key] for key in args.args], teeLogger=logger, header=args.header,
|
|
7220
|
+
createIfNotExist=True, verbose=args.verbose, strict=args.strict,
|
|
7221
|
+
delimiter=delimiter)
|
|
6964
7222
|
return _cliWritten(args, logger)
|
|
6965
|
-
|
|
6966
|
-
|
|
7223
|
+
if args.args == ['-']:
|
|
7224
|
+
rows = _cliStdinRows(args, delimiter, logger, stdin, keysOnly=True)
|
|
7225
|
+
else:
|
|
7226
|
+
rows = [([key], bool(_MARKER_RE.match(key))) for key in args.args]
|
|
7227
|
+
if _specNamesKeys(rows, delimiter):
|
|
7228
|
+
strip, live = True, {} # a store delete creates starts with stripping on (spec §10.3)
|
|
7229
|
+
if _cliStoreExists(args.store):
|
|
7230
|
+
load, readable = _cliSpecLoad(args, delimiter, logger)
|
|
7231
|
+
if not readable:
|
|
7232
|
+
logger.teelog('tsvz: {}: could not read every part of the store; nothing was written'.format(
|
|
7233
|
+
args.store), 'error')
|
|
7234
|
+
return 1
|
|
7235
|
+
strip, live = load.state.strip, load.data
|
|
7236
|
+
rows = _specDeleteRows(rows, delimiter, strip, live)
|
|
7237
|
+
_cliAppendBatch(args, delimiter, logger, [_specFormatRecord(cells, delimiter, marker=marker) for cells, marker in rows])
|
|
6967
7238
|
return _cliWritten(args, logger)
|
|
6968
7239
|
|
|
6969
7240
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tsvz-4.5 → tsvz-4.7}/setup.cfg
RENAMED
|
File without changes
|
{tsvz-4.5 → tsvz-4.7}/setup.py
RENAMED
|
File without changes
|