ygg 0.1.57__py3-none-any.whl → 0.1.60__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.57.dist-info → ygg-0.1.60.dist-info}/METADATA +1 -1
- ygg-0.1.60.dist-info/RECORD +74 -0
- yggdrasil/ai/__init__.py +2 -0
- yggdrasil/ai/session.py +89 -0
- yggdrasil/ai/sql_session.py +310 -0
- yggdrasil/databricks/__init__.py +0 -3
- yggdrasil/databricks/compute/cluster.py +68 -113
- yggdrasil/databricks/compute/command_execution.py +674 -0
- yggdrasil/databricks/compute/exceptions.py +19 -0
- yggdrasil/databricks/compute/execution_context.py +491 -282
- yggdrasil/databricks/compute/remote.py +4 -14
- yggdrasil/databricks/exceptions.py +10 -0
- yggdrasil/databricks/sql/__init__.py +0 -4
- yggdrasil/databricks/sql/engine.py +161 -173
- yggdrasil/databricks/sql/exceptions.py +9 -1
- yggdrasil/databricks/sql/statement_result.py +108 -120
- yggdrasil/databricks/sql/warehouse.py +331 -92
- yggdrasil/databricks/workspaces/io.py +89 -9
- yggdrasil/databricks/workspaces/path.py +120 -72
- yggdrasil/databricks/workspaces/workspace.py +214 -61
- yggdrasil/exceptions.py +7 -0
- yggdrasil/libs/databrickslib.py +23 -18
- yggdrasil/libs/extensions/spark_extensions.py +1 -1
- yggdrasil/libs/pandaslib.py +15 -6
- yggdrasil/libs/polarslib.py +49 -13
- yggdrasil/pyutils/__init__.py +1 -2
- yggdrasil/pyutils/callable_serde.py +12 -19
- yggdrasil/pyutils/exceptions.py +16 -0
- yggdrasil/pyutils/python_env.py +14 -13
- yggdrasil/pyutils/waiting_config.py +171 -0
- yggdrasil/types/cast/arrow_cast.py +3 -0
- yggdrasil/types/cast/pandas_cast.py +157 -169
- yggdrasil/types/cast/polars_cast.py +11 -43
- yggdrasil/types/dummy_class.py +81 -0
- yggdrasil/version.py +1 -1
- ygg-0.1.57.dist-info/RECORD +0 -66
- yggdrasil/databricks/ai/loki.py +0 -53
- {ygg-0.1.57.dist-info → ygg-0.1.60.dist-info}/WHEEL +0 -0
- {ygg-0.1.57.dist-info → ygg-0.1.60.dist-info}/entry_points.txt +0 -0
- {ygg-0.1.57.dist-info → ygg-0.1.60.dist-info}/licenses/LICENSE +0 -0
- {ygg-0.1.57.dist-info → ygg-0.1.60.dist-info}/top_level.txt +0 -0
- /yggdrasil/{databricks/ai/__init__.py → pyutils/mimetypes.py} +0 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import gzip
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from json import JSONDecodeError
|
|
10
|
+
from typing import TYPE_CHECKING, Optional, Any, Callable, Dict, Iterable, Union, Generator, Iterator
|
|
11
|
+
|
|
12
|
+
import dill
|
|
13
|
+
import pyarrow
|
|
14
|
+
|
|
15
|
+
from .exceptions import ClientTerminatedSession
|
|
16
|
+
from ...libs.databrickslib import databricks_sdk, DatabricksDummyClass
|
|
17
|
+
from ...libs.pandaslib import PandasDataFrame
|
|
18
|
+
from ...libs.polarslib import PolarsDataFrame
|
|
19
|
+
from ...pyutils.exceptions import raise_parsed_traceback
|
|
20
|
+
from ...pyutils.waiting_config import WaitingConfig, WaitingConfigArg
|
|
21
|
+
|
|
22
|
+
if databricks_sdk is not None:
|
|
23
|
+
from databricks.sdk.errors import InternalError
|
|
24
|
+
from databricks.sdk.service.compute import (
|
|
25
|
+
Language, CommandExecutionAPI, CommandStatusResponse, CommandStatus, ResultType
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
DONE_STATES = {
|
|
29
|
+
CommandStatus.FINISHED, CommandStatus.CANCELLED, CommandStatus.ERROR
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
PENDING_STATES = {
|
|
33
|
+
CommandStatus.RUNNING, CommandStatus.QUEUED, CommandStatus.RUNNING
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
FAILED_STATES = {
|
|
37
|
+
CommandStatus.ERROR, CommandStatus.CANCELLED
|
|
38
|
+
}
|
|
39
|
+
else:
|
|
40
|
+
InternalError = DatabricksDummyClass
|
|
41
|
+
Language = DatabricksDummyClass
|
|
42
|
+
CommandExecutionAPI = DatabricksDummyClass
|
|
43
|
+
ResultType = DatabricksDummyClass
|
|
44
|
+
|
|
45
|
+
DONE_STATES, PENDING_STATES, FAILED_STATES = set(), set(), set()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from .execution_context import ExecutionContext
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"CommandExecution"
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
LOGGER = logging.getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class CommandExecution:
|
|
62
|
+
context: "ExecutionContext"
|
|
63
|
+
command_id: Optional[str] = None
|
|
64
|
+
|
|
65
|
+
language: Optional[Language] = field(default=None, repr=False, compare=False, hash=False)
|
|
66
|
+
command: Optional[str] = field(default=None, repr=False, compare=False, hash=False)
|
|
67
|
+
environ: Optional[Dict[str, str]] = field(default=None, repr=False, compare=False, hash=False)
|
|
68
|
+
|
|
69
|
+
_details: Optional[CommandStatusResponse] = field(default=None, repr=False, compare=False, hash=False)
|
|
70
|
+
|
|
71
|
+
def __post_init__(self):
|
|
72
|
+
if self.environ:
|
|
73
|
+
if isinstance(self.environ, (list, tuple, set)):
|
|
74
|
+
self.environ = {
|
|
75
|
+
k: os.getenv(k)
|
|
76
|
+
for k in self.environ
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def __call__(self, *args, **kwargs):
|
|
80
|
+
assert self.command, "Cannot call %s, missing command" % self
|
|
81
|
+
|
|
82
|
+
args_blob = dill.dumps([self.encode_object(_) for _ in args])
|
|
83
|
+
kwargs_blob = dill.dumps({k: self.encode_object(v) for k, v in kwargs.items()})
|
|
84
|
+
|
|
85
|
+
if self.environ:
|
|
86
|
+
env_blob = {
|
|
87
|
+
k: os.getenv(k) or v
|
|
88
|
+
for k, v in self.environ.items()
|
|
89
|
+
if os.getenv(k) or v
|
|
90
|
+
}
|
|
91
|
+
else:
|
|
92
|
+
env_blob = {}
|
|
93
|
+
|
|
94
|
+
args_b64 = base64.b64encode(args_blob).decode("ascii")
|
|
95
|
+
kwargs_b64 = base64.b64encode(kwargs_blob).decode("ascii")
|
|
96
|
+
|
|
97
|
+
command = (
|
|
98
|
+
self.command
|
|
99
|
+
.replace("__ARGS__", repr(args_b64))
|
|
100
|
+
.replace("__KWARGS__", repr(kwargs_b64))
|
|
101
|
+
.replace("__ENVIRON__", repr(env_blob))
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
run = (
|
|
105
|
+
self.create(
|
|
106
|
+
context=self.context,
|
|
107
|
+
command=command,
|
|
108
|
+
language=self.language
|
|
109
|
+
)
|
|
110
|
+
.start()
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return run.wait(raise_error=True).result(raise_error=True)
|
|
114
|
+
|
|
115
|
+
def __bool__(self):
|
|
116
|
+
return self.done
|
|
117
|
+
|
|
118
|
+
def __repr__(self):
|
|
119
|
+
return "%s(url=%s)" % (
|
|
120
|
+
self.__class__.__name__,
|
|
121
|
+
self.url()
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def __str__(self):
|
|
125
|
+
return self.url()
|
|
126
|
+
|
|
127
|
+
def url(self) -> str:
|
|
128
|
+
return "%s/command/%s" % (
|
|
129
|
+
self.context.url(),
|
|
130
|
+
self.command_id or "unknown"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def create(
|
|
134
|
+
self,
|
|
135
|
+
context: Optional["ExecutionContext"] = None,
|
|
136
|
+
func: Optional[Callable] = None,
|
|
137
|
+
command: Optional[str] = None,
|
|
138
|
+
language: Optional[Language] = None,
|
|
139
|
+
command_id: Optional[str] = None,
|
|
140
|
+
environ: Optional[Union[Iterable[str], Dict[str, str]]] = None,
|
|
141
|
+
):
|
|
142
|
+
context = self.context if context is None else context
|
|
143
|
+
command = self.command if command is None else command
|
|
144
|
+
environ = self.environ if environ is None else environ
|
|
145
|
+
|
|
146
|
+
if environ is not None:
|
|
147
|
+
if not isinstance(environ, dict):
|
|
148
|
+
environ = {
|
|
149
|
+
str(k): os.getenv(str(k))
|
|
150
|
+
for k in environ
|
|
151
|
+
}
|
|
152
|
+
else:
|
|
153
|
+
environ = {
|
|
154
|
+
str(k): str(v)
|
|
155
|
+
for k, v in environ.items()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if not command:
|
|
159
|
+
if callable(func):
|
|
160
|
+
command = self.make_python_function_command(
|
|
161
|
+
func=func,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if language is None:
|
|
165
|
+
language = context.language or Language.PYTHON
|
|
166
|
+
|
|
167
|
+
assert context is not None, "Missing context to execute command"
|
|
168
|
+
|
|
169
|
+
return CommandExecution(
|
|
170
|
+
context=context,
|
|
171
|
+
language=language,
|
|
172
|
+
command=command,
|
|
173
|
+
command_id=command_id,
|
|
174
|
+
environ=environ
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def start(self, reset: bool = False):
|
|
178
|
+
if self.command_id:
|
|
179
|
+
if not reset:
|
|
180
|
+
return self
|
|
181
|
+
|
|
182
|
+
self._details = None
|
|
183
|
+
self.command_id = None
|
|
184
|
+
|
|
185
|
+
client = self.context.workspace_client().command_execution
|
|
186
|
+
|
|
187
|
+
assert self.command, "Missing command arg in %s" % self
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
details = client.execute(
|
|
191
|
+
cluster_id=self.cluster_id,
|
|
192
|
+
context_id=self.context_id,
|
|
193
|
+
language=self.language,
|
|
194
|
+
command=self.command,
|
|
195
|
+
).response
|
|
196
|
+
except Exception as e:
|
|
197
|
+
if "ontext" in str(e): # context related
|
|
198
|
+
self.context = self.context.connect(reset=True)
|
|
199
|
+
|
|
200
|
+
details = client.execute(
|
|
201
|
+
cluster_id=self.cluster_id,
|
|
202
|
+
context_id=self.context_id,
|
|
203
|
+
language=self.language,
|
|
204
|
+
command=self.command,
|
|
205
|
+
).response
|
|
206
|
+
else:
|
|
207
|
+
raise e
|
|
208
|
+
|
|
209
|
+
self.command_id = details.id
|
|
210
|
+
self._details = None
|
|
211
|
+
|
|
212
|
+
LOGGER.info("Started %s", self)
|
|
213
|
+
|
|
214
|
+
return self
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def workspace(self):
|
|
218
|
+
return self.context.workspace
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def cluster_id(self):
|
|
222
|
+
return self.context.cluster.cluster_id
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def context_id(self):
|
|
226
|
+
if not self.context.context_id:
|
|
227
|
+
self.context = self.context.connect()
|
|
228
|
+
return self.context.context_id
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def state(self):
|
|
232
|
+
return self.details.status
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def running(self):
|
|
236
|
+
return self.state in PENDING_STATES
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def done(self):
|
|
240
|
+
return self.state in DONE_STATES
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def details(self) -> CommandStatusResponse:
|
|
244
|
+
if self._details is None:
|
|
245
|
+
self._details = self.client().command_status(
|
|
246
|
+
cluster_id=self.cluster_id,
|
|
247
|
+
context_id=self.context_id,
|
|
248
|
+
command_id=self.command_id
|
|
249
|
+
)
|
|
250
|
+
elif self._details.status not in DONE_STATES:
|
|
251
|
+
self._details = self.client().command_status(
|
|
252
|
+
cluster_id=self.cluster_id,
|
|
253
|
+
context_id=self.context_id,
|
|
254
|
+
command_id=self.command_id
|
|
255
|
+
)
|
|
256
|
+
return self._details
|
|
257
|
+
|
|
258
|
+
@details.setter
|
|
259
|
+
def details(self, value: Optional[CommandStatusResponse]):
|
|
260
|
+
self._details = value
|
|
261
|
+
|
|
262
|
+
if value is not None:
|
|
263
|
+
assert isinstance(value, CommandStatusResponse), "%s.details must be CommandStatusResponse, got %s" %(
|
|
264
|
+
self,
|
|
265
|
+
type(value)
|
|
266
|
+
)
|
|
267
|
+
self.command_id = value.id
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def results_metadata(self):
|
|
271
|
+
return self.details.results
|
|
272
|
+
|
|
273
|
+
def client(self) -> CommandExecutionAPI:
|
|
274
|
+
return self.context.workspace_client().command_execution
|
|
275
|
+
|
|
276
|
+
def connect(self, reset: bool = False):
|
|
277
|
+
self.context = self.context.connect(language=self.language)
|
|
278
|
+
|
|
279
|
+
return self
|
|
280
|
+
|
|
281
|
+
def cancel(self, raise_error: bool = False):
|
|
282
|
+
if self.command_id:
|
|
283
|
+
try:
|
|
284
|
+
self.client().cancel_and_wait(
|
|
285
|
+
cluster_id=self.cluster_id,
|
|
286
|
+
command_id=self.command_id,
|
|
287
|
+
context_id=self.context_id
|
|
288
|
+
)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
if raise_error:
|
|
291
|
+
raise e
|
|
292
|
+
LOGGER.exception(e)
|
|
293
|
+
|
|
294
|
+
def raise_for_status(self):
|
|
295
|
+
if self.state in FAILED_STATES:
|
|
296
|
+
raise_error_from_response(
|
|
297
|
+
response=self.details,
|
|
298
|
+
language=self.language
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return self
|
|
302
|
+
|
|
303
|
+
def wait(
|
|
304
|
+
self,
|
|
305
|
+
wait: Optional[WaitingConfigArg] = True,
|
|
306
|
+
raise_error: bool = True
|
|
307
|
+
):
|
|
308
|
+
if not self.command_id:
|
|
309
|
+
return self.start().wait(
|
|
310
|
+
wait=wait,
|
|
311
|
+
raise_error=raise_error
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
wait = WaitingConfig.check_arg(wait)
|
|
315
|
+
iteration, start = 0, time.time()
|
|
316
|
+
|
|
317
|
+
if wait.timeout:
|
|
318
|
+
while self.running:
|
|
319
|
+
wait.sleep(iteration=iteration, start=start)
|
|
320
|
+
iteration += 1
|
|
321
|
+
|
|
322
|
+
if raise_error:
|
|
323
|
+
try:
|
|
324
|
+
self.raise_for_status()
|
|
325
|
+
except ModuleNotFoundError as e:
|
|
326
|
+
module_name = e.name
|
|
327
|
+
|
|
328
|
+
if module_name and not module_name.startswith("ygg"):
|
|
329
|
+
self.context.cluster.install_temporary_libraries(
|
|
330
|
+
libraries=[module_name]
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
self
|
|
335
|
+
.start(reset=True)
|
|
336
|
+
.wait(wait=wait, raise_error=raise_error)
|
|
337
|
+
)
|
|
338
|
+
else:
|
|
339
|
+
raise e
|
|
340
|
+
except ClientTerminatedSession as e:
|
|
341
|
+
LOGGER.error(
|
|
342
|
+
"%s aborted: %s",
|
|
343
|
+
self,
|
|
344
|
+
e
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
self.context = self.context.connect(reset=True)
|
|
348
|
+
|
|
349
|
+
return (
|
|
350
|
+
self
|
|
351
|
+
.start(reset=True)
|
|
352
|
+
.wait(wait=wait, raise_error=raise_error)
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
return self
|
|
356
|
+
|
|
357
|
+
def encode_object(
|
|
358
|
+
self,
|
|
359
|
+
obj: Any,
|
|
360
|
+
byte_limit: int = 32 * 1024,
|
|
361
|
+
byref: Any = None,
|
|
362
|
+
recurse: Any = None,
|
|
363
|
+
compression: Optional[str] = None
|
|
364
|
+
) -> str:
|
|
365
|
+
buffer = io.BytesIO()
|
|
366
|
+
|
|
367
|
+
if isinstance(obj, pyarrow.Table):
|
|
368
|
+
import pyarrow.parquet as pq
|
|
369
|
+
|
|
370
|
+
func = "pyarrow.parquet.read_table"
|
|
371
|
+
extension = "parquet"
|
|
372
|
+
pq.write_table(obj, buffer)
|
|
373
|
+
|
|
374
|
+
buffer.seek(0)
|
|
375
|
+
dbx_path = self.workspace.tmp_path(extension=extension)
|
|
376
|
+
dbx_path.write_bytes(buffer)
|
|
377
|
+
|
|
378
|
+
return json.dumps({
|
|
379
|
+
"func": func,
|
|
380
|
+
"file": dbx_path.full_path()
|
|
381
|
+
})
|
|
382
|
+
elif isinstance(obj, PolarsDataFrame):
|
|
383
|
+
func = "polars.read_parquet"
|
|
384
|
+
extension = "parquet"
|
|
385
|
+
obj.write_parquet(buffer)
|
|
386
|
+
|
|
387
|
+
buffer.seek(0)
|
|
388
|
+
dbx_path = self.workspace.tmp_path(extension=extension)
|
|
389
|
+
dbx_path.write_bytes(buffer)
|
|
390
|
+
|
|
391
|
+
return json.dumps({
|
|
392
|
+
"func": func,
|
|
393
|
+
"file": dbx_path.full_path()
|
|
394
|
+
})
|
|
395
|
+
elif isinstance(obj, PandasDataFrame):
|
|
396
|
+
try:
|
|
397
|
+
func = "pandas.read_parquet"
|
|
398
|
+
extension = "parquet"
|
|
399
|
+
obj.to_parquet(path=buffer)
|
|
400
|
+
except Exception as e:
|
|
401
|
+
LOGGER.warning(e)
|
|
402
|
+
|
|
403
|
+
compression = "gzip"
|
|
404
|
+
extension = "pkl.gz"
|
|
405
|
+
func = "pandas.read_pickle"
|
|
406
|
+
obj.to_pickle(path=buffer, compression=compression)
|
|
407
|
+
|
|
408
|
+
buffer.seek(0)
|
|
409
|
+
dbx_path = self.workspace.tmp_path(extension=extension)
|
|
410
|
+
dbx_path.write_bytes(buffer)
|
|
411
|
+
|
|
412
|
+
return json.dumps({
|
|
413
|
+
"func": func,
|
|
414
|
+
"cpr": compression,
|
|
415
|
+
"file": dbx_path.full_path()
|
|
416
|
+
})
|
|
417
|
+
elif isinstance(obj, (Generator, Iterator)):
|
|
418
|
+
return json.dumps({
|
|
419
|
+
"func": "generator",
|
|
420
|
+
"items": [
|
|
421
|
+
self.encode_object(_, byte_limit=byte_limit, byref=byref, recurse=recurse, compression=compression)
|
|
422
|
+
for _ in obj
|
|
423
|
+
]
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
dill.dump(obj, buffer, byref=byref, recurse=recurse)
|
|
427
|
+
|
|
428
|
+
raw = buffer.getvalue()
|
|
429
|
+
|
|
430
|
+
if compression or len(raw) > byte_limit:
|
|
431
|
+
compression = compression or "gzip"
|
|
432
|
+
raw = gzip.compress(raw)
|
|
433
|
+
|
|
434
|
+
if len(raw) > byte_limit:
|
|
435
|
+
buffer.seek(0)
|
|
436
|
+
dbx_path = self.workspace.tmp_path(extension="bin")
|
|
437
|
+
dbx_path.write_bytes(buffer)
|
|
438
|
+
|
|
439
|
+
return json.dumps({
|
|
440
|
+
"func": "dill.load",
|
|
441
|
+
"cpr": compression,
|
|
442
|
+
"file": dbx_path.full_path()
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
return json.dumps({
|
|
446
|
+
"func": "dill.load",
|
|
447
|
+
"cpr": compression,
|
|
448
|
+
"b64": base64.b64encode(raw).decode("ascii")
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
def decode_payload(
|
|
452
|
+
self,
|
|
453
|
+
payload: Union[str, bytes, dict, list]
|
|
454
|
+
):
|
|
455
|
+
if isinstance(payload, (str, bytes)):
|
|
456
|
+
try:
|
|
457
|
+
payload = json.loads(payload)
|
|
458
|
+
except JSONDecodeError:
|
|
459
|
+
return payload
|
|
460
|
+
|
|
461
|
+
if isinstance(payload, dict):
|
|
462
|
+
func, compression, b64, databricks_path = (
|
|
463
|
+
payload.get("func"), payload.get("cpr"),
|
|
464
|
+
payload.get("b64"), payload.get("file")
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
if isinstance(func, str) and func:
|
|
468
|
+
if b64:
|
|
469
|
+
blob = base64.b64decode(b64.encode("ascii"))
|
|
470
|
+
elif databricks_path:
|
|
471
|
+
blob = self.workspace.dbfs_path(databricks_path, temporary=True).read_bytes()
|
|
472
|
+
else:
|
|
473
|
+
blob = None
|
|
474
|
+
|
|
475
|
+
if func == "dill.load":
|
|
476
|
+
if compression == "gzip":
|
|
477
|
+
import gzip
|
|
478
|
+
blob = gzip.decompress(blob)
|
|
479
|
+
|
|
480
|
+
return dill.loads(blob)
|
|
481
|
+
elif func.startswith("pyarrow."):
|
|
482
|
+
import pyarrow.parquet as pq
|
|
483
|
+
|
|
484
|
+
buff = io.BytesIO(blob)
|
|
485
|
+
|
|
486
|
+
return pq.read_table(buff)
|
|
487
|
+
elif func.startswith("pandas."):
|
|
488
|
+
import pandas
|
|
489
|
+
|
|
490
|
+
buff = io.BytesIO(blob)
|
|
491
|
+
|
|
492
|
+
if func == "pandas.read_parquet":
|
|
493
|
+
return pandas.read_parquet(buff)
|
|
494
|
+
elif func == "pandas.read_pickle":
|
|
495
|
+
return pandas.read_pickle(buff, compression=compression)
|
|
496
|
+
else:
|
|
497
|
+
raise NotImplementedError
|
|
498
|
+
elif func == "generator":
|
|
499
|
+
items = payload.get("items")
|
|
500
|
+
|
|
501
|
+
def gen(it: Iterator = items):
|
|
502
|
+
if it:
|
|
503
|
+
for item in it:
|
|
504
|
+
yield self.decode_payload(item)
|
|
505
|
+
|
|
506
|
+
return gen()
|
|
507
|
+
elif func.startswith("polars."):
|
|
508
|
+
import polars
|
|
509
|
+
|
|
510
|
+
buff = io.BytesIO(blob)
|
|
511
|
+
return polars.read_parquet(buff)
|
|
512
|
+
else:
|
|
513
|
+
raise NotImplementedError
|
|
514
|
+
|
|
515
|
+
return payload
|
|
516
|
+
|
|
517
|
+
def make_python_function_command(
|
|
518
|
+
self,
|
|
519
|
+
func: Callable,
|
|
520
|
+
tag: str = "__CALL_RESULT__",
|
|
521
|
+
byref: Any = None,
|
|
522
|
+
recurse: Any = None,
|
|
523
|
+
):
|
|
524
|
+
# Serialize the command object (self) as ASCII-safe base64
|
|
525
|
+
command_bytes = dill.dumps(self)
|
|
526
|
+
command_b64 = base64.b64encode(command_bytes).decode("ascii")
|
|
527
|
+
|
|
528
|
+
# Func serialized by strict encoder: DILL:<compression>:b64:<...> or DATABRICKS_PATH:<compression>:path:<...>
|
|
529
|
+
serialized_func = self.encode_object(func, byref=byref, recurse=recurse)
|
|
530
|
+
|
|
531
|
+
cmd = f"""
|
|
532
|
+
import base64, dill, os
|
|
533
|
+
args_b64 = __ARGS__
|
|
534
|
+
kwargs_b64 = __KWARGS__
|
|
535
|
+
environ = __ENVIRON__
|
|
536
|
+
|
|
537
|
+
if environ:
|
|
538
|
+
for k, v in environ.items():
|
|
539
|
+
if k and v:
|
|
540
|
+
os.environ[k] = v
|
|
541
|
+
|
|
542
|
+
func_payload = {serialized_func!r}
|
|
543
|
+
tag = {tag!r}
|
|
544
|
+
command_b64 = {command_b64!r}
|
|
545
|
+
|
|
546
|
+
command = dill.loads(base64.b64decode(command_b64.encode("ascii")))
|
|
547
|
+
args = dill.loads(base64.b64decode(args_b64.encode("ascii")))
|
|
548
|
+
kwargs = dill.loads(base64.b64decode(kwargs_b64.encode("ascii")))
|
|
549
|
+
|
|
550
|
+
print(tag + command.encode_object(command.decode_payload(func_payload)(
|
|
551
|
+
*[command.decode_payload(x) for x in args],
|
|
552
|
+
**{{k: command.decode_payload(v) for k, v in kwargs.items()}}
|
|
553
|
+
)))"""
|
|
554
|
+
|
|
555
|
+
return cmd
|
|
556
|
+
|
|
557
|
+
def decode_response(
|
|
558
|
+
self,
|
|
559
|
+
response: CommandStatusResponse,
|
|
560
|
+
language: Language,
|
|
561
|
+
raise_error: bool = True,
|
|
562
|
+
tag: str = "__CALL_RESULT__",
|
|
563
|
+
logger: bool = True,
|
|
564
|
+
unpickle: bool = True
|
|
565
|
+
) -> Any:
|
|
566
|
+
"""Mirror the old Cluster.execute_command result handling.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
response: Raw command execution response.
|
|
570
|
+
language: Language executed
|
|
571
|
+
raise_error: Raise error if response is failed
|
|
572
|
+
tag: Result tag
|
|
573
|
+
logger: Print logs
|
|
574
|
+
unpickle: Unpickle
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
The decoded output string.
|
|
578
|
+
"""
|
|
579
|
+
raise_error_from_response(
|
|
580
|
+
response=response,
|
|
581
|
+
language=language,
|
|
582
|
+
raise_error=raise_error
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
results = response.results
|
|
586
|
+
|
|
587
|
+
# normal output
|
|
588
|
+
if results.result_type == ResultType.TEXT:
|
|
589
|
+
data = results.data or ""
|
|
590
|
+
else:
|
|
591
|
+
raise NotImplementedError(
|
|
592
|
+
"Cannot decode result form %s" % response
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
raw_result = data
|
|
596
|
+
|
|
597
|
+
if tag in raw_result:
|
|
598
|
+
logs_text, raw_result = raw_result.split(tag, 1)
|
|
599
|
+
|
|
600
|
+
try:
|
|
601
|
+
if logger:
|
|
602
|
+
for line in logs_text.splitlines():
|
|
603
|
+
stripped_log = line.strip()
|
|
604
|
+
|
|
605
|
+
if stripped_log:
|
|
606
|
+
print(stripped_log)
|
|
607
|
+
except Exception as e:
|
|
608
|
+
LOGGER.warning(
|
|
609
|
+
"Cannot print logs from %s: %s",
|
|
610
|
+
logs_text,
|
|
611
|
+
e
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
if unpickle:
|
|
615
|
+
return self.decode_payload(payload=raw_result)
|
|
616
|
+
return raw_result
|
|
617
|
+
|
|
618
|
+
def result(
|
|
619
|
+
self,
|
|
620
|
+
raise_error: bool = True,
|
|
621
|
+
unpickle: bool = True
|
|
622
|
+
) -> Any:
|
|
623
|
+
try:
|
|
624
|
+
self.wait(raise_error=raise_error)
|
|
625
|
+
|
|
626
|
+
obj = self.decode_response(
|
|
627
|
+
response=self.details,
|
|
628
|
+
language=self.language,
|
|
629
|
+
raise_error=raise_error,
|
|
630
|
+
unpickle=unpickle
|
|
631
|
+
)
|
|
632
|
+
except (InternalError, ClientTerminatedSession):
|
|
633
|
+
self.context = self.context.connect(reset=True)
|
|
634
|
+
|
|
635
|
+
return (
|
|
636
|
+
self
|
|
637
|
+
.start(reset=True)
|
|
638
|
+
.result(raise_error=raise_error, unpickle=unpickle)
|
|
639
|
+
)
|
|
640
|
+
except ModuleNotFoundError as e:
|
|
641
|
+
module_name = e.name
|
|
642
|
+
|
|
643
|
+
if module_name and not module_name.startswith("ygg"):
|
|
644
|
+
self.context.cluster.install_temporary_libraries(libraries=[module_name])
|
|
645
|
+
|
|
646
|
+
return (
|
|
647
|
+
self
|
|
648
|
+
.start(reset=True)
|
|
649
|
+
.result(raise_error=raise_error, unpickle=unpickle)
|
|
650
|
+
)
|
|
651
|
+
else:
|
|
652
|
+
raise e
|
|
653
|
+
|
|
654
|
+
return obj
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def raise_error_from_response(
|
|
658
|
+
response: CommandStatusResponse,
|
|
659
|
+
language: Language,
|
|
660
|
+
raise_error: bool = True
|
|
661
|
+
):
|
|
662
|
+
if raise_error:
|
|
663
|
+
results = response.results
|
|
664
|
+
|
|
665
|
+
if results.result_type == ResultType.ERROR:
|
|
666
|
+
message = results.cause or "Command execution failed"
|
|
667
|
+
|
|
668
|
+
if "client terminated the session" in message:
|
|
669
|
+
raise ClientTerminatedSession(message)
|
|
670
|
+
|
|
671
|
+
if language == Language.PYTHON:
|
|
672
|
+
raise_parsed_traceback(message)
|
|
673
|
+
|
|
674
|
+
raise RuntimeError(str(response))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from ...exceptions import YGGException
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"ComputeException",
|
|
5
|
+
"ClientTerminatedSession",
|
|
6
|
+
"CommandExecutionException"
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ComputeException(YGGException):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CommandExecutionException(ComputeException):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ClientTerminatedSession(CommandExecutionException):
|
|
19
|
+
pass
|