tinykv 0.1.2__tar.gz → 0.2.1__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,65 @@
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.1] - 2026-03-11
13
+
14
+ ### Changed
15
+ - Added an optional `if_not_exists` parameter to `create_schema()` to support
16
+ idempotent table creation when needed while preserving the default error on
17
+ existing tables. Close #1.
18
+
19
+ ## [0.2.0] - 2026-03-11
20
+
21
+ ### Changed
22
+ - **BC BREAK** Minimum Python version is now 3.11 (was 3.7). This enables
23
+ modern type hint syntax and removes compatibility code for older Python
24
+ versions.
25
+ - **BC BREAK** Keys are now case-sensitive. Previously, the database used
26
+ case-insensitive collation, so `'Foo'` and `'foo'` referred to the same key.
27
+ Existing tables using the old schema will continue to work with the old
28
+ behavior, but new tables created with `create_schema()` will use
29
+ case-sensitive keys.
30
+ - Tables created with `create_schema()` now use SQLite `WITHOUT ROWID` storage
31
+ for better performance and smaller database size
32
+
33
+ ## [0.1.3] - 2026-03-11
34
+
35
+ Pickle safety improvements and bug fixes.
36
+
37
+ ### Added
38
+ - Add explicit pickle safety mode with user-facing warning
39
+ - Enforce string key contract: non-string keys raise `TypeError`, empty
40
+ strings raise `ValueError`
41
+
42
+ ### Security
43
+ - Validate table names to prevent SQL injection via table identifier
44
+
45
+ ### Fixed
46
+ - Fix precision loss when roundtripping large integers through the database
47
+ - Fix NaN roundtrip crash by storing as pickle and always deserializing
48
+ - Fix `set_many({})` crash by making empty batch a no-op
49
+ - Fix `remove_many([])` crash by making empty batch a no-op
50
+ - Fix integral float type loss (e.g., `1.0` returned as `1`)
51
+
52
+ ## [0.1.2] - 2025-07-28
53
+
54
+ Package reorganization for better pip compatibility.
55
+
56
+ ### Changed
57
+ - Reorganize package into standard "src" layout for better pip install
58
+ experience
59
+
60
+ ## [0.1.1] - 2023-03-14
61
+
62
+ Initial release of tinykv.
63
+
64
+ ### Added
65
+ - 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.1
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,54 @@ 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
+ If you need idempotent setup, pass `if_not_exists=True`:
259
+
260
+ >>> tinykv.create_schema(conn, if_not_exists=True)
261
+
262
+ You can use a custom table name:
263
+
264
+ >>> tinykv.create_schema(conn, table='my_keys')
265
+
266
+ See the Miscellaneous section for table name requirements.
267
+
268
+
269
+ ## Use cases
270
+
271
+ TinyKV is a good fit when you need a Python SQLite key-value store for:
272
+
273
+ - application configuration and settings
274
+ - local cache data
275
+ - lightweight metadata storage
276
+ - persistent state for command-line tools
277
+ - embedded storage in desktop scripts or services
278
+
279
+
198
280
  Miscellaneous
199
281
  -------------
200
282
 
201
- - TinyKV keys must be string scalars.
283
+ - TinyKV keys must be non-empty string scalars. Non-string keys raise
284
+ `TypeError`, and empty strings raise `ValueError`. Keys are case-sensitive
285
+ (since v0.1.4).
202
286
 
203
287
  - TinyKV does not open or manage transactions. Also, it operates both
204
288
  in autocommit and non-autocommit mode. All operations are atomic.
@@ -220,7 +304,14 @@ Miscellaneous
220
304
  >>>
221
305
  >>> tinykv.create_schema(conn, table=CUSTOM_TABLE)
222
306
  >>>
223
- >>> custom_kv = tinykv.TinyKV(conn, table=CUSTOM_TABLE)
307
+ >>> custom_kv = tinykv.TinyKV(
308
+ ... conn,
309
+ ... table=CUSTOM_TABLE,
310
+ ... allow_pickle=True,
311
+ ... )
312
+
313
+ Table names must match the pattern `[a-zA-Z_][a-zA-Z0-9_]*`. Invalid
314
+ names raise `ValueError`.
224
315
 
225
316
 
226
317
  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,54 @@ 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
+ If you need idempotent setup, pass `if_not_exists=True`:
230
+
231
+ >>> tinykv.create_schema(conn, if_not_exists=True)
232
+
233
+ You can use a custom table name:
234
+
235
+ >>> tinykv.create_schema(conn, table='my_keys')
236
+
237
+ See the Miscellaneous section for table name requirements.
238
+
239
+
240
+ ## Use cases
241
+
242
+ TinyKV is a good fit when you need a Python SQLite key-value store for:
243
+
244
+ - application configuration and settings
245
+ - local cache data
246
+ - lightweight metadata storage
247
+ - persistent state for command-line tools
248
+ - embedded storage in desktop scripts or services
249
+
250
+
176
251
  Miscellaneous
177
252
  -------------
178
253
 
179
- - TinyKV keys must be string scalars.
254
+ - TinyKV keys must be non-empty string scalars. Non-string keys raise
255
+ `TypeError`, and empty strings raise `ValueError`. Keys are case-sensitive
256
+ (since v0.1.4).
180
257
 
181
258
  - TinyKV does not open or manage transactions. Also, it operates both
182
259
  in autocommit and non-autocommit mode. All operations are atomic.
@@ -198,7 +275,14 @@ Miscellaneous
198
275
  >>>
199
276
  >>> tinykv.create_schema(conn, table=CUSTOM_TABLE)
200
277
  >>>
201
- >>> custom_kv = tinykv.TinyKV(conn, table=CUSTOM_TABLE)
278
+ >>> custom_kv = tinykv.TinyKV(
279
+ ... conn,
280
+ ... table=CUSTOM_TABLE,
281
+ ... allow_pickle=True,
282
+ ... )
283
+
284
+ Table names must match the pattern `[a-zA-Z_][a-zA-Z0-9_]*`. Invalid
285
+ names raise `ValueError`.
202
286
 
203
287
 
204
288
  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,32 +1,66 @@
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.1'
10
38
 
11
39
  _DEF_TABLE = 'kv'
40
+ _ALLOW_PICKLE_DEFAULT = object()
12
41
 
13
42
  logger = logging.getLogger(__name__)
14
43
 
15
44
 
16
- def create_schema(conn: sqlite3.Connection, table: str = _DEF_TABLE) -> None:
45
+ def create_schema(conn: sqlite3.Connection, table: str = _DEF_TABLE,
46
+ if_not_exists: bool = False) -> None:
17
47
  """Create a database table for use with tinykv.
18
48
 
19
49
  Args:
20
50
  conn: The SQLite3 connection object.
21
51
  table: The table name (default: 'kv').
52
+ if_not_exists: If true, include SQLite's `IF NOT EXISTS` clause so
53
+ repeated schema creation is a no-op.
22
54
 
23
55
  """
24
- conn.execute(f'CREATE TABLE {table} ('
25
- 'k TEXT NOT NULL COLLATE NOCASE, '
26
- 't TINYINT NOT NULL CHECK (t BETWEEN 1 AND 6), '
56
+ _validate_table_name(table)
57
+ if_not_exists_sql = 'IF NOT EXISTS ' if if_not_exists else ''
58
+ conn.execute(f'CREATE TABLE {if_not_exists_sql}{table} ('
59
+ 'k TEXT NOT NULL, '
60
+ 't TINYINT NOT NULL CHECK (t BETWEEN 1 AND 7), '
27
61
  'v BLOB, '
28
62
  'PRIMARY KEY (k)'
29
- ')')
63
+ ') WITHOUT ROWID')
30
64
 
31
65
 
32
66
  class _DType(enum.IntEnum):
@@ -36,6 +70,7 @@ class _DType(enum.IntEnum):
36
70
  BOOL = 4
37
71
  NUMBER = 5
38
72
  PICKLE = 6
73
+ LONG = 7
39
74
 
40
75
 
41
76
  class TinyKV:
@@ -49,16 +84,35 @@ class TinyKV:
49
84
  >>>
50
85
  >>> create_schema(conn)
51
86
  >>>
52
- >>> kv = TinyKV(conn)
87
+ >>> kv = TinyKV(conn, allow_pickle=True)
53
88
 
54
89
  Args:
55
90
  conn: The SQLite3 connection object.
56
91
  table: The table name (default: 'kv').
92
+ allow_pickle: If true, unsupported values are serialized with pickle.
93
+ If omitted, TinyKV currently behaves as true and emits a
94
+ FutureWarning because the default is expected to change in a
95
+ future release. Set to false to disable pickle-based
96
+ storage/deserialization for safer handling of untrusted database
97
+ content.
57
98
 
58
99
  """
59
100
 
60
- def __init__(self, conn: sqlite3.Connection, table: str = _DEF_TABLE):
61
- """Initialize the key-value object."""
101
+ def __init__(self, conn: sqlite3.Connection, table: str = _DEF_TABLE,
102
+ allow_pickle: bool | object = _ALLOW_PICKLE_DEFAULT) -> None:
103
+ """Initialize the key-value object.
104
+
105
+ Args:
106
+ conn: The SQLite3 connection object.
107
+ table: The table name (default: 'kv').
108
+ allow_pickle: If true, fallback to pickle for unsupported value
109
+ types and unpickle stored pickle rows. If omitted, TinyKV
110
+ currently behaves as true and emits a FutureWarning because the
111
+ default is expected to change to false in a future release. If
112
+ false, both operations raise ValueError.
113
+
114
+ """
115
+ _validate_table_name(table)
62
116
  cur = conn.execute('SELECT name FROM sqlite_master '
63
117
  "WHERE type = 'table' AND name = ?",
64
118
  (table,))
@@ -67,6 +121,15 @@ class TinyKV:
67
121
 
68
122
  self._conn = conn
69
123
  self._table = table
124
+ if allow_pickle is _ALLOW_PICKLE_DEFAULT:
125
+ warnings.warn('allow_pickle currently defaults to True but this '
126
+ 'will change to False in a future release; pass '
127
+ 'allow_pickle explicitly',
128
+ FutureWarning,
129
+ stacklevel=2)
130
+ self._allow_pickle = True
131
+ else:
132
+ self._allow_pickle = bool(allow_pickle)
70
133
 
71
134
  @property
72
135
  def conn(self) -> sqlite3.Connection:
@@ -77,8 +140,7 @@ class TinyKV:
77
140
  """
78
141
  return self._conn
79
142
 
80
- def _serialize(self, data: Any) -> Tuple[_DType,
81
- Optional[Union[float, bytes]]]:
143
+ def _serialize(self, data: Any) -> tuple[_DType, float | bytes | None]:
82
144
  if data is None:
83
145
  return (_DType.NONE, None)
84
146
 
@@ -91,10 +153,20 @@ class TinyKV:
91
153
  if isinstance(data, bool):
92
154
  return (_DType.BOOL, int(data))
93
155
 
94
- if isinstance(data, (int, float)):
156
+ if isinstance(data, int):
157
+ return (_DType.LONG, str(data).encode('utf-8'))
158
+
159
+ if isinstance(data, float):
160
+ if math.isnan(data):
161
+ return (_DType.PICKLE, pickle.dumps(data))
95
162
  return (_DType.NUMBER, data)
96
163
 
97
- return (_DType.PICKLE, pickle.dumps(data))
164
+ if self._allow_pickle:
165
+ return (_DType.PICKLE, pickle.dumps(data))
166
+
167
+ raise ValueError('Cannot store value type without pickle support; '
168
+ 'initialize TinyKV with allow_pickle=True to enable '
169
+ 'legacy pickled values')
98
170
 
99
171
  def _unserialize(self, dtype: _DType, data: bytes) -> Any:
100
172
  if dtype == _DType.NONE:
@@ -110,16 +182,35 @@ class TinyKV:
110
182
  return bool(data)
111
183
 
112
184
  if dtype == _DType.NUMBER:
113
- number = float(data)
114
- return int(number) if number.is_integer() else number
185
+ return float(data)
186
+
187
+ if dtype == _DType.LONG:
188
+ return int(data.decode('utf-8'))
115
189
 
116
190
  if dtype == _DType.PICKLE:
191
+ if not self._allow_pickle:
192
+ value = pickle.loads(data)
193
+ if isinstance(value, float) and math.isnan(value):
194
+ return value
195
+ raise ValueError('Cannot deserialize pickled value with '
196
+ 'allow_pickle=False')
117
197
  return pickle.loads(data)
118
198
 
119
- raise ValueError('Unsupported data type {dtype}')
199
+ raise ValueError(f'Unsupported data type {dtype}')
120
200
 
121
201
  def set(self, key: str, value: Any) -> None:
122
- """Store a value in the database."""
202
+ """Store a value in the database.
203
+
204
+ Args:
205
+ key: The key to store the value under.
206
+ value: The value to store.
207
+
208
+ Raises:
209
+ TypeError: If key is not a string.
210
+ ValueError: If key is an empty string.
211
+
212
+ """
213
+ _validate_key(key)
123
214
  assert self._conn
124
215
  dtype, data = self._serialize(value)
125
216
  self._conn.execute(f'INSERT OR REPLACE INTO {self._table} (k, t, v) '
@@ -136,7 +227,7 @@ class TinyKV:
136
227
  >>> conn = sqlite3.connect(':memory:')
137
228
  >>>
138
229
  >>> create_schema(conn)
139
- >>> kv = TinyKV(conn)
230
+ >>> kv = TinyKV(conn, allow_pickle=True)
140
231
  >>>
141
232
  >>> kv.set('foo', 'bar')
142
233
  >>> kv.get('foo')
@@ -152,8 +243,11 @@ class TinyKV:
152
243
  Raises:
153
244
  KeyError: if the key does not exist and no default is
154
245
  provided.
246
+ TypeError: If key is not a string.
247
+ ValueError: If key is an empty string.
155
248
 
156
249
  """
250
+ _validate_key(key)
157
251
  assert self._conn
158
252
  cur = self._conn.execute(f'SELECT t, v FROM {self._table} '
159
253
  'WHERE k = ?', (key,))
@@ -164,7 +258,7 @@ class TinyKV:
164
258
  raise KeyError(key)
165
259
  return self._unserialize(_DType(row[0]), row[1])
166
260
 
167
- def get_many(self, keys: Iterable[str]) -> Dict[str, Any]:
261
+ def get_many(self, keys: Iterable[str]) -> dict[str, Any]:
168
262
  """Get many values from the database.
169
263
 
170
264
  Args:
@@ -174,16 +268,22 @@ class TinyKV:
174
268
  A dict where with only the keys found on the database, and their
175
269
  respective values.
176
270
 
271
+ Raises:
272
+ TypeError: If any key is not a string.
273
+ ValueError: If any key is an empty string.
274
+
177
275
  """
178
276
  assert self._conn
179
277
  tkeys = tuple(keys)
278
+ for k in tkeys:
279
+ _validate_key(k)
180
280
  rows = self._conn.execute(f'SELECT k, t, v FROM {self._table} WHERE '
181
281
  f'k IN ({", ".join(["?"] * len(tkeys))})',
182
282
  tkeys)
183
283
  return {r[0]: self._unserialize(_DType(r[1]), r[2])
184
284
  for r in rows.fetchall()}
185
285
 
186
- def get_glob(self, glob_key: str) -> Dict[str, Any]:
286
+ def get_glob(self, glob_key: str) -> dict[str, Any]:
187
287
  """Get many values using a glob pattern.
188
288
 
189
289
  Similar to `get_many()`, but using a glob pattern.
@@ -195,7 +295,12 @@ class TinyKV:
195
295
  A dict where keys are the keys matching `glob_key` found
196
296
  on the database, and their respective values.
197
297
 
298
+ Raises:
299
+ TypeError: If glob_key is not a string.
300
+ ValueError: If glob_key is an empty string.
301
+
198
302
  """
303
+ _validate_key(glob_key)
199
304
  assert self._conn
200
305
  rows = self._conn.execute('SELECT k, t, v '
201
306
  f'FROM {self._table} '
@@ -209,8 +314,16 @@ class TinyKV:
209
314
  Args:
210
315
  kvdict: A mapping of keys to values.
211
316
 
317
+ Raises:
318
+ TypeError: If any key is not a string.
319
+ ValueError: If any key is an empty string.
320
+
212
321
  """
213
322
  assert self._conn
323
+ if not kvdict:
324
+ return
325
+ for k in kvdict:
326
+ _validate_key(k)
214
327
  self._conn.execute(f'INSERT OR REPLACE INTO {self._table} (k, t, v) '
215
328
  f'VALUES {", ".join(["(?, ?, ?)"] * len(kvdict))}',
216
329
  tuple(p[i]
@@ -228,8 +341,11 @@ class TinyKV:
228
341
 
229
342
  Raises:
230
343
  KeyError: If the key is not found.
344
+ TypeError: If key is not a string.
345
+ ValueError: If key is an empty string.
231
346
 
232
347
  """
348
+ _validate_key(key)
233
349
  assert self._conn
234
350
  cur = self._conn.execute(f'DELETE FROM {self._table} WHERE k = ?',
235
351
  (key,))
@@ -243,9 +359,17 @@ class TinyKV:
243
359
 
244
360
  Args:
245
361
  keys: Iterable of keys to remove.
362
+
363
+ Raises:
364
+ TypeError: If any key is not a string.
365
+ ValueError: If any key is an empty string.
246
366
  """
247
367
  assert self._conn
248
368
  tkeys = tuple(keys)
369
+ if not tkeys:
370
+ return
371
+ for k in tkeys:
372
+ _validate_key(k)
249
373
  self._conn.execute(f'DELETE FROM {self._table} WHERE '
250
374
  f'k IN ({", ".join(["?"] * len(tkeys))})',
251
375
  tkeys)
@@ -0,0 +1,321 @@
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): # pylint: disable=too-many-public-methods
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_create_schema_existing_table_raises_by_default(self) -> None:
46
+ with self.assertRaises(sqlite3.OperationalError):
47
+ create_schema(self._conn)
48
+
49
+ def test_create_schema_if_not_exists(self) -> None:
50
+ create_schema(self._conn, if_not_exists=True)
51
+
52
+ def test_set_replace(self) -> None:
53
+ db = TinyKV(self._conn, allow_pickle=True)
54
+
55
+ db.set('foo', 1)
56
+ db.set('foo', 'bar')
57
+
58
+ self.assertEqual(db.get('foo'), 'bar')
59
+
60
+ def test_get_default(self) -> None:
61
+ db = TinyKV(self._conn, allow_pickle=True)
62
+ self.assertEqual(db.get('foo', 'bar'), 'bar')
63
+ self.assertIsNone(db.get('foo', None))
64
+
65
+ def test_set_get_persist(self) -> None:
66
+ db = TinyKV(self._conn, allow_pickle=True)
67
+
68
+ for k, v in _TEST_DATA.items():
69
+ db.set(k, v)
70
+
71
+ self._conn.commit()
72
+ self._conn.close()
73
+
74
+ self._conn = sqlite3.connect(self._path)
75
+ db2 = TinyKV(self._conn, allow_pickle=True)
76
+ for k, v in _TEST_DATA.items():
77
+ with self.subTest(k=k):
78
+ self.assertEqual(db2.get(k), v)
79
+
80
+ def test_get_many(self) -> None:
81
+ db = TinyKV(self._conn, allow_pickle=True)
82
+
83
+ for k, v in _TEST_DATA.items():
84
+ db.set(k, v)
85
+
86
+ self.assertEqual(db.get_many(_TEST_DATA.keys()), _TEST_DATA)
87
+
88
+ def test_get_many_nonexisting(self) -> None:
89
+ db = TinyKV(self._conn, allow_pickle=True)
90
+
91
+ db.set('foo', 1)
92
+ db.set('bar', 2)
93
+
94
+ self.assertEqual(db.get_many(('foo', 'bar', 'not-there')),
95
+ {'foo': 1, 'bar': 2})
96
+
97
+ def test_get_glob(self) -> None:
98
+ db = TinyKV(self._conn, allow_pickle=True)
99
+
100
+ db.set('foo:abc', 1)
101
+ db.set('foo:xyz', 2)
102
+ db.set('bar:abc', 3)
103
+
104
+ self.assertEqual(db.get_glob('foo:*'), {'foo:abc': 1, 'foo:xyz': 2})
105
+
106
+ def test_set_many(self) -> None:
107
+ db = TinyKV(self._conn, allow_pickle=True)
108
+
109
+ db.set_many(_TEST_DATA)
110
+
111
+ for k, v in _TEST_DATA.items():
112
+ with self.subTest(k=k):
113
+ self.assertEqual(db.get(k), v)
114
+
115
+ def test_set_many_empty_mapping(self) -> None:
116
+ db = TinyKV(self._conn, allow_pickle=True)
117
+
118
+ db.set_many({})
119
+
120
+ def test_remove(self) -> None:
121
+ db = TinyKV(self._conn, allow_pickle=True)
122
+
123
+ db.set('foo', 'bar')
124
+
125
+ db.remove('foo')
126
+
127
+ with self.assertRaises(KeyError):
128
+ db.get('foo')
129
+
130
+ def test_remove_nonexistent(self) -> None:
131
+ db = TinyKV(self._conn, allow_pickle=True)
132
+ with self.assertRaises(KeyError):
133
+ db.remove('nonexistent')
134
+
135
+ def test_remove_many(self) -> None:
136
+ db = TinyKV(self._conn, allow_pickle=True)
137
+
138
+ db.set('foo', 'bar')
139
+ db.set('bar', 'bar')
140
+
141
+ db.remove_many(('foo', 'bar'))
142
+
143
+ with self.assertRaises(KeyError):
144
+ db.get('foo')
145
+
146
+ with self.assertRaises(KeyError):
147
+ db.get('bar')
148
+
149
+ def test_remove_many_empty(self) -> None:
150
+ db = TinyKV(self._conn, allow_pickle=True)
151
+
152
+ db.remove_many([])
153
+
154
+ def test_safe_mode_rejects_pickle_on_set(self) -> None:
155
+ db = TinyKV(self._conn, allow_pickle=False)
156
+
157
+ with self.assertRaisesRegex(ValueError, 'allow_pickle=True'):
158
+ db.set('now', datetime.datetime.now())
159
+
160
+ def test_safe_mode_rejects_existing_pickled_row(self) -> None:
161
+ payload = pickle.dumps(datetime.datetime(2022, 3, 19, 20, 15, 5))
162
+ self._conn.execute('INSERT INTO kv (k, t, v) VALUES (?, ?, ?)',
163
+ ('pickled', 6, payload))
164
+
165
+ db = TinyKV(self._conn, allow_pickle=False)
166
+ with self.assertRaisesRegex(ValueError, 'allow_pickle=False'):
167
+ db.get('pickled')
168
+
169
+ def test_compat_mode_allows_pickle_roundtrip(self) -> None:
170
+ db = TinyKV(self._conn, allow_pickle=True)
171
+ dt = datetime.datetime(2022, 3, 19, 20, 15, 5)
172
+
173
+ db.set('dt', dt)
174
+
175
+ self.assertEqual(db.get('dt'), dt)
176
+
177
+ def test_implicit_allow_pickle_warns(self) -> None:
178
+ with self.assertWarnsRegex(FutureWarning, 'allow_pickle'):
179
+ TinyKV(self._conn)
180
+
181
+ def test_explicit_allow_pickle_true_does_not_warn(self) -> None:
182
+ with warnings.catch_warnings(record=True) as warns:
183
+ warnings.simplefilter('always')
184
+ TinyKV(self._conn, allow_pickle=True)
185
+ self.assertEqual(warns, [])
186
+
187
+ def test_explicit_allow_pickle_false_does_not_warn(self) -> None:
188
+ with warnings.catch_warnings(record=True) as warns:
189
+ warnings.simplefilter('always')
190
+ TinyKV(self._conn, allow_pickle=False)
191
+ self.assertEqual(warns, [])
192
+
193
+ def test_large_int_roundtrip(self) -> None:
194
+ db = TinyKV(self._conn, allow_pickle=True)
195
+
196
+ test_cases = [
197
+ (2**53 - 1, 'max safe integer'),
198
+ (2**53, 'just over max safe integer'),
199
+ (2**53 + 1, 'larger than max safe integer'),
200
+ (10**20, 'very large positive'),
201
+ (-(10**20), 'very large negative'),
202
+ (0, 'zero'),
203
+ (1, 'small positive'),
204
+ (-1, 'small negative'),
205
+ ]
206
+
207
+ for value, description in test_cases:
208
+ with self.subTest(value=value, description=description):
209
+ db.set('large_int', value)
210
+ self.assertEqual(db.get('large_int'), value)
211
+
212
+ def test_integral_float_roundtrip(self) -> None:
213
+ db = TinyKV(self._conn, allow_pickle=True)
214
+
215
+ test_cases = [
216
+ (1.0, 'one as float'),
217
+ (0.0, 'zero as float'),
218
+ (-0.0, 'negative zero as float'),
219
+ (1.5, 'one point five'),
220
+ (-2.5, 'negative two point five'),
221
+ ]
222
+
223
+ for value, description in test_cases:
224
+ with self.subTest(value=value, description=description):
225
+ db.set('integral_float', value)
226
+ result = db.get('integral_float')
227
+ self.assertEqual(result, value)
228
+ self.assertIsInstance(result, float)
229
+
230
+ def test_nan_roundtrip(self) -> None:
231
+ db = TinyKV(self._conn, allow_pickle=True)
232
+
233
+ db.set('nan', float('nan'))
234
+ result = db.get('nan')
235
+ self.assertTrue(math.isnan(result))
236
+
237
+ def test_nan_roundtrip_safe_mode(self) -> None:
238
+ db = TinyKV(self._conn, allow_pickle=False)
239
+
240
+ db.set('nan', float('nan'))
241
+ result = db.get('nan')
242
+ self.assertTrue(math.isnan(result))
243
+
244
+ def test_inf_roundtrip(self) -> None:
245
+ db = TinyKV(self._conn, allow_pickle=True)
246
+
247
+ db.set('inf', float('inf'))
248
+ self.assertEqual(db.get('inf'), float('inf'))
249
+
250
+ db.set('neg_inf', float('-inf'))
251
+ self.assertEqual(db.get('neg_inf'), float('-inf'))
252
+
253
+ def test_set_rejects_non_string_key(self) -> None:
254
+ db = TinyKV(self._conn, allow_pickle=True)
255
+ with self.assertRaisesRegex(TypeError, 'must be a string'):
256
+ db.set(123, 'value') # type: ignore[arg-type]
257
+
258
+ def test_set_rejects_empty_key(self) -> None:
259
+ db = TinyKV(self._conn, allow_pickle=True)
260
+ with self.assertRaisesRegex(ValueError, 'must not be empty'):
261
+ db.set('', 'value')
262
+
263
+ def test_get_rejects_non_string_key(self) -> None:
264
+ db = TinyKV(self._conn, allow_pickle=True)
265
+ with self.assertRaisesRegex(TypeError, 'must be a string'):
266
+ db.get(123) # type: ignore[arg-type]
267
+
268
+ def test_get_rejects_empty_key(self) -> None:
269
+ db = TinyKV(self._conn, allow_pickle=True)
270
+ with self.assertRaisesRegex(ValueError, 'must not be empty'):
271
+ db.get('')
272
+
273
+ def test_remove_rejects_non_string_key(self) -> None:
274
+ db = TinyKV(self._conn, allow_pickle=True)
275
+ with self.assertRaisesRegex(TypeError, 'must be a string'):
276
+ db.remove(123) # type: ignore[arg-type]
277
+
278
+ def test_remove_rejects_empty_key(self) -> None:
279
+ db = TinyKV(self._conn, allow_pickle=True)
280
+ with self.assertRaisesRegex(ValueError, 'must not be empty'):
281
+ db.remove('')
282
+
283
+ def test_get_many_rejects_non_string_key(self) -> None:
284
+ db = TinyKV(self._conn, allow_pickle=True)
285
+ with self.assertRaisesRegex(TypeError, 'must be a string'):
286
+ db.get_many([123]) # type: ignore[list-item]
287
+
288
+ def test_get_many_rejects_empty_key(self) -> None:
289
+ db = TinyKV(self._conn, allow_pickle=True)
290
+ with self.assertRaisesRegex(ValueError, 'must not be empty'):
291
+ db.get_many([''])
292
+
293
+ def test_set_many_rejects_non_string_key(self) -> None:
294
+ db = TinyKV(self._conn, allow_pickle=True)
295
+ with self.assertRaisesRegex(TypeError, 'must be a string'):
296
+ db.set_many({123: 'value'}) # type: ignore[dict-item]
297
+
298
+ def test_set_many_rejects_empty_key(self) -> None:
299
+ db = TinyKV(self._conn, allow_pickle=True)
300
+ with self.assertRaisesRegex(ValueError, 'must not be empty'):
301
+ db.set_many({'': 'value'})
302
+
303
+ def test_remove_many_rejects_non_string_key(self) -> None:
304
+ db = TinyKV(self._conn, allow_pickle=True)
305
+ with self.assertRaisesRegex(TypeError, 'must be a string'):
306
+ db.remove_many([123]) # type: ignore[list-item]
307
+
308
+ def test_remove_many_rejects_empty_key(self) -> None:
309
+ db = TinyKV(self._conn, allow_pickle=True)
310
+ with self.assertRaisesRegex(ValueError, 'must not be empty'):
311
+ db.remove_many([''])
312
+
313
+ def test_get_glob_rejects_non_string_key(self) -> None:
314
+ db = TinyKV(self._conn, allow_pickle=True)
315
+ with self.assertRaisesRegex(TypeError, 'must be a string'):
316
+ db.get_glob(123) # type: ignore[arg-type]
317
+
318
+ def test_get_glob_rejects_empty_key(self) -> None:
319
+ db = TinyKV(self._conn, allow_pickle=True)
320
+ with self.assertRaisesRegex(ValueError, 'must not be empty'):
321
+ 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