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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ygg
3
- Version: 0.1.32
3
+ Version: 0.1.34
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
@@ -1,15 +1,15 @@
1
- ygg-0.1.32.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
1
+ ygg-0.1.34.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
2
2
  yggdrasil/__init__.py,sha256=PfH7Xwt6uue6oqe6S5V8NhDJcVQClkKrBE1KXhdelZc,117
3
- yggdrasil/version.py,sha256=lFBqLR0HmGJTF2s-4OaO-swIz8GxG2PdFo_uigEXczE,22
3
+ yggdrasil/version.py,sha256=cIz48TZT2Xc-LLdWHdfAlxnIA0OSZqt42ZJcukkGo6s,22
4
4
  yggdrasil/databricks/__init__.py,sha256=skctY2c8W-hI81upx9F_PWRe5ishL3hrdiTuizgDjdw,152
5
5
  yggdrasil/databricks/compute/__init__.py,sha256=NvdzmaJSNYY1uJthv1hHdBuNu3bD_-Z65DWnaJt9yXg,289
6
- yggdrasil/databricks/compute/cluster.py,sha256=Z7rVqGcii0nIlGdkuDTPNCQ2StPJKTs96JN2Yj4fD8k,38005
7
- yggdrasil/databricks/compute/execution_context.py,sha256=_PWn-Jjb-qRtI7HGm7IOUAKoIpmivNiBZTddxpZNBaM,22142
8
- yggdrasil/databricks/compute/remote.py,sha256=bBQnHjsyfEjRu9jt7Wnr0aJZGJZRdNk9bmPc0rIQZJc,2047
6
+ yggdrasil/databricks/compute/cluster.py,sha256=KUyGcpEKiA5XgAbeX1iHzuhJ4pucFqch_galZwYJlnc,39599
7
+ yggdrasil/databricks/compute/execution_context.py,sha256=Z0EvkhdR803Kh1UOh4wR0oyyLXzAJo4Lj5CRNmxW4q4,22287
8
+ yggdrasil/databricks/compute/remote.py,sha256=rrqLMnzI0KvhXghtOrve3W-rudi-cTjS-8dJXKjHM3A,2266
9
9
  yggdrasil/databricks/jobs/__init__.py,sha256=snxGSJb0M5I39v0y3IR-uEeSlZR248cQ_4DJ1sYs-h8,154
10
10
  yggdrasil/databricks/jobs/config.py,sha256=9LGeHD04hbfy0xt8_6oobC4moKJh4_DTjZiK4Q2Tqjk,11557
11
11
  yggdrasil/databricks/sql/__init__.py,sha256=y1n5yg-drZ8QVZbEgznsRG24kdJSnFis9l2YfYCsaCM,234
12
- yggdrasil/databricks/sql/engine.py,sha256=BO2lweaL63etVJ9jia14JJvGVGZ6f0X6XsZ3QlA6_Po,38146
12
+ yggdrasil/databricks/sql/engine.py,sha256=weYHosCVc9CZYaVooexEphNw6W_Ex0dphuGbfA48mEI,41104
13
13
  yggdrasil/databricks/sql/exceptions.py,sha256=Jqd_gT_VyPL8klJEHYEzpv5eHtmdY43WiQ7HZBaEqSk,53
14
14
  yggdrasil/databricks/sql/statement_result.py,sha256=VlHXhTcvTVya_2aJ-uUfUooZF_MqQuOZ8k7g6PBDhOM,17227
15
15
  yggdrasil/databricks/sql/types.py,sha256=5G-BM9_eOsRKEMzeDTWUsWW5g4Idvs-czVCpOCrMhdA,6412
@@ -18,7 +18,7 @@ yggdrasil/databricks/workspaces/filesytem.py,sha256=Z8JXU7_XUEbw9fpTQT1avRQKi-IA
18
18
  yggdrasil/databricks/workspaces/io.py,sha256=Tdde4LaGNJNT50R11OkEYZyNacyIW9QrOXMAicAlIr4,32208
19
19
  yggdrasil/databricks/workspaces/path.py,sha256=-XnCD9p42who3DAwnITVE1KyrZUSoXDKHA8iZi-7wk4,47743
20
20
  yggdrasil/databricks/workspaces/path_kind.py,sha256=Xc319NysH8_6E9C0Q8nCxDHYG07_SnzyUVKHe0dNdDQ,305
21
- yggdrasil/databricks/workspaces/workspace.py,sha256=RixYZYhASeKPPPdP6JPN1XQC8M_rC0N-oBheasF--1k,23183
21
+ yggdrasil/databricks/workspaces/workspace.py,sha256=MW-BEyldROqbX9SBbDspvlys_zehJjK5YgM3sGLfW-g,23382
22
22
  yggdrasil/dataclasses/__init__.py,sha256=6SdfIyTsoM4AuVw5TW4Q-UWXz41EyfsMcpD30cmjbSM,125
23
23
  yggdrasil/dataclasses/dataclass.py,sha256=fKokFUnqe4CmXXGMTdF4XDWbCUl_c_-se-UD48L5s1E,6594
24
24
  yggdrasil/libs/__init__.py,sha256=ulzk-ZkFUI2Pfo93YKtO8MBsEWtRZzLos7HAxN74R0w,168
@@ -30,8 +30,9 @@ yggdrasil/libs/extensions/__init__.py,sha256=mcXW5Li3Cbprbs4Ci-b5A0Ju0wmLcfvEiFu
30
30
  yggdrasil/libs/extensions/polars_extensions.py,sha256=RTkGi8llhPJjX7x9egix7-yXWo2X24zIAPSKXV37SSA,12397
31
31
  yggdrasil/libs/extensions/spark_extensions.py,sha256=E64n-3SFTDgMuXwWitX6vOYP9ln2lpGKb0htoBLEZgc,16745
32
32
  yggdrasil/pyutils/__init__.py,sha256=tl-LapAc71TV7RMgf2ftKwrzr8iiLOGHeJgA3RvO93w,293
33
- yggdrasil/pyutils/callable_serde.py,sha256=prxzYRrjR6-mZ9i1rIWakWL0w1JHPnTKBO_RR1NMacg,20992
33
+ yggdrasil/pyutils/callable_serde.py,sha256=euY7Kiy04i1tpWKuB0b2qQ1FokLC3nq0cv7PObWYUBE,21809
34
34
  yggdrasil/pyutils/exceptions.py,sha256=ssKNm-rjhavHUOZmGA7_1Gq9tSHDrb2EFI-cnBuWgng,3388
35
+ yggdrasil/pyutils/expiring_dict.py,sha256=q9gb09-2EUN-jQZumUw5BXOQGYcj1wb85qKtQlciSxg,5825
35
36
  yggdrasil/pyutils/modules.py,sha256=B7IP99YqUMW6-DIESFzBx8-09V1d0a8qrIJUDFhhL2g,11424
36
37
  yggdrasil/pyutils/parallel.py,sha256=ubuq2m9dJzWYUyKCga4Y_9bpaeMYUrleYxdp49CHr44,6781
37
38
  yggdrasil/pyutils/python_env.py,sha256=tuglnjdqHQjNh18qDladVoSEOjCD0RcnMEPYJ0tArOs,50985
@@ -53,8 +54,8 @@ yggdrasil/types/cast/registry.py,sha256=_zdFGmUBB7P-e_LIcJlOxMcxAkXoA-UXB6HqLMgT
53
54
  yggdrasil/types/cast/spark_cast.py,sha256=_KAsl1DqmKMSfWxqhVE7gosjYdgiL1C5bDQv6eP3HtA,24926
54
55
  yggdrasil/types/cast/spark_pandas_cast.py,sha256=BuTiWrdCANZCdD_p2MAytqm74eq-rdRXd-LGojBRrfU,5023
55
56
  yggdrasil/types/cast/spark_polars_cast.py,sha256=btmZNHXn2NSt3fUuB4xg7coaE0RezIBdZD92H8NK0Jw,9073
56
- ygg-0.1.32.dist-info/METADATA,sha256=AV7sd29RvuL7SNA3Qixw_HUQK0gl5k9hGe0sym9i9OQ,19204
57
- ygg-0.1.32.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
- ygg-0.1.32.dist-info/entry_points.txt,sha256=6q-vpWG3kvw2dhctQ0LALdatoeefkN855Ev02I1dKGY,70
59
- ygg-0.1.32.dist-info/top_level.txt,sha256=iBe9Kk4VIVbLpgv_p8OZUIfxgj4dgJ5wBg6vO3rigso,10
60
- ygg-0.1.32.dist-info/RECORD,,
57
+ ygg-0.1.34.dist-info/METADATA,sha256=iGQcUq6tGnBBLiVo9jPak9PE-Ma8wWPxY2BsWKLGC2w,19204
58
+ ygg-0.1.34.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
59
+ ygg-0.1.34.dist-info/entry_points.txt,sha256=6q-vpWG3kvw2dhctQ0LALdatoeefkN855Ev02I1dKGY,70
60
+ ygg-0.1.34.dist-info/top_level.txt,sha256=iBe9Kk4VIVbLpgv_p8OZUIfxgj4dgJ5wBg6vO3rigso,10
61
+ ygg-0.1.34.dist-info/RECORD,,
@@ -24,6 +24,7 @@ from .execution_context import ExecutionContext
24
24
  from ..workspaces.workspace import WorkspaceService, Workspace
25
25
  from ... import retry, CallableSerde
26
26
  from ...libs.databrickslib import databricks_sdk
27
+ from ...pyutils.expiring_dict import ExpiringDict
27
28
  from ...pyutils.modules import PipIndexSettings
28
29
  from ...pyutils.python_env import PythonEnv
29
30
 
@@ -45,6 +46,31 @@ else: # pragma: no cover - runtime fallback when SDK is missing
45
46
  __all__ = ["Cluster"]
46
47
 
47
48
 
49
+ NAME_ID_CACHE: dict[str, ExpiringDict] = {}
50
+
51
+
52
+ def set_cached_cluster_name(
53
+ host: str,
54
+ cluster_name: str,
55
+ cluster_id: str
56
+ ) -> None:
57
+ existing = NAME_ID_CACHE.get(host)
58
+
59
+ if not existing:
60
+ existing = NAME_ID_CACHE[host] = ExpiringDict(default_ttl=60)
61
+
62
+ existing[cluster_name] = cluster_id
63
+
64
+
65
+ def get_cached_cluster_id(
66
+ host: str,
67
+ cluster_name: str,
68
+ ) -> str:
69
+ existing = NAME_ID_CACHE.get(host)
70
+
71
+ return existing.get(cluster_name) if existing else None
72
+
73
+
48
74
  logger = logging.getLogger(__name__)
49
75
 
50
76
 
@@ -84,6 +110,7 @@ class Cluster(WorkspaceService):
84
110
 
85
111
  _details: Optional["ClusterDetails"] = dataclasses.field(default=None, repr=False)
86
112
  _details_refresh_time: float = dataclasses.field(default=0, repr=False)
113
+ _system_context: Optional[ExecutionContext] = None
87
114
 
88
115
  # host → Cluster instance
89
116
  _env_clusters: ClassVar[Dict[str, "Cluster"]] = {}
@@ -98,10 +125,11 @@ class Cluster(WorkspaceService):
98
125
  """Return the current cluster name."""
99
126
  return self.cluster_name
100
127
 
101
- def __post_init__(self):
102
- """Initialize cached details after dataclass construction."""
103
- if self._details is not None:
104
- self.details = self._details
128
+ @property
129
+ def system_context(self):
130
+ if self._system_context is None:
131
+ self._system_context = self.context(language=Language.PYTHON)
132
+ return self._system_context
105
133
 
106
134
  def is_in_databricks_environment(self):
107
135
  """Return True when running on a Databricks runtime."""
@@ -233,9 +261,8 @@ class Cluster(WorkspaceService):
233
261
  Returns:
234
262
  The updated PythonEnv instance.
235
263
  """
236
- with self.context() as c:
237
- m = c.remote_metadata
238
- version_info = m.version_info
264
+ m = self.system_context.remote_metadata
265
+ version_info = m.version_info
239
266
 
240
267
  python_version = ".".join(str(_) for _ in version_info)
241
268
 
@@ -258,7 +285,7 @@ class Cluster(WorkspaceService):
258
285
  )
259
286
 
260
287
  return target
261
-
288
+
262
289
  @property
263
290
  def details(self):
264
291
  """Return cached cluster details, refreshing when needed."""
@@ -300,21 +327,6 @@ class Cluster(WorkspaceService):
300
327
  return details.state
301
328
  return State.UNKNOWN
302
329
 
303
- def get_state(self, max_delay: float = None):
304
- """Return the cluster state with a custom refresh delay.
305
-
306
- Args:
307
- max_delay: Maximum age in seconds before refresh.
308
-
309
- Returns:
310
- The current cluster state.
311
- """
312
- details = self.fresh_details(max_delay=max_delay)
313
-
314
- if details is not None:
315
- return details.state
316
- return State.UNKNOWN
317
-
318
330
  @property
319
331
  def is_running(self):
320
332
  """Return True when the cluster is running."""
@@ -323,7 +335,10 @@ class Cluster(WorkspaceService):
323
335
  @property
324
336
  def is_pending(self):
325
337
  """Return True when the cluster is starting, resizing, or terminating."""
326
- return self.state in (State.PENDING, State.RESIZING, State.RESTARTING, State.TERMINATING)
338
+ return self.state in (
339
+ State.PENDING, State.RESIZING, State.RESTARTING,
340
+ State.TERMINATING
341
+ )
327
342
 
328
343
  @property
329
344
  def is_error(self):
@@ -507,45 +522,51 @@ class Cluster(WorkspaceService):
507
522
  ):
508
523
  pip_settings = PipIndexSettings.default_settings()
509
524
 
510
- if kwargs:
511
- details = ClusterDetails(**{
512
- **details.as_shallow_dict(),
513
- **kwargs
514
- })
525
+ new_details = ClusterDetails(**{
526
+ **details.as_shallow_dict(),
527
+ **kwargs
528
+ })
529
+
530
+ default_tags = self.workspace.default_tags()
531
+
532
+ if new_details.custom_tags is None:
533
+ new_details.custom_tags = default_tags
534
+ elif default_tags:
535
+ new_tags = new_details.custom_tags.copy()
536
+ new_tags.update(default_tags)
515
537
 
516
- if details.custom_tags is None:
517
- details.custom_tags = self.workspace.default_tags()
538
+ new_details.custom_tags = new_tags
518
539
 
519
- if details.cluster_name is None:
520
- details.cluster_name = self.workspace.current_user.user_name
540
+ if new_details.cluster_name is None:
541
+ new_details.cluster_name = self.workspace.current_user.user_name
521
542
 
522
- if details.spark_version is None or python_version:
523
- details.spark_version = self.latest_spark_version(
543
+ if new_details.spark_version is None or python_version:
544
+ new_details.spark_version = self.latest_spark_version(
524
545
  photon=False, python_version=python_version
525
546
  ).key
526
547
 
527
- if details.single_user_name:
528
- if not details.data_security_mode:
529
- details.data_security_mode = DataSecurityMode.DATA_SECURITY_MODE_DEDICATED
548
+ if new_details.single_user_name:
549
+ if not new_details.data_security_mode:
550
+ new_details.data_security_mode = DataSecurityMode.DATA_SECURITY_MODE_DEDICATED
530
551
 
531
- if not details.node_type_id:
532
- details.node_type_id = "rd-fleet.xlarge"
552
+ if not new_details.node_type_id:
553
+ new_details.node_type_id = "rd-fleet.xlarge"
533
554
 
534
- if getattr(details, "virtual_cluster_size", None) is None and details.num_workers is None and details.autoscale is None:
535
- if details.is_single_node is None:
536
- details.is_single_node = True
555
+ if getattr(new_details, "virtual_cluster_size", None) is None and new_details.num_workers is None and new_details.autoscale is None:
556
+ if new_details.is_single_node is None:
557
+ new_details.is_single_node = True
537
558
 
538
- if details.is_single_node is not None and details.kind is None:
539
- details.kind = Kind.CLASSIC_PREVIEW
559
+ if new_details.is_single_node is not None and new_details.kind is None:
560
+ new_details.kind = Kind.CLASSIC_PREVIEW
540
561
 
541
562
  if pip_settings.extra_index_urls:
542
- if details.spark_env_vars is None:
543
- details.spark_env_vars = {}
563
+ if new_details.spark_env_vars is None:
564
+ new_details.spark_env_vars = {}
544
565
  str_urls = " ".join(pip_settings.extra_index_urls)
545
- details.spark_env_vars["UV_EXTRA_INDEX_URL"] = details.spark_env_vars.get("UV_INDEX", str_urls)
546
- details.spark_env_vars["PIP_EXTRA_INDEX_URL"] = details.spark_env_vars.get("PIP_EXTRA_INDEX_URL", str_urls)
566
+ new_details.spark_env_vars["UV_EXTRA_INDEX_URL"] = new_details.spark_env_vars.get("UV_INDEX", str_urls)
567
+ new_details.spark_env_vars["PIP_EXTRA_INDEX_URL"] = new_details.spark_env_vars.get("PIP_EXTRA_INDEX_URL", str_urls)
547
568
 
548
- return details
569
+ return new_details
549
570
 
550
571
  def create_or_update(
551
572
  self,
@@ -658,7 +679,9 @@ class Cluster(WorkspaceService):
658
679
  )
659
680
 
660
681
  self.wait_for_status()
661
- self.details = self.clusters_client().edit_and_wait(**update_details)
682
+ self.details = retry(tries=4, delay=0.5, max_delay=2)(
683
+ self.clusters_client().edit_and_wait
684
+ )(**update_details)
662
685
 
663
686
  logger.info(
664
687
  "Updated %s",
@@ -704,6 +727,12 @@ class Cluster(WorkspaceService):
704
727
  if not cluster_name and not cluster_id:
705
728
  raise ValueError("Either name or cluster_id must be provided")
706
729
 
730
+ if not cluster_id:
731
+ cluster_id = get_cached_cluster_id(
732
+ host=self.workspace.safe_host,
733
+ cluster_name=cluster_name
734
+ )
735
+
707
736
  if cluster_id:
708
737
  try:
709
738
  details = self.clusters_client().get(cluster_id=cluster_id)
@@ -716,10 +745,13 @@ class Cluster(WorkspaceService):
716
745
  workspace=self.workspace, cluster_id=details.cluster_id, _details=details
717
746
  )
718
747
 
719
- cluster_name_cf = cluster_name.casefold()
720
-
721
748
  for cluster in self.list_clusters():
722
- if cluster_name_cf == cluster.details.cluster_name.casefold():
749
+ if cluster_name == cluster.details.cluster_name:
750
+ set_cached_cluster_name(
751
+ host=self.workspace.safe_host,
752
+ cluster_name=cluster.cluster_name,
753
+ cluster_id=cluster.cluster_id
754
+ )
723
755
  return cluster
724
756
 
725
757
  if raise_error:
@@ -812,6 +844,7 @@ class Cluster(WorkspaceService):
812
844
  env_keys: Optional[List[str]] = None,
813
845
  timeout: Optional[dt.timedelta] = None,
814
846
  result_tag: Optional[str] = None,
847
+ context: Optional[ExecutionContext] = None,
815
848
  ):
816
849
  """Execute a command or callable on the cluster.
817
850
 
@@ -823,11 +856,14 @@ class Cluster(WorkspaceService):
823
856
  env_keys: Optional environment variable names to pass.
824
857
  timeout: Optional timeout for execution.
825
858
  result_tag: Optional result tag for parsing output.
859
+ context: ExecutionContext to run or create new one
826
860
 
827
861
  Returns:
828
862
  The decoded result from the execution context.
829
863
  """
830
- return self.context(language=language).execute(
864
+ context = self.system_context if context is None else context
865
+
866
+ return context.execute(
831
867
  obj=obj,
832
868
  args=args,
833
869
  kwargs=kwargs,
@@ -848,6 +884,8 @@ class Cluster(WorkspaceService):
848
884
  env_variables: Optional[Dict[str, str]] = None,
849
885
  timeout: Optional[dt.timedelta] = None,
850
886
  result_tag: Optional[str] = None,
887
+ force_local: bool = False,
888
+ context: Optional[ExecutionContext] = None,
851
889
  **options
852
890
  ):
853
891
  """
@@ -873,16 +911,29 @@ class Cluster(WorkspaceService):
873
911
  env_variables: Optional environment variables to inject.
874
912
  timeout: Optional timeout for remote execution.
875
913
  result_tag: Optional tag for parsing remote output.
914
+ force_local: force local execution
915
+ context: ExecutionContext to run or create new one
876
916
  **options: Additional execution options passed through.
877
917
 
878
918
  Returns:
879
919
  A decorator or wrapped function that executes remotely.
880
920
  """
921
+ if force_local or self.is_in_databricks_environment():
922
+ # Support both @ws.remote and @ws.remote(...)
923
+ if _func is not None and callable(_func):
924
+ return _func
925
+
926
+ def identity(x):
927
+ return x
928
+
929
+ return identity
930
+
931
+ context = self.system_context if context is None else context
932
+
881
933
  def decorator(func: Callable):
882
- if os.getenv("DATABRICKS_RUNTIME_VERSION") is not None:
934
+ if force_local or self.is_in_databricks_environment():
883
935
  return func
884
936
 
885
- context = self.context(language=language or Language.PYTHON)
886
937
  serialized = CallableSerde.from_callable(func)
887
938
 
888
939
  @functools.wraps(func)
@@ -1109,7 +1160,7 @@ class Cluster(WorkspaceService):
1109
1160
  )
1110
1161
 
1111
1162
  with open(value, mode="rb") as f:
1112
- target_path.write_bytes(f.read())
1163
+ target_path.open().write_all_bytes(f.read())
1113
1164
 
1114
1165
  value = str(target_path)
1115
1166
  elif "." in value and not "/" in value:
@@ -367,6 +367,8 @@ print(json.dumps(meta))"""
367
367
  args=args,
368
368
  kwargs=kwargs,
369
369
  result_tag=result_tag,
370
+ env_keys=env_keys,
371
+ env_variables=env_variables
370
372
  ) if not command else command
371
373
 
372
374
  raw_result = self.execute_command(
@@ -382,8 +384,9 @@ print(json.dumps(meta))"""
382
384
  module_name = module_name.group(1) if module_name else None
383
385
  module_name = module_name.split(".")[0]
384
386
 
385
- if module_name:
387
+ if module_name and "yggdrasil" not in module_name:
386
388
  self.close()
389
+
387
390
  self.cluster.install_libraries(
388
391
  libraries=[module_name],
389
392
  raise_error=True,
@@ -442,7 +445,7 @@ print(json.dumps(meta))"""
442
445
  module_name = module_name.group(1) if module_name else None
443
446
  module_name = module_name.split(".")[0]
444
447
 
445
- if module_name:
448
+ if module_name and "yggdrasil" not in module_name:
446
449
  self.close()
447
450
  self.cluster.install_libraries(
448
451
  libraries=[module_name],
@@ -14,6 +14,12 @@ if TYPE_CHECKING:
14
14
 
15
15
  from ..workspaces.workspace import Workspace
16
16
 
17
+
18
+ __all__ = [
19
+ "databricks_remote_compute"
20
+ ]
21
+
22
+
17
23
  ReturnType = TypeVar("ReturnType")
18
24
 
19
25
  logger = logging.getLogger(__name__)
@@ -26,6 +32,7 @@ def databricks_remote_compute(
26
32
  cluster: Optional["Cluster"] = None,
27
33
  timeout: Optional[dt.timedelta] = None,
28
34
  env_keys: Optional[List[str]] = None,
35
+ force_local: bool = False,
29
36
  **options
30
37
  ) -> Callable[[Callable[..., ReturnType]], Callable[..., ReturnType]]:
31
38
  """Return a decorator that executes functions on a remote cluster.
@@ -37,11 +44,18 @@ def databricks_remote_compute(
37
44
  cluster: Pre-configured Cluster instance to reuse.
38
45
  timeout: Optional execution timeout for remote calls.
39
46
  env_keys: Optional environment variable names to forward.
47
+ force_local: Force local execution
40
48
  **options: Extra options forwarded to the execution decorator.
41
49
 
42
50
  Returns:
43
51
  A decorator that runs functions on the resolved Databricks cluster.
44
52
  """
53
+ if force_local or Workspace.is_in_databricks_environment():
54
+ def identity(x):
55
+ return x
56
+
57
+ return identity
58
+
45
59
  if isinstance(workspace, str):
46
60
  workspace = Workspace(host=workspace)
47
61
 
@@ -62,8 +76,3 @@ def databricks_remote_compute(
62
76
  timeout=timeout,
63
77
  **options
64
78
  )
65
-
66
-
67
- __all__ = [
68
- "databricks_remote_compute",
69
- ]