databricks-bundle-decorators 0.4.2__tar.gz → 0.4.3__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 (22) hide show
  1. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/PKG-INFO +1 -1
  2. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/pyproject.toml +1 -1
  3. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/decorators.py +47 -3
  4. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/io_managers/polars_csv.py +2 -2
  5. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/io_managers/polars_delta.py +3 -4
  6. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/io_managers/polars_json.py +2 -2
  7. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/io_managers/polars_parquet.py +2 -2
  8. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/io_managers/spark_delta.py +2 -2
  9. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/io_managers/spark_parquet.py +2 -2
  10. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/runtime.py +20 -20
  11. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/task_values.py +13 -4
  12. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/README.md +0 -0
  13. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/__init__.py +0 -0
  14. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/cli.py +0 -0
  15. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/codegen.py +0 -0
  16. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/context.py +0 -0
  17. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/discovery.py +0 -0
  18. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/io_manager.py +0 -0
  19. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/io_managers/__init__.py +0 -0
  20. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/io_managers/spark_uc.py +0 -0
  21. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/registry.py +0 -0
  22. {databricks_bundle_decorators-0.4.2 → databricks_bundle_decorators-0.4.3}/src/databricks_bundle_decorators/sdk_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: databricks-bundle-decorators
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Decorator-based framework for defining Databricks jobs and tasks as Python code.
5
5
  Author: boccileonardo
6
6
  Author-email: boccileonardo <leonardobocci99@hotmail.com>
@@ -4,7 +4,7 @@ requires = [ "uv-build>=0.10.4,<0.11" ]
4
4
 
5
5
  [project]
6
6
  name = "databricks-bundle-decorators"
7
- version = "0.4.2"
7
+ version = "0.4.3"
8
8
  description = "Decorator-based framework for defining Databricks jobs and tasks as Python code."
9
9
  readme = "README.md"
10
10
  authors = [ { name = "boccileonardo", email = "leonardobocci99@hotmail.com" } ]
@@ -31,6 +31,32 @@ from databricks_bundle_decorators.registry import (
31
31
  from databricks_bundle_decorators.sdk_types import ClusterConfig, JobConfig, TaskConfig
32
32
 
33
33
 
34
+ # ---------------------------------------------------------------------------
35
+ # Reserved parameter namespace
36
+ # ---------------------------------------------------------------------------
37
+
38
+ #: Parameter names that are reserved for internal runtime wiring.
39
+ _RESERVED_PARAM_NAMES: frozenset[str] = frozenset(
40
+ {"__job_name__", "__task_key__", "__run_id__"}
41
+ )
42
+
43
+ #: Parameter name prefixes that are reserved for internal runtime wiring.
44
+ _RESERVED_PARAM_PREFIXES: tuple[str, ...] = ("__upstream__",)
45
+
46
+
47
+ def _validate_user_params(params: dict[str, str], context: str) -> None:
48
+ """Raise ``ValueError`` if any *params* key collides with reserved names."""
49
+ for name in params:
50
+ if name in _RESERVED_PARAM_NAMES or any(
51
+ name.startswith(p) for p in _RESERVED_PARAM_PREFIXES
52
+ ):
53
+ raise ValueError(
54
+ f"{context}: parameter name {name!r} is reserved for internal "
55
+ f"runtime use. Reserved names: {sorted(_RESERVED_PARAM_NAMES)}; "
56
+ f"reserved prefix: {list(_RESERVED_PARAM_PREFIXES)}."
57
+ )
58
+
59
+
34
60
  # ---------------------------------------------------------------------------
35
61
  # Job context – tracks which @job body is currently being executed
36
62
  # ---------------------------------------------------------------------------
@@ -95,7 +121,8 @@ def task(
95
121
 
96
122
  When used **outside** a ``@job`` body (e.g. at module level), the
97
123
  function is registered under its short name for use in tests or
98
- standalone execution.
124
+ standalone execution. Duplicate names at module level raise
125
+ `DuplicateResourceError`.
99
126
 
100
127
  Parameters
101
128
  ----------
@@ -110,7 +137,12 @@ def task(
110
137
  directly to the ``databricks.bundles.jobs.Task`` constructor at
111
138
  deploy time. See `TaskConfig`
112
139
  for the full list of supported fields.
113
- """
140
+ Notes
141
+ -----
142
+ Dependency edges are detected only for `TaskProxy` objects passed as
143
+ **direct** positional or keyword arguments. Proxies nested inside
144
+ lists, dicts, or other container types are **not** inspected and will
145
+ not register dependency edges."""
114
146
 
115
147
  def decorator(fn: types.FunctionType) -> Callable[..., Any]:
116
148
  task_key = fn.__name__
@@ -128,13 +160,21 @@ def task(
128
160
  _current_job_tasks[task_key] = meta
129
161
  else:
130
162
  # Module-level definition (standalone / test usage).
131
- _TASK_REGISTRY[task_key] = meta
163
+ _register_unique(_TASK_REGISTRY, task_key, meta, "task")
132
164
 
133
165
  @functools.wraps(fn)
134
166
  def wrapper(*args, **kwargs):
135
167
  if _current_job_name is not None:
136
168
  # We're being *called* inside a @job body – return a
137
169
  # TaskProxy and record DAG edges from any proxy args.
170
+ if task_key in _current_job_dag:
171
+ raise DuplicateResourceError(
172
+ f"Task '{task_key}' is called more than once in job "
173
+ f"'{_current_job_name}'. Each @task may only be invoked "
174
+ "once per @job body. Use a unique function name for "
175
+ "each logical step."
176
+ )
177
+
138
178
  upstream_deps: list[str] = []
139
179
  edge_map: dict[str, str] = {}
140
180
 
@@ -314,6 +354,10 @@ def job(
314
354
  f"Duplicate job '{job_name}'. Each job must have a unique name."
315
355
  )
316
356
 
357
+ # --- validate param names -----------------------------------------
358
+ if params:
359
+ _validate_user_params(params, f"@job('{job_name}')")
360
+
317
361
  # --- validate cluster type -----------------------------------------
318
362
  if cluster is not None and not isinstance(cluster, ClusterMeta):
319
363
  raise TypeError(
@@ -11,7 +11,7 @@ Requires the ``polars`` optional dependency::
11
11
  from __future__ import annotations
12
12
 
13
13
  from collections.abc import Callable
14
- from typing import Any
14
+ from typing import Any, cast
15
15
 
16
16
  from databricks_bundle_decorators.io_manager import (
17
17
  InputContext,
@@ -107,7 +107,7 @@ class PolarsCsvIoManager(IoManager):
107
107
  def storage_options(self) -> dict[str, str] | None:
108
108
  """Resolve *storage_options*, calling it first if it is a callable."""
109
109
  if callable(self._storage_options):
110
- return self._storage_options()
110
+ return cast(Callable[[], dict[str, str]], self._storage_options)()
111
111
  return self._storage_options
112
112
 
113
113
  def _uri(self, key: str) -> str:
@@ -12,7 +12,7 @@ Requires the ``polars`` and ``deltalake`` optional dependencies::
12
12
  from __future__ import annotations
13
13
 
14
14
  from collections.abc import Callable
15
- from typing import Any
15
+ from typing import Any, cast
16
16
 
17
17
  from databricks_bundle_decorators.io_manager import (
18
18
  InputContext,
@@ -81,8 +81,7 @@ class PolarsDeltaIoManager(IoManager):
81
81
  they are managed by the IoManager.
82
82
  mode : str
83
83
  Delta write mode. One of ``"overwrite"``, ``"append"``,
84
- ``"error"``, or ``"ignore"``. Defaults to ``"overwrite"``
85
- for idempotent task outputs.
84
+ ``"error"``, or ``"ignore"``. Defaults to ``"error"``.
86
85
 
87
86
  For **merge** operations, ignore this parameter and return a
88
87
  fully-configured `deltalake.table.TableMerger` from your task
@@ -129,7 +128,7 @@ class PolarsDeltaIoManager(IoManager):
129
128
  def storage_options(self) -> dict[str, str] | None:
130
129
  """Resolve *storage_options*, calling it first if it is a callable."""
131
130
  if callable(self._storage_options):
132
- return self._storage_options()
131
+ return cast(Callable[[], dict[str, str]], self._storage_options)()
133
132
  return self._storage_options
134
133
 
135
134
  def _uri(self, key: str) -> str:
@@ -15,7 +15,7 @@ Requires the ``polars`` optional dependency::
15
15
  from __future__ import annotations
16
16
 
17
17
  from collections.abc import Callable
18
- from typing import Any
18
+ from typing import Any, cast
19
19
 
20
20
  from databricks_bundle_decorators.io_manager import (
21
21
  InputContext,
@@ -114,7 +114,7 @@ class PolarsJsonIoManager(IoManager):
114
114
  def storage_options(self) -> dict[str, str] | None:
115
115
  """Resolve *storage_options*, calling it first if it is a callable."""
116
116
  if callable(self._storage_options):
117
- return self._storage_options()
117
+ return cast(Callable[[], dict[str, str]], self._storage_options)()
118
118
  return self._storage_options
119
119
 
120
120
  def _uri(self, key: str) -> str:
@@ -11,7 +11,7 @@ Requires the ``polars`` optional dependency::
11
11
  from __future__ import annotations
12
12
 
13
13
  from collections.abc import Callable
14
- from typing import Any
14
+ from typing import Any, cast
15
15
 
16
16
  from databricks_bundle_decorators.io_manager import (
17
17
  InputContext,
@@ -114,7 +114,7 @@ class PolarsParquetIoManager(IoManager):
114
114
  def storage_options(self) -> dict[str, str] | None:
115
115
  """Resolve *storage_options*, calling it first if it is a callable."""
116
116
  if callable(self._storage_options):
117
- return self._storage_options()
117
+ return cast(Callable[[], dict[str, str]], self._storage_options)()
118
118
  return self._storage_options
119
119
 
120
120
  def _uri(self, key: str) -> str:
@@ -14,7 +14,7 @@ Requires PySpark, which is pre-installed on Databricks clusters.
14
14
  from __future__ import annotations
15
15
 
16
16
  from collections.abc import Callable
17
- from typing import Any
17
+ from typing import Any, cast
18
18
 
19
19
  from databricks_bundle_decorators.io_manager import (
20
20
  InputContext,
@@ -194,7 +194,7 @@ class SparkDeltaIoManager(_SparkDeltaBase):
194
194
  def spark_configs(self) -> dict[str, str] | None:
195
195
  """Resolve *spark_configs*, calling it first if it is a callable."""
196
196
  if callable(self._spark_configs):
197
- return self._spark_configs()
197
+ return cast(Callable[[], dict[str, str]], self._spark_configs)()
198
198
  return self._spark_configs
199
199
 
200
200
  def setup(self) -> None:
@@ -14,7 +14,7 @@ Requires PySpark, which is pre-installed on Databricks clusters.
14
14
  from __future__ import annotations
15
15
 
16
16
  from collections.abc import Callable
17
- from typing import Any
17
+ from typing import Any, cast
18
18
 
19
19
  from databricks_bundle_decorators.io_manager import (
20
20
  InputContext,
@@ -153,7 +153,7 @@ class SparkParquetIoManager(_SparkParquetBase):
153
153
  def spark_configs(self) -> dict[str, str] | None:
154
154
  """Resolve *spark_configs*, calling it first if it is a callable."""
155
155
  if callable(self._spark_configs):
156
- return self._spark_configs()
156
+ return cast(Callable[[], dict[str, str]], self._spark_configs)()
157
157
  return self._spark_configs
158
158
 
159
159
  def setup(self) -> None:
@@ -11,8 +11,8 @@ Databricks invokes the ``dbxdec-run`` console-script, which calls
11
11
  """
12
12
 
13
13
  import argparse
14
+ import logging
14
15
  import os
15
- import sys
16
16
  import typing
17
17
  from typing import Any
18
18
 
@@ -20,6 +20,8 @@ from databricks_bundle_decorators.context import _populate_params
20
20
  from databricks_bundle_decorators.io_manager import InputContext, OutputContext
21
21
  from databricks_bundle_decorators.registry import _TASK_REGISTRY
22
22
 
23
+ _logger = logging.getLogger(__name__)
24
+
23
25
 
24
26
  def _parse_named_args(argv: list[str]) -> dict[str, str]:
25
27
  """Parse ``--key=value`` pairs from an argv list into a dict."""
@@ -58,14 +60,14 @@ def run_task(task_key: str, cli_params: dict[str, str]) -> None:
58
60
  _populate_params(cli_params)
59
61
 
60
62
  # ---- look up task metadata -------------------------------------------
61
- # Try qualified key (job_name.task_key) first, then short key.
62
63
  qualified_key = f"{job_name}.{task_key}"
63
- task_meta = _TASK_REGISTRY.get(qualified_key) or _TASK_REGISTRY.get(task_key)
64
+ task_meta = _TASK_REGISTRY.get(qualified_key)
64
65
  if task_meta is None:
65
- print(f"Error: Task '{task_key}' not found in registry.", file=sys.stderr)
66
- print(f"Tried: '{qualified_key}', '{task_key}'", file=sys.stderr)
67
- print(f"Available: {list(_TASK_REGISTRY.keys())}", file=sys.stderr)
68
- sys.exit(1)
66
+ available = list(_TASK_REGISTRY.keys())
67
+ raise RuntimeError(
68
+ f"Task '{task_key}' not found by qualified key '{qualified_key}'. "
69
+ f"Available: {available}"
70
+ )
69
71
 
70
72
  # ---- resolve type hints for the current task's parameters ------------
71
73
  try:
@@ -77,9 +79,7 @@ def run_task(task_key: str, cli_params: dict[str, str]) -> None:
77
79
  kwargs: dict[str, Any] = {}
78
80
  for param_name, upstream_task_key in upstream_map.items():
79
81
  upstream_qualified = f"{job_name}.{upstream_task_key}"
80
- upstream_meta = _TASK_REGISTRY.get(upstream_qualified) or _TASK_REGISTRY.get(
81
- upstream_task_key
82
- )
82
+ upstream_meta = _TASK_REGISTRY.get(upstream_qualified)
83
83
  if upstream_meta and upstream_meta.io_manager:
84
84
  upstream_meta.io_manager._ensure_setup()
85
85
  context = InputContext(
@@ -91,10 +91,11 @@ def run_task(task_key: str, cli_params: dict[str, str]) -> None:
91
91
  )
92
92
  kwargs[param_name] = upstream_meta.io_manager.read(context)
93
93
  else:
94
- print(
95
- f"Warning: upstream task '{upstream_task_key}' has no IoManager – "
96
- f"cannot auto-load data for parameter '{param_name}'.",
97
- file=sys.stderr,
94
+ _logger.warning(
95
+ "Upstream task '%s' has no IoManager – "
96
+ "cannot auto-load data for parameter '%s'.",
97
+ upstream_task_key,
98
+ param_name,
98
99
  )
99
100
 
100
101
  # ---- execute the task function ---------------------------------------
@@ -116,10 +117,10 @@ def run_task(task_key: str, cli_params: dict[str, str]) -> None:
116
117
  )
117
118
  task_meta.io_manager.write(context, result)
118
119
  elif result is not None and not task_meta.io_manager:
119
- print(
120
- f"Warning: task '{task_key}' returned a value but has no IoManager – "
121
- f"the return value will be discarded.",
122
- file=sys.stderr,
120
+ _logger.warning(
121
+ "Task '%s' returned a value but has no IoManager – "
122
+ "the return value will be discarded.",
123
+ task_key,
123
124
  )
124
125
 
125
126
 
@@ -142,8 +143,7 @@ def main() -> None:
142
143
 
143
144
  task_key = cli_params.get("__task_key__")
144
145
  if not task_key:
145
- print("Error: --__task_key__=<name> is required.", file=sys.stderr)
146
- sys.exit(1)
146
+ raise RuntimeError("--__task_key__=<name> is required.")
147
147
 
148
148
  # 3. Run the task.
149
149
  run_task(task_key, cli_params)
@@ -6,6 +6,7 @@ small primitive payloads (~48 KB) and must be opted-in explicitly by calling
6
6
  `set_task_value` inside a ``@task`` function.
7
7
  """
8
8
 
9
+ import os
9
10
  from typing import Any
10
11
 
11
12
  # Module-level fallback store used during local / test execution.
@@ -16,6 +17,11 @@ _local_task_values: dict[str, dict[str, Any]] = {}
16
17
  _current_task_key: str | None = None
17
18
 
18
19
 
20
+ def _is_databricks_runtime() -> bool:
21
+ """Return ``True`` when running inside a live Databricks job cluster."""
22
+ return bool(os.environ.get("DATABRICKS_RUNTIME_VERSION"))
23
+
24
+
19
25
  def set_task_value(key: str, value: str | int | float | bool) -> None:
20
26
  """Write a small value into Databricks task values.
21
27
 
@@ -37,14 +43,16 @@ def set_task_value(key: str, value: str | int | float | bool) -> None:
37
43
  f"got {type(value).__name__}. Use an IoManager for complex data."
38
44
  )
39
45
 
40
- try:
46
+ if _is_databricks_runtime():
47
+ # On Databricks: let any API/permission error propagate so that
48
+ # real runtime failures are visible rather than silently swallowed.
41
49
  from pyspark.dbutils import DBUtils # type: ignore[import-untyped]
42
50
  from pyspark.sql import SparkSession # type: ignore[import-untyped]
43
51
 
44
52
  spark = SparkSession.builder.getOrCreate()
45
53
  dbutils = DBUtils(spark)
46
54
  dbutils.jobs.taskValues.set(key=key, value=value)
47
- except Exception:
55
+ else:
48
56
  # Local / testing fallback — use the current task key if set by
49
57
  # the runtime runner, otherwise fall back to "__current__".
50
58
  store_key = _current_task_key or "__current__"
@@ -61,12 +69,13 @@ def get_task_value(task_key: str, key: str) -> Any:
61
69
  key:
62
70
  The key passed to `set_task_value`.
63
71
  """
64
- try:
72
+ if _is_databricks_runtime():
73
+ # On Databricks: let any API/permission error propagate.
65
74
  from pyspark.dbutils import DBUtils # type: ignore[import-untyped]
66
75
  from pyspark.sql import SparkSession # type: ignore[import-untyped]
67
76
 
68
77
  spark = SparkSession.builder.getOrCreate()
69
78
  dbutils = DBUtils(spark)
70
79
  return dbutils.jobs.taskValues.get(taskKey=task_key, key=key)
71
- except Exception:
80
+ else:
72
81
  return _local_task_values.get(task_key, {}).get(key)