ygg 0.1.47__tar.gz → 0.1.49__tar.gz

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 (74) hide show
  1. {ygg-0.1.47 → ygg-0.1.49}/PKG-INFO +3 -29
  2. {ygg-0.1.47 → ygg-0.1.49}/README.md +0 -28
  3. {ygg-0.1.47 → ygg-0.1.49}/pyproject.toml +15 -8
  4. {ygg-0.1.47 → ygg-0.1.49}/src/ygg.egg-info/PKG-INFO +3 -29
  5. {ygg-0.1.47 → ygg-0.1.49}/src/ygg.egg-info/SOURCES.txt +0 -1
  6. {ygg-0.1.47 → ygg-0.1.49}/src/ygg.egg-info/requires.txt +2 -0
  7. ygg-0.1.49/src/yggdrasil/__init__.py +1 -0
  8. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/compute/cluster.py +99 -20
  9. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/compute/execution_context.py +19 -11
  10. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/compute/remote.py +4 -1
  11. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/sql/__init__.py +1 -2
  12. ygg-0.1.49/src/yggdrasil/databricks/sql/exceptions.py +45 -0
  13. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/sql/statement_result.py +17 -40
  14. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/workspaces/__init__.py +0 -1
  15. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/workspaces/io.py +21 -9
  16. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/workspaces/path.py +9 -5
  17. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/workspaces/workspace.py +45 -27
  18. ygg-0.1.49/src/yggdrasil/dataclasses/__init__.py +3 -0
  19. ygg-0.1.49/src/yggdrasil/dataclasses/dataclass.py +40 -0
  20. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/libs/__init__.py +0 -3
  21. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/pyutils/python_env.py +7 -4
  22. ygg-0.1.49/src/yggdrasil/requests/__init__.py +4 -0
  23. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/types/cast/cast_options.py +3 -4
  24. ygg-0.1.49/src/yggdrasil/version.py +1 -0
  25. ygg-0.1.47/src/yggdrasil/__init__.py +0 -5
  26. ygg-0.1.47/src/yggdrasil/databricks/sql/exceptions.py +0 -1
  27. ygg-0.1.47/src/yggdrasil/dataclasses/__init__.py +0 -5
  28. ygg-0.1.47/src/yggdrasil/dataclasses/dataclass.py +0 -206
  29. ygg-0.1.47/src/yggdrasil/requests/__init__.py +0 -5
  30. ygg-0.1.47/src/yggdrasil/types/libs.py +0 -12
  31. ygg-0.1.47/src/yggdrasil/version.py +0 -1
  32. {ygg-0.1.47 → ygg-0.1.49}/LICENSE +0 -0
  33. {ygg-0.1.47 → ygg-0.1.49}/setup.cfg +0 -0
  34. {ygg-0.1.47 → ygg-0.1.49}/src/ygg.egg-info/dependency_links.txt +0 -0
  35. {ygg-0.1.47 → ygg-0.1.49}/src/ygg.egg-info/entry_points.txt +0 -0
  36. {ygg-0.1.47 → ygg-0.1.49}/src/ygg.egg-info/top_level.txt +0 -0
  37. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/__init__.py +0 -0
  38. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/compute/__init__.py +0 -0
  39. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/jobs/__init__.py +0 -0
  40. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/jobs/config.py +0 -0
  41. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/sql/engine.py +0 -0
  42. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/sql/types.py +0 -0
  43. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/sql/warehouse.py +0 -0
  44. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/workspaces/filesytem.py +0 -0
  45. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/databricks/workspaces/path_kind.py +0 -0
  46. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/libs/databrickslib.py +0 -0
  47. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/libs/extensions/__init__.py +0 -0
  48. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/libs/extensions/polars_extensions.py +0 -0
  49. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/libs/extensions/spark_extensions.py +0 -0
  50. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/libs/pandaslib.py +0 -0
  51. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/libs/polarslib.py +0 -0
  52. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/libs/sparklib.py +0 -0
  53. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/pyutils/__init__.py +0 -0
  54. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/pyutils/callable_serde.py +0 -0
  55. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/pyutils/equality.py +0 -0
  56. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/pyutils/exceptions.py +0 -0
  57. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/pyutils/expiring_dict.py +0 -0
  58. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/pyutils/modules.py +0 -0
  59. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/pyutils/parallel.py +0 -0
  60. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/pyutils/retry.py +0 -0
  61. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/requests/msal.py +0 -0
  62. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/requests/session.py +0 -0
  63. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/types/__init__.py +0 -0
  64. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/types/cast/__init__.py +0 -0
  65. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/types/cast/arrow_cast.py +0 -0
  66. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/types/cast/pandas_cast.py +0 -0
  67. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/types/cast/polars_cast.py +0 -0
  68. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/types/cast/polars_pandas_cast.py +0 -0
  69. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/types/cast/registry.py +0 -0
  70. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/types/cast/spark_cast.py +0 -0
  71. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/types/cast/spark_pandas_cast.py +0 -0
  72. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/types/cast/spark_polars_cast.py +0 -0
  73. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/types/python_arrow.py +0 -0
  74. {ygg-0.1.47 → ygg-0.1.49}/src/yggdrasil/types/python_defaults.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ygg
3
- Version: 0.1.47
3
+ Version: 0.1.49
4
4
  Summary: Type-friendly utilities for moving data between Python objects, Arrow, Polars, Pandas, Spark, and Databricks
5
5
  Author: Yggdrasil contributors
6
6
  License: Apache License
@@ -235,6 +235,8 @@ Requires-Dist: pytest-asyncio; extra == "dev"
235
235
  Requires-Dist: black; extra == "dev"
236
236
  Requires-Dist: ruff; extra == "dev"
237
237
  Requires-Dist: mypy; extra == "dev"
238
+ Requires-Dist: build; extra == "dev"
239
+ Requires-Dist: twine; extra == "dev"
238
240
  Dynamic: license-file
239
241
 
240
242
  # Yggdrasil (Python)
@@ -270,34 +272,6 @@ Extras are grouped by engine:
270
272
  - `.[polars]`, `.[pandas]`, `.[spark]`, `.[databricks]` – install only the integrations you need.
271
273
  - `.[dev]` – adds testing, linting, and typing tools (`pytest`, `ruff`, `black`, `mypy`).
272
274
 
273
- ## Quickstart
274
- Define an Arrow-aware dataclass, coerce inputs, and cast across containers:
275
-
276
- ```python
277
- from yggdrasil import yggdataclass
278
- from yggdrasil.types.cast import convert
279
- from yggdrasil.types import arrow_field_from_hint
280
-
281
- @yggdataclass
282
- class User:
283
- id: int
284
- email: str
285
- active: bool = True
286
-
287
- user = User.__safe_init__("123", email="alice@example.com")
288
- assert user.id == 123 and user.active is True
289
-
290
- payload = {"id": "45", "email": "bob@example.com", "active": "false"}
291
- clean = User.from_dict(payload)
292
- print(clean.to_dict())
293
-
294
- field = arrow_field_from_hint(User, name="user")
295
- print(field) # user: struct<id: int64, email: string, active: bool>
296
-
297
- numbers = convert(["1", "2", "3"], list[int])
298
- print(numbers)
299
- ```
300
-
301
275
  ### Databricks example
302
276
  Install the `databricks` extra and run SQL with typed results:
303
277
 
@@ -31,34 +31,6 @@ Extras are grouped by engine:
31
31
  - `.[polars]`, `.[pandas]`, `.[spark]`, `.[databricks]` – install only the integrations you need.
32
32
  - `.[dev]` – adds testing, linting, and typing tools (`pytest`, `ruff`, `black`, `mypy`).
33
33
 
34
- ## Quickstart
35
- Define an Arrow-aware dataclass, coerce inputs, and cast across containers:
36
-
37
- ```python
38
- from yggdrasil import yggdataclass
39
- from yggdrasil.types.cast import convert
40
- from yggdrasil.types import arrow_field_from_hint
41
-
42
- @yggdataclass
43
- class User:
44
- id: int
45
- email: str
46
- active: bool = True
47
-
48
- user = User.__safe_init__("123", email="alice@example.com")
49
- assert user.id == 123 and user.active is True
50
-
51
- payload = {"id": "45", "email": "bob@example.com", "active": "false"}
52
- clean = User.from_dict(payload)
53
- print(clean.to_dict())
54
-
55
- field = arrow_field_from_hint(User, name="user")
56
- print(field) # user: struct<id: int64, email: string, active: bool>
57
-
58
- numbers = convert(["1", "2", "3"], list[int])
59
- print(numbers)
60
- ```
61
-
62
34
  ### Databricks example
63
35
  Install the `databricks` extra and run SQL with typed results:
64
36
 
@@ -1,17 +1,16 @@
1
1
  [build-system]
2
- requires = ["setuptools>=61", "wheel"]
2
+ # bump setuptools so type-info files are handled sanely
3
+ requires = ["setuptools>=69", "wheel"]
3
4
  build-backend = "setuptools.build_meta"
4
5
 
5
6
  [project]
6
7
  name = "ygg"
7
- version = "0.1.47"
8
+ version = "0.1.49"
8
9
  description = "Type-friendly utilities for moving data between Python objects, Arrow, Polars, Pandas, Spark, and Databricks"
9
10
  readme = { file = "README.md", content-type = "text/markdown" }
10
11
  license = { file = "LICENSE" }
11
12
  requires-python = ">=3.10"
12
- authors = [
13
- { name = "Yggdrasil contributors" },
14
- ]
13
+ authors = [{ name = "Yggdrasil contributors" }]
15
14
  keywords = ["arrow", "polars", "pandas", "spark", "databricks", "typing", "dataclass", "serialization"]
16
15
  classifiers = [
17
16
  "Development Status :: 3 - Alpha",
@@ -42,6 +41,8 @@ dev = [
42
41
  "black",
43
42
  "ruff",
44
43
  "mypy",
44
+ "build",
45
+ "twine",
45
46
  ]
46
47
 
47
48
  [project.scripts]
@@ -55,9 +56,15 @@ Documentation = "https://github.com/Platob/Yggdrasil/tree/main/python/docs"
55
56
  [tool.setuptools]
56
57
  package-dir = { "" = "src" }
57
58
  license-files = ["LICENSE"]
58
-
59
- [tool.uv]
60
- native-tls = true
59
+ include-package-data = true
61
60
 
62
61
  [tool.setuptools.packages.find]
63
62
  where = ["src"]
63
+
64
+ # If your import package is yggdrasil (seems likely from yggenv entrypoint),
65
+ # ship the PEP 561 marker file. Put `py.typed` inside src/yggdrasil/
66
+ [tool.setuptools.package-data]
67
+ yggdrasil = ["py.typed"]
68
+
69
+ [tool.uv]
70
+ native-tls = true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ygg
3
- Version: 0.1.47
3
+ Version: 0.1.49
4
4
  Summary: Type-friendly utilities for moving data between Python objects, Arrow, Polars, Pandas, Spark, and Databricks
5
5
  Author: Yggdrasil contributors
6
6
  License: Apache License
@@ -235,6 +235,8 @@ Requires-Dist: pytest-asyncio; extra == "dev"
235
235
  Requires-Dist: black; extra == "dev"
236
236
  Requires-Dist: ruff; extra == "dev"
237
237
  Requires-Dist: mypy; extra == "dev"
238
+ Requires-Dist: build; extra == "dev"
239
+ Requires-Dist: twine; extra == "dev"
238
240
  Dynamic: license-file
239
241
 
240
242
  # Yggdrasil (Python)
@@ -270,34 +272,6 @@ Extras are grouped by engine:
270
272
  - `.[polars]`, `.[pandas]`, `.[spark]`, `.[databricks]` – install only the integrations you need.
271
273
  - `.[dev]` – adds testing, linting, and typing tools (`pytest`, `ruff`, `black`, `mypy`).
272
274
 
273
- ## Quickstart
274
- Define an Arrow-aware dataclass, coerce inputs, and cast across containers:
275
-
276
- ```python
277
- from yggdrasil import yggdataclass
278
- from yggdrasil.types.cast import convert
279
- from yggdrasil.types import arrow_field_from_hint
280
-
281
- @yggdataclass
282
- class User:
283
- id: int
284
- email: str
285
- active: bool = True
286
-
287
- user = User.__safe_init__("123", email="alice@example.com")
288
- assert user.id == 123 and user.active is True
289
-
290
- payload = {"id": "45", "email": "bob@example.com", "active": "false"}
291
- clean = User.from_dict(payload)
292
- print(clean.to_dict())
293
-
294
- field = arrow_field_from_hint(User, name="user")
295
- print(field) # user: struct<id: int64, email: string, active: bool>
296
-
297
- numbers = convert(["1", "2", "3"], list[int])
298
- print(numbers)
299
- ```
300
-
301
275
  ### Databricks example
302
276
  Install the `databricks` extra and run SQL with typed results:
303
277
 
@@ -51,7 +51,6 @@ src/yggdrasil/requests/__init__.py
51
51
  src/yggdrasil/requests/msal.py
52
52
  src/yggdrasil/requests/session.py
53
53
  src/yggdrasil/types/__init__.py
54
- src/yggdrasil/types/libs.py
55
54
  src/yggdrasil/types/python_arrow.py
56
55
  src/yggdrasil/types/python_defaults.py
57
56
  src/yggdrasil/types/cast/__init__.py
@@ -11,3 +11,5 @@ pytest-asyncio
11
11
  black
12
12
  ruff
13
13
  mypy
14
+ build
15
+ twine
@@ -0,0 +1 @@
1
+ from .version import *
@@ -22,8 +22,8 @@ from typing import Any, Iterator, Optional, Union, List, Callable, Dict, ClassVa
22
22
 
23
23
  from .execution_context import ExecutionContext
24
24
  from ..workspaces.workspace import WorkspaceService, Workspace
25
- from ... import CallableSerde
26
25
  from ...libs.databrickslib import databricks_sdk
26
+ from ...pyutils.callable_serde import CallableSerde
27
27
  from ...pyutils.equality import dicts_equal, dict_diff
28
28
  from ...pyutils.expiring_dict import ExpiringDict
29
29
  from ...pyutils.modules import PipIndexSettings
@@ -36,7 +36,8 @@ else: # pragma: no cover - runtime fallback when SDK is missing
36
36
  from databricks.sdk.errors import DatabricksError
37
37
  from databricks.sdk.errors.platform import ResourceDoesNotExist
38
38
  from databricks.sdk.service.compute import (
39
- ClusterDetails, Language, Kind, State, DataSecurityMode, Library, PythonPyPiLibrary, LibraryInstallStatus
39
+ ClusterDetails, Language, Kind, State, DataSecurityMode, Library, PythonPyPiLibrary, LibraryInstallStatus,
40
+ ClusterAccessControlRequest, ClusterPermissionLevel
40
41
  )
41
42
  from databricks.sdk.service.compute import SparkVersion, RuntimeEngine
42
43
 
@@ -143,6 +144,7 @@ class Cluster(WorkspaceService):
143
144
  single_user_name: Optional[str] = None,
144
145
  runtime_engine: Optional["RuntimeEngine"] = None,
145
146
  libraries: Optional[list[str]] = None,
147
+ update_timeout: Optional[Union[float, dt.timedelta]] = dt.timedelta(minutes=20),
146
148
  **kwargs
147
149
  ) -> "Cluster":
148
150
  """Create or reuse a cluster that mirrors the current Python environment.
@@ -151,9 +153,10 @@ class Cluster(WorkspaceService):
151
153
  workspace: Workspace to use for the cluster.
152
154
  cluster_id: Optional cluster id to reuse.
153
155
  cluster_name: Optional cluster name to reuse.
154
- single_user_name: Optional user name for single-user clusters.
156
+ single_user_name: Optional username for single-user clusters.
155
157
  runtime_engine: Optional Databricks runtime engine.
156
158
  libraries: Optional list of libraries to install.
159
+ update_timeout: wait timeout, if None it will not wait completion
157
160
  **kwargs: Additional cluster specification overrides.
158
161
 
159
162
  Returns:
@@ -175,6 +178,7 @@ class Cluster(WorkspaceService):
175
178
  single_user_name=single_user_name,
176
179
  runtime_engine=runtime_engine,
177
180
  libraries=libraries,
181
+ update_timeout=update_timeout,
178
182
  **kwargs
179
183
  )
180
184
  )
@@ -189,6 +193,7 @@ class Cluster(WorkspaceService):
189
193
  single_user_name: Optional[str] = "current",
190
194
  runtime_engine: Optional["RuntimeEngine"] = None,
191
195
  libraries: Optional[list[str]] = None,
196
+ update_timeout: Optional[Union[float, dt.timedelta]] = dt.timedelta(minutes=20),
192
197
  **kwargs
193
198
  ) -> "Cluster":
194
199
  """Create/update a cluster to match the local Python environment.
@@ -197,9 +202,10 @@ class Cluster(WorkspaceService):
197
202
  source: Optional PythonEnv to mirror (defaults to current).
198
203
  cluster_id: Optional cluster id to update.
199
204
  cluster_name: Optional cluster name to update.
200
- single_user_name: Optional single user name for the cluster.
205
+ single_user_name: Optional single username for the cluster.
201
206
  runtime_engine: Optional runtime engine selection.
202
207
  libraries: Optional list of libraries to install.
208
+ update_timeout: wait timeout, if None it will not wait completion
203
209
  **kwargs: Additional cluster specification overrides.
204
210
 
205
211
  Returns:
@@ -241,6 +247,7 @@ class Cluster(WorkspaceService):
241
247
  single_user_name=single_user_name,
242
248
  runtime_engine=runtime_engine or RuntimeEngine.PHOTON,
243
249
  libraries=libraries,
250
+ update_timeout=update_timeout,
244
251
  **kwargs
245
252
  )
246
253
 
@@ -379,7 +386,9 @@ class Cluster(WorkspaceService):
379
386
  start = time.time()
380
387
  sleep_time = tick
381
388
 
382
- if isinstance(timeout, dt.timedelta):
389
+ if not timeout:
390
+ timeout = 20 * 60.0
391
+ elif isinstance(timeout, dt.timedelta):
383
392
  timeout = timeout.total_seconds()
384
393
 
385
394
  while self.is_pending:
@@ -411,12 +420,14 @@ class Cluster(WorkspaceService):
411
420
  # Extract "major.minor" from strings like "17.3.x-scala2.13-ml-gpu"
412
421
  v = self.spark_version
413
422
 
414
- if v is None:
423
+ if not v:
415
424
  return None
416
425
 
417
426
  parts = v.split(".")
427
+
418
428
  if len(parts) < 2:
419
429
  return None
430
+
420
431
  return ".".join(parts[:2]) # e.g. "17.3"
421
432
 
422
433
  @property
@@ -427,8 +438,10 @@ class Cluster(WorkspaceService):
427
438
  When the runtime can't be mapped, returns ``None``.
428
439
  """
429
440
  v = self.runtime_version
430
- if v is None:
441
+
442
+ if not v:
431
443
  return None
444
+
432
445
  return _PYTHON_BY_DBR.get(v)
433
446
 
434
447
  # ------------------------------------------------------------------ #
@@ -585,6 +598,7 @@ class Cluster(WorkspaceService):
585
598
  cluster_id: Optional[str] = None,
586
599
  cluster_name: Optional[str] = None,
587
600
  libraries: Optional[List[Union[str, "Library"]]] = None,
601
+ update_timeout: Optional[Union[float, dt.timedelta]] = dt.timedelta(minutes=20),
588
602
  **cluster_spec: Any
589
603
  ):
590
604
  """Create a new cluster or update an existing one.
@@ -593,6 +607,7 @@ class Cluster(WorkspaceService):
593
607
  cluster_id: Optional cluster id to update.
594
608
  cluster_name: Optional cluster name to update or create.
595
609
  libraries: Optional libraries to install.
610
+ update_timeout: wait timeout, if None it will not wait completion
596
611
  **cluster_spec: Cluster specification overrides.
597
612
 
598
613
  Returns:
@@ -608,24 +623,28 @@ class Cluster(WorkspaceService):
608
623
  return found.update(
609
624
  cluster_name=cluster_name,
610
625
  libraries=libraries,
626
+ wait_timeout=update_timeout,
611
627
  **cluster_spec
612
628
  )
613
629
 
614
630
  return self.create(
615
631
  cluster_name=cluster_name,
616
632
  libraries=libraries,
633
+ wait_timeout=update_timeout,
617
634
  **cluster_spec
618
635
  )
619
636
 
620
637
  def create(
621
638
  self,
622
639
  libraries: Optional[List[Union[str, "Library"]]] = None,
640
+ wait_timeout: Union[float, dt.timedelta] = dt.timedelta(minutes=20),
623
641
  **cluster_spec: Any
624
642
  ) -> str:
625
643
  """Create a new cluster and optionally install libraries.
626
644
 
627
645
  Args:
628
646
  libraries: Optional list of libraries to install after creation.
647
+ wait_timeout: wait timeout, if None it will not wait completion
629
648
  **cluster_spec: Cluster specification overrides.
630
649
 
631
650
  Returns:
@@ -645,27 +664,32 @@ class Cluster(WorkspaceService):
645
664
  update_details,
646
665
  )
647
666
 
648
- self.details = self.clusters_client().create_and_wait(**update_details)
667
+ self.details = self.clusters_client().create(**update_details)
649
668
 
650
669
  LOGGER.info(
651
670
  "Created %s",
652
671
  self
653
672
  )
654
673
 
655
- self.install_libraries(libraries=libraries, raise_error=False)
674
+ self.install_libraries(libraries=libraries, raise_error=False, wait_timeout=None)
675
+
676
+ if wait_timeout:
677
+ self.wait_for_status(timeout=wait_timeout)
656
678
 
657
679
  return self
658
680
 
659
681
  def update(
660
682
  self,
661
683
  libraries: Optional[List[Union[str, "Library"]]] = None,
662
- wait_timeout: Union[float, dt.timedelta] = dt.timedelta(minutes=20),
684
+ access_control_list: Optional[List["ClusterAccessControlRequest"]] = None,
685
+ wait_timeout: Optional[Union[float, dt.timedelta]] = dt.timedelta(minutes=20),
663
686
  **cluster_spec: Any
664
687
  ) -> "Cluster":
665
688
  """Update cluster configuration and optionally install libraries.
666
689
 
667
690
  Args:
668
691
  libraries: Optional libraries to install.
692
+ access_control_list: List of permissions
669
693
  wait_timeout: waiting timeout until done, if None it does not wait
670
694
  **cluster_spec: Cluster specification overrides.
671
695
 
@@ -705,8 +729,9 @@ class Cluster(WorkspaceService):
705
729
  self, diff
706
730
  )
707
731
 
708
- self.wait_for_status()
732
+ self.wait_for_status(timeout=wait_timeout)
709
733
  self.clusters_client().edit(**update_details)
734
+ self.update_permissions(access_control_list=access_control_list)
710
735
 
711
736
  LOGGER.info(
712
737
  "Updated %s",
@@ -718,6 +743,56 @@ class Cluster(WorkspaceService):
718
743
 
719
744
  return self
720
745
 
746
+ def update_permissions(
747
+ self,
748
+ access_control_list: Optional[List["ClusterAccessControlRequest"]] = None,
749
+ ):
750
+ if not access_control_list:
751
+ return self
752
+
753
+ access_control_list = self._check_permission(access_control_list)
754
+
755
+ self.clusters_client().update_permissions(
756
+ cluster_id=self.cluster_id,
757
+ access_control_list=access_control_list
758
+ )
759
+
760
+ def default_permissions(self):
761
+ current_groups = self.current_user.groups or []
762
+
763
+ return [
764
+ ClusterAccessControlRequest(
765
+ group_name=name,
766
+ permission_level=ClusterPermissionLevel.CAN_MANAGE
767
+ )
768
+ for name in current_groups
769
+ if name not in {"users"}
770
+ ]
771
+
772
+ def _check_permission(
773
+ self,
774
+ permission: Union[str, "ClusterAccessControlRequest", List[Union[str, "ClusterAccessControlRequest"]]],
775
+ ):
776
+ if isinstance(permission, ClusterAccessControlRequest):
777
+ return permission
778
+
779
+ if isinstance(permission, str):
780
+ if "@" in permission:
781
+ group_name, user_name = None, permission
782
+ else:
783
+ group_name, user_name = permission, None
784
+
785
+ return ClusterAccessControlRequest(
786
+ group_name=group_name,
787
+ user_name=user_name,
788
+ permission_level=ClusterPermissionLevel.CAN_MANAGE
789
+ )
790
+
791
+ return [
792
+ self._check_permission(_)
793
+ for _ in permission
794
+ ]
795
+
721
796
  def list_clusters(self) -> Iterator["Cluster"]:
722
797
  """Iterate clusters, yielding helpers annotated with metadata.
723
798
 
@@ -809,18 +884,22 @@ class Cluster(WorkspaceService):
809
884
  Returns:
810
885
  The current Cluster instance.
811
886
  """
887
+ if self.is_running:
888
+ return self
889
+
812
890
  self.wait_for_status()
813
891
 
814
- if not self.is_running:
815
- LOGGER.debug("Starting %s", self)
892
+ if self.is_running:
893
+ return self
816
894
 
817
- if wait_timeout:
818
- self.clusters_client().start(cluster_id=self.cluster_id)
819
- self.wait_for_status(timeout=wait_timeout.total_seconds())
820
- else:
821
- self.clusters_client().start(cluster_id=self.cluster_id)
895
+ LOGGER.debug("Starting %s", self)
896
+
897
+ self.clusters_client().start(cluster_id=self.cluster_id)
822
898
 
823
- LOGGER.info("Started %s", self)
899
+ LOGGER.info("Started %s", self)
900
+
901
+ if wait_timeout:
902
+ self.wait_for_status(timeout=wait_timeout.total_seconds())
824
903
 
825
904
  return self
826
905
 
@@ -836,7 +915,7 @@ class Cluster(WorkspaceService):
836
915
 
837
916
  if self.is_running:
838
917
  self.details = self.clusters_client().restart_and_wait(cluster_id=self.cluster_id)
839
- return self.wait_for_status()
918
+ return self
840
919
 
841
920
  return self.start()
842
921
 
@@ -180,7 +180,7 @@ print(json.dumps(meta))"""
180
180
  """
181
181
  return self.cluster.workspace.sdk()
182
182
 
183
- def _create_command(
183
+ def create_command(
184
184
  self,
185
185
  language: "Language",
186
186
  ) -> any:
@@ -192,17 +192,29 @@ print(json.dumps(meta))"""
192
192
  Returns:
193
193
  The created command execution context response.
194
194
  """
195
- self.cluster.ensure_running()
196
-
197
195
  LOGGER.debug(
198
196
  "Creating Databricks command execution context for %s",
199
197
  self.cluster
200
198
  )
201
199
 
202
- created = self._workspace_client().command_execution.create_and_wait(
203
- cluster_id=self.cluster.cluster_id,
204
- language=language,
200
+ try:
201
+ created = self._workspace_client().command_execution.create_and_wait(
202
+ cluster_id=self.cluster.cluster_id,
203
+ language=language,
204
+ )
205
+ except:
206
+ self.cluster.ensure_running()
207
+
208
+ created = self._workspace_client().command_execution.create_and_wait(
209
+ cluster_id=self.cluster.cluster_id,
210
+ language=language,
211
+ )
212
+
213
+ LOGGER.info(
214
+ "Created Databricks command execution context %s",
215
+ self
205
216
  )
217
+
206
218
  created = getattr(created, "response", created)
207
219
 
208
220
  return created
@@ -220,10 +232,6 @@ print(json.dumps(meta))"""
220
232
  The connected ExecutionContext instance.
221
233
  """
222
234
  if self.context_id is not None:
223
- LOGGER.debug(
224
- "Execution context already open for %s",
225
- self
226
- )
227
235
  return self
228
236
 
229
237
  self.language = language or self.language
@@ -231,7 +239,7 @@ print(json.dumps(meta))"""
231
239
  if self.language is None:
232
240
  self.language = Language.PYTHON
233
241
 
234
- ctx = self._create_command(language=self.language)
242
+ ctx = self.create_command(language=self.language)
235
243
 
236
244
  context_id = ctx.id
237
245
  if not context_id:
@@ -39,6 +39,7 @@ def databricks_remote_compute(
39
39
  timeout: Optional[dt.timedelta] = None,
40
40
  env_keys: Optional[List[str]] = None,
41
41
  force_local: bool = False,
42
+ update_timeout: Optional[Union[float, dt.timedelta]] = None,
42
43
  **options
43
44
  ) -> Callable[[Callable[..., ReturnType]], Callable[..., ReturnType]]:
44
45
  """Return a decorator that executes functions on a remote cluster.
@@ -52,6 +53,7 @@ def databricks_remote_compute(
52
53
  timeout: Optional execution timeout for remote calls.
53
54
  env_keys: Optional environment variable names to forward.
54
55
  force_local: Force local execution
56
+ update_timeout: creation or update wait timeout
55
57
  **options: Extra options forwarded to the execution decorator.
56
58
 
57
59
  Returns:
@@ -82,7 +84,8 @@ def databricks_remote_compute(
82
84
  cluster = workspace.clusters().replicated_current_environment(
83
85
  workspace=workspace,
84
86
  cluster_name=cluster_name,
85
- single_user_name=workspace.current_user.user_name
87
+ single_user_name=workspace.current_user.user_name,
88
+ update_timeout=update_timeout
86
89
  )
87
90
 
88
91
  cluster.ensure_running(wait_timeout=None)
@@ -1,9 +1,8 @@
1
1
  """Databricks SQL helpers and engine wrappers."""
2
2
 
3
3
  from .engine import SQLEngine, StatementResult
4
+ from .exceptions import SqlStatementError
4
5
 
5
6
  # Backwards compatibility
6
7
  DBXSQL = SQLEngine
7
8
  DBXStatementResult = StatementResult
8
-
9
- __all__ = ["SQLEngine", "StatementResult"]
@@ -0,0 +1,45 @@
1
+ """Custom exceptions for Databricks SQL helpers."""
2
+ from dataclasses import dataclass
3
+ from typing import Optional, Any
4
+
5
+ __all__ = [
6
+ "SqlStatementError"
7
+ ]
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class SqlStatementError(RuntimeError):
12
+ statement_id: str
13
+ state: str
14
+ message: str
15
+ error_code: Optional[str] = None
16
+ sql_state: Optional[str] = None
17
+
18
+ def __str__(self) -> str:
19
+ meta = []
20
+ if self.error_code:
21
+ meta.append(f"code={self.error_code}")
22
+ if self.sql_state:
23
+ meta.append(f"state={self.sql_state}")
24
+
25
+ meta_str = f" ({', '.join(meta)})" if meta else ""
26
+ return f"SQL statement {self.statement_id} failed [{self.state}]: {self.message}{meta_str}"
27
+
28
+ @classmethod
29
+ def from_statement(cls, stmt: Any) -> "SqlStatementError":
30
+ statement_id = getattr(stmt, "statement_id", "<unknown>")
31
+ state = getattr(stmt, "state", "<unknown>")
32
+
33
+ err = getattr(getattr(stmt, "status", None), "error", None)
34
+
35
+ message = getattr(err, "message", None) or "Unknown SQL error"
36
+ error_code = getattr(err, "error_code", None)
37
+ sql_state = getattr(err, "sql_state", None)
38
+
39
+ return cls(
40
+ statement_id=str(statement_id),
41
+ state=str(state),
42
+ message=str(message),
43
+ error_code=str(error_code) if error_code is not None else None,
44
+ sql_state=str(sql_state) if sql_state is not None else None,
45
+ )