ygg 0.1.44__py3-none-any.whl → 0.1.46__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.
- {ygg-0.1.44.dist-info → ygg-0.1.46.dist-info}/METADATA +1 -1
- {ygg-0.1.44.dist-info → ygg-0.1.46.dist-info}/RECORD +14 -13
- yggdrasil/databricks/compute/cluster.py +20 -16
- yggdrasil/databricks/compute/execution_context.py +46 -64
- yggdrasil/databricks/sql/engine.py +5 -2
- yggdrasil/databricks/sql/warehouse.py +355 -0
- yggdrasil/databricks/workspaces/workspace.py +19 -9
- yggdrasil/pyutils/callable_serde.py +296 -308
- yggdrasil/pyutils/expiring_dict.py +114 -25
- yggdrasil/version.py +1 -1
- {ygg-0.1.44.dist-info → ygg-0.1.46.dist-info}/WHEEL +0 -0
- {ygg-0.1.44.dist-info → ygg-0.1.46.dist-info}/entry_points.txt +0 -0
- {ygg-0.1.44.dist-info → ygg-0.1.46.dist-info}/licenses/LICENSE +0 -0
- {ygg-0.1.44.dist-info → ygg-0.1.46.dist-info}/top_level.txt +0 -0
|
@@ -4,17 +4,16 @@ import heapq
|
|
|
4
4
|
import itertools
|
|
5
5
|
import threading
|
|
6
6
|
import time
|
|
7
|
+
import pickle
|
|
7
8
|
from collections.abc import MutableMapping, Iterator
|
|
8
9
|
from dataclasses import dataclass
|
|
9
|
-
from typing import Callable, Generic, Optional, TypeVar, Dict, Tuple
|
|
10
|
+
from typing import Callable, Generic, Optional, TypeVar, Dict, Tuple, Any, Mapping
|
|
10
11
|
|
|
11
12
|
K = TypeVar("K")
|
|
12
13
|
V = TypeVar("V")
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
__all__ = [
|
|
16
|
-
"ExpiringDict"
|
|
17
|
-
]
|
|
16
|
+
__all__ = ["ExpiringDict"]
|
|
18
17
|
|
|
19
18
|
|
|
20
19
|
@dataclass(frozen=True)
|
|
@@ -27,12 +26,14 @@ class ExpiringDict(MutableMapping[K, V]):
|
|
|
27
26
|
"""
|
|
28
27
|
Dict with per-key TTL expiration.
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
|
|
29
|
+
Serialization note:
|
|
30
|
+
- Internally uses time.monotonic() for expires_at (great locally).
|
|
31
|
+
- For serialization, we store remaining TTLs so it can be reconstructed
|
|
32
|
+
in a different process with a different monotonic clock origin.
|
|
34
33
|
"""
|
|
35
34
|
|
|
35
|
+
_SER_VERSION = 1
|
|
36
|
+
|
|
36
37
|
def __init__(
|
|
37
38
|
self,
|
|
38
39
|
default_ttl: Optional[float] = None,
|
|
@@ -41,12 +42,6 @@ class ExpiringDict(MutableMapping[K, V]):
|
|
|
41
42
|
on_expire: Optional[Callable[[K, V], None]] = None,
|
|
42
43
|
thread_safe: bool = False,
|
|
43
44
|
) -> None:
|
|
44
|
-
"""
|
|
45
|
-
default_ttl: seconds, if provided used when ttl isn't passed to set()
|
|
46
|
-
refresh_on_get: if True, get()/__getitem__ extends TTL using default_ttl
|
|
47
|
-
on_expire: callback(key, value) called when an item is expired during cleanup
|
|
48
|
-
thread_safe: wrap operations in an RLock (extra overhead, but safe)
|
|
49
|
-
"""
|
|
50
45
|
self.default_ttl = default_ttl
|
|
51
46
|
self.refresh_on_get = refresh_on_get
|
|
52
47
|
self.on_expire = on_expire
|
|
@@ -55,24 +50,22 @@ class ExpiringDict(MutableMapping[K, V]):
|
|
|
55
50
|
self._heap: list[Tuple[float, int, K]] = [] # (expires_at, seq, key)
|
|
56
51
|
self._seq = itertools.count()
|
|
57
52
|
|
|
53
|
+
self._thread_safe = thread_safe
|
|
58
54
|
self._lock = threading.RLock() if thread_safe else None
|
|
59
55
|
|
|
60
56
|
def _now(self) -> float:
|
|
61
57
|
return time.monotonic()
|
|
62
58
|
|
|
63
59
|
def _with_lock(self):
|
|
64
|
-
# tiny helper to avoid repeating if/else everywhere
|
|
65
60
|
return self._lock or _NoopLock()
|
|
66
61
|
|
|
67
62
|
def _prune(self) -> None:
|
|
68
|
-
"""Remove expired entries. Ignores stale heap rows from overwrites."""
|
|
69
63
|
now = self._now()
|
|
70
64
|
while self._heap and self._heap[0][0] <= now:
|
|
71
65
|
exp, _, key = heapq.heappop(self._heap)
|
|
72
66
|
entry = self._store.get(key)
|
|
73
67
|
if entry is None:
|
|
74
68
|
continue
|
|
75
|
-
# Only expire if this heap expiry matches current entry expiry
|
|
76
69
|
if entry.expires_at == exp:
|
|
77
70
|
del self._store[key]
|
|
78
71
|
if self.on_expire:
|
|
@@ -84,11 +77,9 @@ class ExpiringDict(MutableMapping[K, V]):
|
|
|
84
77
|
if ttl is None:
|
|
85
78
|
ttl = self.default_ttl
|
|
86
79
|
if ttl is None:
|
|
87
|
-
# no expiration
|
|
88
80
|
expires_at = float("inf")
|
|
89
81
|
else:
|
|
90
82
|
if ttl <= 0:
|
|
91
|
-
# immediate expiration: just delete if exists
|
|
92
83
|
self._store.pop(key, None)
|
|
93
84
|
return
|
|
94
85
|
expires_at = self._now() + ttl
|
|
@@ -98,22 +89,19 @@ class ExpiringDict(MutableMapping[K, V]):
|
|
|
98
89
|
|
|
99
90
|
# --- MutableMapping interface ---
|
|
100
91
|
def __setitem__(self, key: K, value: V) -> None:
|
|
101
|
-
# Uses default_ttl (if any)
|
|
102
92
|
self.set(key, value, ttl=self.default_ttl)
|
|
103
93
|
|
|
104
94
|
def __getitem__(self, key: K) -> V:
|
|
105
95
|
with self._with_lock():
|
|
106
96
|
self._prune()
|
|
107
|
-
entry = self._store[key]
|
|
97
|
+
entry = self._store[key]
|
|
108
98
|
if entry.expires_at <= self._now():
|
|
109
|
-
# edge case: expired but not yet pruned (rare)
|
|
110
99
|
del self._store[key]
|
|
111
100
|
raise KeyError(key)
|
|
112
101
|
|
|
113
102
|
if self.refresh_on_get:
|
|
114
103
|
if self.default_ttl is None:
|
|
115
104
|
raise ValueError("refresh_on_get=True requires default_ttl")
|
|
116
|
-
# refresh TTL
|
|
117
105
|
self.set(key, entry.value, ttl=self.default_ttl)
|
|
118
106
|
return entry.value
|
|
119
107
|
|
|
@@ -129,7 +117,6 @@ class ExpiringDict(MutableMapping[K, V]):
|
|
|
129
117
|
with self._with_lock():
|
|
130
118
|
self._prune()
|
|
131
119
|
del self._store[key]
|
|
132
|
-
# heap keeps stale rows; they'll be ignored during prune
|
|
133
120
|
|
|
134
121
|
def __iter__(self) -> Iterator[K]:
|
|
135
122
|
with self._with_lock():
|
|
@@ -150,7 +137,6 @@ class ExpiringDict(MutableMapping[K, V]):
|
|
|
150
137
|
return False
|
|
151
138
|
|
|
152
139
|
def cleanup(self) -> int:
|
|
153
|
-
"""Force prune and return number of remaining items."""
|
|
154
140
|
with self._with_lock():
|
|
155
141
|
self._prune()
|
|
156
142
|
return len(self._store)
|
|
@@ -170,6 +156,109 @@ class ExpiringDict(MutableMapping[K, V]):
|
|
|
170
156
|
self._prune()
|
|
171
157
|
return [e.value for e in self._store.values()]
|
|
172
158
|
|
|
159
|
+
# ----------------------------
|
|
160
|
+
# Serialization / Pickling
|
|
161
|
+
# ----------------------------
|
|
162
|
+
def to_state(self) -> dict[str, Any]:
|
|
163
|
+
"""
|
|
164
|
+
Returns a JSON-ish friendly state (assuming keys/values are JSON-friendly).
|
|
165
|
+
Stores remaining TTL (seconds) per key; None => no expiration.
|
|
166
|
+
Expired entries are dropped.
|
|
167
|
+
"""
|
|
168
|
+
with self._with_lock():
|
|
169
|
+
self._prune()
|
|
170
|
+
now = self._now()
|
|
171
|
+
|
|
172
|
+
items: list[tuple[Any, Any, Optional[float]]] = []
|
|
173
|
+
for k, e in self._store.items():
|
|
174
|
+
if e.expires_at == float("inf"):
|
|
175
|
+
rem = None
|
|
176
|
+
else:
|
|
177
|
+
rem = e.expires_at - now
|
|
178
|
+
if rem <= 0:
|
|
179
|
+
continue
|
|
180
|
+
items.append((k, e.value, rem))
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
"v": self._SER_VERSION,
|
|
184
|
+
"default_ttl": self.default_ttl,
|
|
185
|
+
"refresh_on_get": self.refresh_on_get,
|
|
186
|
+
"thread_safe": self._thread_safe,
|
|
187
|
+
# NOTE: on_expire is intentionally not serialized
|
|
188
|
+
"items": items,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def from_state(
|
|
193
|
+
cls,
|
|
194
|
+
state: Mapping[str, Any],
|
|
195
|
+
*,
|
|
196
|
+
on_expire: Optional[Callable[[K, V], None]] = None,
|
|
197
|
+
) -> "ExpiringDict[K, V]":
|
|
198
|
+
"""
|
|
199
|
+
Rebuild from state produced by to_state().
|
|
200
|
+
"""
|
|
201
|
+
if state.get("v") != cls._SER_VERSION:
|
|
202
|
+
raise ValueError(f"Unsupported serialized version: {state.get('v')}")
|
|
203
|
+
|
|
204
|
+
d = cls(
|
|
205
|
+
default_ttl=state.get("default_ttl"),
|
|
206
|
+
refresh_on_get=bool(state.get("refresh_on_get", False)),
|
|
207
|
+
on_expire=on_expire,
|
|
208
|
+
thread_safe=bool(state.get("thread_safe", False)),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
now = d._now()
|
|
212
|
+
items = state.get("items", [])
|
|
213
|
+
for k, v, rem in items:
|
|
214
|
+
if rem is None:
|
|
215
|
+
expires_at = float("inf")
|
|
216
|
+
else:
|
|
217
|
+
if rem <= 0:
|
|
218
|
+
continue
|
|
219
|
+
expires_at = now + float(rem)
|
|
220
|
+
|
|
221
|
+
d._store[k] = _Entry(value=v, expires_at=expires_at)
|
|
222
|
+
heapq.heappush(d._heap, (expires_at, next(d._seq), k))
|
|
223
|
+
|
|
224
|
+
return d
|
|
225
|
+
|
|
226
|
+
def to_bytes(self, protocol: int = pickle.HIGHEST_PROTOCOL) -> bytes:
|
|
227
|
+
"""
|
|
228
|
+
Pickle to bytes using the portable state representation.
|
|
229
|
+
"""
|
|
230
|
+
return pickle.dumps(self.to_state(), protocol=protocol)
|
|
231
|
+
|
|
232
|
+
@classmethod
|
|
233
|
+
def from_bytes(
|
|
234
|
+
cls,
|
|
235
|
+
data: bytes,
|
|
236
|
+
*,
|
|
237
|
+
on_expire: Optional[Callable[[K, V], None]] = None,
|
|
238
|
+
) -> "ExpiringDict[K, V]":
|
|
239
|
+
state = pickle.loads(data)
|
|
240
|
+
return cls.from_state(state, on_expire=on_expire)
|
|
241
|
+
|
|
242
|
+
def __getstate__(self) -> dict[str, Any]:
|
|
243
|
+
# Called by pickle
|
|
244
|
+
return self.to_state()
|
|
245
|
+
|
|
246
|
+
def __setstate__(self, state: dict[str, Any]) -> None:
|
|
247
|
+
# Called by pickle; rebuild "self" in-place
|
|
248
|
+
rebuilt = self.from_state(state, on_expire=None)
|
|
249
|
+
|
|
250
|
+
# Copy rebuilt internals into self (keep pickling contract)
|
|
251
|
+
self.default_ttl = rebuilt.default_ttl
|
|
252
|
+
self.refresh_on_get = rebuilt.refresh_on_get
|
|
253
|
+
self.on_expire = None # not serialized
|
|
254
|
+
|
|
255
|
+
self._store = rebuilt._store
|
|
256
|
+
self._heap = rebuilt._heap
|
|
257
|
+
self._seq = rebuilt._seq
|
|
258
|
+
|
|
259
|
+
self._thread_safe = rebuilt._thread_safe
|
|
260
|
+
self._lock = threading.RLock() if self._thread_safe else None
|
|
261
|
+
|
|
173
262
|
|
|
174
263
|
class _NoopLock:
|
|
175
264
|
def __enter__(self): return self
|
yggdrasil/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
1
|
+
__version__ = "0.1.46"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|