persidict 0.37.2__py3-none-any.whl → 0.103.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/__init__.py +41 -24
- persidict/basic_s3_dict.py +595 -0
- persidict/cached_appendonly_dict.py +247 -0
- persidict/cached_mutable_dict.py +248 -0
- persidict/empty_dict.py +171 -0
- persidict/file_dir_dict.py +130 -122
- persidict/local_dict.py +502 -0
- persidict/overlapping_multi_dict.py +23 -15
- persidict/persi_dict.py +281 -148
- persidict/s3_dict_file_dir_cached.py +215 -0
- persidict/{s3_dict.py → s3_dict_legacy.py} +111 -90
- persidict/safe_chars.py +13 -0
- persidict/safe_str_tuple.py +28 -6
- persidict/singletons.py +232 -0
- persidict/write_once_dict.py +47 -30
- {persidict-0.37.2.dist-info → persidict-0.103.0.dist-info}/METADATA +35 -25
- persidict-0.103.0.dist-info/RECORD +19 -0
- {persidict-0.37.2.dist-info → persidict-0.103.0.dist-info}/WHEEL +1 -1
- persidict/.DS_Store +0 -0
- persidict/jokers.py +0 -99
- persidict-0.37.2.dist-info/RECORD +0 -14
persidict/persi_dict.py
CHANGED
|
@@ -23,37 +23,24 @@ from parameterizable import ParameterizableClass, sort_dict_by_keys
|
|
|
23
23
|
from typing import Any, Sequence, Optional
|
|
24
24
|
from collections.abc import MutableMapping
|
|
25
25
|
|
|
26
|
-
from .
|
|
26
|
+
from . import NonEmptySafeStrTuple
|
|
27
|
+
from .singletons import (KEEP_CURRENT, DELETE_CURRENT, Joker,
|
|
28
|
+
CONTINUE_NORMAL_EXECUTION, StatusFlag, EXECUTION_IS_COMPLETE,
|
|
29
|
+
ETagHasNotChangedFlag, ETAG_HAS_NOT_CHANGED)
|
|
30
|
+
from .safe_chars import contains_unsafe_chars
|
|
27
31
|
from .safe_str_tuple import SafeStrTuple
|
|
28
32
|
|
|
29
33
|
PersiDictKey = SafeStrTuple | Sequence[str] | str
|
|
34
|
+
NonEmptyPersiDictKey = NonEmptySafeStrTuple | Sequence[str] | str
|
|
30
35
|
"""A value which can be used as a key for PersiDict.
|
|
31
36
|
|
|
32
|
-
PersiDict instances accept keys in the form of SafeStrTuple,
|
|
33
|
-
or a string, or a sequence of strings.
|
|
37
|
+
PersiDict instances accept keys in the form of (NonEmpty)SafeStrTuple,
|
|
38
|
+
or a string, or a (non-empty) sequence of strings.
|
|
34
39
|
The characters within strings must be URL/filename-safe.
|
|
35
40
|
If a string (or a sequence of strings) is passed to a PersiDict as a key,
|
|
36
41
|
it will be automatically converted into SafeStrTuple.
|
|
37
42
|
"""
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
def non_empty_persidict_key(*args) -> SafeStrTuple:
|
|
41
|
-
"""Create a non-empty SafeStrTuple from the given arguments.
|
|
42
|
-
This is a convenience function that ensures the resulting SafeStrTuple is
|
|
43
|
-
not empty, raising a KeyError if it is.
|
|
44
|
-
Args:
|
|
45
|
-
*args: Arguments to pass to SafeStrTuple constructor.
|
|
46
|
-
Returns:
|
|
47
|
-
SafeStrTuple: A non-empty SafeStrTuple instance.
|
|
48
|
-
Raises:
|
|
49
|
-
KeyError: If the resulting SafeStrTuple is empty.
|
|
50
|
-
"""
|
|
51
|
-
result = SafeStrTuple(*args)
|
|
52
|
-
if len(result) == 0:
|
|
53
|
-
raise KeyError("Key cannot be empty")
|
|
54
|
-
return result
|
|
55
|
-
|
|
56
|
-
|
|
57
44
|
class PersiDict(MutableMapping, ParameterizableClass):
|
|
58
45
|
"""Abstract dict-like interface for durable key-value stores.
|
|
59
46
|
|
|
@@ -62,52 +49,65 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
62
49
|
similar to Python's dict but does not guarantee insertion order and adds
|
|
63
50
|
persistence-specific helpers (e.g., timestamp()).
|
|
64
51
|
|
|
65
|
-
Attributes:
|
|
66
|
-
|
|
67
|
-
If True, items are
|
|
68
|
-
deleted.
|
|
69
|
-
digest_len (int):
|
|
70
|
-
Length of a base32 MD5 digest fragment used to suffix each key
|
|
71
|
-
component to avoid collisions on case-insensitive filesystems. 0
|
|
72
|
-
disables suffixing.
|
|
52
|
+
Attributes (can't be changed after initialization):
|
|
53
|
+
append_only (bool):
|
|
54
|
+
If True, items are immutable and non-removable: existing values
|
|
55
|
+
cannot be modified or deleted.
|
|
73
56
|
base_class_for_values (Optional[type]):
|
|
74
57
|
Optional base class that all values must inherit from. If None, any
|
|
75
58
|
type is accepted.
|
|
59
|
+
serialization_format (str):
|
|
60
|
+
File extension/format for stored values (e.g., "pkl", "json").
|
|
76
61
|
"""
|
|
77
62
|
|
|
78
|
-
|
|
79
|
-
immutable_items:bool
|
|
63
|
+
append_only:bool
|
|
80
64
|
base_class_for_values:Optional[type]
|
|
65
|
+
serialization_format:str
|
|
81
66
|
|
|
82
67
|
def __init__(self,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
68
|
+
append_only: bool = False,
|
|
69
|
+
base_class_for_values: type|None = None,
|
|
70
|
+
serialization_format: str = "pkl",
|
|
86
71
|
*args, **kwargs):
|
|
87
72
|
"""Initialize base parameters shared by all persistent dictionaries.
|
|
88
73
|
|
|
89
74
|
Args:
|
|
90
|
-
|
|
75
|
+
append_only (bool): If True, items cannot be modified or deleted.
|
|
91
76
|
Defaults to False.
|
|
92
|
-
digest_len (int): Number of hash characters to append to key components
|
|
93
|
-
to avoid case-insensitive collisions. Must be non-negative.
|
|
94
|
-
Defaults to 8.
|
|
95
77
|
base_class_for_values (Optional[type]): Optional base class that values
|
|
96
78
|
must inherit from. If None, values are not type-restricted.
|
|
97
79
|
Defaults to None.
|
|
80
|
+
serialization_format (str): File extension/format for stored values.
|
|
81
|
+
Defaults to "pkl".
|
|
98
82
|
*args: Additional positional arguments (ignored in base class, reserved
|
|
99
83
|
for subclasses).
|
|
100
84
|
**kwargs: Additional keyword arguments (ignored in base class, reserved
|
|
101
85
|
for subclasses).
|
|
102
86
|
|
|
103
87
|
Raises:
|
|
104
|
-
ValueError: If
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
88
|
+
ValueError: If serialization_format is an empty string,
|
|
89
|
+
or contains unsafe characters, or not 'jason' or 'pkl'
|
|
90
|
+
for non-string values.
|
|
91
|
+
|
|
92
|
+
TypeError: If base_class_for_values is not a type or None.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
self._append_only = bool(append_only)
|
|
96
|
+
|
|
97
|
+
if len(serialization_format) == 0:
|
|
98
|
+
raise ValueError("serialization_format must be a non-empty string")
|
|
99
|
+
if contains_unsafe_chars(serialization_format):
|
|
100
|
+
raise ValueError("serialization_format must contain only URL/filename-safe characters")
|
|
101
|
+
self.serialization_format = str(serialization_format)
|
|
102
|
+
|
|
103
|
+
if not isinstance(base_class_for_values, (type, type(None))):
|
|
104
|
+
raise TypeError("base_class_for_values must be a type or None")
|
|
105
|
+
if (base_class_for_values is None or
|
|
106
|
+
not issubclass(base_class_for_values, str)):
|
|
107
|
+
if serialization_format not in {"json", "pkl"}:
|
|
108
|
+
raise ValueError("For non-string values serialization_format must be either 'pkl' or 'json'.")
|
|
110
109
|
self.base_class_for_values = base_class_for_values
|
|
110
|
+
|
|
111
111
|
ParameterizableClass.__init__(self)
|
|
112
112
|
|
|
113
113
|
|
|
@@ -120,41 +120,38 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
120
120
|
built-in dict.
|
|
121
121
|
"""
|
|
122
122
|
params = dict(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
append_only=self.append_only,
|
|
124
|
+
base_class_for_values=self.base_class_for_values,
|
|
125
|
+
serialization_format=self.serialization_format
|
|
126
126
|
)
|
|
127
127
|
sorted_params = sort_dict_by_keys(params)
|
|
128
128
|
return sorted_params
|
|
129
129
|
|
|
130
|
+
def __copy__(self) -> 'PersiDict':
|
|
131
|
+
"""Return a shallow copy of the dictionary.
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def base_url(self):
|
|
134
|
-
"""Base URL identifying the storage location.
|
|
133
|
+
This creates a new PersiDict instance with the same parameters, pointing
|
|
134
|
+
to the same underlying storage. This is analogous to `dict.copy()`.
|
|
135
135
|
|
|
136
136
|
Returns:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
Raises:
|
|
140
|
-
NotImplementedError: Must be provided by subclasses.
|
|
137
|
+
PersiDict: A new PersiDict instance that is a shallow copy of this one.
|
|
141
138
|
"""
|
|
142
|
-
|
|
139
|
+
if type(self) is PersiDict:
|
|
140
|
+
raise NotImplementedError("PersiDict is an abstract base class"
|
|
141
|
+
" and cannot be copied directly")
|
|
142
|
+
params = self.get_params()
|
|
143
|
+
return self.__class__(**params)
|
|
143
144
|
|
|
144
145
|
|
|
145
146
|
@property
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
"""Base directory on the local filesystem, if applicable.
|
|
147
|
+
def append_only(self) -> bool:
|
|
148
|
+
"""Whether the store is append-only.
|
|
149
149
|
|
|
150
150
|
Returns:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
Raises:
|
|
154
|
-
NotImplementedError: Must be provided by subclasses that use local
|
|
155
|
-
storage.
|
|
151
|
+
bool: True if the store is append-only (contains immutable items
|
|
152
|
+
that cannot be modified or deleted), False otherwise.
|
|
156
153
|
"""
|
|
157
|
-
|
|
154
|
+
return self._append_only
|
|
158
155
|
|
|
159
156
|
|
|
160
157
|
def __repr__(self) -> str:
|
|
@@ -178,7 +175,7 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
178
175
|
|
|
179
176
|
|
|
180
177
|
@abstractmethod
|
|
181
|
-
def __contains__(self, key:
|
|
178
|
+
def __contains__(self, key:NonEmptyPersiDictKey) -> bool:
|
|
182
179
|
"""Check whether a key exists in the store.
|
|
183
180
|
|
|
184
181
|
Args:
|
|
@@ -187,13 +184,40 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
187
184
|
Returns:
|
|
188
185
|
bool: True if key exists, False otherwise.
|
|
189
186
|
"""
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
raise NotImplementedError("PersiDict is an abstract base class"
|
|
188
|
+
" and cannot check items directly")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def get_item_if_etag_changed(self, key: NonEmptyPersiDictKey, etag: str | None
|
|
192
|
+
) -> tuple[Any, str|None]|ETagHasNotChangedFlag:
|
|
193
|
+
"""Retrieve the value for a key only if its ETag has changed.
|
|
194
|
+
|
|
195
|
+
This method is absent in the original dict API.
|
|
196
|
+
By default, the timestamp is used in lieu of ETag.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
key: Dictionary key (string or sequence of strings)
|
|
200
|
+
or NonEmptySafeStrTuple.
|
|
201
|
+
etag: The ETag value to compare against.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
tuple[Any, str|None] | ETagHasNotChangedFlag: The deserialized
|
|
205
|
+
value if the ETag has changed, along with the new ETag,
|
|
206
|
+
or ETAG_HAS_NOT_CHANGED if it matches the provided etag.
|
|
207
|
+
|
|
208
|
+
Raises:
|
|
209
|
+
KeyError: If the key does not exist.
|
|
210
|
+
"""
|
|
211
|
+
key = NonEmptySafeStrTuple(key)
|
|
212
|
+
current_etag = self.etag(key)
|
|
213
|
+
if etag == current_etag:
|
|
214
|
+
return ETAG_HAS_NOT_CHANGED
|
|
215
|
+
else:
|
|
216
|
+
return self[key], current_etag
|
|
193
217
|
|
|
194
218
|
|
|
195
219
|
@abstractmethod
|
|
196
|
-
def __getitem__(self, key:
|
|
220
|
+
def __getitem__(self, key:NonEmptyPersiDictKey) -> Any:
|
|
197
221
|
"""Retrieve the value for a key.
|
|
198
222
|
|
|
199
223
|
Args:
|
|
@@ -202,98 +226,167 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
202
226
|
Returns:
|
|
203
227
|
Any: The stored value.
|
|
204
228
|
"""
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
" and cannot retrieve items directly")
|
|
229
|
+
raise NotImplementedError("PersiDict is an abstract base class"
|
|
230
|
+
" and cannot retrieve items directly")
|
|
208
231
|
|
|
209
232
|
|
|
210
|
-
def __setitem__(self, key:PersiDictKey, value:Any):
|
|
211
|
-
"""Set the value for a key.
|
|
212
233
|
|
|
213
|
-
|
|
214
|
-
|
|
234
|
+
def _process_setitem_args(self, key: NonEmptyPersiDictKey, value: Any
|
|
235
|
+
) -> StatusFlag:
|
|
236
|
+
"""Perform the first steps for setting an item.
|
|
215
237
|
|
|
216
238
|
Args:
|
|
217
|
-
key:
|
|
218
|
-
|
|
239
|
+
key: Dictionary key (string or sequence of strings
|
|
240
|
+
or NonEmptySafeStrTuple).
|
|
241
|
+
value: Value to store, or a joker command (KEEP_CURRENT or
|
|
242
|
+
DELETE_CURRENT).
|
|
219
243
|
|
|
220
244
|
Raises:
|
|
221
|
-
KeyError: If attempting to modify an existing
|
|
222
|
-
|
|
223
|
-
|
|
245
|
+
KeyError: If attempting to modify an existing item when
|
|
246
|
+
append_only is True.
|
|
247
|
+
TypeError: If the value is a PersiDict instance or does not match
|
|
248
|
+
the required base_class_for_values when specified.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
StatusFlag: CONTINUE_NORMAL_EXECUTION if the caller should
|
|
252
|
+
proceed with storing the value; EXECUTION_IS_COMPLETE if a
|
|
253
|
+
joker command was processed and no further action is needed.
|
|
224
254
|
"""
|
|
255
|
+
|
|
225
256
|
if value is KEEP_CURRENT:
|
|
226
|
-
return
|
|
227
|
-
elif self.
|
|
228
|
-
|
|
229
|
-
|
|
257
|
+
return EXECUTION_IS_COMPLETE
|
|
258
|
+
elif self.append_only and (value is DELETE_CURRENT or key in self):
|
|
259
|
+
raise KeyError("Can't modify an immutable key-value pair")
|
|
260
|
+
elif isinstance(value, PersiDict):
|
|
261
|
+
raise TypeError("Cannot store a PersiDict instance directly")
|
|
230
262
|
|
|
231
|
-
key =
|
|
263
|
+
key = NonEmptySafeStrTuple(key)
|
|
232
264
|
|
|
233
265
|
if value is DELETE_CURRENT:
|
|
234
|
-
self.
|
|
235
|
-
return
|
|
266
|
+
self.discard(key)
|
|
267
|
+
return EXECUTION_IS_COMPLETE
|
|
236
268
|
|
|
237
269
|
if self.base_class_for_values is not None:
|
|
238
270
|
if not isinstance(value, self.base_class_for_values):
|
|
239
271
|
raise TypeError(f"Value must be an instance of"
|
|
240
272
|
f" {self.base_class_for_values.__name__}")
|
|
241
273
|
|
|
242
|
-
|
|
243
|
-
raise NotImplementedError("PersiDict is an abstract base class"
|
|
244
|
-
" and cannot store items directly")
|
|
274
|
+
return CONTINUE_NORMAL_EXECUTION
|
|
245
275
|
|
|
246
276
|
|
|
247
|
-
def
|
|
248
|
-
"""
|
|
277
|
+
def set_item_get_etag(self, key: NonEmptyPersiDictKey, value: Any) -> str|None:
|
|
278
|
+
"""Store a value for a key directly in the dict and return the new ETag.
|
|
279
|
+
|
|
280
|
+
Handles special joker values (KEEP_CURRENT, DELETE_CURRENT) for
|
|
281
|
+
conditional operations. Validates value types against base_class_for_values
|
|
282
|
+
if specified, then serializes and uploads directly to S3.
|
|
283
|
+
|
|
284
|
+
This method is absent in the original dict API.
|
|
285
|
+
By default, the timestamp is used in lieu of ETag.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
key: Dictionary key (string or sequence of strings)
|
|
289
|
+
or NonEmptySafeStrTuple.
|
|
290
|
+
value: Value to store, or a joker command (KEEP_CURRENT or
|
|
291
|
+
DELETE_CURRENT).
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
str|None: The ETag of the newly stored object,
|
|
295
|
+
or None if the ETag was not provided as a result of the operation.
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
KeyError: If attempting to modify an existing item when
|
|
299
|
+
append_only is True.
|
|
300
|
+
TypeError: If the value is a PersiDict instance or does not match
|
|
301
|
+
the required base_class_for_values when specified.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
key = NonEmptySafeStrTuple(key)
|
|
305
|
+
if self._process_setitem_args(key, value) is EXECUTION_IS_COMPLETE:
|
|
306
|
+
return None
|
|
307
|
+
self[key] = value
|
|
308
|
+
return self.etag(key)
|
|
309
|
+
|
|
310
|
+
@abstractmethod
|
|
311
|
+
def __setitem__(self, key:NonEmptyPersiDictKey, value:Any):
|
|
312
|
+
"""Set the value for a key.
|
|
313
|
+
|
|
314
|
+
Special values KEEP_CURRENT and DELETE_CURRENT are interpreted as
|
|
315
|
+
commands to keep or delete the current value respectively.
|
|
249
316
|
|
|
250
317
|
Args:
|
|
251
318
|
key: Key (string or sequence of strings) or SafeStrTuple.
|
|
319
|
+
value: Value to store, or a Joker command.
|
|
252
320
|
|
|
253
321
|
Raises:
|
|
254
|
-
KeyError: If
|
|
255
|
-
|
|
322
|
+
KeyError: If attempting to modify an existing key when
|
|
323
|
+
append_only is True.
|
|
324
|
+
NotImplementedError: Subclasses must implement actual writing.
|
|
256
325
|
"""
|
|
257
|
-
if self.
|
|
326
|
+
if self._process_setitem_args(key, value) is EXECUTION_IS_COMPLETE:
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
raise NotImplementedError("PersiDict is an abstract base class"
|
|
330
|
+
" and cannot store items directly")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _process_delitem_args(self, key: NonEmptyPersiDictKey) -> None:
|
|
334
|
+
"""Perform the first steps for deleting an item.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
key: Dictionary key (string or sequence of strings
|
|
338
|
+
or NonEmptySafeStrTuple).
|
|
339
|
+
Raises:
|
|
340
|
+
KeyError: If attempting to delete an item when
|
|
341
|
+
append_only is True or if the key does not exist.
|
|
342
|
+
"""
|
|
343
|
+
if self.append_only:
|
|
258
344
|
raise KeyError("Can't delete an immutable key-value pair")
|
|
259
|
-
if type(self) is PersiDict:
|
|
260
|
-
raise NotImplementedError("PersiDict is an abstract base class"
|
|
261
|
-
" and cannot delete items directly")
|
|
262
345
|
|
|
263
|
-
key =
|
|
346
|
+
key = NonEmptySafeStrTuple(key)
|
|
264
347
|
|
|
265
348
|
if key not in self:
|
|
266
349
|
raise KeyError(f"Key {key} not found")
|
|
267
350
|
|
|
268
351
|
|
|
352
|
+
@abstractmethod
|
|
353
|
+
def __delitem__(self, key:NonEmptyPersiDictKey):
|
|
354
|
+
"""Delete a key and its value.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
key: Key (string or sequence of strings) or SafeStrTuple.
|
|
358
|
+
|
|
359
|
+
Raises:
|
|
360
|
+
KeyError: If append_only is True or if the key does not exist.
|
|
361
|
+
NotImplementedError: Subclasses must implement deletion.
|
|
362
|
+
"""
|
|
363
|
+
self._process_delitem_args(key)
|
|
364
|
+
raise NotImplementedError("PersiDict is an abstract base class"
|
|
365
|
+
" and cannot delete items directly")
|
|
366
|
+
|
|
367
|
+
|
|
269
368
|
@abstractmethod
|
|
270
369
|
def __len__(self) -> int:
|
|
271
370
|
"""Return the number of stored items.
|
|
272
371
|
|
|
273
372
|
Returns:
|
|
274
373
|
int: Number of key-value pairs.
|
|
374
|
+
Raises:
|
|
375
|
+
NotImplementedError: Subclasses must implement counting.
|
|
275
376
|
"""
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
" and cannot count items directly")
|
|
377
|
+
raise NotImplementedError("PersiDict is an abstract base class"
|
|
378
|
+
" and cannot count items directly")
|
|
279
379
|
|
|
280
380
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
"""Underlying implementation for iterator helpers.
|
|
381
|
+
def _process_generic_iter_args(self, result_type: set[str]) -> None:
|
|
382
|
+
"""Validate arguments for iterator helpers.
|
|
284
383
|
|
|
285
384
|
Args:
|
|
286
385
|
result_type: A set indicating desired fields among {'keys',
|
|
287
386
|
'values', 'timestamps'}.
|
|
288
|
-
|
|
289
|
-
Returns:
|
|
290
|
-
Any: An iterator yielding keys, values, and/or timestamps based on
|
|
291
|
-
result_type.
|
|
292
|
-
|
|
293
387
|
Raises:
|
|
294
388
|
TypeError: If result_type is not a set.
|
|
295
389
|
ValueError: If result_type contains invalid entries or an invalid number of items.
|
|
296
|
-
NotImplementedError: Subclasses must implement the concrete iterator.
|
|
297
390
|
"""
|
|
298
391
|
if not isinstance(result_type, set):
|
|
299
392
|
raise TypeError("result_type must be a set of strings")
|
|
@@ -304,9 +397,28 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
304
397
|
raise ValueError("result_type can only contain 'keys', 'values', 'timestamps'")
|
|
305
398
|
if not (1 <= len(result_type & allowed) <= 3):
|
|
306
399
|
raise ValueError("result_type must include at least one of 'keys', 'values', 'timestamps'")
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@abstractmethod
|
|
403
|
+
def _generic_iter(self, result_type: set[str]) -> Any:
|
|
404
|
+
"""Underlying implementation for iterator helpers.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
result_type: A set indicating desired fields among {'keys',
|
|
408
|
+
'values', 'timestamps'}.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Any: An iterator yielding keys, values, and/or timestamps based on
|
|
412
|
+
result_type.
|
|
413
|
+
|
|
414
|
+
Raises:
|
|
415
|
+
TypeError: If result_type is not a set.
|
|
416
|
+
ValueError: If result_type contains invalid entries or an invalid number of items.
|
|
417
|
+
NotImplementedError: Subclasses must implement the concrete iterator.
|
|
418
|
+
"""
|
|
419
|
+
self._process_generic_iter_args(result_type)
|
|
420
|
+
raise NotImplementedError("PersiDict is an abstract base class"
|
|
421
|
+
" and cannot iterate items directly")
|
|
310
422
|
|
|
311
423
|
|
|
312
424
|
def __iter__(self):
|
|
@@ -372,7 +484,7 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
372
484
|
return self._generic_iter({"keys", "values", "timestamps"})
|
|
373
485
|
|
|
374
486
|
|
|
375
|
-
def setdefault(self, key:
|
|
487
|
+
def setdefault(self, key: NonEmptyPersiDictKey, default: Any = None) -> Any:
|
|
376
488
|
"""Insert key with default value if absent; return the current value.
|
|
377
489
|
|
|
378
490
|
Behaves like the built-in dict.setdefault() method: if the key exists,
|
|
@@ -389,20 +501,19 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
389
501
|
Raises:
|
|
390
502
|
TypeError: If default is a Joker command (KEEP_CURRENT/DELETE_CURRENT).
|
|
391
503
|
"""
|
|
392
|
-
key =
|
|
504
|
+
key = NonEmptySafeStrTuple(key)
|
|
393
505
|
if isinstance(default, Joker):
|
|
394
506
|
raise TypeError("default must be a regular value, not a Joker command")
|
|
395
|
-
|
|
507
|
+
try:
|
|
396
508
|
return self[key]
|
|
397
|
-
|
|
509
|
+
except KeyError:
|
|
398
510
|
self[key] = default
|
|
399
511
|
return default
|
|
400
512
|
|
|
401
|
-
|
|
402
513
|
def __eq__(self, other: PersiDict) -> bool:
|
|
403
514
|
"""Compare dictionaries for equality.
|
|
404
515
|
|
|
405
|
-
If other is a PersiDict instance, compares
|
|
516
|
+
If other is a PersiDict instance, compares parameters for equality.
|
|
406
517
|
Otherwise, attempts to compare as a mapping by comparing all keys and values.
|
|
407
518
|
|
|
408
519
|
Args:
|
|
@@ -411,17 +522,21 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
411
522
|
Returns:
|
|
412
523
|
bool: True if the dictionaries are considered equal, False otherwise.
|
|
413
524
|
"""
|
|
414
|
-
if isinstance(other, PersiDict):
|
|
415
|
-
#TODO: decide whether to keep this semantics
|
|
416
|
-
return self.get_portable_params() == other.get_portable_params()
|
|
417
525
|
try:
|
|
526
|
+
if type(self) is type(other) :
|
|
527
|
+
if self.get_params() == other.get_params():
|
|
528
|
+
return True
|
|
529
|
+
except:
|
|
530
|
+
pass
|
|
531
|
+
|
|
532
|
+
try: #TODO: refactor to improve performance
|
|
418
533
|
if len(self) != len(other):
|
|
419
534
|
return False
|
|
420
535
|
for k in other.keys():
|
|
421
536
|
if self[k] != other[k]:
|
|
422
537
|
return False
|
|
423
538
|
return True
|
|
424
|
-
except:
|
|
539
|
+
except KeyError:
|
|
425
540
|
return False
|
|
426
541
|
|
|
427
542
|
|
|
@@ -451,9 +566,9 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
451
566
|
"""Remove all items from the dictionary.
|
|
452
567
|
|
|
453
568
|
Raises:
|
|
454
|
-
KeyError: If
|
|
569
|
+
KeyError: If the dictionary is append-only.
|
|
455
570
|
"""
|
|
456
|
-
if self.
|
|
571
|
+
if self.append_only:
|
|
457
572
|
raise KeyError("Can't delete an immutable key-value pair")
|
|
458
573
|
|
|
459
574
|
for k in self.keys():
|
|
@@ -463,7 +578,7 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
463
578
|
pass
|
|
464
579
|
|
|
465
580
|
|
|
466
|
-
def
|
|
581
|
+
def discard(self, key: NonEmptyPersiDictKey) -> bool:
|
|
467
582
|
"""Delete an item without raising an exception if it doesn't exist.
|
|
468
583
|
|
|
469
584
|
This method is absent in the original dict API.
|
|
@@ -475,36 +590,42 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
475
590
|
bool: True if the item existed and was deleted; False otherwise.
|
|
476
591
|
|
|
477
592
|
Raises:
|
|
478
|
-
KeyError: If
|
|
593
|
+
KeyError: If the dictionary is append-only.
|
|
479
594
|
"""
|
|
480
595
|
|
|
481
|
-
if self.
|
|
596
|
+
if self.append_only:
|
|
482
597
|
raise KeyError("Can't delete an immutable key-value pair")
|
|
483
598
|
|
|
484
|
-
key =
|
|
599
|
+
key = NonEmptySafeStrTuple(key)
|
|
485
600
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
except:
|
|
491
|
-
return False
|
|
492
|
-
else:
|
|
601
|
+
try:
|
|
602
|
+
del self[key]
|
|
603
|
+
return True
|
|
604
|
+
except KeyError:
|
|
493
605
|
return False
|
|
494
606
|
|
|
495
607
|
|
|
608
|
+
def delete_if_exists(self, key: NonEmptyPersiDictKey) -> bool:
|
|
609
|
+
"""Backward-compatible wrapper for discard().
|
|
610
|
+
|
|
611
|
+
This method is kept for backward compatibility; new code should use
|
|
612
|
+
discard(). Behavior is identical to discard().
|
|
613
|
+
"""
|
|
614
|
+
return self.discard(key)
|
|
615
|
+
|
|
496
616
|
def get_subdict(self, prefix_key:PersiDictKey) -> PersiDict:
|
|
497
617
|
"""Get a sub-dictionary containing items with the given prefix key.
|
|
498
618
|
|
|
499
619
|
Items whose keys start with the provided prefix are visible through the
|
|
500
620
|
returned sub-dictionary. If the prefix does not exist, an empty
|
|
501
|
-
sub-dictionary is returned.
|
|
621
|
+
sub-dictionary is returned. If the prefix is empty, the entire
|
|
622
|
+
dictionary is returned.
|
|
502
623
|
|
|
503
624
|
This method is absent in the original Python dict API.
|
|
504
625
|
|
|
505
626
|
Args:
|
|
506
627
|
prefix_key: Key prefix (string, sequence of strings, or SafeStrTuple)
|
|
507
|
-
identifying the sub-
|
|
628
|
+
identifying the sub-dict to expose.
|
|
508
629
|
|
|
509
630
|
Returns:
|
|
510
631
|
PersiDict: A dictionary-like view restricted to keys under the
|
|
@@ -514,6 +635,7 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
514
635
|
NotImplementedError: Must be implemented by subclasses that support
|
|
515
636
|
hierarchical key spaces.
|
|
516
637
|
"""
|
|
638
|
+
|
|
517
639
|
if type(self) is PersiDict:
|
|
518
640
|
raise NotImplementedError("PersiDict is an abstract base class"
|
|
519
641
|
" and cannot create sub-dictionaries directly")
|
|
@@ -533,7 +655,7 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
533
655
|
return result_subdicts
|
|
534
656
|
|
|
535
657
|
|
|
536
|
-
def random_key(self) ->
|
|
658
|
+
def random_key(self) -> NonEmptySafeStrTuple | None:
|
|
537
659
|
"""Return a random key from the dictionary.
|
|
538
660
|
|
|
539
661
|
This method is absent in the original Python dict API.
|
|
@@ -556,7 +678,7 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
556
678
|
# Reservoir sampling algorithm
|
|
557
679
|
i = 2
|
|
558
680
|
for key in iterator:
|
|
559
|
-
# Select current key with probability 1/i
|
|
681
|
+
# Select the current key with probability 1/i
|
|
560
682
|
if random.random() < 1/i:
|
|
561
683
|
result = key
|
|
562
684
|
i += 1
|
|
@@ -565,7 +687,7 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
565
687
|
|
|
566
688
|
|
|
567
689
|
@abstractmethod
|
|
568
|
-
def timestamp(self, key:
|
|
690
|
+
def timestamp(self, key:NonEmptyPersiDictKey) -> float:
|
|
569
691
|
"""Return the last modification time of a key.
|
|
570
692
|
|
|
571
693
|
This method is absent in the original dict API.
|
|
@@ -584,8 +706,19 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
584
706
|
raise NotImplementedError("PersiDict is an abstract base class"
|
|
585
707
|
" and cannot provide timestamps directly")
|
|
586
708
|
|
|
709
|
+
def etag(self, key:NonEmptyPersiDictKey) -> str|None:
|
|
710
|
+
"""Return the ETag of a key.
|
|
711
|
+
|
|
712
|
+
By default, this returns a stringified timestamp of the last
|
|
713
|
+
modification time. Subclasses may override to provide true
|
|
714
|
+
backend-specific ETags (e.g., S3).
|
|
715
|
+
|
|
716
|
+
This method is absent in the original Python dict API.
|
|
717
|
+
"""
|
|
718
|
+
return f"{self.timestamp(key):.6f}"
|
|
719
|
+
|
|
587
720
|
|
|
588
|
-
def oldest_keys(self, max_n=None):
|
|
721
|
+
def oldest_keys(self, max_n=None) -> list[NonEmptySafeStrTuple]:
|
|
589
722
|
"""Return up to max_n oldest keys in the dictionary.
|
|
590
723
|
|
|
591
724
|
This method is absent in the original Python dict API.
|
|
@@ -613,7 +746,7 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
613
746
|
return [key for key,_ in smallest_pairs]
|
|
614
747
|
|
|
615
748
|
|
|
616
|
-
def oldest_values(self, max_n=None):
|
|
749
|
+
def oldest_values(self, max_n=None) -> list[Any]:
|
|
617
750
|
"""Return up to max_n oldest values in the dictionary.
|
|
618
751
|
|
|
619
752
|
This method is absent in the original Python dict API.
|
|
@@ -629,7 +762,7 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
629
762
|
return [self[k] for k in self.oldest_keys(max_n)]
|
|
630
763
|
|
|
631
764
|
|
|
632
|
-
def newest_keys(self, max_n=None):
|
|
765
|
+
def newest_keys(self, max_n=None) -> list[NonEmptySafeStrTuple]:
|
|
633
766
|
"""Return up to max_n newest keys in the dictionary.
|
|
634
767
|
|
|
635
768
|
This method is absent in the original Python dict API.
|
|
@@ -657,7 +790,7 @@ class PersiDict(MutableMapping, ParameterizableClass):
|
|
|
657
790
|
return [key for key,_ in largest_pairs]
|
|
658
791
|
|
|
659
792
|
|
|
660
|
-
def newest_values(self, max_n=None):
|
|
793
|
+
def newest_values(self, max_n=None) -> list[Any]:
|
|
661
794
|
"""Return up to max_n newest values in the dictionary.
|
|
662
795
|
|
|
663
796
|
This method is absent in the original Python dict API.
|