tinykv 0.1.2__tar.gz → 0.2.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.
@@ -0,0 +1,58 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ This project adheres to [Semantic
6
+ Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ Entries marked as **BC BREAK** indicate backward-incompatible changes.
9
+
10
+ ## [Unreleased]
11
+
12
+ ## [0.2.0] - 2026-03-11
13
+
14
+ ### Changed
15
+ - **BC BREAK** Minimum Python version is now 3.11 (was 3.7). This enables
16
+ modern type hint syntax and removes compatibility code for older Python
17
+ versions.
18
+ - **BC BREAK** Keys are now case-sensitive. Previously, the database used
19
+ case-insensitive collation, so `'Foo'` and `'foo'` referred to the same key.
20
+ Existing tables using the old schema will continue to work with the old
21
+ behavior, but new tables created with `create_schema()` will use
22
+ case-sensitive keys.
23
+ - Tables created with `create_schema()` now use SQLite `WITHOUT ROWID` storage
24
+ for better performance and smaller database size
25
+
26
+ ## [0.1.3] - 2026-03-11
27
+
28
+ Pickle safety improvements and bug fixes.
29
+
30
+ ### Added
31
+ - Add explicit pickle safety mode with user-facing warning
32
+ - Enforce string key contract: non-string keys raise `TypeError`, empty
33
+ strings raise `ValueError`
34
+
35
+ ### Security
36
+ - Validate table names to prevent SQL injection via table identifier
37
+
38
+ ### Fixed
39
+ - Fix precision loss when roundtripping large integers through the database
40
+ - Fix NaN roundtrip crash by storing as pickle and always deserializing
41
+ - Fix `set_many({})` crash by making empty batch a no-op
42
+ - Fix `remove_many([])` crash by making empty batch a no-op
43
+ - Fix integral float type loss (e.g., `1.0` returned as `1`)
44
+
45
+ ## [0.1.2] - 2025-07-28
46
+
47
+ Package reorganization for better pip compatibility.
48
+
49
+ ### Changed
50
+ - Reorganize package into standard "src" layout for better pip install
51
+ experience
52
+
53
+ ## [0.1.1] - 2023-03-14
54
+
55
+ Initial release of tinykv.
56
+
57
+ ### Added
58
+ - Initial release
@@ -1,11 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinykv
3
- Version: 0.1.2
4
- Summary: A tiny key-value store using SQLite3.
3
+ Version: 0.2.0
4
+ Summary: A lightweight Python SQLite key-value store built on sqlite3.
5
+ Keywords: python,sqlite,sqlite3,key-value store,kv
5
6
  Author-email: Flavio Veloso Soares <flaviovs@magnux.com>
6
- Requires-Python: >=3.7
7
+ Requires-Python: >=3.11
7
8
  Description-Content-Type: text/markdown
8
9
  Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Database
12
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
9
13
  License-File: LICENSE
10
14
  Requires-Dist: codespell ; extra == "dev"
11
15
  Requires-Dist: flake8-bugbear ; extra == "dev"
@@ -18,17 +22,35 @@ Requires-Dist: flit ; extra == "dev"
18
22
  Requires-Dist: mypy ; extra == "dev"
19
23
  Requires-Dist: pylint ; extra == "dev"
20
24
  Requires-Dist: taskipy ; extra == "dev"
25
+ Project-URL: Homepage, https://github.com/flaviovs/tinykv
26
+ Project-URL: Issues, https://github.com/flaviovs/tinykv/issues
27
+ Project-URL: Repository, https://github.com/flaviovs/tinykv
21
28
  Provides-Extra: dev
22
29
 
23
- TinyKV - a minimal key-value database for Python using SQLite3
24
- ==============================================================
30
+ TinyKV: Python SQLite key-value store
31
+ =====================================
25
32
 
26
- TinyKV implements a key-value database interface for Python 3 using
33
+ TinyKV is a lightweight Python SQLite key-value store built on top of
27
34
  the [sqlite3](https://docs.python.org/3/library/sqlite3.html) module
28
- from the standard library. There is no dependency on external
29
- packages.
35
+ from the standard library. It provides a small key-value database API
36
+ for Python applications that need persistent local storage without
37
+ external dependencies.
30
38
 
31
- TinyKV requires Python 3.7 or above.
39
+ Use TinyKV when you want an embedded SQLite-backed key-value database
40
+ for configuration data, local caches, application state, or small
41
+ metadata stores.
42
+
43
+ TinyKV requires Python 3.11 or above.
44
+
45
+
46
+ Why TinyKV?
47
+ -----------
48
+
49
+ - Python key-value store backed by SQLite
50
+ - Uses the standard library `sqlite3` module
51
+ - Works with in-memory and file-based SQLite databases
52
+ - Stores strings, numbers, bytes, and other Python objects
53
+ - Keeps connection and transaction control with the caller
32
54
 
33
55
 
34
56
  Installation
@@ -37,7 +59,7 @@ Installation
37
59
  pip install tinykv
38
60
 
39
61
 
40
- Basic usage
62
+ Quick start
41
63
  -----------
42
64
 
43
65
  First let’s import _sqlite3_ and the TinyKV module:
@@ -45,15 +67,15 @@ First let’s import _sqlite3_ and the TinyKV module:
45
67
  >>> import sqlite3
46
68
  >>> import tinykv
47
69
 
48
- TinyKV does not create a SQLite3 database to operate on. Instead, it
49
- operate on connections managed by the caller. So let’s create a
50
- database for using in the examples:
70
+ TinyKV does not create a SQLite database connection for you. Instead,
71
+ it operates on connections managed by the caller. So let’s create a
72
+ database to use in the examples:
51
73
 
52
74
  >>> conn = sqlite3.connect(':memory:')
53
75
 
54
76
  This is how you create a TinyKV object:
55
77
 
56
- >>> kv = tinykv.TinyKV(conn)
78
+ >>> kv = tinykv.TinyKV(conn, allow_pickle=True)
57
79
  Traceback (most recent call last):
58
80
  ...
59
81
  RuntimeError: Table 'kv' not found in the database
@@ -65,12 +87,20 @@ that the application can use `create_schema()`:
65
87
 
66
88
  Let’s try again:
67
89
 
68
- >>> kv = tinykv.TinyKV(conn)
90
+ >>> kv = tinykv.TinyKV(conn, allow_pickle=True)
69
91
  >>> kv # doctest: +ELLIPSIS
70
92
  <tinykv.TinyKV object at 0x...>
71
93
 
72
94
  Now it works!
73
95
 
96
+ By default, TinyKV currently allows pickle-based value
97
+ serialization/deserialization for compatibility.
98
+ If `allow_pickle` is omitted, TinyKV currently behaves as if
99
+ `allow_pickle=True` and emits a `FutureWarning`.
100
+ Applications should not rely on this behavior and should explicitly set
101
+ `allow_pickle=False` when handling databases that might be untrusted.
102
+ The default is expected to change to `False` in a future release.
103
+
74
104
 
75
105
  ## Storing and retrieving data
76
106
 
@@ -111,13 +141,19 @@ You can store any regular Python scalar in the key-value database:
111
141
  >>> kv.get('pi')
112
142
  3.1415926
113
143
 
144
+ >>> kv.set('nan', float('nan'))
145
+ >>> import math
146
+ >>> math.isnan(kv.get('nan'))
147
+ True
148
+
114
149
  Same for container objects:
115
150
 
116
151
  >>> kv.set('a_list', ['one', 'two', 'three'])
117
152
  >>> kv.get('a_list')
118
153
  ['one', 'two', 'three']
119
154
 
120
- In fact, you can store any pickable object:
155
+ In fact, you can store any pickable object when
156
+ `allow_pickle=True` (the current default):
121
157
 
122
158
  >>> import datetime
123
159
  >>>
@@ -131,6 +167,10 @@ In fact, you can store any pickable object:
131
167
  >>> type(a_long_time_ago)
132
168
  <class 'datetime.datetime'>
133
169
 
170
+ For safer behavior with untrusted database contents, disable pickle:
171
+
172
+ >>> safe_kv = tinykv.TinyKV(conn, allow_pickle=False)
173
+
134
174
 
135
175
  ## Removing entries
136
176
 
@@ -179,11 +219,11 @@ You can also use `get_glob()` to fetch entries based a glob pattern:
179
219
  {'bar:123': 3}
180
220
 
181
221
  Notice that `get_many()` and `get_glob()` never raise _KeyError_ for
182
- nonexistent keys. Instead, those keys are simple not present in the
222
+ nonexistent keys. Instead, those keys are simply not present in the
183
223
  returned _dict_.
184
224
 
185
225
  You can also remove many entries in one call with
186
- `remove_many()`. Nonexistent keys are silently ignored:
226
+ `remove_many()`. Nonexistent keys are silently ignored.
187
227
 
188
228
  >>> kv.get('one')
189
229
  1
@@ -195,10 +235,50 @@ You can also remove many entries in one call with
195
235
  KeyError: 'one'
196
236
 
197
237
 
238
+ ## Using glob patterns
239
+
240
+ Use `get_glob()` to fetch entries using a shell-like wildcard pattern
241
+ from the SQLite key-value store:
242
+
243
+ The pattern uses SQLite's [GLOB](https://www.sqlite.org/lang_expr.html#glob)
244
+ syntax: `*` matches any sequence of characters, and `?` matches a single
245
+ character. Note that patterns are case-sensitive and use literal character
246
+ matching (not regex).
247
+
248
+
249
+ ## Database setup
250
+
251
+ TinyKV requires the database table to exist before use. Create it with
252
+ `create_schema()`:
253
+
254
+ >>> import sqlite3
255
+ >>> conn = sqlite3.connect(':memory:')
256
+ >>> tinykv.create_schema(conn)
257
+
258
+ You can use a custom table name:
259
+
260
+ >>> tinykv.create_schema(conn, table='my_keys')
261
+
262
+ See the Miscellaneous section for table name requirements.
263
+
264
+
265
+ ## Use cases
266
+
267
+ TinyKV is a good fit when you need a Python SQLite key-value store for:
268
+
269
+ - application configuration and settings
270
+ - local cache data
271
+ - lightweight metadata storage
272
+ - persistent state for command-line tools
273
+ - embedded storage in desktop scripts or services
274
+
275
+
198
276
  Miscellaneous
199
277
  -------------
200
278
 
201
- - TinyKV keys must be string scalars.
279
+ - TinyKV keys must be non-empty string scalars. Non-string keys raise
280
+ `TypeError`, and empty strings raise `ValueError`. Keys are case-sensitive
281
+ (since v0.1.4).
202
282
 
203
283
  - TinyKV does not open or manage transactions. Also, it operates both
204
284
  in autocommit and non-autocommit mode. All operations are atomic.
@@ -220,7 +300,14 @@ Miscellaneous
220
300
  >>>
221
301
  >>> tinykv.create_schema(conn, table=CUSTOM_TABLE)
222
302
  >>>
223
- >>> custom_kv = tinykv.TinyKV(conn, table=CUSTOM_TABLE)
303
+ >>> custom_kv = tinykv.TinyKV(
304
+ ... conn,
305
+ ... table=CUSTOM_TABLE,
306
+ ... allow_pickle=True,
307
+ ... )
308
+
309
+ Table names must match the pattern `[a-zA-Z_][a-zA-Z0-9_]*`. Invalid
310
+ names raise `ValueError`.
224
311
 
225
312
 
226
313
  Questions? Bugs? Suggestions?
@@ -1,12 +1,27 @@
1
- TinyKV - a minimal key-value database for Python using SQLite3
2
- ==============================================================
1
+ TinyKV: Python SQLite key-value store
2
+ =====================================
3
3
 
4
- TinyKV implements a key-value database interface for Python 3 using
4
+ TinyKV is a lightweight Python SQLite key-value store built on top of
5
5
  the [sqlite3](https://docs.python.org/3/library/sqlite3.html) module
6
- from the standard library. There is no dependency on external
7
- packages.
6
+ from the standard library. It provides a small key-value database API
7
+ for Python applications that need persistent local storage without
8
+ external dependencies.
8
9
 
9
- TinyKV requires Python 3.7 or above.
10
+ Use TinyKV when you want an embedded SQLite-backed key-value database
11
+ for configuration data, local caches, application state, or small
12
+ metadata stores.
13
+
14
+ TinyKV requires Python 3.11 or above.
15
+
16
+
17
+ Why TinyKV?
18
+ -----------
19
+
20
+ - Python key-value store backed by SQLite
21
+ - Uses the standard library `sqlite3` module
22
+ - Works with in-memory and file-based SQLite databases
23
+ - Stores strings, numbers, bytes, and other Python objects
24
+ - Keeps connection and transaction control with the caller
10
25
 
11
26
 
12
27
  Installation
@@ -15,7 +30,7 @@ Installation
15
30
  pip install tinykv
16
31
 
17
32
 
18
- Basic usage
33
+ Quick start
19
34
  -----------
20
35
 
21
36
  First let’s import _sqlite3_ and the TinyKV module:
@@ -23,15 +38,15 @@ First let’s import _sqlite3_ and the TinyKV module:
23
38
  >>> import sqlite3
24
39
  >>> import tinykv
25
40
 
26
- TinyKV does not create a SQLite3 database to operate on. Instead, it
27
- operate on connections managed by the caller. So let’s create a
28
- database for using in the examples:
41
+ TinyKV does not create a SQLite database connection for you. Instead,
42
+ it operates on connections managed by the caller. So let’s create a
43
+ database to use in the examples:
29
44
 
30
45
  >>> conn = sqlite3.connect(':memory:')
31
46
 
32
47
  This is how you create a TinyKV object:
33
48
 
34
- >>> kv = tinykv.TinyKV(conn)
49
+ >>> kv = tinykv.TinyKV(conn, allow_pickle=True)
35
50
  Traceback (most recent call last):
36
51
  ...
37
52
  RuntimeError: Table 'kv' not found in the database
@@ -43,12 +58,20 @@ that the application can use `create_schema()`:
43
58
 
44
59
  Let’s try again:
45
60
 
46
- >>> kv = tinykv.TinyKV(conn)
61
+ >>> kv = tinykv.TinyKV(conn, allow_pickle=True)
47
62
  >>> kv # doctest: +ELLIPSIS
48
63
  <tinykv.TinyKV object at 0x...>
49
64
 
50
65
  Now it works!
51
66
 
67
+ By default, TinyKV currently allows pickle-based value
68
+ serialization/deserialization for compatibility.
69
+ If `allow_pickle` is omitted, TinyKV currently behaves as if
70
+ `allow_pickle=True` and emits a `FutureWarning`.
71
+ Applications should not rely on this behavior and should explicitly set
72
+ `allow_pickle=False` when handling databases that might be untrusted.
73
+ The default is expected to change to `False` in a future release.
74
+
52
75
 
53
76
  ## Storing and retrieving data
54
77
 
@@ -89,13 +112,19 @@ You can store any regular Python scalar in the key-value database:
89
112
  >>> kv.get('pi')
90
113
  3.1415926
91
114
 
115
+ >>> kv.set('nan', float('nan'))
116
+ >>> import math
117
+ >>> math.isnan(kv.get('nan'))
118
+ True
119
+
92
120
  Same for container objects:
93
121
 
94
122
  >>> kv.set('a_list', ['one', 'two', 'three'])
95
123
  >>> kv.get('a_list')
96
124
  ['one', 'two', 'three']
97
125
 
98
- In fact, you can store any pickable object:
126
+ In fact, you can store any pickable object when
127
+ `allow_pickle=True` (the current default):
99
128
 
100
129
  >>> import datetime
101
130
  >>>
@@ -109,6 +138,10 @@ In fact, you can store any pickable object:
109
138
  >>> type(a_long_time_ago)
110
139
  <class 'datetime.datetime'>
111
140
 
141
+ For safer behavior with untrusted database contents, disable pickle:
142
+
143
+ >>> safe_kv = tinykv.TinyKV(conn, allow_pickle=False)
144
+
112
145
 
113
146
  ## Removing entries
114
147
 
@@ -157,11 +190,11 @@ You can also use `get_glob()` to fetch entries based a glob pattern:
157
190
  {'bar:123': 3}
158
191
 
159
192
  Notice that `get_many()` and `get_glob()` never raise _KeyError_ for
160
- nonexistent keys. Instead, those keys are simple not present in the
193
+ nonexistent keys. Instead, those keys are simply not present in the
161
194
  returned _dict_.
162
195
 
163
196
  You can also remove many entries in one call with
164
- `remove_many()`. Nonexistent keys are silently ignored:
197
+ `remove_many()`. Nonexistent keys are silently ignored.
165
198
 
166
199
  >>> kv.get('one')
167
200
  1
@@ -173,10 +206,50 @@ You can also remove many entries in one call with
173
206
  KeyError: 'one'
174
207
 
175
208
 
209
+ ## Using glob patterns
210
+
211
+ Use `get_glob()` to fetch entries using a shell-like wildcard pattern
212
+ from the SQLite key-value store:
213
+
214
+ The pattern uses SQLite's [GLOB](https://www.sqlite.org/lang_expr.html#glob)
215
+ syntax: `*` matches any sequence of characters, and `?` matches a single
216
+ character. Note that patterns are case-sensitive and use literal character
217
+ matching (not regex).
218
+
219
+
220
+ ## Database setup
221
+
222
+ TinyKV requires the database table to exist before use. Create it with
223
+ `create_schema()`:
224
+
225
+ >>> import sqlite3
226
+ >>> conn = sqlite3.connect(':memory:')
227
+ >>> tinykv.create_schema(conn)
228
+
229
+ You can use a custom table name:
230
+
231
+ >>> tinykv.create_schema(conn, table='my_keys')
232
+
233
+ See the Miscellaneous section for table name requirements.
234
+
235
+
236
+ ## Use cases
237
+
238
+ TinyKV is a good fit when you need a Python SQLite key-value store for:
239
+
240
+ - application configuration and settings
241
+ - local cache data
242
+ - lightweight metadata storage
243
+ - persistent state for command-line tools
244
+ - embedded storage in desktop scripts or services
245
+
246
+
176
247
  Miscellaneous
177
248
  -------------
178
249
 
179
- - TinyKV keys must be string scalars.
250
+ - TinyKV keys must be non-empty string scalars. Non-string keys raise
251
+ `TypeError`, and empty strings raise `ValueError`. Keys are case-sensitive
252
+ (since v0.1.4).
180
253
 
181
254
  - TinyKV does not open or manage transactions. Also, it operates both
182
255
  in autocommit and non-autocommit mode. All operations are atomic.
@@ -198,7 +271,14 @@ Miscellaneous
198
271
  >>>
199
272
  >>> tinykv.create_schema(conn, table=CUSTOM_TABLE)
200
273
  >>>
201
- >>> custom_kv = tinykv.TinyKV(conn, table=CUSTOM_TABLE)
274
+ >>> custom_kv = tinykv.TinyKV(
275
+ ... conn,
276
+ ... table=CUSTOM_TABLE,
277
+ ... allow_pickle=True,
278
+ ... )
279
+
280
+ Table names must match the pattern `[a-zA-Z_][a-zA-Z0-9_]*`. Invalid
281
+ names raise `ValueError`.
202
282
 
203
283
 
204
284
  Questions? Bugs? Suggestions?
@@ -7,13 +7,22 @@ name = "tinykv"
7
7
  authors = [{name = "Flavio Veloso Soares", email = "flaviovs@magnux.com"}]
8
8
  license = {file = "LICENSE"}
9
9
  readme = "README.md"
10
+ keywords = ["python", "sqlite", "sqlite3", "key-value store", "kv"]
10
11
  classifiers = [
11
12
  "License :: OSI Approved :: MIT License",
13
+ "Programming Language :: Python :: 3",
14
+ "Topic :: Database",
15
+ "Topic :: Software Development :: Libraries :: Python Modules",
12
16
  ]
13
- requires-python = ">=3.7"
17
+ requires-python = ">=3.11"
14
18
 
15
19
  dynamic = ["version", "description"]
16
20
 
21
+ [project.urls]
22
+ Homepage = "https://github.com/flaviovs/tinykv"
23
+ Repository = "https://github.com/flaviovs/tinykv"
24
+ Issues = "https://github.com/flaviovs/tinykv/issues"
25
+
17
26
  [project.optional-dependencies]
18
27
  dev = [
19
28
  "codespell",
@@ -40,7 +49,7 @@ pre-commit = "task spellcheck && task test && task lint"
40
49
 
41
50
  [tool.mypy]
42
51
  files = "."
43
- python_version = "3.7"
52
+ python_version = "3.11"
44
53
  exclude = [".git", ".venv"]
45
54
  strict = true
46
55
  warn_redundant_casts = true
@@ -72,4 +81,5 @@ disable = [
72
81
  "missing-function-docstring",
73
82
  "missing-module-docstring",
74
83
  "missing-class-docstring",
84
+ "too-many-return-statements",
75
85
  ]
@@ -1,14 +1,43 @@
1
- """A tiny key-value store using SQLite3."""
1
+ """A lightweight Python SQLite key-value store built on sqlite3."""
2
2
  import enum
3
3
  import logging
4
+ import math
4
5
  import pickle
6
+ import re
5
7
  import sqlite3
8
+ import warnings
6
9
 
7
- from typing import Any, Optional, Union, Iterable, Mapping, Tuple, Dict
10
+ from collections.abc import Iterable, Mapping
11
+ from typing import Any
8
12
 
9
- __version__ = '0.1.2'
13
+ _TABLE_NAME_RE = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$')
14
+
15
+
16
+ def _validate_table_name(table: str) -> None:
17
+ if not isinstance(table, str):
18
+ raise ValueError(
19
+ f'table name must be a string, got {type(table).__name__}'
20
+ )
21
+ if not _TABLE_NAME_RE.match(table):
22
+ raise ValueError(
23
+ f'Invalid table name {table!r}: must match pattern '
24
+ r'[a-zA-Z_][a-zA-Z0-9_]*'
25
+ )
26
+
27
+
28
+ def _validate_key(key: str) -> None:
29
+ if not isinstance(key, str):
30
+ raise TypeError(
31
+ f'key must be a string, got {type(key).__name__}'
32
+ )
33
+ if not key:
34
+ raise ValueError('key must not be empty')
35
+
36
+
37
+ __version__ = '0.2.0'
10
38
 
11
39
  _DEF_TABLE = 'kv'
40
+ _ALLOW_PICKLE_DEFAULT = object()
12
41
 
13
42
  logger = logging.getLogger(__name__)
14
43
 
@@ -21,12 +50,13 @@ def create_schema(conn: sqlite3.Connection, table: str = _DEF_TABLE) -> None:
21
50
  table: The table name (default: 'kv').
22
51
 
23
52
  """
53
+ _validate_table_name(table)
24
54
  conn.execute(f'CREATE TABLE {table} ('
25
- 'k TEXT NOT NULL COLLATE NOCASE, '
26
- 't TINYINT NOT NULL CHECK (t BETWEEN 1 AND 6), '
55
+ 'k TEXT NOT NULL, '
56
+ 't TINYINT NOT NULL CHECK (t BETWEEN 1 AND 7), '
27
57
  'v BLOB, '
28
58
  'PRIMARY KEY (k)'
29
- ')')
59
+ ') WITHOUT ROWID')
30
60
 
31
61
 
32
62
  class _DType(enum.IntEnum):
@@ -36,6 +66,7 @@ class _DType(enum.IntEnum):
36
66
  BOOL = 4
37
67
  NUMBER = 5
38
68
  PICKLE = 6
69
+ LONG = 7
39
70
 
40
71
 
41
72
  class TinyKV:
@@ -49,16 +80,35 @@ class TinyKV:
49
80
  >>>
50
81
  >>> create_schema(conn)
51
82
  >>>
52
- >>> kv = TinyKV(conn)
83
+ >>> kv = TinyKV(conn, allow_pickle=True)
53
84
 
54
85
  Args:
55
86
  conn: The SQLite3 connection object.
56
87
  table: The table name (default: 'kv').
88
+ allow_pickle: If true, unsupported values are serialized with pickle.
89
+ If omitted, TinyKV currently behaves as true and emits a
90
+ FutureWarning because the default is expected to change in a
91
+ future release. Set to false to disable pickle-based
92
+ storage/deserialization for safer handling of untrusted database
93
+ content.
57
94
 
58
95
  """
59
96
 
60
- def __init__(self, conn: sqlite3.Connection, table: str = _DEF_TABLE):
61
- """Initialize the key-value object."""
97
+ def __init__(self, conn: sqlite3.Connection, table: str = _DEF_TABLE,
98
+ allow_pickle: bool | object = _ALLOW_PICKLE_DEFAULT) -> None:
99
+ """Initialize the key-value object.
100
+
101
+ Args:
102
+ conn: The SQLite3 connection object.
103
+ table: The table name (default: 'kv').
104
+ allow_pickle: If true, fallback to pickle for unsupported value
105
+ types and unpickle stored pickle rows. If omitted, TinyKV
106
+ currently behaves as true and emits a FutureWarning because the
107
+ default is expected to change to false in a future release. If
108
+ false, both operations raise ValueError.
109
+
110
+ """
111
+ _validate_table_name(table)
62
112
  cur = conn.execute('SELECT name FROM sqlite_master '
63
113
  "WHERE type = 'table' AND name = ?",
64
114
  (table,))
@@ -67,6 +117,15 @@ class TinyKV:
67
117
 
68
118
  self._conn = conn
69
119
  self._table = table
120
+ if allow_pickle is _ALLOW_PICKLE_DEFAULT:
121
+ warnings.warn('allow_pickle currently defaults to True but this '
122
+ 'will change to False in a future release; pass '
123
+ 'allow_pickle explicitly',
124
+ FutureWarning,
125
+ stacklevel=2)
126
+ self._allow_pickle = True
127
+ else:
128
+ self._allow_pickle = bool(allow_pickle)
70
129
 
71
130
  @property
72
131
  def conn(self) -> sqlite3.Connection:
@@ -77,8 +136,7 @@ class TinyKV:
77
136
  """
78
137
  return self._conn
79
138
 
80
- def _serialize(self, data: Any) -> Tuple[_DType,
81
- Optional[Union[float, bytes]]]:
139
+ def _serialize(self, data: Any) -> tuple[_DType, float | bytes | None]:
82
140
  if data is None:
83
141
  return (_DType.NONE, None)
84
142
 
@@ -91,10 +149,20 @@ class TinyKV:
91
149
  if isinstance(data, bool):
92
150
  return (_DType.BOOL, int(data))
93
151
 
94
- if isinstance(data, (int, float)):
152
+ if isinstance(data, int):
153
+ return (_DType.LONG, str(data).encode('utf-8'))
154
+
155
+ if isinstance(data, float):
156
+ if math.isnan(data):
157
+ return (_DType.PICKLE, pickle.dumps(data))
95
158
  return (_DType.NUMBER, data)
96
159
 
97
- return (_DType.PICKLE, pickle.dumps(data))
160
+ if self._allow_pickle:
161
+ return (_DType.PICKLE, pickle.dumps(data))
162
+
163
+ raise ValueError('Cannot store value type without pickle support; '
164
+ 'initialize TinyKV with allow_pickle=True to enable '
165
+ 'legacy pickled values')
98
166
 
99
167
  def _unserialize(self, dtype: _DType, data: bytes) -> Any:
100
168
  if dtype == _DType.NONE:
@@ -110,16 +178,35 @@ class TinyKV:
110
178
  return bool(data)
111
179
 
112
180
  if dtype == _DType.NUMBER:
113
- number = float(data)
114
- return int(number) if number.is_integer() else number
181
+ return float(data)
182
+
183
+ if dtype == _DType.LONG:
184
+ return int(data.decode('utf-8'))
115
185
 
116
186
  if dtype == _DType.PICKLE:
187
+ if not self._allow_pickle:
188
+ value = pickle.loads(data)
189
+ if isinstance(value, float) and math.isnan(value):
190
+ return value
191
+ raise ValueError('Cannot deserialize pickled value with '
192
+ 'allow_pickle=False')
117
193
  return pickle.loads(data)
118
194
 
119
- raise ValueError('Unsupported data type {dtype}')
195
+ raise ValueError(f'Unsupported data type {dtype}')
120
196
 
121
197
  def set(self, key: str, value: Any) -> None:
122
- """Store a value in the database."""
198
+ """Store a value in the database.
199
+
200
+ Args:
201
+ key: The key to store the value under.
202
+ value: The value to store.
203
+
204
+ Raises:
205
+ TypeError: If key is not a string.
206
+ ValueError: If key is an empty string.
207
+
208
+ """
209
+ _validate_key(key)
123
210
  assert self._conn
124
211
  dtype, data = self._serialize(value)
125
212
  self._conn.execute(f'INSERT OR REPLACE INTO {self._table} (k, t, v) '
@@ -136,7 +223,7 @@ class TinyKV:
136
223
  >>> conn = sqlite3.connect(':memory:')
137
224
  >>>
138
225
  >>> create_schema(conn)
139
- >>> kv = TinyKV(conn)
226
+ >>> kv = TinyKV(conn, allow_pickle=True)
140
227
  >>>
141
228
  >>> kv.set('foo', 'bar')
142
229
  >>> kv.get('foo')
@@ -152,8 +239,11 @@ class TinyKV:
152
239
  Raises:
153
240
  KeyError: if the key does not exist and no default is
154
241
  provided.
242
+ TypeError: If key is not a string.
243
+ ValueError: If key is an empty string.
155
244
 
156
245
  """
246
+ _validate_key(key)
157
247
  assert self._conn
158
248
  cur = self._conn.execute(f'SELECT t, v FROM {self._table} '
159
249
  'WHERE k = ?', (key,))
@@ -164,7 +254,7 @@ class TinyKV:
164
254
  raise KeyError(key)
165
255
  return self._unserialize(_DType(row[0]), row[1])
166
256
 
167
- def get_many(self, keys: Iterable[str]) -> Dict[str, Any]:
257
+ def get_many(self, keys: Iterable[str]) -> dict[str, Any]:
168
258
  """Get many values from the database.
169
259
 
170
260
  Args:
@@ -174,16 +264,22 @@ class TinyKV:
174
264
  A dict where with only the keys found on the database, and their
175
265
  respective values.
176
266
 
267
+ Raises:
268
+ TypeError: If any key is not a string.
269
+ ValueError: If any key is an empty string.
270
+
177
271
  """
178
272
  assert self._conn
179
273
  tkeys = tuple(keys)
274
+ for k in tkeys:
275
+ _validate_key(k)
180
276
  rows = self._conn.execute(f'SELECT k, t, v FROM {self._table} WHERE '
181
277
  f'k IN ({", ".join(["?"] * len(tkeys))})',
182
278
  tkeys)
183
279
  return {r[0]: self._unserialize(_DType(r[1]), r[2])
184
280
  for r in rows.fetchall()}
185
281
 
186
- def get_glob(self, glob_key: str) -> Dict[str, Any]:
282
+ def get_glob(self, glob_key: str) -> dict[str, Any]:
187
283
  """Get many values using a glob pattern.
188
284
 
189
285
  Similar to `get_many()`, but using a glob pattern.
@@ -195,7 +291,12 @@ class TinyKV:
195
291
  A dict where keys are the keys matching `glob_key` found
196
292
  on the database, and their respective values.
197
293
 
294
+ Raises:
295
+ TypeError: If glob_key is not a string.
296
+ ValueError: If glob_key is an empty string.
297
+
198
298
  """
299
+ _validate_key(glob_key)
199
300
  assert self._conn
200
301
  rows = self._conn.execute('SELECT k, t, v '
201
302
  f'FROM {self._table} '
@@ -209,8 +310,16 @@ class TinyKV:
209
310
  Args:
210
311
  kvdict: A mapping of keys to values.
211
312
 
313
+ Raises:
314
+ TypeError: If any key is not a string.
315
+ ValueError: If any key is an empty string.
316
+
212
317
  """
213
318
  assert self._conn
319
+ if not kvdict:
320
+ return
321
+ for k in kvdict:
322
+ _validate_key(k)
214
323
  self._conn.execute(f'INSERT OR REPLACE INTO {self._table} (k, t, v) '
215
324
  f'VALUES {", ".join(["(?, ?, ?)"] * len(kvdict))}',
216
325
  tuple(p[i]
@@ -228,8 +337,11 @@ class TinyKV:
228
337
 
229
338
  Raises:
230
339
  KeyError: If the key is not found.
340
+ TypeError: If key is not a string.
341
+ ValueError: If key is an empty string.
231
342
 
232
343
  """
344
+ _validate_key(key)
233
345
  assert self._conn
234
346
  cur = self._conn.execute(f'DELETE FROM {self._table} WHERE k = ?',
235
347
  (key,))
@@ -243,9 +355,17 @@ class TinyKV:
243
355
 
244
356
  Args:
245
357
  keys: Iterable of keys to remove.
358
+
359
+ Raises:
360
+ TypeError: If any key is not a string.
361
+ ValueError: If any key is an empty string.
246
362
  """
247
363
  assert self._conn
248
364
  tkeys = tuple(keys)
365
+ if not tkeys:
366
+ return
367
+ for k in tkeys:
368
+ _validate_key(k)
249
369
  self._conn.execute(f'DELETE FROM {self._table} WHERE '
250
370
  f'k IN ({", ".join(["?"] * len(tkeys))})',
251
371
  tkeys)
@@ -0,0 +1,314 @@
1
+ import datetime
2
+ import math
3
+ import pickle
4
+ import sqlite3
5
+ import secrets
6
+ import tempfile
7
+ import unittest
8
+ import warnings
9
+ from pathlib import Path
10
+ from tinykv import TinyKV, create_schema
11
+
12
+ _TEST_DATA = {
13
+ 'none': None,
14
+ 'foo': 'bar',
15
+ 'bytes': secrets.token_bytes(20),
16
+ 'maybe': True,
17
+ 'one': 1,
18
+ 'pi': 3.1415926,
19
+ 'complex': complex(1, 2),
20
+ 'now': datetime.datetime.now(),
21
+ }
22
+
23
+
24
+ class TestKV(unittest.TestCase):
25
+
26
+ def setUp(self) -> None:
27
+ # pylint: disable-next=consider-using-with
28
+ self._tempdir = tempfile.TemporaryDirectory()
29
+ self._path = Path(self._tempdir.name) / 'db.sqlite3'
30
+ self._conn = sqlite3.connect(self._path)
31
+ create_schema(self._conn)
32
+
33
+ def tearDown(self) -> None:
34
+ self._conn.close()
35
+ self._tempdir.cleanup()
36
+
37
+ def test_set_get(self) -> None:
38
+ db = TinyKV(self._conn, allow_pickle=True)
39
+
40
+ for k, v in _TEST_DATA.items():
41
+ with self.subTest(k=k):
42
+ db.set(k, v)
43
+ self.assertEqual(db.get(k), v)
44
+
45
+ def test_set_replace(self) -> None:
46
+ db = TinyKV(self._conn, allow_pickle=True)
47
+
48
+ db.set('foo', 1)
49
+ db.set('foo', 'bar')
50
+
51
+ self.assertEqual(db.get('foo'), 'bar')
52
+
53
+ def test_get_default(self) -> None:
54
+ db = TinyKV(self._conn, allow_pickle=True)
55
+ self.assertEqual(db.get('foo', 'bar'), 'bar')
56
+ self.assertIsNone(db.get('foo', None))
57
+
58
+ def test_set_get_persist(self) -> None:
59
+ db = TinyKV(self._conn, allow_pickle=True)
60
+
61
+ for k, v in _TEST_DATA.items():
62
+ db.set(k, v)
63
+
64
+ self._conn.commit()
65
+ self._conn.close()
66
+
67
+ self._conn = sqlite3.connect(self._path)
68
+ db2 = TinyKV(self._conn, allow_pickle=True)
69
+ for k, v in _TEST_DATA.items():
70
+ with self.subTest(k=k):
71
+ self.assertEqual(db2.get(k), v)
72
+
73
+ def test_get_many(self) -> None:
74
+ db = TinyKV(self._conn, allow_pickle=True)
75
+
76
+ for k, v in _TEST_DATA.items():
77
+ db.set(k, v)
78
+
79
+ self.assertEqual(db.get_many(_TEST_DATA.keys()), _TEST_DATA)
80
+
81
+ def test_get_many_nonexisting(self) -> None:
82
+ db = TinyKV(self._conn, allow_pickle=True)
83
+
84
+ db.set('foo', 1)
85
+ db.set('bar', 2)
86
+
87
+ self.assertEqual(db.get_many(('foo', 'bar', 'not-there')),
88
+ {'foo': 1, 'bar': 2})
89
+
90
+ def test_get_glob(self) -> None:
91
+ db = TinyKV(self._conn, allow_pickle=True)
92
+
93
+ db.set('foo:abc', 1)
94
+ db.set('foo:xyz', 2)
95
+ db.set('bar:abc', 3)
96
+
97
+ self.assertEqual(db.get_glob('foo:*'), {'foo:abc': 1, 'foo:xyz': 2})
98
+
99
+ def test_set_many(self) -> None:
100
+ db = TinyKV(self._conn, allow_pickle=True)
101
+
102
+ db.set_many(_TEST_DATA)
103
+
104
+ for k, v in _TEST_DATA.items():
105
+ with self.subTest(k=k):
106
+ self.assertEqual(db.get(k), v)
107
+
108
+ def test_set_many_empty_mapping(self) -> None:
109
+ db = TinyKV(self._conn, allow_pickle=True)
110
+
111
+ db.set_many({})
112
+
113
+ def test_remove(self) -> None:
114
+ db = TinyKV(self._conn, allow_pickle=True)
115
+
116
+ db.set('foo', 'bar')
117
+
118
+ db.remove('foo')
119
+
120
+ with self.assertRaises(KeyError):
121
+ db.get('foo')
122
+
123
+ def test_remove_nonexistent(self) -> None:
124
+ db = TinyKV(self._conn, allow_pickle=True)
125
+ with self.assertRaises(KeyError):
126
+ db.remove('nonexistent')
127
+
128
+ def test_remove_many(self) -> None:
129
+ db = TinyKV(self._conn, allow_pickle=True)
130
+
131
+ db.set('foo', 'bar')
132
+ db.set('bar', 'bar')
133
+
134
+ db.remove_many(('foo', 'bar'))
135
+
136
+ with self.assertRaises(KeyError):
137
+ db.get('foo')
138
+
139
+ with self.assertRaises(KeyError):
140
+ db.get('bar')
141
+
142
+ def test_remove_many_empty(self) -> None:
143
+ db = TinyKV(self._conn, allow_pickle=True)
144
+
145
+ db.remove_many([])
146
+
147
+ def test_safe_mode_rejects_pickle_on_set(self) -> None:
148
+ db = TinyKV(self._conn, allow_pickle=False)
149
+
150
+ with self.assertRaisesRegex(ValueError, 'allow_pickle=True'):
151
+ db.set('now', datetime.datetime.now())
152
+
153
+ def test_safe_mode_rejects_existing_pickled_row(self) -> None:
154
+ payload = pickle.dumps(datetime.datetime(2022, 3, 19, 20, 15, 5))
155
+ self._conn.execute('INSERT INTO kv (k, t, v) VALUES (?, ?, ?)',
156
+ ('pickled', 6, payload))
157
+
158
+ db = TinyKV(self._conn, allow_pickle=False)
159
+ with self.assertRaisesRegex(ValueError, 'allow_pickle=False'):
160
+ db.get('pickled')
161
+
162
+ def test_compat_mode_allows_pickle_roundtrip(self) -> None:
163
+ db = TinyKV(self._conn, allow_pickle=True)
164
+ dt = datetime.datetime(2022, 3, 19, 20, 15, 5)
165
+
166
+ db.set('dt', dt)
167
+
168
+ self.assertEqual(db.get('dt'), dt)
169
+
170
+ def test_implicit_allow_pickle_warns(self) -> None:
171
+ with self.assertWarnsRegex(FutureWarning, 'allow_pickle'):
172
+ TinyKV(self._conn)
173
+
174
+ def test_explicit_allow_pickle_true_does_not_warn(self) -> None:
175
+ with warnings.catch_warnings(record=True) as warns:
176
+ warnings.simplefilter('always')
177
+ TinyKV(self._conn, allow_pickle=True)
178
+ self.assertEqual(warns, [])
179
+
180
+ def test_explicit_allow_pickle_false_does_not_warn(self) -> None:
181
+ with warnings.catch_warnings(record=True) as warns:
182
+ warnings.simplefilter('always')
183
+ TinyKV(self._conn, allow_pickle=False)
184
+ self.assertEqual(warns, [])
185
+
186
+ def test_large_int_roundtrip(self) -> None:
187
+ db = TinyKV(self._conn, allow_pickle=True)
188
+
189
+ test_cases = [
190
+ (2**53 - 1, 'max safe integer'),
191
+ (2**53, 'just over max safe integer'),
192
+ (2**53 + 1, 'larger than max safe integer'),
193
+ (10**20, 'very large positive'),
194
+ (-(10**20), 'very large negative'),
195
+ (0, 'zero'),
196
+ (1, 'small positive'),
197
+ (-1, 'small negative'),
198
+ ]
199
+
200
+ for value, description in test_cases:
201
+ with self.subTest(value=value, description=description):
202
+ db.set('large_int', value)
203
+ self.assertEqual(db.get('large_int'), value)
204
+
205
+ def test_integral_float_roundtrip(self) -> None:
206
+ db = TinyKV(self._conn, allow_pickle=True)
207
+
208
+ test_cases = [
209
+ (1.0, 'one as float'),
210
+ (0.0, 'zero as float'),
211
+ (-0.0, 'negative zero as float'),
212
+ (1.5, 'one point five'),
213
+ (-2.5, 'negative two point five'),
214
+ ]
215
+
216
+ for value, description in test_cases:
217
+ with self.subTest(value=value, description=description):
218
+ db.set('integral_float', value)
219
+ result = db.get('integral_float')
220
+ self.assertEqual(result, value)
221
+ self.assertIsInstance(result, float)
222
+
223
+ def test_nan_roundtrip(self) -> None:
224
+ db = TinyKV(self._conn, allow_pickle=True)
225
+
226
+ db.set('nan', float('nan'))
227
+ result = db.get('nan')
228
+ self.assertTrue(math.isnan(result))
229
+
230
+ def test_nan_roundtrip_safe_mode(self) -> None:
231
+ db = TinyKV(self._conn, allow_pickle=False)
232
+
233
+ db.set('nan', float('nan'))
234
+ result = db.get('nan')
235
+ self.assertTrue(math.isnan(result))
236
+
237
+ def test_inf_roundtrip(self) -> None:
238
+ db = TinyKV(self._conn, allow_pickle=True)
239
+
240
+ db.set('inf', float('inf'))
241
+ self.assertEqual(db.get('inf'), float('inf'))
242
+
243
+ db.set('neg_inf', float('-inf'))
244
+ self.assertEqual(db.get('neg_inf'), float('-inf'))
245
+
246
+ def test_set_rejects_non_string_key(self) -> None:
247
+ db = TinyKV(self._conn, allow_pickle=True)
248
+ with self.assertRaisesRegex(TypeError, 'must be a string'):
249
+ db.set(123, 'value') # type: ignore[arg-type]
250
+
251
+ def test_set_rejects_empty_key(self) -> None:
252
+ db = TinyKV(self._conn, allow_pickle=True)
253
+ with self.assertRaisesRegex(ValueError, 'must not be empty'):
254
+ db.set('', 'value')
255
+
256
+ def test_get_rejects_non_string_key(self) -> None:
257
+ db = TinyKV(self._conn, allow_pickle=True)
258
+ with self.assertRaisesRegex(TypeError, 'must be a string'):
259
+ db.get(123) # type: ignore[arg-type]
260
+
261
+ def test_get_rejects_empty_key(self) -> None:
262
+ db = TinyKV(self._conn, allow_pickle=True)
263
+ with self.assertRaisesRegex(ValueError, 'must not be empty'):
264
+ db.get('')
265
+
266
+ def test_remove_rejects_non_string_key(self) -> None:
267
+ db = TinyKV(self._conn, allow_pickle=True)
268
+ with self.assertRaisesRegex(TypeError, 'must be a string'):
269
+ db.remove(123) # type: ignore[arg-type]
270
+
271
+ def test_remove_rejects_empty_key(self) -> None:
272
+ db = TinyKV(self._conn, allow_pickle=True)
273
+ with self.assertRaisesRegex(ValueError, 'must not be empty'):
274
+ db.remove('')
275
+
276
+ def test_get_many_rejects_non_string_key(self) -> None:
277
+ db = TinyKV(self._conn, allow_pickle=True)
278
+ with self.assertRaisesRegex(TypeError, 'must be a string'):
279
+ db.get_many([123]) # type: ignore[list-item]
280
+
281
+ def test_get_many_rejects_empty_key(self) -> None:
282
+ db = TinyKV(self._conn, allow_pickle=True)
283
+ with self.assertRaisesRegex(ValueError, 'must not be empty'):
284
+ db.get_many([''])
285
+
286
+ def test_set_many_rejects_non_string_key(self) -> None:
287
+ db = TinyKV(self._conn, allow_pickle=True)
288
+ with self.assertRaisesRegex(TypeError, 'must be a string'):
289
+ db.set_many({123: 'value'}) # type: ignore[dict-item]
290
+
291
+ def test_set_many_rejects_empty_key(self) -> None:
292
+ db = TinyKV(self._conn, allow_pickle=True)
293
+ with self.assertRaisesRegex(ValueError, 'must not be empty'):
294
+ db.set_many({'': 'value'})
295
+
296
+ def test_remove_many_rejects_non_string_key(self) -> None:
297
+ db = TinyKV(self._conn, allow_pickle=True)
298
+ with self.assertRaisesRegex(TypeError, 'must be a string'):
299
+ db.remove_many([123]) # type: ignore[list-item]
300
+
301
+ def test_remove_many_rejects_empty_key(self) -> None:
302
+ db = TinyKV(self._conn, allow_pickle=True)
303
+ with self.assertRaisesRegex(ValueError, 'must not be empty'):
304
+ db.remove_many([''])
305
+
306
+ def test_get_glob_rejects_non_string_key(self) -> None:
307
+ db = TinyKV(self._conn, allow_pickle=True)
308
+ with self.assertRaisesRegex(TypeError, 'must be a string'):
309
+ db.get_glob(123) # type: ignore[arg-type]
310
+
311
+ def test_get_glob_rejects_empty_key(self) -> None:
312
+ db = TinyKV(self._conn, allow_pickle=True)
313
+ with self.assertRaisesRegex(ValueError, 'must not be empty'):
314
+ db.get_glob('')
@@ -1,132 +0,0 @@
1
- import datetime
2
- import unittest
3
- import sqlite3
4
- import secrets
5
- import tempfile
6
- from pathlib import Path
7
- from tinykv import TinyKV, create_schema
8
-
9
- _TEST_DATA = {
10
- 'none': None,
11
- 'foo': 'bar',
12
- 'bytes': secrets.token_bytes(20),
13
- 'maybe': True,
14
- 'one': 1,
15
- 'pi': 3.1415926,
16
- 'complex': complex(1, 2),
17
- 'now': datetime.datetime.now(),
18
- }
19
-
20
-
21
- class TestKV(unittest.TestCase):
22
-
23
- def setUp(self) -> None:
24
- # pylint: disable-next=consider-using-with
25
- self._tempdir = tempfile.TemporaryDirectory()
26
- self._path = Path(self._tempdir.name) / 'db.sqlite3'
27
- self._conn = sqlite3.connect(self._path)
28
- create_schema(self._conn)
29
-
30
- def tearDown(self) -> None:
31
- self._conn.close()
32
- self._tempdir.cleanup()
33
-
34
- def test_set_get(self) -> None:
35
- db = TinyKV(self._conn)
36
-
37
- for k, v in _TEST_DATA.items():
38
- with self.subTest(k=k):
39
- db.set(k, v)
40
- self.assertEqual(db.get(k), v)
41
-
42
- def test_set_replace(self) -> None:
43
- db = TinyKV(self._conn)
44
-
45
- db.set('foo', 1)
46
- db.set('foo', 'bar')
47
-
48
- self.assertEqual(db.get('foo'), 'bar')
49
-
50
- def test_get_default(self) -> None:
51
- db = TinyKV(self._conn)
52
- self.assertEqual(db.get('foo', 'bar'), 'bar')
53
- self.assertIsNone(db.get('foo', None))
54
-
55
- def test_set_get_persist(self) -> None:
56
- db = TinyKV(self._conn)
57
-
58
- for k, v in _TEST_DATA.items():
59
- db.set(k, v)
60
-
61
- self._conn.commit()
62
- self._conn.close()
63
-
64
- self._conn = sqlite3.connect(self._path)
65
- db2 = TinyKV(self._conn)
66
- for k, v in _TEST_DATA.items():
67
- with self.subTest(k=k):
68
- self.assertEqual(db2.get(k), v)
69
-
70
- def test_get_many(self) -> None:
71
- db = TinyKV(self._conn)
72
-
73
- for k, v in _TEST_DATA.items():
74
- db.set(k, v)
75
-
76
- self.assertEqual(db.get_many(_TEST_DATA.keys()), _TEST_DATA)
77
-
78
- def test_get_many_nonexisting(self) -> None:
79
- db = TinyKV(self._conn)
80
-
81
- db.set('foo', 1)
82
- db.set('bar', 2)
83
-
84
- self.assertEqual(db.get_many(('foo', 'bar', 'not-there')),
85
- {'foo': 1, 'bar': 2})
86
-
87
- def test_get_glob(self) -> None:
88
- db = TinyKV(self._conn)
89
-
90
- db.set('foo:abc', 1)
91
- db.set('foo:xyz', 2)
92
- db.set('bar:abc', 3)
93
-
94
- self.assertEqual(db.get_glob('foo:*'), {'foo:abc': 1, 'foo:xyz': 2})
95
-
96
- def test_set_many(self) -> None:
97
- db = TinyKV(self._conn)
98
-
99
- db.set_many(_TEST_DATA)
100
-
101
- for k, v in _TEST_DATA.items():
102
- with self.subTest(k=k):
103
- self.assertEqual(db.get(k), v)
104
-
105
- def test_remove(self) -> None:
106
- db = TinyKV(self._conn)
107
-
108
- db.set('foo', 'bar')
109
-
110
- db.remove('foo')
111
-
112
- with self.assertRaises(KeyError):
113
- db.get('foo')
114
-
115
- def test_remove_nonexistent(self) -> None:
116
- db = TinyKV(self._conn)
117
- with self.assertRaises(KeyError):
118
- db.remove('nonexistent')
119
-
120
- def test_remove_many(self) -> None:
121
- db = TinyKV(self._conn)
122
-
123
- db.set('foo', 'bar')
124
- db.set('bar', 'bar')
125
-
126
- db.remove_many(('foo', 'bar'))
127
-
128
- with self.assertRaises(KeyError):
129
- db.get('foo')
130
-
131
- with self.assertRaises(KeyError):
132
- db.get('bar')
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes