ygg 0.1.32__py3-none-any.whl → 0.1.34__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.32.dist-info → ygg-0.1.34.dist-info}/METADATA +1 -1
- {ygg-0.1.32.dist-info → ygg-0.1.34.dist-info}/RECORD +14 -13
- yggdrasil/databricks/compute/cluster.py +109 -58
- yggdrasil/databricks/compute/execution_context.py +5 -2
- yggdrasil/databricks/compute/remote.py +14 -5
- yggdrasil/databricks/sql/engine.py +295 -321
- yggdrasil/databricks/workspaces/workspace.py +12 -1
- yggdrasil/pyutils/callable_serde.py +27 -2
- yggdrasil/pyutils/expiring_dict.py +176 -0
- yggdrasil/version.py +1 -1
- {ygg-0.1.32.dist-info → ygg-0.1.34.dist-info}/WHEEL +0 -0
- {ygg-0.1.32.dist-info → ygg-0.1.34.dist-info}/entry_points.txt +0 -0
- {ygg-0.1.32.dist-info → ygg-0.1.34.dist-info}/licenses/LICENSE +0 -0
- {ygg-0.1.32.dist-info → ygg-0.1.34.dist-info}/top_level.txt +0 -0
|
@@ -330,6 +330,12 @@ class Workspace:
|
|
|
330
330
|
if local_cache:
|
|
331
331
|
os.remove(local_cache)
|
|
332
332
|
|
|
333
|
+
@property
|
|
334
|
+
def safe_host(self):
|
|
335
|
+
if not self.host:
|
|
336
|
+
return self.connect().host
|
|
337
|
+
return self.host
|
|
338
|
+
|
|
333
339
|
@property
|
|
334
340
|
def current_user(self):
|
|
335
341
|
"""Return the current Databricks user.
|
|
@@ -625,7 +631,12 @@ class Workspace:
|
|
|
625
631
|
"""
|
|
626
632
|
from ..compute.cluster import Cluster
|
|
627
633
|
|
|
628
|
-
return Cluster(
|
|
634
|
+
return Cluster(
|
|
635
|
+
workspace=self,
|
|
636
|
+
cluster_id=cluster_id,
|
|
637
|
+
cluster_name=cluster_name,
|
|
638
|
+
**kwargs
|
|
639
|
+
)
|
|
629
640
|
|
|
630
641
|
# ---------------------------------------------------------------------------
|
|
631
642
|
# Workspace-bound base class
|
|
@@ -7,12 +7,13 @@ import dis
|
|
|
7
7
|
import importlib
|
|
8
8
|
import inspect
|
|
9
9
|
import json
|
|
10
|
+
import os
|
|
10
11
|
import struct
|
|
11
12
|
import sys
|
|
12
13
|
import zlib
|
|
13
14
|
from dataclasses import dataclass
|
|
14
15
|
from pathlib import Path
|
|
15
|
-
from typing import Any, Callable, Dict, Optional, Set, Tuple, TypeVar, Union
|
|
16
|
+
from typing import Any, Callable, Dict, Optional, Set, Tuple, TypeVar, Union, Iterable
|
|
16
17
|
|
|
17
18
|
import dill
|
|
18
19
|
|
|
@@ -378,6 +379,8 @@ class CallableSerde:
|
|
|
378
379
|
prefer: str = "import", # "import" | "dill"
|
|
379
380
|
dump_env: str = "none", # "none" | "globals" | "closure" | "both"
|
|
380
381
|
filter_used_globals: bool = True,
|
|
382
|
+
env_keys: Optional[Iterable[str]] = None,
|
|
383
|
+
env_variables: Optional[Dict[str, str]] = None,
|
|
381
384
|
) -> Dict[str, Any]:
|
|
382
385
|
"""Serialize the callable into a dict for transport.
|
|
383
386
|
|
|
@@ -385,6 +388,8 @@ class CallableSerde:
|
|
|
385
388
|
prefer: Preferred serialization kind.
|
|
386
389
|
dump_env: Environment payload selection.
|
|
387
390
|
filter_used_globals: Filter globals to referenced names.
|
|
391
|
+
env_keys: environment keys
|
|
392
|
+
env_variables: environment key values
|
|
388
393
|
|
|
389
394
|
Returns:
|
|
390
395
|
Serialized payload dict.
|
|
@@ -411,6 +416,17 @@ class CallableSerde:
|
|
|
411
416
|
self._dill_b64 = base64.b64encode(payload).decode("ascii")
|
|
412
417
|
out["dill_b64"] = self._dill_b64
|
|
413
418
|
|
|
419
|
+
env_variables = env_variables or {}
|
|
420
|
+
if env_keys:
|
|
421
|
+
for env_key in env_keys:
|
|
422
|
+
existing = os.getenv(env_key)
|
|
423
|
+
|
|
424
|
+
if existing:
|
|
425
|
+
env_variables[env_key] = existing
|
|
426
|
+
|
|
427
|
+
if env_variables:
|
|
428
|
+
out["osenv"] = env_variables
|
|
429
|
+
|
|
414
430
|
if dump_env != "none":
|
|
415
431
|
if self.fn is None:
|
|
416
432
|
raise ValueError("dump_env requested but fn is not present.")
|
|
@@ -523,6 +539,8 @@ class CallableSerde:
|
|
|
523
539
|
byte_limit: int = 256_000,
|
|
524
540
|
dump_env: str = "none", # "none" | "globals" | "closure" | "both"
|
|
525
541
|
filter_used_globals: bool = True,
|
|
542
|
+
env_keys: Optional[Iterable[str]] = None,
|
|
543
|
+
env_variables: Optional[Dict[str, str]] = None,
|
|
526
544
|
) -> str:
|
|
527
545
|
"""
|
|
528
546
|
Returns Python code string to execute in another interpreter.
|
|
@@ -536,6 +554,8 @@ class CallableSerde:
|
|
|
536
554
|
prefer=prefer,
|
|
537
555
|
dump_env=dump_env,
|
|
538
556
|
filter_used_globals=filter_used_globals,
|
|
557
|
+
env_keys=env_keys,
|
|
558
|
+
env_variables=env_variables
|
|
539
559
|
)
|
|
540
560
|
serde_json = json.dumps(serde_dict, ensure_ascii=False)
|
|
541
561
|
|
|
@@ -545,7 +565,7 @@ class CallableSerde:
|
|
|
545
565
|
|
|
546
566
|
# NOTE: plain string template + replace. No f-string. No brace escaping.
|
|
547
567
|
template = r"""
|
|
548
|
-
import base64, json, sys, struct, zlib, importlib, dis
|
|
568
|
+
import base64, json, sys, struct, zlib, importlib, dis, os
|
|
549
569
|
import dill
|
|
550
570
|
|
|
551
571
|
RESULT_TAG = __RESULT_TAG__
|
|
@@ -626,6 +646,11 @@ else:
|
|
|
626
646
|
else:
|
|
627
647
|
fn = dill.loads(base64.b64decode(serde["dill_b64"]))
|
|
628
648
|
|
|
649
|
+
osenv = serde.get("osenv")
|
|
650
|
+
if osenv:
|
|
651
|
+
for k, v in osenv.items():
|
|
652
|
+
os.environ[k] = v
|
|
653
|
+
|
|
629
654
|
env_b64 = serde.get("env_b64")
|
|
630
655
|
if env_b64:
|
|
631
656
|
env = dill.loads(base64.b64decode(env_b64))
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import heapq
|
|
4
|
+
import itertools
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import MutableMapping, Iterator
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Callable, Generic, Optional, TypeVar, Dict, Tuple
|
|
10
|
+
|
|
11
|
+
K = TypeVar("K")
|
|
12
|
+
V = TypeVar("V")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ExpiringDict"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class _Entry(Generic[V]):
|
|
22
|
+
value: V
|
|
23
|
+
expires_at: float # monotonic timestamp
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ExpiringDict(MutableMapping[K, V]):
|
|
27
|
+
"""
|
|
28
|
+
Dict with per-key TTL expiration.
|
|
29
|
+
|
|
30
|
+
- Uses time.monotonic() (safe against system clock changes)
|
|
31
|
+
- O(log n) cleanup amortized via a min-heap of expirations
|
|
32
|
+
- Overwrites are handled (stale heap entries are ignored)
|
|
33
|
+
- Optional refresh_on_get: touching a key extends its TTL
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
default_ttl: Optional[float] = None,
|
|
39
|
+
*,
|
|
40
|
+
refresh_on_get: bool = False,
|
|
41
|
+
on_expire: Optional[Callable[[K, V], None]] = None,
|
|
42
|
+
thread_safe: bool = False,
|
|
43
|
+
) -> 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
|
+
self.default_ttl = default_ttl
|
|
51
|
+
self.refresh_on_get = refresh_on_get
|
|
52
|
+
self.on_expire = on_expire
|
|
53
|
+
|
|
54
|
+
self._store: Dict[K, _Entry[V]] = {}
|
|
55
|
+
self._heap: list[Tuple[float, int, K]] = [] # (expires_at, seq, key)
|
|
56
|
+
self._seq = itertools.count()
|
|
57
|
+
|
|
58
|
+
self._lock = threading.RLock() if thread_safe else None
|
|
59
|
+
|
|
60
|
+
def _now(self) -> float:
|
|
61
|
+
return time.monotonic()
|
|
62
|
+
|
|
63
|
+
def _with_lock(self):
|
|
64
|
+
# tiny helper to avoid repeating if/else everywhere
|
|
65
|
+
return self._lock or _NoopLock()
|
|
66
|
+
|
|
67
|
+
def _prune(self) -> None:
|
|
68
|
+
"""Remove expired entries. Ignores stale heap rows from overwrites."""
|
|
69
|
+
now = self._now()
|
|
70
|
+
while self._heap and self._heap[0][0] <= now:
|
|
71
|
+
exp, _, key = heapq.heappop(self._heap)
|
|
72
|
+
entry = self._store.get(key)
|
|
73
|
+
if entry is None:
|
|
74
|
+
continue
|
|
75
|
+
# Only expire if this heap expiry matches current entry expiry
|
|
76
|
+
if entry.expires_at == exp:
|
|
77
|
+
del self._store[key]
|
|
78
|
+
if self.on_expire:
|
|
79
|
+
self.on_expire(key, entry.value)
|
|
80
|
+
|
|
81
|
+
def set(self, key: K, value: V, ttl: Optional[float] = None) -> None:
|
|
82
|
+
with self._with_lock():
|
|
83
|
+
self._prune()
|
|
84
|
+
if ttl is None:
|
|
85
|
+
ttl = self.default_ttl
|
|
86
|
+
if ttl is None:
|
|
87
|
+
# no expiration
|
|
88
|
+
expires_at = float("inf")
|
|
89
|
+
else:
|
|
90
|
+
if ttl <= 0:
|
|
91
|
+
# immediate expiration: just delete if exists
|
|
92
|
+
self._store.pop(key, None)
|
|
93
|
+
return
|
|
94
|
+
expires_at = self._now() + ttl
|
|
95
|
+
|
|
96
|
+
self._store[key] = _Entry(value=value, expires_at=expires_at)
|
|
97
|
+
heapq.heappush(self._heap, (expires_at, next(self._seq), key))
|
|
98
|
+
|
|
99
|
+
# --- MutableMapping interface ---
|
|
100
|
+
def __setitem__(self, key: K, value: V) -> None:
|
|
101
|
+
# Uses default_ttl (if any)
|
|
102
|
+
self.set(key, value, ttl=self.default_ttl)
|
|
103
|
+
|
|
104
|
+
def __getitem__(self, key: K) -> V:
|
|
105
|
+
with self._with_lock():
|
|
106
|
+
self._prune()
|
|
107
|
+
entry = self._store[key] # may raise KeyError
|
|
108
|
+
if entry.expires_at <= self._now():
|
|
109
|
+
# edge case: expired but not yet pruned (rare)
|
|
110
|
+
del self._store[key]
|
|
111
|
+
raise KeyError(key)
|
|
112
|
+
|
|
113
|
+
if self.refresh_on_get:
|
|
114
|
+
if self.default_ttl is None:
|
|
115
|
+
raise ValueError("refresh_on_get=True requires default_ttl")
|
|
116
|
+
# refresh TTL
|
|
117
|
+
self.set(key, entry.value, ttl=self.default_ttl)
|
|
118
|
+
return entry.value
|
|
119
|
+
|
|
120
|
+
return entry.value
|
|
121
|
+
|
|
122
|
+
def get(self, key: K, default: Optional[V] = None) -> Optional[V]:
|
|
123
|
+
try:
|
|
124
|
+
return self[key]
|
|
125
|
+
except KeyError:
|
|
126
|
+
return default
|
|
127
|
+
|
|
128
|
+
def __delitem__(self, key: K) -> None:
|
|
129
|
+
with self._with_lock():
|
|
130
|
+
self._prune()
|
|
131
|
+
del self._store[key]
|
|
132
|
+
# heap keeps stale rows; they'll be ignored during prune
|
|
133
|
+
|
|
134
|
+
def __iter__(self) -> Iterator[K]:
|
|
135
|
+
with self._with_lock():
|
|
136
|
+
self._prune()
|
|
137
|
+
return iter(list(self._store.keys()))
|
|
138
|
+
|
|
139
|
+
def __len__(self) -> int:
|
|
140
|
+
with self._with_lock():
|
|
141
|
+
self._prune()
|
|
142
|
+
return len(self._store)
|
|
143
|
+
|
|
144
|
+
def __contains__(self, key: object) -> bool:
|
|
145
|
+
with self._with_lock():
|
|
146
|
+
self._prune()
|
|
147
|
+
if key in self._store:
|
|
148
|
+
entry = self._store[key] # type: ignore[index]
|
|
149
|
+
return entry.expires_at > self._now()
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
def cleanup(self) -> int:
|
|
153
|
+
"""Force prune and return number of remaining items."""
|
|
154
|
+
with self._with_lock():
|
|
155
|
+
self._prune()
|
|
156
|
+
return len(self._store)
|
|
157
|
+
|
|
158
|
+
def items(self):
|
|
159
|
+
with self._with_lock():
|
|
160
|
+
self._prune()
|
|
161
|
+
return [(k, e.value) for k, e in self._store.items()]
|
|
162
|
+
|
|
163
|
+
def keys(self):
|
|
164
|
+
with self._with_lock():
|
|
165
|
+
self._prune()
|
|
166
|
+
return list(self._store.keys())
|
|
167
|
+
|
|
168
|
+
def values(self):
|
|
169
|
+
with self._with_lock():
|
|
170
|
+
self._prune()
|
|
171
|
+
return [e.value for e in self._store.values()]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class _NoopLock:
|
|
175
|
+
def __enter__(self): return self
|
|
176
|
+
def __exit__(self, exc_type, exc, tb): return False
|
yggdrasil/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
1
|
+
__version__ = "0.1.34"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|