tinykv 0.3.0__tar.gz → 0.4.0__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.
- {tinykv-0.3.0 → tinykv-0.4.0}/PKG-INFO +16 -1
- {tinykv-0.3.0 → tinykv-0.4.0}/README.md +15 -0
- {tinykv-0.3.0 → tinykv-0.4.0}/src/tinykv/__init__.py +140 -29
- {tinykv-0.3.0 → tinykv-0.4.0}/LICENSE +0 -0
- {tinykv-0.3.0 → tinykv-0.4.0}/pyproject.toml +0 -0
- {tinykv-0.3.0 → tinykv-0.4.0}/src/tinykv/__init__.py~ +0 -0
- {tinykv-0.3.0 → tinykv-0.4.0}/src/tinykv/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tinykv
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A lightweight Python SQLite key-value store built on sqlite3.
|
|
5
5
|
Keywords: python,sqlite,sqlite3,key-value store,kv
|
|
6
6
|
Author-email: Flavio Veloso Soares <flaviovs@magnux.com>
|
|
@@ -97,6 +97,21 @@ database, respectively:
|
|
|
97
97
|
>>> kv.get('foo')
|
|
98
98
|
'bar'
|
|
99
99
|
|
|
100
|
+
Use `inc()`, `dec()`, and `add()` to update integer counters atomically:
|
|
101
|
+
|
|
102
|
+
>>> kv.inc('visits')
|
|
103
|
+
1
|
|
104
|
+
>>> kv.inc('visits', 2)
|
|
105
|
+
3
|
|
106
|
+
>>> kv.dec('visits')
|
|
107
|
+
2
|
|
108
|
+
>>> kv.add('visits', -5)
|
|
109
|
+
-3
|
|
110
|
+
|
|
111
|
+
Missing keys start at zero. Amounts must be integers (not booleans), and
|
|
112
|
+
`inc()` and `dec()` require nonnegative amounts. An existing value must be an
|
|
113
|
+
integer; otherwise the operation raises `TypeError`.
|
|
114
|
+
|
|
100
115
|
Trying to get a nonexistent key raises _KeyError_:
|
|
101
116
|
|
|
102
117
|
>>> kv.get('not-there')
|
|
@@ -79,6 +79,21 @@ database, respectively:
|
|
|
79
79
|
>>> kv.get('foo')
|
|
80
80
|
'bar'
|
|
81
81
|
|
|
82
|
+
Use `inc()`, `dec()`, and `add()` to update integer counters atomically:
|
|
83
|
+
|
|
84
|
+
>>> kv.inc('visits')
|
|
85
|
+
1
|
|
86
|
+
>>> kv.inc('visits', 2)
|
|
87
|
+
3
|
|
88
|
+
>>> kv.dec('visits')
|
|
89
|
+
2
|
|
90
|
+
>>> kv.add('visits', -5)
|
|
91
|
+
-3
|
|
92
|
+
|
|
93
|
+
Missing keys start at zero. Amounts must be integers (not booleans), and
|
|
94
|
+
`inc()` and `dec()` require nonnegative amounts. An existing value must be an
|
|
95
|
+
integer; otherwise the operation raises `TypeError`.
|
|
96
|
+
|
|
82
97
|
Trying to get a nonexistent key raises _KeyError_:
|
|
83
98
|
|
|
84
99
|
>>> kv.get('not-there')
|
|
@@ -8,6 +8,7 @@ import pickle
|
|
|
8
8
|
import re
|
|
9
9
|
import sqlite3
|
|
10
10
|
from collections.abc import Iterable, Mapping
|
|
11
|
+
from types import TracebackType
|
|
11
12
|
from typing import Any
|
|
12
13
|
|
|
13
14
|
_TABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*\Z")
|
|
@@ -54,15 +55,45 @@ def _validate_key(key: str) -> None:
|
|
|
54
55
|
raise ValueError("key must not be empty")
|
|
55
56
|
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
def _validate_int(value: Any, name: str) -> None: # noqa: ANN401
|
|
59
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
60
|
+
raise TypeError(
|
|
61
|
+
f"{name} must be an integer, got {type(value).__name__}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
__version__ = "0.4.0"
|
|
58
66
|
|
|
59
67
|
_DEF_TABLE = "kv"
|
|
60
68
|
_NAN_PICKLE = pickle.dumps(float("nan"))
|
|
61
69
|
_MISSING = object()
|
|
70
|
+
_SAVEPOINT_IDS = itertools.count()
|
|
62
71
|
|
|
63
72
|
logger = logging.getLogger(__name__)
|
|
64
73
|
|
|
65
74
|
|
|
75
|
+
class _Savepoint:
|
|
76
|
+
"""Manage an atomic operation using a uniquely named savepoint."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, conn: sqlite3.Connection) -> None:
|
|
79
|
+
self._conn = conn
|
|
80
|
+
self._name = f"__tinykv_{next(_SAVEPOINT_IDS)}"
|
|
81
|
+
|
|
82
|
+
def __enter__(self) -> "_Savepoint":
|
|
83
|
+
self._conn.execute(f"SAVEPOINT {self._name}")
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def __exit__(
|
|
87
|
+
self,
|
|
88
|
+
exc_type: type[BaseException] | None,
|
|
89
|
+
exc_value: BaseException | None,
|
|
90
|
+
traceback: TracebackType | None,
|
|
91
|
+
) -> None:
|
|
92
|
+
if exc_type is not None:
|
|
93
|
+
self._conn.execute(f"ROLLBACK TO {self._name}")
|
|
94
|
+
self._conn.execute(f"RELEASE {self._name}")
|
|
95
|
+
|
|
96
|
+
|
|
66
97
|
def create_schema(
|
|
67
98
|
conn: sqlite3.Connection,
|
|
68
99
|
table: str = _DEF_TABLE,
|
|
@@ -293,6 +324,94 @@ class TinyKV:
|
|
|
293
324
|
(key, dtype, data),
|
|
294
325
|
)
|
|
295
326
|
|
|
327
|
+
def add(self, key: str, n: int) -> int:
|
|
328
|
+
"""Add an integer amount to the value stored under ``key``.
|
|
329
|
+
|
|
330
|
+
If the key does not exist, it is treated as having value zero.
|
|
331
|
+
Existing values must be integers; booleans and other types are not
|
|
332
|
+
accepted. The operation is atomic and returns the new value.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
key: The key to update.
|
|
336
|
+
n: A signed integer amount to add.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
The updated integer value.
|
|
340
|
+
|
|
341
|
+
Raises:
|
|
342
|
+
TypeError: If ``key`` or ``n`` has an invalid type, or if the
|
|
343
|
+
existing value is not an integer.
|
|
344
|
+
ValueError: If ``key`` is empty or the stored row is malformed.
|
|
345
|
+
|
|
346
|
+
"""
|
|
347
|
+
_validate_key(key)
|
|
348
|
+
_validate_int(n, "n")
|
|
349
|
+
|
|
350
|
+
assert self._conn
|
|
351
|
+
with _Savepoint(self._conn):
|
|
352
|
+
cur = self._conn.execute(
|
|
353
|
+
f"SELECT t, v FROM {self._quoted_table} WHERE k = ?", (key,)
|
|
354
|
+
)
|
|
355
|
+
row = cur.fetchone()
|
|
356
|
+
if row is None:
|
|
357
|
+
value = 0
|
|
358
|
+
else:
|
|
359
|
+
value = self._decode_row(row[0], row[1])
|
|
360
|
+
_validate_int(value, f"value for key {key!r}")
|
|
361
|
+
|
|
362
|
+
result = value + n
|
|
363
|
+
dtype, data = self._serialize(result)
|
|
364
|
+
self._conn.execute(
|
|
365
|
+
f"INSERT OR REPLACE INTO {self._quoted_table} (k, t, v) "
|
|
366
|
+
"VALUES (?, ?, ?)",
|
|
367
|
+
(key, dtype, data),
|
|
368
|
+
)
|
|
369
|
+
return result
|
|
370
|
+
|
|
371
|
+
def inc(self, key: str, n: int = 1) -> int:
|
|
372
|
+
"""Increment the integer value stored under ``key`` by ``n``.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
key: The key to update.
|
|
376
|
+
n: A nonnegative integer amount to add.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
The updated integer value.
|
|
380
|
+
|
|
381
|
+
Raises:
|
|
382
|
+
TypeError: If ``key`` or ``n`` has an invalid type, or if the
|
|
383
|
+
existing value is not an integer.
|
|
384
|
+
ValueError: If ``key`` is empty, ``n`` is negative, or the stored
|
|
385
|
+
row is malformed.
|
|
386
|
+
|
|
387
|
+
"""
|
|
388
|
+
_validate_int(n, "n")
|
|
389
|
+
if n < 0:
|
|
390
|
+
raise ValueError("n must be nonnegative")
|
|
391
|
+
return self.add(key, n)
|
|
392
|
+
|
|
393
|
+
def dec(self, key: str, n: int = 1) -> int:
|
|
394
|
+
"""Decrement the integer value stored under ``key`` by ``n``.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
key: The key to update.
|
|
398
|
+
n: A nonnegative integer amount to subtract.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
The updated integer value.
|
|
402
|
+
|
|
403
|
+
Raises:
|
|
404
|
+
TypeError: If ``key`` or ``n`` has an invalid type, or if the
|
|
405
|
+
existing value is not an integer.
|
|
406
|
+
ValueError: If ``key`` is empty, ``n`` is negative, or the stored
|
|
407
|
+
row is malformed.
|
|
408
|
+
|
|
409
|
+
"""
|
|
410
|
+
_validate_int(n, "n")
|
|
411
|
+
if n < 0:
|
|
412
|
+
raise ValueError("n must be nonnegative")
|
|
413
|
+
return self.add(key, -n)
|
|
414
|
+
|
|
296
415
|
def get(
|
|
297
416
|
self,
|
|
298
417
|
key: str,
|
|
@@ -362,20 +481,24 @@ class TinyKV:
|
|
|
362
481
|
1,
|
|
363
482
|
self._conn.getlimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER),
|
|
364
483
|
)
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
484
|
+
with _Savepoint(self._conn):
|
|
485
|
+
while True:
|
|
486
|
+
tkeys = tuple(itertools.islice(key_iter, chunk_size))
|
|
487
|
+
if not tkeys:
|
|
488
|
+
break
|
|
489
|
+
for k in tkeys:
|
|
490
|
+
_validate_key(k)
|
|
491
|
+
rows = self._conn.execute(
|
|
492
|
+
f"SELECT k, t, v FROM {self._quoted_table} WHERE "
|
|
493
|
+
f"k IN ({', '.join(['?'] * len(tkeys))})",
|
|
494
|
+
tkeys,
|
|
495
|
+
)
|
|
496
|
+
result.update(
|
|
497
|
+
{
|
|
498
|
+
r[0]: self._decode_row(r[1], r[2])
|
|
499
|
+
for r in rows.fetchall()
|
|
500
|
+
}
|
|
501
|
+
)
|
|
379
502
|
return result
|
|
380
503
|
|
|
381
504
|
def get_glob(self, glob_key: str) -> dict[str, Any]:
|
|
@@ -424,8 +547,7 @@ class TinyKV:
|
|
|
424
547
|
1,
|
|
425
548
|
self._conn.getlimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER) // 3,
|
|
426
549
|
)
|
|
427
|
-
self._conn
|
|
428
|
-
try:
|
|
550
|
+
with _Savepoint(self._conn):
|
|
429
551
|
chunk = first_chunk
|
|
430
552
|
while chunk:
|
|
431
553
|
params: list[Any] = []
|
|
@@ -438,11 +560,6 @@ class TinyKV:
|
|
|
438
560
|
params,
|
|
439
561
|
)
|
|
440
562
|
chunk = list(itertools.islice(item_iter, chunk_size))
|
|
441
|
-
except Exception:
|
|
442
|
-
self._conn.execute("ROLLBACK TO tinykv_batch")
|
|
443
|
-
self._conn.execute("RELEASE tinykv_batch")
|
|
444
|
-
raise
|
|
445
|
-
self._conn.execute("RELEASE tinykv_batch")
|
|
446
563
|
|
|
447
564
|
def remove(self, key: str) -> None:
|
|
448
565
|
"""Remove a key from the database.
|
|
@@ -485,8 +602,7 @@ class TinyKV:
|
|
|
485
602
|
1,
|
|
486
603
|
self._conn.getlimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER),
|
|
487
604
|
)
|
|
488
|
-
self._conn
|
|
489
|
-
try:
|
|
605
|
+
with _Savepoint(self._conn):
|
|
490
606
|
tkeys = first_chunk
|
|
491
607
|
while tkeys:
|
|
492
608
|
for k in tkeys:
|
|
@@ -497,8 +613,3 @@ class TinyKV:
|
|
|
497
613
|
tkeys,
|
|
498
614
|
)
|
|
499
615
|
tkeys = tuple(itertools.islice(key_iter, chunk_size))
|
|
500
|
-
except Exception:
|
|
501
|
-
self._conn.execute("ROLLBACK TO tinykv_batch")
|
|
502
|
-
self._conn.execute("RELEASE tinykv_batch")
|
|
503
|
-
raise
|
|
504
|
-
self._conn.execute("RELEASE tinykv_batch")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|