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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tinykv
3
- Version: 0.3.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
- __version__ = "0.3.0"
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
- while True:
366
- tkeys = tuple(itertools.islice(key_iter, chunk_size))
367
- if not tkeys:
368
- break
369
- for k in tkeys:
370
- _validate_key(k)
371
- rows = self._conn.execute(
372
- f"SELECT k, t, v FROM {self._quoted_table} WHERE "
373
- f"k IN ({', '.join(['?'] * len(tkeys))})",
374
- tkeys,
375
- )
376
- result.update(
377
- {r[0]: self._decode_row(r[1], r[2]) for r in rows.fetchall()}
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.execute("SAVEPOINT tinykv_batch")
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.execute("SAVEPOINT tinykv_batch")
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