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
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from .persi_dict import PersiDict, NonEmptyPersiDictKey
|
|
6
|
+
from .safe_str_tuple import NonEmptySafeStrTuple
|
|
7
|
+
from .singletons import ETAG_HAS_NOT_CHANGED, EXECUTION_IS_COMPLETE
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AppendOnlyDictCached(PersiDict):
|
|
11
|
+
"""Append-only dict facade with a read-through cache.
|
|
12
|
+
|
|
13
|
+
This adapter wraps two concrete PersiDict instances:
|
|
14
|
+
- main_dict: the source of truth that actually persists data.
|
|
15
|
+
- data_cache: a second PersiDict used purely as a cache for values.
|
|
16
|
+
|
|
17
|
+
Both the main dict and the cache must have append_only=True. Keys can
|
|
18
|
+
be added once but never modified or deleted. Because of that contract, the
|
|
19
|
+
cache can be trusted when it already has a value for a key without
|
|
20
|
+
re-validating the main dict.
|
|
21
|
+
|
|
22
|
+
Behavior summary:
|
|
23
|
+
- Reads: __getitem__ first tries the cache, falls back to the main dict and
|
|
24
|
+
populates the cache on a miss.
|
|
25
|
+
- Membership: __contains__ returns True if the key is in the cache; else it
|
|
26
|
+
checks the main dict.
|
|
27
|
+
- Writes: __setitem__ writes to the main dict and mirrors the value into
|
|
28
|
+
the cache after argument validation by the PersiDict base.
|
|
29
|
+
- set_item_get_etag delegates the write to the main dict, mirrors the value
|
|
30
|
+
into the cache, and returns the ETag from the main dict.
|
|
31
|
+
- Deletion is not supported and will raise TypeError (append-only).
|
|
32
|
+
- Iteration, length, timestamps, base_url and base_dir are delegated to the
|
|
33
|
+
main dict. get_item_if_new_etag is delegated too, and on change the
|
|
34
|
+
value is cached.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
main_dict: The authoritative append-only PersiDict.
|
|
38
|
+
data_cache: A PersiDict used as a value cache; must be append-only and
|
|
39
|
+
compatible with main_dict's base_class_for_values and serialization_format.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
TypeError: If main_dict or data_cache are not PersiDict instances.
|
|
43
|
+
ValueError: If either dict is not immutable (append-only) or their
|
|
44
|
+
base_class_for_values differ.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self,
|
|
48
|
+
main_dict: PersiDict,
|
|
49
|
+
data_cache: PersiDict) -> None:
|
|
50
|
+
"""Initialize the adapter with a main dict and a value cache.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
main_dict: The authoritative append-only PersiDict instance.
|
|
54
|
+
data_cache: A PersiDict used as a read-through cache for values.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
TypeError: If main_dict or data_cache are not PersiDict instances.
|
|
58
|
+
ValueError: If append_only is False for either dict, or the
|
|
59
|
+
base_class_for_values between the two does not match.
|
|
60
|
+
"""
|
|
61
|
+
if not isinstance(main_dict, PersiDict):
|
|
62
|
+
raise TypeError("main_dict must be a PersiDict")
|
|
63
|
+
if not isinstance(data_cache, PersiDict):
|
|
64
|
+
raise TypeError("data_cache must be a PersiDict")
|
|
65
|
+
if (not main_dict.append_only) or (not data_cache.append_only):
|
|
66
|
+
raise ValueError("append_only must be set to True")
|
|
67
|
+
if main_dict.base_class_for_values != data_cache.base_class_for_values:
|
|
68
|
+
raise ValueError("main_dict and data_cache must have the same "
|
|
69
|
+
"base_class_for_values")
|
|
70
|
+
|
|
71
|
+
# Initialize PersiDict base with parameters mirroring the main dict.
|
|
72
|
+
super().__init__(
|
|
73
|
+
append_only=True,
|
|
74
|
+
base_class_for_values=main_dict.base_class_for_values,
|
|
75
|
+
serialization_format=main_dict.serialization_format,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
self._main: PersiDict = main_dict
|
|
79
|
+
self._data_cache: PersiDict = data_cache
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def __contains__(self, key: NonEmptyPersiDictKey) -> bool:
|
|
83
|
+
"""Check whether a key exists in the cache or main dict.
|
|
84
|
+
|
|
85
|
+
The cache is checked first and trusted because both dicts are
|
|
86
|
+
append-only. On a cache miss, the main dict is consulted.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
key: Dictionary key (string or sequence of strings or
|
|
90
|
+
NonEmptySafeStrTuple).
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
bool: True if the key exists.
|
|
94
|
+
"""
|
|
95
|
+
key = NonEmptySafeStrTuple(key)
|
|
96
|
+
if key in self._data_cache:
|
|
97
|
+
# Items, added to the main_dict, are expected to never be removed.
|
|
98
|
+
# Hence, it's OK to trust the cache without verifying the main dict
|
|
99
|
+
return True
|
|
100
|
+
else:
|
|
101
|
+
return key in self._main
|
|
102
|
+
|
|
103
|
+
def __len__(self) -> int:
|
|
104
|
+
"""int: Number of items, delegated to the main dict."""
|
|
105
|
+
return len(self._main)
|
|
106
|
+
|
|
107
|
+
def _generic_iter(self, result_type: set[str]):
|
|
108
|
+
"""Internal iterator dispatcher delegated to the main dict.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
result_type: A set describing what to iterate, as used by
|
|
112
|
+
PersiDict internals (e.g., {"keys"}, {"items"}, etc.).
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
An iterator over the requested view, produced by the main dict.
|
|
116
|
+
"""
|
|
117
|
+
return self._main._generic_iter(result_type)
|
|
118
|
+
|
|
119
|
+
def timestamp(self, key: NonEmptyPersiDictKey) -> float:
|
|
120
|
+
"""Return item's timestamp from the main dict.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
key: Dictionary key (string or sequence of strings) or
|
|
124
|
+
NonEmptySafeStrTuple.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
float: POSIX timestamp of the last write for the key.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
KeyError: If the key does not exist in the main dict.
|
|
131
|
+
"""
|
|
132
|
+
key = NonEmptySafeStrTuple(key)
|
|
133
|
+
return self._main.timestamp(key)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def __getitem__(self, key: NonEmptyPersiDictKey) -> Any:
|
|
138
|
+
"""Retrieve a value using a read-through cache.
|
|
139
|
+
|
|
140
|
+
Tries the cache first; on a miss, reads from the main dict, stores the
|
|
141
|
+
value into the cache, and returns it.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
key: Dictionary key (string or sequence of strings) or
|
|
145
|
+
NonEmptySafeStrTuple.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Any: The stored value.
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
KeyError: If the key is missing in the main dict (and therefore
|
|
152
|
+
also not present in the cache).
|
|
153
|
+
"""
|
|
154
|
+
key = NonEmptySafeStrTuple(key)
|
|
155
|
+
try:
|
|
156
|
+
# Items, added to the main_dict, are expected to never be removed
|
|
157
|
+
# Hence, it's OK to trust the cache without verifying the main dict
|
|
158
|
+
return self._data_cache[key]
|
|
159
|
+
except KeyError:
|
|
160
|
+
value = self._main[key]
|
|
161
|
+
self._data_cache[key] = value
|
|
162
|
+
return value
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_item_if_etag_changed(self, key: NonEmptyPersiDictKey, etag: Optional[str]):
|
|
166
|
+
"""Return value only if its ETag changed; cache the value if so.
|
|
167
|
+
|
|
168
|
+
Delegates to the main dict. If the ETag differs from the provided one,
|
|
169
|
+
the new value is cached and the (value, etag) tuple is returned.
|
|
170
|
+
Otherwise, returns ETAG_HAS_NOT_CHANGED.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
key: Dictionary key (string or sequence of strings) or
|
|
174
|
+
NonEmptySafeStrTuple.
|
|
175
|
+
etag: Previously seen ETag or None.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
tuple[Any, str|None] | ETagHasNotChangedFlag: The value and the new
|
|
179
|
+
ETag when changed; ETAG_HAS_NOT_CHANGED otherwise.
|
|
180
|
+
|
|
181
|
+
Raises:
|
|
182
|
+
KeyError: If the key does not exist in the main dict.
|
|
183
|
+
"""
|
|
184
|
+
key = NonEmptySafeStrTuple(key)
|
|
185
|
+
res = self._main.get_item_if_etag_changed(key, etag)
|
|
186
|
+
if not res is ETAG_HAS_NOT_CHANGED:
|
|
187
|
+
value, _ = res
|
|
188
|
+
self._data_cache[key] = value
|
|
189
|
+
return res
|
|
190
|
+
|
|
191
|
+
def __setitem__(self, key: NonEmptyPersiDictKey, value: Any):
|
|
192
|
+
"""Store a value in the main dict and mirror it into the cache.
|
|
193
|
+
|
|
194
|
+
The PersiDict base validates special joker values and the
|
|
195
|
+
base_class_for_values via _process_setitem_args. On successful
|
|
196
|
+
validation, the value is written to the main dict and then cached.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
key: Dictionary key (string or sequence of strings) or
|
|
200
|
+
NonEmptySafeStrTuple.
|
|
201
|
+
value: The value to store, or a joker (KEEP_CURRENT/DELETE_CURRENT).
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
KeyError: If attempting to modify an existing item when
|
|
205
|
+
append_only is True.
|
|
206
|
+
TypeError: If the value fails base_class_for_values validation.
|
|
207
|
+
"""
|
|
208
|
+
key = NonEmptySafeStrTuple(key)
|
|
209
|
+
if self._process_setitem_args(key, value) is EXECUTION_IS_COMPLETE:
|
|
210
|
+
return
|
|
211
|
+
self._main[key] = value
|
|
212
|
+
self._data_cache[key] = value
|
|
213
|
+
|
|
214
|
+
def set_item_get_etag(self, key: NonEmptyPersiDictKey, value: Any) -> Optional[str]:
|
|
215
|
+
"""Store a value and return the ETag from the main dict.
|
|
216
|
+
|
|
217
|
+
After validation via _process_setitem_args, the value is written to the
|
|
218
|
+
main dict using its ETag-aware API, then mirrored into the cache.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
key: Dictionary key (string or sequence of strings) or
|
|
222
|
+
NonEmptySafeStrTuple.
|
|
223
|
+
value: The value to store, or a joker (KEEP_CURRENT/DELETE_CURRENT).
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
str | None: The ETag produced by the main dict, or None if a joker
|
|
227
|
+
short-circuited the operation or the backend doesn't support ETags.
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
KeyError: If attempting to modify an existing item when
|
|
231
|
+
append_only is True.
|
|
232
|
+
TypeError: If the value fails base_class_for_values validation.
|
|
233
|
+
"""
|
|
234
|
+
key = NonEmptySafeStrTuple(key)
|
|
235
|
+
if self._process_setitem_args(key, value) is EXECUTION_IS_COMPLETE:
|
|
236
|
+
return None
|
|
237
|
+
etag = self._main.set_item_get_etag(key, value)
|
|
238
|
+
self._data_cache[key] = value
|
|
239
|
+
return etag
|
|
240
|
+
|
|
241
|
+
def __delitem__(self, key: NonEmptyPersiDictKey):
|
|
242
|
+
"""Deletion is not supported for append-only dictionaries.
|
|
243
|
+
|
|
244
|
+
Raises:
|
|
245
|
+
TypeError: Always raised to indicate append-only restriction.
|
|
246
|
+
"""
|
|
247
|
+
raise TypeError("append-only dicts do not support deletion")
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from .persi_dict import PersiDict, NonEmptyPersiDictKey
|
|
6
|
+
from .safe_str_tuple import NonEmptySafeStrTuple
|
|
7
|
+
from .singletons import ETAG_HAS_NOT_CHANGED, EXECUTION_IS_COMPLETE
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MutableDictCached(PersiDict):
|
|
11
|
+
"""PersiDict adapter with read-through caching and ETag validation.
|
|
12
|
+
|
|
13
|
+
This adapter composes three concrete PersiDict instances:
|
|
14
|
+
- main_dict: the source of truth that persists data and supports ETags.
|
|
15
|
+
- data_cache: a PersiDict used purely as a cache for values.
|
|
16
|
+
- etag_cache: a PersiDict used to cache ETag strings per key.
|
|
17
|
+
|
|
18
|
+
For reads, the adapter consults etag_cache to decide whether the cached
|
|
19
|
+
value is still valid. If the ETag hasn't changed in the main dict, the
|
|
20
|
+
cached value is returned; otherwise the fresh value and ETag are fetched
|
|
21
|
+
from main_dict and both caches are updated. All writes and deletions are
|
|
22
|
+
performed against main_dict and mirrored into caches to keep them in sync.
|
|
23
|
+
|
|
24
|
+
Notes:
|
|
25
|
+
- main_dict must fully support ETag operations; caches must be mutable
|
|
26
|
+
(append_only=False).
|
|
27
|
+
- This class inherits type and serialization settings from main_dict.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self,
|
|
31
|
+
main_dict: PersiDict,
|
|
32
|
+
data_cache: PersiDict,
|
|
33
|
+
etag_cache: PersiDict) -> None:
|
|
34
|
+
"""Initialize with a main dict and two caches (data and ETag).
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
main_dict: The authoritative PersiDict that supports full ETag
|
|
38
|
+
operations. All reads/writes/deletes are ultimately delegated
|
|
39
|
+
here.
|
|
40
|
+
data_cache: A mutable PersiDict used as a cache for values.
|
|
41
|
+
etag_cache: A mutable PersiDict used to cache ETag strings.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
TypeError: If any of main_dict, data_cache, or etag_cache is not a
|
|
45
|
+
PersiDict instance.
|
|
46
|
+
ValueError: If either cache is append-only (append_only=True) or
|
|
47
|
+
if main_dict does not fully support ETag operations.
|
|
48
|
+
|
|
49
|
+
Notes:
|
|
50
|
+
The adapter inherits base settings (base_class_for_values,
|
|
51
|
+
serialization_format, and immutability) from main_dict to ensure compatibility.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
inputs = dict(main_dict=main_dict
|
|
55
|
+
, data_cache=data_cache
|
|
56
|
+
, etag_cache=etag_cache)
|
|
57
|
+
|
|
58
|
+
for k, v in inputs.items():
|
|
59
|
+
if not isinstance(v, PersiDict):
|
|
60
|
+
raise TypeError(f"{k} must be a PersiDict")
|
|
61
|
+
if v.append_only:
|
|
62
|
+
raise ValueError(f"{k} can't be append-only.")
|
|
63
|
+
|
|
64
|
+
super().__init__(
|
|
65
|
+
append_only=main_dict.append_only,
|
|
66
|
+
base_class_for_values=main_dict.base_class_for_values,
|
|
67
|
+
serialization_format=main_dict.serialization_format,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
self._main_dict = main_dict
|
|
71
|
+
self._data_cache = data_cache
|
|
72
|
+
self._etag_cache = etag_cache
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def __contains__(self, key: NonEmptyPersiDictKey) -> bool:
|
|
76
|
+
"""Check membership against the main dict.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
key: Non-empty key (tuple or coercible) to check.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
bool: True if the key exists in the main dict, False otherwise.
|
|
83
|
+
"""
|
|
84
|
+
key = NonEmptySafeStrTuple(key)
|
|
85
|
+
return key in self._main_dict
|
|
86
|
+
|
|
87
|
+
def __len__(self) -> int:
|
|
88
|
+
"""Number of items in the main dict.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
int: Count of keys according to the main dict.
|
|
92
|
+
"""
|
|
93
|
+
return len(self._main_dict)
|
|
94
|
+
|
|
95
|
+
def _generic_iter(self, result_type: set[str]):
|
|
96
|
+
"""Delegate iteration to the main dict.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
result_type: A set describing which items to iterate (implementation detail
|
|
100
|
+
of PersiDict).
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Iterator over keys/values as provided by the main dict.
|
|
104
|
+
"""
|
|
105
|
+
return self._main_dict._generic_iter(result_type)
|
|
106
|
+
|
|
107
|
+
def timestamp(self, key: NonEmptyPersiDictKey) -> float:
|
|
108
|
+
"""Get the last-modified timestamp from the main dict.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
key: Non-empty key to query.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
float: POSIX timestamp (seconds since epoch) as provided by the main dict.
|
|
115
|
+
"""
|
|
116
|
+
key = NonEmptySafeStrTuple(key)
|
|
117
|
+
return self._main_dict.timestamp(key)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _set_cached_etag(self, key: NonEmptySafeStrTuple, etag: Optional[str]) -> None:
|
|
123
|
+
"""Update the cached ETag for a key, or clear it if None.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
key: Normalized non-empty key.
|
|
127
|
+
etag: The ETag string to store, or None to remove any cached ETag.
|
|
128
|
+
"""
|
|
129
|
+
if etag is None:
|
|
130
|
+
self._etag_cache.discard(key)
|
|
131
|
+
else:
|
|
132
|
+
self._etag_cache[key] = etag
|
|
133
|
+
|
|
134
|
+
def __getitem__(self, key: NonEmptyPersiDictKey) -> Any:
|
|
135
|
+
"""Return the value for key using ETag-aware read-through caching.
|
|
136
|
+
|
|
137
|
+
The method looks up the previously cached ETag for the key and asks the
|
|
138
|
+
main dict if the item has changed. If not changed, it returns the value
|
|
139
|
+
from the data cache; on a cache miss it fetches fresh data from the main
|
|
140
|
+
dict, updates both caches, and returns the value.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
key: Non-empty key to fetch.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Any: The value associated with the key.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
KeyError: If the key does not exist in the main dict.
|
|
150
|
+
"""
|
|
151
|
+
key = NonEmptySafeStrTuple(key)
|
|
152
|
+
old_etag = self._etag_cache.get(key, None)
|
|
153
|
+
res = self.get_item_if_etag_changed(key, old_etag)
|
|
154
|
+
if res is ETAG_HAS_NOT_CHANGED:
|
|
155
|
+
try:
|
|
156
|
+
return self._data_cache[key]
|
|
157
|
+
except KeyError:
|
|
158
|
+
value, _ = self.get_item_if_etag_changed(key, None)
|
|
159
|
+
return value
|
|
160
|
+
else:
|
|
161
|
+
value, _ = res
|
|
162
|
+
return value
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_item_if_etag_changed(self, key: NonEmptyPersiDictKey, etag: Optional[str]):
|
|
166
|
+
"""Fetch value if the ETag is different from the provided one.
|
|
167
|
+
|
|
168
|
+
Delegates to main_dict.get_item_if_new_etag. On change, updates both
|
|
169
|
+
the data cache and the cached ETag. If the ETag has not changed, returns
|
|
170
|
+
the ETAG_HAS_NOT_CHANGED sentinel.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
key: Non-empty key to fetch.
|
|
174
|
+
etag: Previously known ETag, or None to force fetching the value.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
tuple[Any, str] | ETAG_HAS_NOT_CHANGED: Either (value, new_etag) when
|
|
178
|
+
the item is new or changed, or the ETAG_HAS_NOT_CHANGED sentinel when
|
|
179
|
+
the supplied ETag matches the current one.
|
|
180
|
+
"""
|
|
181
|
+
key = NonEmptySafeStrTuple(key)
|
|
182
|
+
res = self._main_dict.get_item_if_etag_changed(key, etag)
|
|
183
|
+
if res is ETAG_HAS_NOT_CHANGED:
|
|
184
|
+
return res
|
|
185
|
+
value, new_etag = res
|
|
186
|
+
self._data_cache[key] = value
|
|
187
|
+
self._set_cached_etag(key, new_etag)
|
|
188
|
+
return res
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def __setitem__(self, key: NonEmptyPersiDictKey, value: Any):
|
|
192
|
+
"""Set value for key via main dict and keep caches in sync.
|
|
193
|
+
|
|
194
|
+
This method writes to the main dict and mirrors the value
|
|
195
|
+
and ETag into caches.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
key: Non-empty key to set.
|
|
199
|
+
value: The value to store for the key.
|
|
200
|
+
"""
|
|
201
|
+
# Reuse the base processing for jokers and type checks, but route actual
|
|
202
|
+
# writes/deletes to the main dict and keep caches in sync via the
|
|
203
|
+
# set_item_get_etag helper below.
|
|
204
|
+
self.set_item_get_etag(key, value)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def set_item_get_etag(self, key: NonEmptyPersiDictKey, value: Any) -> Optional[str]:
|
|
208
|
+
"""Set item and return its ETag, updating caches.
|
|
209
|
+
|
|
210
|
+
This method delegates the actual write to the main dict.
|
|
211
|
+
After a successful write, it mirrors the value to data_cache
|
|
212
|
+
and stores the returned ETag in etag_cache.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
key: Non-empty key to set.
|
|
216
|
+
value: The value to store.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Optional[str]: The new ETag string from the main dict, or None if
|
|
220
|
+
execution was handled entirely by base-class joker processing.
|
|
221
|
+
"""
|
|
222
|
+
key = NonEmptySafeStrTuple(key)
|
|
223
|
+
if self._process_setitem_args(key, value) is EXECUTION_IS_COMPLETE:
|
|
224
|
+
return None
|
|
225
|
+
etag = self._main_dict.set_item_get_etag(key, value)
|
|
226
|
+
self._data_cache[key] = value
|
|
227
|
+
self._set_cached_etag(key, etag)
|
|
228
|
+
return etag
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def __delitem__(self, key: NonEmptyPersiDictKey):
|
|
232
|
+
"""Delete key from main dict and purge caches if present.
|
|
233
|
+
|
|
234
|
+
Deletion is delegated to the main dict using del.
|
|
235
|
+
Cached value and ETag for the key (if any) are removed.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
key: Non-empty key to delete.
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
KeyError: If the key does not exist in the main dict.
|
|
242
|
+
"""
|
|
243
|
+
key = NonEmptySafeStrTuple(key)
|
|
244
|
+
del self._main_dict[key] # This will raise KeyError if key doesn't exist
|
|
245
|
+
self._etag_cache.discard(key)
|
|
246
|
+
self._data_cache.discard(key)
|
|
247
|
+
|
|
248
|
+
|
persidict/empty_dict.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""EmptyDict: EmptyDict implementation that discards writes, always appears empty.
|
|
2
|
+
|
|
3
|
+
This module provides EmptyDict, a persistent dictionary that behaves like
|
|
4
|
+
/dev/null - accepting all writes but discarding them, and always appearing
|
|
5
|
+
empty on reads. Useful for testing, debugging, or as a no-op placeholder.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Iterator
|
|
10
|
+
|
|
11
|
+
from .safe_str_tuple import NonEmptySafeStrTuple
|
|
12
|
+
from .persi_dict import PersiDict, PersiDictKey, NonEmptyPersiDictKey
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EmptyDict(PersiDict):
|
|
16
|
+
"""
|
|
17
|
+
An equivalent of the null device in OS - accepts all writes but discards them,
|
|
18
|
+
returns nothing on reads. Always appears empty regardless of operations performed on it.
|
|
19
|
+
|
|
20
|
+
This class is useful for testing, debugging, or as a placeholder when you want to
|
|
21
|
+
disable persistent storage without changing the interface.
|
|
22
|
+
|
|
23
|
+
Key characteristics:
|
|
24
|
+
- All write operations are accepted, but data is discarded
|
|
25
|
+
- All read operations behave as if the dict is empty
|
|
26
|
+
- Length is always 0
|
|
27
|
+
- Iteration always yields no results
|
|
28
|
+
- Subdict operations return new EmptyDict instances
|
|
29
|
+
- All timestamp operations raise KeyError (no data exists)
|
|
30
|
+
|
|
31
|
+
Performance note: If validation is not needed, consider overriding __setitem__
|
|
32
|
+
to simply pass for better performance.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __contains__(self, key: NonEmptyPersiDictKey) -> bool:
|
|
36
|
+
"""Always returns False as EmptyDict contains nothing."""
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def __getitem__(self, key: NonEmptyPersiDictKey) -> Any:
|
|
41
|
+
"""Always raises KeyError as EmptyDict contains nothing."""
|
|
42
|
+
raise KeyError(key)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_item_if_etag_changed(self, key: NonEmptyPersiDictKey, etag: str | None
|
|
46
|
+
) -> tuple[Any, str|None]:
|
|
47
|
+
"""Always raises KeyError as EmptyDict contains nothing.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
key: Dictionary key (ignored, as EmptyDict has no items).
|
|
51
|
+
etag: ETag value to compare against (ignored).
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
tuple[Any, str|None]: Never returns as KeyError is always raised.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
KeyError: Always raised as EmptyDict contains no items.
|
|
58
|
+
"""
|
|
59
|
+
raise KeyError(key)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def __setitem__(self, key: NonEmptyPersiDictKey, value: Any) -> None:
|
|
63
|
+
"""Accepts any write operation, discards the data (like /dev/null)."""
|
|
64
|
+
# Run base validations (immutable checks, key normalization,
|
|
65
|
+
# type checks, jokers) to ensure API consistency, then discard.
|
|
66
|
+
self._process_setitem_args(key, value)
|
|
67
|
+
# Do nothing - discard the write like /dev/null
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def set_item_get_etag(self, key: NonEmptyPersiDictKey, value: Any) -> str|None:
|
|
71
|
+
"""Accepts any write operation, discards the data, returns None as etag.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
key: Dictionary key (processed for validation but discarded).
|
|
75
|
+
value: Value to store (processed for validation but discarded).
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
str|None: Always returns None as no actual storage occurs.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
KeyError: If attempting to modify when append_only is True.
|
|
82
|
+
TypeError: If value doesn't match base_class_for_values when specified.
|
|
83
|
+
"""
|
|
84
|
+
# Run base validations (immutable checks, key normalization,
|
|
85
|
+
# type checks, jokers) to ensure API consistency, then discard.
|
|
86
|
+
self._process_setitem_args(key, value)
|
|
87
|
+
# Do nothing - discard the write like /dev/null
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def __delitem__(self, key: NonEmptyPersiDictKey) -> None:
|
|
91
|
+
"""Always raises KeyError as there's nothing to delete."""
|
|
92
|
+
raise KeyError(key)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def __len__(self) -> int:
|
|
96
|
+
"""Always returns 0 as EmptyDict is always empty."""
|
|
97
|
+
return 0
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def __iter__(self) -> Iterator[PersiDictKey]:
|
|
101
|
+
"""Returns an empty iterator as EmptyDict contains no keys."""
|
|
102
|
+
return iter(())
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _generic_iter(self, result_type: set[str]) -> Iterator[tuple]:
|
|
106
|
+
"""Returns empty iterator for any generic iteration.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
result_type: Set indicating desired fields among {'keys', 'values',
|
|
110
|
+
'timestamps'}. Validated but result is always empty.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Iterator[tuple]: Always returns an empty iterator.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
ValueError: If result_type is invalid or contains unsupported fields.
|
|
117
|
+
"""
|
|
118
|
+
self._process_generic_iter_args(result_type)
|
|
119
|
+
return iter(())
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def clear(self) -> None:
|
|
123
|
+
"""No-op since EmptyDict is always empty."""
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get(self, key: NonEmptyPersiDictKey, default: Any = None) -> Any:
|
|
128
|
+
"""Always returns the default value since key is never found."""
|
|
129
|
+
return default
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def setdefault(self, key: NonEmptyPersiDictKey, default: Any = None) -> Any:
|
|
133
|
+
"""Always returns the default value without storing it."""
|
|
134
|
+
return default
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def timestamp(self, key: NonEmptyPersiDictKey) -> float:
|
|
138
|
+
"""Always raises KeyError as EmptyDict contains nothing."""
|
|
139
|
+
raise KeyError(key)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def discard(self, key: NonEmptyPersiDictKey) -> bool:
|
|
143
|
+
"""Always returns False as the key never exists."""
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
def delete_if_exists(self, key: NonEmptyPersiDictKey) -> bool:
|
|
147
|
+
"""Backward-compatible wrapper for discard()."""
|
|
148
|
+
return self.discard(key)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def random_key(self) -> NonEmptySafeStrTuple|None:
|
|
152
|
+
"""Returns None as EmptyDict contains no keys."""
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_params(self) -> dict[str, Any]:
|
|
157
|
+
"""Return parameters for this EmptyDict."""
|
|
158
|
+
params = super().get_params()
|
|
159
|
+
return params
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_subdict(self, prefix_key: PersiDictKey) -> 'EmptyDict':
|
|
163
|
+
"""Returns a new EmptyDict as subdictionary.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
prefix_key: Key prefix (ignored, as EmptyDict has no hierarchical structure).
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
EmptyDict: A new EmptyDict instance with the same configuration.
|
|
170
|
+
"""
|
|
171
|
+
return EmptyDict(**self.get_params())
|