persidict 0.38.0__py3-none-any.whl → 0.104.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.

@@ -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
+
@@ -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())