persidict 0.34.2__py3-none-any.whl → 0.35.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.
Potentially problematic release.
This version of persidict might be problematic. Click here for more details.
- persidict/file_dir_dict.py +259 -53
- persidict/jokers.py +52 -12
- persidict/overlapping_multi_dict.py +99 -36
- persidict/persi_dict.py +293 -105
- persidict/s3_dict.py +167 -38
- persidict/safe_chars.py +22 -3
- persidict/safe_str_tuple.py +142 -34
- persidict/safe_str_tuple_signing.py +140 -36
- persidict/write_once_dict.py +180 -38
- {persidict-0.34.2.dist-info → persidict-0.35.0.dist-info}/METADATA +4 -5
- persidict-0.35.0.dist-info/RECORD +13 -0
- {persidict-0.34.2.dist-info → persidict-0.35.0.dist-info}/WHEEL +1 -1
- persidict/.DS_Store +0 -0
- persidict-0.34.2.dist-info/RECORD +0 -14
persidict/file_dir_dict.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
"""Persistent dictionary implementation backed by local files.
|
|
2
|
+
|
|
3
|
+
FileDirDict stores each key-value pair in a separate file under a base
|
|
4
|
+
directory. Keys determine directory structure and filename; values are
|
|
5
|
+
serialized depending on ``file_type``.
|
|
6
|
+
|
|
7
|
+
- file_type="pkl" or "json": arbitrary Python objects via pickle/jsonpickle.
|
|
8
|
+
- any other value: strings are stored as plain text.
|
|
9
9
|
"""
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
@@ -56,29 +56,35 @@ class FileDirDict(PersiDict):
|
|
|
56
56
|
, immutable_items:bool = False
|
|
57
57
|
, digest_len:int = 8
|
|
58
58
|
, base_class_for_values: Optional[type] = None):
|
|
59
|
-
"""
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
59
|
+
"""Initialize a filesystem-backed persistent dictionary.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
base_dir (str): Base directory where all files are stored. Created
|
|
63
|
+
if it does not exist.
|
|
64
|
+
file_type (str): File extension/format to use for stored values.
|
|
65
|
+
- "pkl" or "json": arbitrary Python objects are supported.
|
|
66
|
+
- any other value: only strings are supported and stored as text.
|
|
67
|
+
immutable_items (bool): If True, existing items cannot be modified
|
|
68
|
+
or deleted.
|
|
69
|
+
digest_len (int): Length of a hash suffix appended to each key path
|
|
70
|
+
element to avoid case-insensitive collisions. Use 0 to disable.
|
|
71
|
+
base_class_for_values (Optional[type]): Optional base class that all
|
|
72
|
+
stored values must be instances of. If provided and not ``str``,
|
|
73
|
+
then file_type must be either "pkl" or "json".
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValueError: If base_dir points to a file; if file_type is "__etag__";
|
|
77
|
+
if file_type contains unsafe characters; or if configuration is
|
|
78
|
+
inconsistent (e.g., non-str values with unsupported file_type).
|
|
79
|
+
RuntimeError: If base_dir cannot be created or is not a directory.
|
|
75
80
|
"""
|
|
76
81
|
|
|
77
82
|
super().__init__(immutable_items = immutable_items
|
|
78
83
|
,digest_len = digest_len
|
|
79
84
|
,base_class_for_values = base_class_for_values)
|
|
80
85
|
|
|
81
|
-
|
|
86
|
+
if file_type != replace_unsafe_chars(file_type, ""):
|
|
87
|
+
raise ValueError("file_type contains unsafe characters")
|
|
82
88
|
self.file_type = file_type
|
|
83
89
|
if self.file_type == "__etag__":
|
|
84
90
|
raise ValueError(
|
|
@@ -87,8 +93,8 @@ class FileDirDict(PersiDict):
|
|
|
87
93
|
|
|
88
94
|
if (base_class_for_values is None or
|
|
89
95
|
not issubclass(base_class_for_values,str)):
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
if file_type not in {"json", "pkl"}:
|
|
97
|
+
raise ValueError("For non-string values file_type must be either 'pkl' or 'json'.")
|
|
92
98
|
|
|
93
99
|
base_dir = str(base_dir)
|
|
94
100
|
|
|
@@ -96,7 +102,8 @@ class FileDirDict(PersiDict):
|
|
|
96
102
|
raise ValueError(f"{base_dir} is a file, not a directory.")
|
|
97
103
|
|
|
98
104
|
os.makedirs(base_dir, exist_ok=True)
|
|
99
|
-
|
|
105
|
+
if not os.path.isdir(base_dir):
|
|
106
|
+
raise RuntimeError(f"Failed to create or access directory: {base_dir}")
|
|
100
107
|
|
|
101
108
|
# self.base_dir_param = _base_dir
|
|
102
109
|
self._base_dir = os.path.abspath(base_dir)
|
|
@@ -105,8 +112,12 @@ class FileDirDict(PersiDict):
|
|
|
105
112
|
def get_params(self):
|
|
106
113
|
"""Return configuration parameters of the dictionary.
|
|
107
114
|
|
|
108
|
-
This method is needed to support Parameterizable API
|
|
109
|
-
|
|
115
|
+
This method is needed to support the Parameterizable API and is absent
|
|
116
|
+
in the standard dict API.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
dict: A mapping of parameter names to values including base_dir and
|
|
120
|
+
file_type merged with the base PersiDict parameters.
|
|
110
121
|
"""
|
|
111
122
|
params = PersiDict.get_params(self)
|
|
112
123
|
additional_params = dict(
|
|
@@ -122,6 +133,9 @@ class FileDirDict(PersiDict):
|
|
|
122
133
|
"""Return dictionary's URL.
|
|
123
134
|
|
|
124
135
|
This property is absent in the original dict API.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
str: URL of the underlying storage in the form "file://<abs_path>".
|
|
125
139
|
"""
|
|
126
140
|
return f"file://{self._base_dir}"
|
|
127
141
|
|
|
@@ -131,16 +145,25 @@ class FileDirDict(PersiDict):
|
|
|
131
145
|
"""Return dictionary's base directory.
|
|
132
146
|
|
|
133
147
|
This property is absent in the original dict API.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
str: Absolute path to the base directory used by this dictionary.
|
|
134
151
|
"""
|
|
135
152
|
return self._base_dir
|
|
136
153
|
|
|
137
154
|
|
|
138
155
|
def __len__(self) -> int:
|
|
139
|
-
"""
|
|
156
|
+
"""Return the number of key-value pairs in the dictionary.
|
|
157
|
+
|
|
158
|
+
This performs a recursive traversal of the base directory.
|
|
140
159
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
160
|
+
Returns:
|
|
161
|
+
int: Count of stored items.
|
|
162
|
+
|
|
163
|
+
Note:
|
|
164
|
+
This operation can be slow on large dictionaries as it walks the
|
|
165
|
+
entire directory tree. Avoid using it in performance-sensitive
|
|
166
|
+
code paths.
|
|
144
167
|
"""
|
|
145
168
|
|
|
146
169
|
suffix = "." + self.file_type
|
|
@@ -149,7 +172,11 @@ class FileDirDict(PersiDict):
|
|
|
149
172
|
|
|
150
173
|
|
|
151
174
|
def clear(self) -> None:
|
|
152
|
-
"""
|
|
175
|
+
"""Remove all elements from the dictionary.
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
KeyError: If immutable_items is True.
|
|
179
|
+
"""
|
|
153
180
|
|
|
154
181
|
if self.immutable_items:
|
|
155
182
|
raise KeyError("Can't clear a dict that contains immutable items")
|
|
@@ -172,7 +199,26 @@ class FileDirDict(PersiDict):
|
|
|
172
199
|
, key:SafeStrTuple
|
|
173
200
|
, create_subdirs:bool=False
|
|
174
201
|
, is_file_path:bool=True) -> str:
|
|
175
|
-
"""Convert a key into
|
|
202
|
+
"""Convert a key into an absolute filesystem path.
|
|
203
|
+
|
|
204
|
+
Transforms a SafeStrTuple into either a directory path or a file path
|
|
205
|
+
inside this dictionary's base directory. When is_file_path is True, the
|
|
206
|
+
final component is treated as a filename with the configured file_type
|
|
207
|
+
extension. When create_subdirs is True, missing intermediate directories
|
|
208
|
+
are created.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
key (SafeStrTuple): The key to convert. It will be temporarily
|
|
212
|
+
signed according to digest_len to produce collision-safe names.
|
|
213
|
+
create_subdirs (bool): If True, create any missing intermediate
|
|
214
|
+
directories.
|
|
215
|
+
is_file_path (bool): If True, return a file path ending with
|
|
216
|
+
".{file_type}"; otherwise return just the directory path for
|
|
217
|
+
the key prefix.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
str: An absolute path within base_dir corresponding to the key.
|
|
221
|
+
"""
|
|
176
222
|
|
|
177
223
|
key = sign_safe_str_tuple(key, self.digest_len)
|
|
178
224
|
key = [self._base_dir] + list(key.strings)
|
|
@@ -190,7 +236,22 @@ class FileDirDict(PersiDict):
|
|
|
190
236
|
|
|
191
237
|
|
|
192
238
|
def _build_key_from_full_path(self, full_path:str)->SafeStrTuple:
|
|
193
|
-
"""Convert
|
|
239
|
+
"""Convert an absolute filesystem path back into a SafeStrTuple key.
|
|
240
|
+
|
|
241
|
+
This function reverses _build_full_path, stripping base_dir, removing the
|
|
242
|
+
file_type extension if the path points to a file, and unsigning the key
|
|
243
|
+
components according to digest_len.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
full_path (str): Absolute path within the dictionary's base
|
|
247
|
+
directory.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
SafeStrTuple: The reconstructed (unsigned) key.
|
|
251
|
+
|
|
252
|
+
Raises:
|
|
253
|
+
ValueError: If full_path is not located under base_dir.
|
|
254
|
+
"""
|
|
194
255
|
|
|
195
256
|
# Ensure we're working with absolute paths
|
|
196
257
|
full_path = os.path.abspath(full_path)
|
|
@@ -225,8 +286,15 @@ class FileDirDict(PersiDict):
|
|
|
225
286
|
"""Get a subdictionary containing items with the same prefix key.
|
|
226
287
|
|
|
227
288
|
For non-existing prefix key, an empty sub-dictionary is returned.
|
|
228
|
-
|
|
229
289
|
This method is absent in the original dict API.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
key (PersiDictKey): Prefix key (string or sequence of strings) that
|
|
293
|
+
identifies the subdirectory.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
FileDirDict: A new FileDirDict instance rooted at the specified
|
|
297
|
+
subdirectory, sharing the same parameters as this dictionary.
|
|
230
298
|
"""
|
|
231
299
|
key = SafeStrTuple(key)
|
|
232
300
|
full_dir_path = self._build_full_path(
|
|
@@ -240,7 +308,14 @@ class FileDirDict(PersiDict):
|
|
|
240
308
|
|
|
241
309
|
|
|
242
310
|
def _read_from_file_impl(self, file_name:str) -> Any:
|
|
243
|
-
"""Read a value from a file.
|
|
311
|
+
"""Read a value from a single file without retries.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
file_name (str): Absolute path to the file to read.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Any: The deserialized value according to file_type.
|
|
318
|
+
"""
|
|
244
319
|
|
|
245
320
|
if self.file_type == "pkl":
|
|
246
321
|
with open(file_name, 'rb') as f:
|
|
@@ -255,7 +330,22 @@ class FileDirDict(PersiDict):
|
|
|
255
330
|
|
|
256
331
|
|
|
257
332
|
def _read_from_file(self,file_name:str) -> Any:
|
|
258
|
-
"""Read a value from a file.
|
|
333
|
+
"""Read a value from a file with retry/backoff for concurrency.
|
|
334
|
+
|
|
335
|
+
Validates that the configured file_type is compatible with the allowed
|
|
336
|
+
value types, then attempts to read the file using an exponential backoff
|
|
337
|
+
to better tolerate concurrent writers.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
file_name (str): Absolute path of the file to read.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Any: The deserialized value according to file_type.
|
|
344
|
+
|
|
345
|
+
Raises:
|
|
346
|
+
ValueError: If file_type is incompatible with non-string values.
|
|
347
|
+
Exception: Propagates the last exception if all retries fail.
|
|
348
|
+
"""
|
|
259
349
|
|
|
260
350
|
if not (self.file_type in {"pkl", "json"} or issubclass(
|
|
261
351
|
self.base_class_for_values, str)):
|
|
@@ -275,7 +365,15 @@ class FileDirDict(PersiDict):
|
|
|
275
365
|
|
|
276
366
|
|
|
277
367
|
def _save_to_file_impl(self, file_name:str, value:Any) -> None:
|
|
278
|
-
"""
|
|
368
|
+
"""Write a single value to a file atomically (no retries).
|
|
369
|
+
|
|
370
|
+
Uses a temporary file and atomic rename to avoid partial writes and to
|
|
371
|
+
reduce the chance of readers observing corrupted data.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
file_name (str): Absolute destination file path.
|
|
375
|
+
value (Any): Value to serialize and save.
|
|
376
|
+
"""
|
|
279
377
|
|
|
280
378
|
dir_name = os.path.dirname(file_name)
|
|
281
379
|
# Use a temporary file and atomic rename to prevent data corruption
|
|
@@ -313,7 +411,20 @@ class FileDirDict(PersiDict):
|
|
|
313
411
|
raise
|
|
314
412
|
|
|
315
413
|
def _save_to_file(self, file_name:str, value:Any) -> None:
|
|
316
|
-
"""Save a value to a file.
|
|
414
|
+
"""Save a value to a file with retry/backoff.
|
|
415
|
+
|
|
416
|
+
Ensures the configured file_type is compatible with value types and then
|
|
417
|
+
writes the value using an exponential backoff to better tolerate
|
|
418
|
+
concurrent readers/writers.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
file_name (str): Absolute destination file path.
|
|
422
|
+
value (Any): Value to serialize and save.
|
|
423
|
+
|
|
424
|
+
Raises:
|
|
425
|
+
ValueError: If file_type is incompatible with non-string values.
|
|
426
|
+
Exception: Propagates the last exception if all retries fail.
|
|
427
|
+
"""
|
|
317
428
|
|
|
318
429
|
if not (self.file_type in {"pkl", "json"} or issubclass(
|
|
319
430
|
self.base_class_for_values, str)):
|
|
@@ -334,14 +445,36 @@ class FileDirDict(PersiDict):
|
|
|
334
445
|
|
|
335
446
|
|
|
336
447
|
def __contains__(self, key:PersiDictKey) -> bool:
|
|
337
|
-
"""
|
|
448
|
+
"""Check whether a key exists in the dictionary.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
key (PersiDictKey): Key (string or sequence of strings) or SafeStrTuple.
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
bool: True if a file for the key exists; False otherwise.
|
|
455
|
+
"""
|
|
338
456
|
key = SafeStrTuple(key)
|
|
339
457
|
filename = self._build_full_path(key)
|
|
340
458
|
return os.path.isfile(filename)
|
|
341
459
|
|
|
342
460
|
|
|
343
461
|
def __getitem__(self, key:PersiDictKey) -> Any:
|
|
344
|
-
"""
|
|
462
|
+
"""Retrieve the value stored for a key.
|
|
463
|
+
|
|
464
|
+
Equivalent to obj[key]. Reads the corresponding file from the disk and
|
|
465
|
+
deserializes according to file_type.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
key (PersiDictKey): Key (string or sequence of strings) or SafeStrTuple.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Any: The stored value.
|
|
472
|
+
|
|
473
|
+
Raises:
|
|
474
|
+
KeyError: If the file for the key does not exist.
|
|
475
|
+
TypeError: If the deserialized value does not match base_class_for_values
|
|
476
|
+
when it is set.
|
|
477
|
+
"""
|
|
345
478
|
key = SafeStrTuple(key)
|
|
346
479
|
filename = self._build_full_path(key)
|
|
347
480
|
if not os.path.isfile(filename):
|
|
@@ -356,7 +489,22 @@ class FileDirDict(PersiDict):
|
|
|
356
489
|
|
|
357
490
|
|
|
358
491
|
def __setitem__(self, key:PersiDictKey, value:Any):
|
|
359
|
-
"""
|
|
492
|
+
"""Store a value for a key on the disk.
|
|
493
|
+
|
|
494
|
+
Interprets joker values KEEP_CURRENT and DELETE_CURRENT accordingly.
|
|
495
|
+
Validates value type if base_class_for_values is set, then serializes
|
|
496
|
+
and writes to a file determined by the key and file_type.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
key (PersiDictKey): Key (string or sequence of strings) or SafeStrTuple.
|
|
500
|
+
value (Any): Value to store, or a joker command.
|
|
501
|
+
|
|
502
|
+
Raises:
|
|
503
|
+
KeyError: If attempting to modify an existing item when
|
|
504
|
+
immutable_items is True.
|
|
505
|
+
TypeError: If the value is a PersiDict or does not match
|
|
506
|
+
base_class_for_values when it is set.
|
|
507
|
+
"""
|
|
360
508
|
|
|
361
509
|
if value is KEEP_CURRENT:
|
|
362
510
|
return
|
|
@@ -384,9 +532,17 @@ class FileDirDict(PersiDict):
|
|
|
384
532
|
|
|
385
533
|
|
|
386
534
|
def __delitem__(self, key:PersiDictKey) -> None:
|
|
387
|
-
"""Delete
|
|
535
|
+
"""Delete the stored value for a key.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
key (PersiDictKey): Key (string or sequence of strings) or SafeStrTuple.
|
|
539
|
+
|
|
540
|
+
Raises:
|
|
541
|
+
KeyError: If immutable_items is True or if the key does not exist.
|
|
542
|
+
"""
|
|
388
543
|
key = SafeStrTuple(key)
|
|
389
|
-
|
|
544
|
+
if self.immutable_items:
|
|
545
|
+
raise KeyError("Can't delete immutable items")
|
|
390
546
|
filename = self._build_full_path(key)
|
|
391
547
|
if not os.path.isfile(filename):
|
|
392
548
|
raise KeyError(f"File {filename} does not exist")
|
|
@@ -394,22 +550,54 @@ class FileDirDict(PersiDict):
|
|
|
394
550
|
|
|
395
551
|
|
|
396
552
|
def _generic_iter(self, result_type: set[str]):
|
|
397
|
-
"""Underlying implementation for .items()/.keys()/.values() iterators
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
553
|
+
"""Underlying implementation for .items()/.keys()/.values() iterators.
|
|
554
|
+
|
|
555
|
+
Produces generators over keys, values, and/or timestamps by traversing
|
|
556
|
+
the directory tree under base_dir. Keys are converted back from paths by
|
|
557
|
+
removing the file extension and unsigning according to digest_len.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
result_type (set[str]): Any non-empty subset of {"keys", "values",
|
|
561
|
+
"timestamps"} specifying which fields to yield.
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
Iterator: A generator yielding:
|
|
565
|
+
- SafeStrTuple if result_type == {"keys"}
|
|
566
|
+
- Any if result_type == {"values"}
|
|
567
|
+
- tuple[SafeStrTuple, Any] if result_type == {"keys", "values"}
|
|
568
|
+
- tuple[..., float] including POSIX timestamp if "timestamps" is requested.
|
|
569
|
+
|
|
570
|
+
Raises:
|
|
571
|
+
TypeError: If result_type is not a set.
|
|
572
|
+
ValueError: If result_type is empty or contains unsupported labels.
|
|
573
|
+
"""
|
|
574
|
+
if not isinstance(result_type, set):
|
|
575
|
+
raise TypeError("result_type must be a set")
|
|
576
|
+
if not (1 <= len(result_type) <= 3):
|
|
577
|
+
raise ValueError("result_type must be a non-empty subset of {'keys','values','timestamps'}")
|
|
578
|
+
allowed = {"keys", "values", "timestamps"}
|
|
579
|
+
invalid = result_type - allowed
|
|
580
|
+
if invalid:
|
|
581
|
+
raise ValueError(f"Unsupported result_type entries: {sorted(invalid)}; allowed: {sorted(allowed)}")
|
|
402
582
|
|
|
403
583
|
walk_results = os.walk(self._base_dir)
|
|
404
584
|
ext_len = len(self.file_type) + 1
|
|
405
585
|
|
|
406
586
|
def splitter(dir_path: str):
|
|
407
|
-
"""Transform a dirname into
|
|
587
|
+
"""Transform a relative dirname into SafeStrTuple components.
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
dir_path (str): Relative path under base_dir (e.g., "a/b").
|
|
591
|
+
|
|
592
|
+
Returns:
|
|
593
|
+
list[str]: List of safe string components (may be empty).
|
|
594
|
+
"""
|
|
408
595
|
if dir_path == ".":
|
|
409
596
|
return []
|
|
410
597
|
return dir_path.split(os.sep)
|
|
411
598
|
|
|
412
599
|
def step():
|
|
600
|
+
"""Generator that yields entries based on result_type."""
|
|
413
601
|
suffix = "." + self.file_type
|
|
414
602
|
for dir_name, _, files in walk_results:
|
|
415
603
|
for f in files:
|
|
@@ -448,6 +636,15 @@ class FileDirDict(PersiDict):
|
|
|
448
636
|
"""Get last modification time (in seconds, Unix epoch time).
|
|
449
637
|
|
|
450
638
|
This method is absent in the original dict API.
|
|
639
|
+
|
|
640
|
+
Args:
|
|
641
|
+
key (PersiDictKey): Key whose timestamp to return.
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
float: POSIX timestamp of the underlying file.
|
|
645
|
+
|
|
646
|
+
Raises:
|
|
647
|
+
FileNotFoundError: If the key does not exist.
|
|
451
648
|
"""
|
|
452
649
|
key = SafeStrTuple(key)
|
|
453
650
|
filename = self._build_full_path(key)
|
|
@@ -455,6 +652,15 @@ class FileDirDict(PersiDict):
|
|
|
455
652
|
|
|
456
653
|
|
|
457
654
|
def random_key(self) -> PersiDictKey | None:
|
|
655
|
+
"""Return a uniformly random key from the dictionary, or None if empty.
|
|
656
|
+
|
|
657
|
+
Performs a full directory traversal using reservoir sampling
|
|
658
|
+
(k=1) to select a random file matching the configured file_type without
|
|
659
|
+
loading all keys into memory.
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
PersiDictKey | None: A random key if any items exist; otherwise None.
|
|
663
|
+
"""
|
|
458
664
|
# canonicalise extension once
|
|
459
665
|
ext = None
|
|
460
666
|
if self.file_type:
|
persidict/jokers.py
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Special singleton markers used to modify values in PersiDict without data payload.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
This module defines two singleton flags used as "joker" values when writing to
|
|
4
|
+
persistent dictionaries:
|
|
5
|
+
|
|
6
|
+
- KEEP_CURRENT: keep the current value unchanged.
|
|
7
|
+
- DELETE_CURRENT: delete the current value if it exists.
|
|
8
|
+
|
|
9
|
+
These flags are intended to be passed as the value part in dict-style
|
|
10
|
+
assignments (e.g., d[key] = KEEP_CURRENT) and are interpreted by PersiDict
|
|
11
|
+
implementations.
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
>>> from persidict.jokers import KEEP_CURRENT, DELETE_CURRENT
|
|
15
|
+
>>> d[key] = KEEP_CURRENT # Do not alter existing value
|
|
16
|
+
>>> d[key] = DELETE_CURRENT # Remove key if present
|
|
6
17
|
"""
|
|
7
18
|
from typing import Any
|
|
8
19
|
|
|
@@ -12,32 +23,61 @@ from parameterizable import (
|
|
|
12
23
|
|
|
13
24
|
|
|
14
25
|
class Joker(ParameterizableClass):
|
|
26
|
+
"""Base class for singleton joker flags.
|
|
27
|
+
|
|
28
|
+
Implements a per-subclass singleton pattern and integrates with the
|
|
29
|
+
parameterizable framework. Subclasses represent value-less commands that
|
|
30
|
+
alter persistence behavior when assigned to a key.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Joker: The singleton instance for the subclass when instantiated.
|
|
34
|
+
"""
|
|
15
35
|
_instances = {}
|
|
16
36
|
|
|
17
37
|
def get_params(self) -> dict[str, Any]:
|
|
38
|
+
"""Return parameters for parameterizable API.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
dict[str, Any]: Always an empty dict for joker flags.
|
|
42
|
+
"""
|
|
18
43
|
return {}
|
|
19
44
|
|
|
20
45
|
def __new__(cls):
|
|
46
|
+
"""Create or return the singleton instance for the subclass."""
|
|
21
47
|
if cls not in Joker._instances:
|
|
22
48
|
Joker._instances[cls] = super().__new__(cls)
|
|
23
49
|
return Joker._instances[cls]
|
|
24
50
|
|
|
25
51
|
|
|
26
52
|
class KeepCurrentFlag(Joker):
|
|
27
|
-
"""
|
|
53
|
+
"""Flag instructing PersiDict to keep the current value unchanged.
|
|
54
|
+
|
|
55
|
+
Usage:
|
|
56
|
+
Assign this flag instead of a real value to indicate that an existing
|
|
57
|
+
value should not be modified.
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
>>> d[key] = KEEP_CURRENT
|
|
28
61
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
62
|
+
Note:
|
|
63
|
+
This is a singleton class; constructing it repeatedly returns the same
|
|
64
|
+
instance.
|
|
32
65
|
"""
|
|
33
66
|
pass
|
|
34
67
|
|
|
35
68
|
class DeleteCurrentFlag(Joker):
|
|
36
|
-
"""
|
|
69
|
+
"""Flag instructing PersiDict to delete the current value for a key.
|
|
70
|
+
|
|
71
|
+
Usage:
|
|
72
|
+
Assign this flag instead of a real value to remove the key if it
|
|
73
|
+
exists. If the key is absent, implementations will typically no-op.
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
>>> d[key] = DELETE_CURRENT
|
|
37
77
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
78
|
+
Note:
|
|
79
|
+
This is a singleton class; constructing it repeatedly returns the same
|
|
80
|
+
instance.
|
|
41
81
|
"""
|
|
42
82
|
pass
|
|
43
83
|
|