ygg 0.1.56__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.
Files changed (42) hide show
  1. {ygg-0.1.56.dist-info → ygg-0.1.60.dist-info}/METADATA +1 -1
  2. ygg-0.1.60.dist-info/RECORD +74 -0
  3. {ygg-0.1.56.dist-info → ygg-0.1.60.dist-info}/WHEEL +1 -1
  4. yggdrasil/ai/__init__.py +2 -0
  5. yggdrasil/ai/session.py +89 -0
  6. yggdrasil/ai/sql_session.py +310 -0
  7. yggdrasil/databricks/__init__.py +0 -3
  8. yggdrasil/databricks/compute/cluster.py +68 -113
  9. yggdrasil/databricks/compute/command_execution.py +674 -0
  10. yggdrasil/databricks/compute/exceptions.py +7 -2
  11. yggdrasil/databricks/compute/execution_context.py +465 -277
  12. yggdrasil/databricks/compute/remote.py +4 -14
  13. yggdrasil/databricks/exceptions.py +10 -0
  14. yggdrasil/databricks/sql/__init__.py +0 -4
  15. yggdrasil/databricks/sql/engine.py +161 -173
  16. yggdrasil/databricks/sql/exceptions.py +9 -1
  17. yggdrasil/databricks/sql/statement_result.py +108 -120
  18. yggdrasil/databricks/sql/warehouse.py +331 -92
  19. yggdrasil/databricks/workspaces/io.py +92 -9
  20. yggdrasil/databricks/workspaces/path.py +120 -74
  21. yggdrasil/databricks/workspaces/workspace.py +212 -68
  22. yggdrasil/libs/databrickslib.py +23 -18
  23. yggdrasil/libs/extensions/spark_extensions.py +1 -1
  24. yggdrasil/libs/pandaslib.py +15 -6
  25. yggdrasil/libs/polarslib.py +49 -13
  26. yggdrasil/pyutils/__init__.py +1 -0
  27. yggdrasil/pyutils/callable_serde.py +12 -19
  28. yggdrasil/pyutils/exceptions.py +16 -0
  29. yggdrasil/pyutils/mimetypes.py +0 -0
  30. yggdrasil/pyutils/python_env.py +13 -12
  31. yggdrasil/pyutils/waiting_config.py +171 -0
  32. yggdrasil/types/cast/arrow_cast.py +3 -0
  33. yggdrasil/types/cast/pandas_cast.py +157 -169
  34. yggdrasil/types/cast/polars_cast.py +11 -43
  35. yggdrasil/types/dummy_class.py +81 -0
  36. yggdrasil/version.py +1 -1
  37. ygg-0.1.56.dist-info/RECORD +0 -68
  38. yggdrasil/databricks/ai/__init__.py +0 -1
  39. yggdrasil/databricks/ai/loki.py +0 -374
  40. {ygg-0.1.56.dist-info → ygg-0.1.60.dist-info}/entry_points.txt +0 -0
  41. {ygg-0.1.56.dist-info → ygg-0.1.60.dist-info}/licenses/LICENSE +0 -0
  42. {ygg-0.1.56.dist-info → ygg-0.1.60.dist-info}/top_level.txt +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))
@@ -2,7 +2,8 @@ from ...exceptions import YGGException
2
2
 
3
3
  __all__ = [
4
4
  "ComputeException",
5
- "CommandAborted"
5
+ "ClientTerminatedSession",
6
+ "CommandExecutionException"
6
7
  ]
7
8
 
8
9
 
@@ -10,5 +11,9 @@ class ComputeException(YGGException):
10
11
  pass
11
12
 
12
13
 
13
- class CommandAborted(YGGException):
14
+ class CommandExecutionException(ComputeException):
15
+ pass
16
+
17
+
18
+ class ClientTerminatedSession(CommandExecutionException):
14
19
  pass