sdata 1.5.0__py3-none-any.whl
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.
- sdata/__init__.py +140 -0
- sdata/base.py +684 -0
- sdata/contrib/__init__.py +0 -0
- sdata/contrib/piexif/__init__.py +12 -0
- sdata/contrib/piexif/_common.py +94 -0
- sdata/contrib/piexif/_dump.py +346 -0
- sdata/contrib/piexif/_exceptions.py +2 -0
- sdata/contrib/piexif/_exif.py +638 -0
- sdata/contrib/piexif/_insert.py +60 -0
- sdata/contrib/piexif/_load.py +270 -0
- sdata/contrib/piexif/_remove.py +56 -0
- sdata/contrib/piexif/_transplant.py +45 -0
- sdata/contrib/piexif/_webp.py +240 -0
- sdata/contrib/piexif/helper.py +66 -0
- sdata/contrib/ranger/__init__.py +8 -0
- sdata/contrib/ranger/collections/__init__.py +0 -0
- sdata/contrib/ranger/collections/rangebucketmap.py +346 -0
- sdata/contrib/ranger/collections/rangemap.py +396 -0
- sdata/contrib/ranger/collections/rangeset.py +483 -0
- sdata/contrib/ranger/range/__init__.py +0 -0
- sdata/contrib/ranger/range/cut.py +285 -0
- sdata/contrib/ranger/range/range.py +644 -0
- sdata/contrib/sobol_seq.py +394 -0
- sdata/contrib/sortedcontainers/LICENSE +13 -0
- sdata/contrib/sortedcontainers/__init__.py +74 -0
- sdata/contrib/sortedcontainers/sorteddict.py +789 -0
- sdata/contrib/sortedcontainers/sortedlist.py +2646 -0
- sdata/contrib/sortedcontainers/sortedset.py +733 -0
- sdata/contrib/sqlitedict.py +598 -0
- sdata/contrib/timeflake/__init__.py +40 -0
- sdata/contrib/timeflake/flake.py +69 -0
- sdata/contrib/timeflake/utils.py +47 -0
- sdata/deprecated/__init__.py +10 -0
- sdata/deprecated/data.py +1503 -0
- sdata/did/__init__.py +129 -0
- sdata/did/_http.py +67 -0
- sdata/did/base58btc.py +49 -0
- sdata/did/did_github.py +152 -0
- sdata/did/did_peer4.py +155 -0
- sdata/did/did_web.py +222 -0
- sdata/did/diddoc.py +75 -0
- sdata/did/ed25519.py +225 -0
- sdata/did/eddsa.py +107 -0
- sdata/did/errors.py +40 -0
- sdata/did/issue_vc.py +193 -0
- sdata/did/jose.py +126 -0
- sdata/did/keys.py +104 -0
- sdata/did/utils_didkey.py +161 -0
- sdata/did/verify_vp.py +158 -0
- sdata/doe.py +101 -0
- sdata/dtypes.py +510 -0
- sdata/experiments/__init__.py +331 -0
- sdata/experiments/blob0.py +131 -0
- sdata/experiments/file.py +42 -0
- sdata/experiments/filegroup.py +144 -0
- sdata/experiments/image_old.py +313 -0
- sdata/experiments/ks2.py +15 -0
- sdata/experiments/lapshear.py +8 -0
- sdata/experiments/material.py +7 -0
- sdata/experiments/parameters_deprecated.py +394 -0
- sdata/experiments/workbook.py +226 -0
- sdata/format.py +125 -0
- sdata/imagemeta.py +533 -0
- sdata/interactive.py +118 -0
- sdata/iolib/__init__.py +51 -0
- sdata/iolib/hashtree.py +264 -0
- sdata/iolib/json1sqlitestore.py +554 -0
- sdata/iolib/jsonsqlitestore.py +704 -0
- sdata/iolib/pud.py +242 -0
- sdata/iolib/reader.py +262 -0
- sdata/iolib/writer.py +419 -0
- sdata/metadata.py +882 -0
- sdata/parameter.py +414 -0
- sdata/schema.py +273 -0
- sdata/sclass/__init__.py +111 -0
- sdata/sclass/bfo_classes.py +362 -0
- sdata/sclass/blob.py +362 -0
- sdata/sclass/content.py +87 -0
- sdata/sclass/dataframe.py +1251 -0
- sdata/sclass/dataframegroup.py +187 -0
- sdata/sclass/dependency_graph.py +85 -0
- sdata/sclass/filereference.py +124 -0
- sdata/sclass/image.py +218 -0
- sdata/sclass/lazy_namespace.py +155 -0
- sdata/sclass/process.py +383 -0
- sdata/sclass/process_graph.py +221 -0
- sdata/sclass/zipfile.py +135 -0
- sdata/semantic.py +346 -0
- sdata/suuid.py +136 -0
- sdata/timestamp.py +336 -0
- sdata/tools.py +14 -0
- sdata/units.py +602 -0
- sdata/vocab.py +149 -0
- sdata-1.5.0.dist-info/METADATA +264 -0
- sdata-1.5.0.dist-info/RECORD +98 -0
- sdata-1.5.0.dist-info/WHEEL +5 -0
- sdata-1.5.0.dist-info/licenses/LICENSE-MIT +21 -0
- sdata-1.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sqlite3
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import zlib
|
|
6
|
+
import base64
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class JSONSQLiteStore:
|
|
13
|
+
"""
|
|
14
|
+
JSON-based store on SQLite with:
|
|
15
|
+
- CRUD methods
|
|
16
|
+
- Dynamic index creation, listing, regeneration
|
|
17
|
+
- Wildcard searches
|
|
18
|
+
- Transactions (manual & context manager)
|
|
19
|
+
- Batch operations
|
|
20
|
+
- Existence/Count methods
|
|
21
|
+
- Pagination
|
|
22
|
+
- Schema versioning/migration
|
|
23
|
+
- Backup & Restore
|
|
24
|
+
- Raw SQL hooks
|
|
25
|
+
- TTL expiration
|
|
26
|
+
- Event callbacks
|
|
27
|
+
- Optional compression
|
|
28
|
+
- Type parsing on read
|
|
29
|
+
|
|
30
|
+
Example usage:
|
|
31
|
+
db_path = "/path/to/database.sqlite"
|
|
32
|
+
with JSONSQLiteStore(db_path) as store:
|
|
33
|
+
# Insert a record
|
|
34
|
+
uid = store.insert({"user": "alice", "score": 42})
|
|
35
|
+
print("New ID:", uid)
|
|
36
|
+
|
|
37
|
+
# Fetch a record
|
|
38
|
+
alice = store.get(uid)
|
|
39
|
+
print("Fetched:", alice)
|
|
40
|
+
|
|
41
|
+
# Update a record
|
|
42
|
+
alice["score"] = 99
|
|
43
|
+
store.update(uid, alice)
|
|
44
|
+
|
|
45
|
+
# Delete a record
|
|
46
|
+
store.delete(uid)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
filename: str,
|
|
52
|
+
index_keys: Optional[List[str]] = None,
|
|
53
|
+
timeout: float = 5.0,
|
|
54
|
+
check_same_thread: bool = True,
|
|
55
|
+
compression: bool = False,
|
|
56
|
+
type_parsers: Optional[Dict[str, Callable[[Any], Any]]] = None,
|
|
57
|
+
hooks: Optional[Dict[str, Callable[..., None]]] = None,
|
|
58
|
+
):
|
|
59
|
+
"""
|
|
60
|
+
Initializes the JSONSQLiteStore with the specified parameters.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
filename (str): The path to the SQLite database file.
|
|
64
|
+
index_keys (Optional[List[str]]): List of keys to create indices on.
|
|
65
|
+
timeout (float): Timeout for database connections.
|
|
66
|
+
check_same_thread (bool): Whether to check the same thread for connections.
|
|
67
|
+
compression (bool): Whether to enable compression for stored data.
|
|
68
|
+
type_parsers (Optional[Dict[str, Callable[[Any], Any]]]): Custom type parsers for deserialization.
|
|
69
|
+
hooks (Optional[Dict[str, Callable[..., None]]]): Event hooks for various operations.
|
|
70
|
+
"""
|
|
71
|
+
import warnings
|
|
72
|
+
warnings.warn(
|
|
73
|
+
"JSONSQLiteStore is deprecated (RFC 0011); use "
|
|
74
|
+
"sdata.iolib.json1sqlitestore.JSON1SQLiteStore. Removal in sdata 2.0.",
|
|
75
|
+
DeprecationWarning, stacklevel=2)
|
|
76
|
+
self.filename = filename
|
|
77
|
+
self.compression = compression
|
|
78
|
+
self.type_parsers = type_parsers or {}
|
|
79
|
+
self.hooks = hooks or {}
|
|
80
|
+
self.conn = sqlite3.connect(
|
|
81
|
+
filename,
|
|
82
|
+
timeout=timeout,
|
|
83
|
+
check_same_thread=check_same_thread,
|
|
84
|
+
isolation_level=None,
|
|
85
|
+
detect_types=sqlite3.PARSE_DECLTYPES
|
|
86
|
+
)
|
|
87
|
+
self.conn.execute("PRAGMA journal_mode = WAL")
|
|
88
|
+
self.conn.execute("PRAGMA synchronous = NORMAL")
|
|
89
|
+
self.conn.execute("PRAGMA temp_store = MEMORY")
|
|
90
|
+
self.conn.row_factory = sqlite3.Row
|
|
91
|
+
self._ensure_table()
|
|
92
|
+
if index_keys:
|
|
93
|
+
for key in index_keys:
|
|
94
|
+
self.create_index(key)
|
|
95
|
+
|
|
96
|
+
def _ensure_table(self) -> None:
|
|
97
|
+
"""
|
|
98
|
+
Ensures that the data table exists in the database.
|
|
99
|
+
|
|
100
|
+
Creates the table if it does not already exist.
|
|
101
|
+
"""
|
|
102
|
+
self.conn.execute(
|
|
103
|
+
"""
|
|
104
|
+
CREATE TABLE IF NOT EXISTS data (
|
|
105
|
+
id INTEGER PRIMARY KEY,
|
|
106
|
+
payload TEXT NOT NULL,
|
|
107
|
+
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
108
|
+
);
|
|
109
|
+
"""
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def create_index(self, key: str) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Creates an index on the specified key in the data table.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
key (str): The key to create an index on.
|
|
118
|
+
"""
|
|
119
|
+
idx_name = f"idx_json_{key}"
|
|
120
|
+
sql = (
|
|
121
|
+
f"CREATE INDEX IF NOT EXISTS {idx_name} "
|
|
122
|
+
f"ON data ( json_extract(payload, '$.{key}') );"
|
|
123
|
+
)
|
|
124
|
+
self.conn.execute(sql)
|
|
125
|
+
|
|
126
|
+
def list_indices(self) -> List[Tuple[str, bool]]:
|
|
127
|
+
"""
|
|
128
|
+
Lists all indices on the data table.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
List[Tuple[str, bool]]: A list of tuples containing index names and their uniqueness.
|
|
132
|
+
"""
|
|
133
|
+
cur = self.conn.execute("PRAGMA index_list('data')")
|
|
134
|
+
return [(row['name'], bool(row['unique'])) for row in cur]
|
|
135
|
+
|
|
136
|
+
def regenerate_index(self, key: str) -> None:
|
|
137
|
+
"""
|
|
138
|
+
Regenerates the index for the specified key.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
key (str): The key for which to regenerate the index.
|
|
142
|
+
"""
|
|
143
|
+
idx_name = f"idx_json_{key}"
|
|
144
|
+
self.conn.execute(f"DROP INDEX IF EXISTS {idx_name}")
|
|
145
|
+
self.create_index(key)
|
|
146
|
+
|
|
147
|
+
def _serialize(self, obj: Dict[str, Any]) -> str:
|
|
148
|
+
"""
|
|
149
|
+
Serializes a dictionary to a JSON string, optionally compressing it.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
obj (Dict[str, Any]): The object to serialize.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
str: The serialized JSON string.
|
|
156
|
+
"""
|
|
157
|
+
try:
|
|
158
|
+
js = json.dumps(obj, separators=(',', ':'))
|
|
159
|
+
except (TypeError, ValueError) as e:
|
|
160
|
+
raise ValueError(f"Serialization error: {e} for object: {obj}")
|
|
161
|
+
|
|
162
|
+
if self.compression:
|
|
163
|
+
comp = zlib.compress(js.encode('utf-8'))
|
|
164
|
+
return base64.b64encode(comp).decode('ascii')
|
|
165
|
+
return js
|
|
166
|
+
|
|
167
|
+
def _deserialize(self, payload: str) -> Dict[str, Any]:
|
|
168
|
+
"""
|
|
169
|
+
Deserializes a JSON string back into a dictionary, applying type parsing if necessary.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
payload (str): The JSON string to deserialize.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Dict[str, Any]: The deserialized object.
|
|
176
|
+
"""
|
|
177
|
+
raw = payload
|
|
178
|
+
if self.compression:
|
|
179
|
+
comp = base64.b64decode(payload.encode('ascii'))
|
|
180
|
+
raw = zlib.decompress(comp).decode('utf-8')
|
|
181
|
+
obj = json.loads(raw)
|
|
182
|
+
# Typ-Parsing
|
|
183
|
+
for key, parser in self.type_parsers.items():
|
|
184
|
+
if key in obj:
|
|
185
|
+
obj[key] = parser(obj[key])
|
|
186
|
+
return obj
|
|
187
|
+
|
|
188
|
+
def _payload_expr(self) -> str:
|
|
189
|
+
"""SQL-Ausdruck für das Payload-Bind.
|
|
190
|
+
|
|
191
|
+
Ohne Compression validiert ``json(?)`` das Payload als JSON; mit
|
|
192
|
+
Compression ist das Payload ein base64-String (kein JSON), daher roh ``?``.
|
|
193
|
+
"""
|
|
194
|
+
return "?" if self.compression else "json(?)"
|
|
195
|
+
|
|
196
|
+
def insert(self, obj: Dict[str, Any]) -> int:
|
|
197
|
+
"""
|
|
198
|
+
Inserts a new record into the database.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
obj (Dict[str, Any]): The object to insert.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
int: The ID of the newly inserted record.
|
|
205
|
+
"""
|
|
206
|
+
payload = self._serialize(obj)
|
|
207
|
+
cur = self.conn.execute(
|
|
208
|
+
f"INSERT INTO data (payload) VALUES ({self._payload_expr()})", (payload,)
|
|
209
|
+
)
|
|
210
|
+
rid = cur.lastrowid
|
|
211
|
+
if hook := self.hooks.get('on_insert'):
|
|
212
|
+
hook(rid, obj)
|
|
213
|
+
return rid
|
|
214
|
+
|
|
215
|
+
def insert_many(self, objs: List[Dict[str, Any]]) -> List[int]:
|
|
216
|
+
"""
|
|
217
|
+
Inserts multiple records into the database.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
objs (List[Dict[str, Any]]): The list of objects to insert.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
List[int]: A list of IDs of the newly inserted records.
|
|
224
|
+
"""
|
|
225
|
+
arr = [(self._serialize(o),) for o in objs]
|
|
226
|
+
cur = self.conn.executemany(
|
|
227
|
+
f"INSERT INTO data (payload) VALUES ({self._payload_expr()})", arr
|
|
228
|
+
)
|
|
229
|
+
if hook := self.hooks.get('on_insert_many'):
|
|
230
|
+
hook(objs)
|
|
231
|
+
# SQLite executemany doesn't return rowids list; fetch max id minus count +1
|
|
232
|
+
max_id = self.conn.execute("SELECT last_insert_rowid()").fetchone()[0]
|
|
233
|
+
return list(range(max_id - len(objs) + 1, max_id + 1))
|
|
234
|
+
|
|
235
|
+
def get(self, record_id: int) -> Optional[Dict[str, Any]]:
|
|
236
|
+
"""
|
|
237
|
+
Retrieves a record by its ID.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
record_id (int): The ID of the record to retrieve.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Optional[Dict[str, Any]]: The retrieved object, or None if not found.
|
|
244
|
+
"""
|
|
245
|
+
row = self.conn.execute(
|
|
246
|
+
"SELECT payload FROM data WHERE id = ?", (record_id,)
|
|
247
|
+
).fetchone()
|
|
248
|
+
return self._deserialize(row['payload']) if row else None
|
|
249
|
+
|
|
250
|
+
def fetch_all(self) -> List[Dict[str, Any]]:
|
|
251
|
+
"""
|
|
252
|
+
Fetches all records from the database.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
List[Dict[str, Any]]: A list of all records.
|
|
256
|
+
"""
|
|
257
|
+
cur = self.conn.execute("SELECT payload FROM data")
|
|
258
|
+
return [self._deserialize(r[0]) for r in cur] # Access the first element of the tuple
|
|
259
|
+
|
|
260
|
+
def find_by(
|
|
261
|
+
self,
|
|
262
|
+
key: str,
|
|
263
|
+
op: str,
|
|
264
|
+
value: Any
|
|
265
|
+
) -> List[Dict[str, Any]]:
|
|
266
|
+
"""
|
|
267
|
+
Finds records based on a specific key and operation.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
key (str): The key to search by.
|
|
271
|
+
op (str): The operation to perform (e.g., '=', '>', '<', 'LIKE').
|
|
272
|
+
value (Any): The value to compare against.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
List[Dict[str, Any]]: A list of matching records.
|
|
276
|
+
"""
|
|
277
|
+
op_sql = op.upper()
|
|
278
|
+
val = value
|
|
279
|
+
if isinstance(value, str) and '*' in value:
|
|
280
|
+
op_sql = 'LIKE'
|
|
281
|
+
val = value.replace('*', '%')
|
|
282
|
+
sql = (
|
|
283
|
+
"SELECT payload FROM data "
|
|
284
|
+
f"WHERE json_extract(payload, '$.{key}') {op_sql} ? "
|
|
285
|
+
f"ORDER BY json_extract(payload, '$.{key}') DESC"
|
|
286
|
+
)
|
|
287
|
+
cur = self.conn.execute(sql, (val,))
|
|
288
|
+
return [self._deserialize(r['payload']) for r in cur]
|
|
289
|
+
|
|
290
|
+
def get_dict(self, key: str, value: Any) -> List[Dict[str, Any]]:
|
|
291
|
+
"""
|
|
292
|
+
Retrieves records where a specific key matches a value.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
key (str): The key to match.
|
|
296
|
+
value (Any): The value to match against.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
List[Dict[str, Any]]: A list of matching records.
|
|
300
|
+
|
|
301
|
+
Example usage:
|
|
302
|
+
users = store.get_dict("user", "alice")
|
|
303
|
+
print(users)
|
|
304
|
+
"""
|
|
305
|
+
return self.find_by(key, '=', value)
|
|
306
|
+
|
|
307
|
+
def update(
|
|
308
|
+
self,
|
|
309
|
+
record_id: int,
|
|
310
|
+
new_obj: Dict[str, Any]
|
|
311
|
+
) -> None:
|
|
312
|
+
"""
|
|
313
|
+
Updates an existing record in the database.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
record_id (int): The ID of the record to update.
|
|
317
|
+
new_obj (Dict[str, Any]): The new object data to update the record with.
|
|
318
|
+
|
|
319
|
+
Example usage:
|
|
320
|
+
store.update(uid, {"user": "alice", "score": 100})
|
|
321
|
+
"""
|
|
322
|
+
payload = self._serialize(new_obj)
|
|
323
|
+
self.conn.execute(
|
|
324
|
+
f"UPDATE data SET payload = {self._payload_expr()} WHERE id = ?", (payload, record_id)
|
|
325
|
+
)
|
|
326
|
+
if hook := self.hooks.get('on_update'):
|
|
327
|
+
hook(record_id, new_obj)
|
|
328
|
+
|
|
329
|
+
def update_many(self, pairs: List[Tuple[int, Dict[str, Any]]]) -> None:
|
|
330
|
+
"""
|
|
331
|
+
Updates multiple records in the database.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
pairs (List[Tuple[int, Dict[str, Any]]]): A list of tuples containing record IDs and their new data.
|
|
335
|
+
|
|
336
|
+
Example usage:
|
|
337
|
+
updates = [(uid1, {"score": 50}), (uid2, {"score": 75})]
|
|
338
|
+
store.update_many(updates)
|
|
339
|
+
"""
|
|
340
|
+
arr = [(self._serialize(obj), rid) for rid, obj in pairs]
|
|
341
|
+
self.conn.executemany(
|
|
342
|
+
f"UPDATE data SET payload = {self._payload_expr()} WHERE id = ?", arr
|
|
343
|
+
)
|
|
344
|
+
if hook := self.hooks.get('on_update_many'):
|
|
345
|
+
hook(pairs)
|
|
346
|
+
|
|
347
|
+
def delete(self, record_id: int) -> None:
|
|
348
|
+
"""
|
|
349
|
+
Deletes a record from the database by its ID.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
record_id (int): The ID of the record to delete.
|
|
353
|
+
|
|
354
|
+
Example usage:
|
|
355
|
+
store.delete(uid)
|
|
356
|
+
"""
|
|
357
|
+
self.conn.execute("DELETE FROM data WHERE id = ?", (record_id,))
|
|
358
|
+
if hook := self.hooks.get('on_delete'):
|
|
359
|
+
hook(record_id)
|
|
360
|
+
|
|
361
|
+
def exists(self, record_id: int) -> bool:
|
|
362
|
+
"""
|
|
363
|
+
Checks if a record exists in the database by its ID.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
record_id (int): The ID of the record to check.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
bool: True if the record exists, False otherwise.
|
|
370
|
+
|
|
371
|
+
Example usage:
|
|
372
|
+
exists = store.exists(uid)
|
|
373
|
+
print("Record exists:", exists)
|
|
374
|
+
"""
|
|
375
|
+
cur = self.conn.execute(
|
|
376
|
+
"SELECT 1 FROM data WHERE id = ? LIMIT 1", (record_id,)
|
|
377
|
+
)
|
|
378
|
+
return cur.fetchone() is not None
|
|
379
|
+
|
|
380
|
+
def exists_where(self, key: str, value: Any) -> bool:
|
|
381
|
+
"""
|
|
382
|
+
Checks if a record exists based on a specific key and value.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
key (str): The key to check.
|
|
386
|
+
value (Any): The value to match against.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
bool: True if a matching record exists, False otherwise.
|
|
390
|
+
|
|
391
|
+
Example usage:
|
|
392
|
+
exists = store.exists_where("user", "alice")
|
|
393
|
+
print("User exists:", exists)
|
|
394
|
+
"""
|
|
395
|
+
cur = self.conn.execute(
|
|
396
|
+
"SELECT 1 FROM data WHERE json_extract(payload, '$." + key + "') = ? LIMIT 1", (value,)
|
|
397
|
+
)
|
|
398
|
+
return cur.fetchone() is not None
|
|
399
|
+
|
|
400
|
+
def count(self) -> int:
|
|
401
|
+
"""
|
|
402
|
+
Counts the total number of records in the database.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
int: The total number of records.
|
|
406
|
+
|
|
407
|
+
Example usage:
|
|
408
|
+
total_count = store.count()
|
|
409
|
+
print("Total records:", total_count)
|
|
410
|
+
"""
|
|
411
|
+
return self.conn.execute("SELECT COUNT(*) FROM data").fetchone()[0]
|
|
412
|
+
|
|
413
|
+
def count_where(
|
|
414
|
+
self,
|
|
415
|
+
key: str,
|
|
416
|
+
op: str,
|
|
417
|
+
value: Any
|
|
418
|
+
) -> int:
|
|
419
|
+
"""
|
|
420
|
+
Counts the number of records that match a specific key and operation.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
key (str): The key to search by.
|
|
424
|
+
op (str): The operation to perform (e.g., '=', '>', '<', 'LIKE').
|
|
425
|
+
value (Any): The value to compare against.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
int: The count of matching records.
|
|
429
|
+
|
|
430
|
+
Example usage:
|
|
431
|
+
count_high_scores = store.count_where("score", ">", 40)
|
|
432
|
+
print("High scores count:", count_high_scores)
|
|
433
|
+
"""
|
|
434
|
+
op_sql = op.upper()
|
|
435
|
+
val = value
|
|
436
|
+
if isinstance(value, str) and '*' in value:
|
|
437
|
+
op_sql = 'LIKE'
|
|
438
|
+
val = value.replace('*', '%')
|
|
439
|
+
sql = (
|
|
440
|
+
"SELECT COUNT(*) FROM data "
|
|
441
|
+
f"WHERE json_extract(payload, '$.{key}') {op_sql} ?"
|
|
442
|
+
)
|
|
443
|
+
return self.conn.execute(sql, (val,)).fetchone()[0]
|
|
444
|
+
|
|
445
|
+
def fetch_page(
|
|
446
|
+
self,
|
|
447
|
+
limit: int,
|
|
448
|
+
offset: int,
|
|
449
|
+
key: Optional[str] = None,
|
|
450
|
+
op: str = '=',
|
|
451
|
+
value: Any = None
|
|
452
|
+
) -> List[Dict[str, Any]]:
|
|
453
|
+
"""
|
|
454
|
+
Fetches a paginated list of records from the database.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
limit (int): The maximum number of records to return.
|
|
458
|
+
offset (int): The number of records to skip before starting to collect the result set.
|
|
459
|
+
key (Optional[str]): The key to filter by (if provided).
|
|
460
|
+
op (str): The operation to perform (default is '=').
|
|
461
|
+
value (Any): The value to compare against (if provided).
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
List[Dict[str, Any]]: A list of records for the specified page.
|
|
465
|
+
|
|
466
|
+
Example usage:
|
|
467
|
+
page_records = store.fetch_page(10, 0)
|
|
468
|
+
print("First page records:", page_records)
|
|
469
|
+
"""
|
|
470
|
+
if key and value is not None:
|
|
471
|
+
op_sql = op.upper()
|
|
472
|
+
val = value
|
|
473
|
+
if isinstance(value, str) and '*' in value:
|
|
474
|
+
op_sql = 'LIKE'
|
|
475
|
+
val = value.replace('*', '%')
|
|
476
|
+
sql = (
|
|
477
|
+
"SELECT payload FROM data "
|
|
478
|
+
f"WHERE json_extract(payload, '$.{key}') {op_sql} ? "
|
|
479
|
+
"ORDER BY id ASC LIMIT ? OFFSET ?"
|
|
480
|
+
)
|
|
481
|
+
cur = self.conn.execute(sql, (val, limit, offset))
|
|
482
|
+
else:
|
|
483
|
+
cur = self.conn.execute(
|
|
484
|
+
"SELECT payload FROM data ORDER BY id ASC LIMIT ? OFFSET ?", (limit, offset)
|
|
485
|
+
)
|
|
486
|
+
return [self._deserialize(r['payload']) for r in cur]
|
|
487
|
+
|
|
488
|
+
def get_version(self) -> int:
|
|
489
|
+
"""
|
|
490
|
+
Retrieves the current version of the database schema.
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
int: The current user version of the database.
|
|
494
|
+
|
|
495
|
+
Example usage:
|
|
496
|
+
version = store.get_version()
|
|
497
|
+
print("Current database version:", version)
|
|
498
|
+
"""
|
|
499
|
+
return self.conn.execute("PRAGMA user_version").fetchone()[0]
|
|
500
|
+
|
|
501
|
+
def set_version(self, version: int) -> None:
|
|
502
|
+
"""
|
|
503
|
+
Sets the user version of the database schema.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
version (int): The new user version to set.
|
|
507
|
+
|
|
508
|
+
Example usage:
|
|
509
|
+
store.set_version(2)
|
|
510
|
+
print("Database version updated to 2.")
|
|
511
|
+
"""
|
|
512
|
+
self.conn.execute(f"PRAGMA user_version = {version}")
|
|
513
|
+
|
|
514
|
+
def migrate(self, migration_sql: str) -> None:
|
|
515
|
+
"""
|
|
516
|
+
Applies a migration script to the database.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
migration_sql (str): The SQL commands to execute for the migration.
|
|
520
|
+
|
|
521
|
+
Example usage:
|
|
522
|
+
store.migrate(migration_script)
|
|
523
|
+
"""
|
|
524
|
+
self.conn.executescript(migration_sql)
|
|
525
|
+
|
|
526
|
+
def backup(self, dest_path: str) -> None:
|
|
527
|
+
"""
|
|
528
|
+
Creates a backup of the database to the specified destination path.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
dest_path (str): The file path where the backup should be saved.
|
|
532
|
+
|
|
533
|
+
Example usage:
|
|
534
|
+
store.backup("/path/to/backup.sqlite")
|
|
535
|
+
print("Backup created successfully.")
|
|
536
|
+
"""
|
|
537
|
+
dest = sqlite3.connect(dest_path)
|
|
538
|
+
with dest:
|
|
539
|
+
self.conn.backup(dest)
|
|
540
|
+
dest.close()
|
|
541
|
+
|
|
542
|
+
def restore(self, src_path: str) -> None:
|
|
543
|
+
"""
|
|
544
|
+
Restores the database from a backup file.
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
src_path (str): The file path of the backup to restore from.
|
|
548
|
+
|
|
549
|
+
Example usage:
|
|
550
|
+
store.restore("/path/to/backup.sqlite")
|
|
551
|
+
print("Database restored from backup.")
|
|
552
|
+
"""
|
|
553
|
+
self.conn.close()
|
|
554
|
+
shutil.copy(src_path, self.filename)
|
|
555
|
+
self.conn = sqlite3.connect(self.filename)
|
|
556
|
+
|
|
557
|
+
def execute_raw(
|
|
558
|
+
self,
|
|
559
|
+
sql: str,
|
|
560
|
+
params: Optional[Union[Tuple[Any, ...], List[Any]]] = None
|
|
561
|
+
) -> sqlite3.Cursor:
|
|
562
|
+
"""
|
|
563
|
+
Executes a raw SQL command against the database.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
sql (str): The SQL command to execute.
|
|
567
|
+
params (Optional[Union[Tuple[Any, ...], List[Any]]]): Optional parameters for the SQL command.
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
sqlite3.Cursor: The cursor object containing the result of the query.
|
|
571
|
+
|
|
572
|
+
Example usage:
|
|
573
|
+
cursor = store.execute_raw("SELECT * FROM data WHERE id = ?", (uid,))
|
|
574
|
+
results = cursor.fetchall()
|
|
575
|
+
print("Query results:", results)
|
|
576
|
+
"""
|
|
577
|
+
if params:
|
|
578
|
+
return self.conn.execute(sql, params)
|
|
579
|
+
return self.conn.execute(sql)
|
|
580
|
+
|
|
581
|
+
@contextmanager
|
|
582
|
+
def transaction(self) -> Iterator[None]:
|
|
583
|
+
"""
|
|
584
|
+
Context manager for handling transactions.
|
|
585
|
+
|
|
586
|
+
This method begins a transaction, yields control to the caller,
|
|
587
|
+
and commits the transaction if no exceptions occur. If an exception
|
|
588
|
+
is raised, the transaction is rolled back.
|
|
589
|
+
|
|
590
|
+
Example usage:
|
|
591
|
+
with store.transaction():
|
|
592
|
+
store.insert({"user": "alice", "score": 42})
|
|
593
|
+
store.insert({"user": "bob", "score": 37})
|
|
594
|
+
"""
|
|
595
|
+
try:
|
|
596
|
+
self.conn.execute("BEGIN")
|
|
597
|
+
yield
|
|
598
|
+
self.conn.execute("COMMIT")
|
|
599
|
+
except:
|
|
600
|
+
self.conn.execute("ROLLBACK")
|
|
601
|
+
raise
|
|
602
|
+
|
|
603
|
+
def delete_expired(
|
|
604
|
+
self,
|
|
605
|
+
created_before: Union[datetime, str]
|
|
606
|
+
) -> int:
|
|
607
|
+
"""
|
|
608
|
+
Deletes records that were created before a specified date.
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
created_before (Union[datetime, str]): The cutoff date for deletion.
|
|
612
|
+
Can be a datetime object or a string in ISO format.
|
|
613
|
+
|
|
614
|
+
Returns:
|
|
615
|
+
int: The number of records deleted.
|
|
616
|
+
|
|
617
|
+
Example usage:
|
|
618
|
+
deleted_count = store.delete_expired(datetime(2022, 1, 1))
|
|
619
|
+
print(f"Deleted {deleted_count} expired records.")
|
|
620
|
+
"""
|
|
621
|
+
if isinstance(created_before, datetime):
|
|
622
|
+
ts = created_before.strftime('%Y-%m-%dT%H:%M:%fZ')
|
|
623
|
+
else:
|
|
624
|
+
ts = created_before
|
|
625
|
+
cur = self.conn.execute(
|
|
626
|
+
"DELETE FROM data WHERE created_at < ?", (ts,)
|
|
627
|
+
)
|
|
628
|
+
return cur.rowcount
|
|
629
|
+
|
|
630
|
+
def __enter__(self) -> 'JSONSQLiteStore':
|
|
631
|
+
"""
|
|
632
|
+
Enters the context of the JSONSQLiteStore.
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
JSONSQLiteStore: The current instance of the store.
|
|
636
|
+
|
|
637
|
+
Example usage:
|
|
638
|
+
with JSONSQLiteStore(db_path) as store:
|
|
639
|
+
# Perform operations with the store
|
|
640
|
+
pass
|
|
641
|
+
"""
|
|
642
|
+
return self
|
|
643
|
+
|
|
644
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
645
|
+
"""
|
|
646
|
+
Exits the context of the JSONSQLiteStore.
|
|
647
|
+
|
|
648
|
+
Closes the database connection when exiting the context.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
exc_type (Optional[type]): The type of exception raised, if any.
|
|
652
|
+
exc (Optional[BaseException]): The exception instance, if any.
|
|
653
|
+
tb (Optional[traceback]): The traceback object, if any.
|
|
654
|
+
"""
|
|
655
|
+
self.conn.close()
|
|
656
|
+
|
|
657
|
+
def __iter__(self) -> Iterator[Dict[str, Any]]:
|
|
658
|
+
"""
|
|
659
|
+
Iterates over all records in the store.
|
|
660
|
+
|
|
661
|
+
Yields:
|
|
662
|
+
Dict[str, Any]: Each record in the store.
|
|
663
|
+
|
|
664
|
+
Example usage:
|
|
665
|
+
for record in store:
|
|
666
|
+
print(record)
|
|
667
|
+
"""
|
|
668
|
+
yield from self.fetch_all()
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
if __name__ == '__main__':
|
|
672
|
+
|
|
673
|
+
db_path = os.path.join("/tmp", "json1store.sqlite")
|
|
674
|
+
|
|
675
|
+
with JSONSQLiteStore(db_path) as store:
|
|
676
|
+
# Einfügen
|
|
677
|
+
uid = store.insert({"user": "alice", "score": 42})
|
|
678
|
+
print("Neue ID:", uid)
|
|
679
|
+
uid = store.insert({"user": "alices", "score": 43})
|
|
680
|
+
print("Neue ID:", uid)
|
|
681
|
+
|
|
682
|
+
# Lesen
|
|
683
|
+
alice = store.get(uid)
|
|
684
|
+
print("Gelesen:", alice)
|
|
685
|
+
|
|
686
|
+
# Batch-Einfüge
|
|
687
|
+
for u in [{"user": "bob", "score": 37}, {"user": "carol", "score": 58}]:
|
|
688
|
+
store.insert(u)
|
|
689
|
+
|
|
690
|
+
# Suche nach high scores
|
|
691
|
+
highs = store.find_by("score", ">", 40)
|
|
692
|
+
print("Highscores:", highs)
|
|
693
|
+
|
|
694
|
+
# Update (ganzes Objekt)
|
|
695
|
+
if alice:
|
|
696
|
+
alice["score"] = 99
|
|
697
|
+
store.update(uid, alice)
|
|
698
|
+
|
|
699
|
+
# Löschen
|
|
700
|
+
store.delete(uid)
|
|
701
|
+
|
|
702
|
+
# Alle Datensätze via Iterator
|
|
703
|
+
for rec in store:
|
|
704
|
+
print("Datensatz:", rec)
|