Skip to content

Commit fa6e025

Browse files
committed
Add databricks.bundles.core
1 parent e6ddb84 commit fa6e025

27 files changed

+3155
-0
lines changed

experimental/python/databricks/bundles/build.py

Lines changed: 479 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
__all__ = [
2+
"Bundle",
3+
"Diagnostic",
4+
"Diagnostics",
5+
"Location",
6+
"Resource",
7+
"ResourceMutator",
8+
"Resources",
9+
"Severity",
10+
"Variable",
11+
"VariableOr",
12+
"VariableOrDict",
13+
"VariableOrList",
14+
"VariableOrOptional",
15+
"job_mutator",
16+
"load_resources_from_current_package_module",
17+
"load_resources_from_module",
18+
"load_resources_from_modules",
19+
"load_resources_from_package_module",
20+
"variables",
21+
]
22+
23+
from databricks.bundles.core._bundle import Bundle
24+
from databricks.bundles.core._diagnostics import (
25+
Diagnostic,
26+
Diagnostics,
27+
Severity,
28+
)
29+
from databricks.bundles.core._load import (
30+
load_resources_from_current_package_module,
31+
load_resources_from_module,
32+
load_resources_from_modules,
33+
load_resources_from_package_module,
34+
)
35+
from databricks.bundles.core._location import Location
36+
from databricks.bundles.core._resource import Resource
37+
from databricks.bundles.core._resource_mutator import ResourceMutator, job_mutator
38+
from databricks.bundles.core._resources import Resources
39+
from databricks.bundles.core._variable import (
40+
Variable,
41+
VariableOr,
42+
VariableOrDict,
43+
VariableOrList,
44+
VariableOrOptional,
45+
variables,
46+
)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from dataclasses import dataclass, field
2+
from typing import Any, TypeVar, Union, get_origin
3+
4+
from databricks.bundles.core._variable import Variable, VariableOr, VariableOrList
5+
6+
__all__ = [
7+
"Bundle",
8+
]
9+
10+
_T = TypeVar("_T")
11+
12+
_VAR_PREFIX = "var"
13+
14+
15+
@dataclass(frozen=True, kw_only=True)
16+
class Bundle:
17+
"""
18+
Bundle contains information about a bundle accessible in functions
19+
loading and mutating resources.
20+
"""
21+
22+
target: str
23+
"""
24+
Selected target where the bundle is being loaded. E.g.: 'development', 'staging', or 'production'.
25+
"""
26+
27+
variables: dict[str, Any] = field(default_factory=dict)
28+
"""
29+
Values of bundle variables resolved for selected target. Bundle variables are defined in databricks.yml.
30+
For accessing variables as structured data, use :meth:`resolve_variable`.
31+
32+
Example:
33+
34+
.. code-block:: yaml
35+
36+
variables:
37+
default_dbr_version:
38+
description: Default version of Databricks Runtime
39+
default: "14.3.x-scala2.12"
40+
"""
41+
42+
def resolve_variable(self, variable: VariableOr[_T]) -> _T:
43+
"""
44+
Resolve a variable to its value.
45+
46+
If the value is a variable, it will be resolved and returned.
47+
Otherwise, the value will be returned as is.
48+
"""
49+
if not isinstance(variable, Variable):
50+
return variable
51+
52+
if not variable.path.startswith(_VAR_PREFIX + "."):
53+
raise ValueError(
54+
"You can only get values of variables starting with 'var.*'"
55+
)
56+
else:
57+
variable_name = variable.path[len(_VAR_PREFIX + ".") :]
58+
59+
if variable_name not in self.variables:
60+
raise ValueError(
61+
f"Can't find '{variable_name}' variable. Did you define it in databricks.yml?"
62+
)
63+
64+
value = self.variables.get(variable_name)
65+
66+
# avoid circular import
67+
from databricks.bundles.core._transform import (
68+
_display_type,
69+
_find_union_arg,
70+
_transform,
71+
_unwrap_variable_path,
72+
)
73+
74+
if nested := _unwrap_variable_path(value):
75+
can_be_variable = get_origin(variable.type) == Union and _find_union_arg(
76+
nested, variable.type
77+
)
78+
can_be_variable = can_be_variable or get_origin(variable.type) == Variable
79+
80+
if not can_be_variable:
81+
display_type = _display_type(variable.type)
82+
83+
raise ValueError(
84+
f"Failed to resolve '{variable_name}' because refers to another "
85+
f"variable '{nested}'. Change variable type to "
86+
f"Variable[VariableOr[{display_type}]]"
87+
)
88+
89+
try:
90+
return _transform(variable.type, value)
91+
except Exception as e:
92+
raise ValueError(f"Failed to read '{variable_name}' variable value") from e
93+
94+
def resolve_variable_list(self, variable: VariableOrList[_T]) -> list[_T]:
95+
"""
96+
Resolve a list variable to its value.
97+
98+
If the value is a variable, or the list item is a variable, it will be resolved and returned.
99+
Otherwise, the value will be returned as is.
100+
"""
101+
102+
return [self.resolve_variable(item) for item in self.resolve_variable(variable)]
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import traceback
2+
from dataclasses import dataclass, field, replace
3+
from enum import Enum
4+
from io import StringIO
5+
from typing import TYPE_CHECKING, Optional, TypeVar
6+
7+
from databricks.bundles.core._location import Location
8+
9+
_T = TypeVar("_T")
10+
11+
if TYPE_CHECKING:
12+
from typing_extensions import Self
13+
14+
__all__ = [
15+
"Diagnostic",
16+
"Diagnostics",
17+
"Severity",
18+
]
19+
20+
21+
class Severity(Enum):
22+
WARNING = "warning"
23+
ERROR = "error"
24+
25+
26+
@dataclass(kw_only=True, frozen=True)
27+
class Diagnostic:
28+
severity: Severity
29+
"""
30+
Severity of the diagnostics item.
31+
"""
32+
33+
summary: str
34+
"""
35+
Short summary of the error or warning.
36+
"""
37+
38+
detail: Optional[str] = None
39+
"""
40+
Explanation of the error or warning.
41+
"""
42+
43+
path: Optional[tuple[str, ...]] = None
44+
"""
45+
Path in databricks.yml where the error or warning occurred.
46+
"""
47+
48+
location: Optional[Location] = None
49+
"""
50+
Source code location where the error or warning occurred.
51+
"""
52+
53+
def as_dict(self) -> dict:
54+
def omit_none(values: dict):
55+
return {key: value for key, value in values.items() if value is not None}
56+
57+
if self.location:
58+
location = self.location.as_dict()
59+
else:
60+
location = None
61+
62+
return omit_none(
63+
{
64+
"severity": self.severity.value,
65+
"summary": self.summary,
66+
"detail": self.detail,
67+
"path": self.path,
68+
"location": location,
69+
}
70+
)
71+
72+
73+
@dataclass(frozen=True)
74+
class Diagnostics:
75+
"""
76+
Diagnostics is a collection of errors and warnings we print to users.
77+
78+
Each item can have source location or path associated, that is reported in output to
79+
indicate where the error or warning occurred.
80+
"""
81+
82+
items: tuple[Diagnostic, ...] = field(default_factory=tuple, kw_only=False)
83+
84+
def extend(self, diagnostics: "Self") -> "Self":
85+
"""
86+
Extend items with another diagnostics. This pattern allows
87+
to accumulate errors and warnings.
88+
89+
Example:
90+
91+
.. code-block:: python
92+
93+
def foo() -> Diagnostics: ...
94+
def bar() -> Diagnostics: ...
95+
96+
diagnostics = Diagnostics()
97+
diagnostics = diagnostics.extend(foo())
98+
diagnostics = diagnostics.extend(bar())
99+
"""
100+
101+
return replace(
102+
self,
103+
items=(*self.items, *diagnostics.items),
104+
)
105+
106+
def extend_tuple(self, pair: tuple[_T, "Self"]) -> tuple[_T, "Self"]:
107+
"""
108+
Extend items with another diagnostics. This variant is useful when
109+
methods return a pair of value and diagnostics. This pattern allows
110+
to accumulate errors and warnings.
111+
112+
Example:
113+
114+
.. code-block:: python
115+
116+
def foo() -> (int, Diagnostics): ...
117+
118+
diagnostics = Diagnostics()
119+
value, diagnostics = diagnostics.extend_tuple(foo())
120+
"""
121+
122+
value, other_diagnostics = pair
123+
124+
return value, self.extend(other_diagnostics)
125+
126+
def has_error(self) -> bool:
127+
"""
128+
Returns True if there is at least one error in diagnostics.
129+
"""
130+
131+
for item in self.items:
132+
if item.severity == Severity.ERROR:
133+
return True
134+
135+
return False
136+
137+
@classmethod
138+
def create_error(
139+
cls,
140+
msg: str,
141+
*,
142+
detail: Optional[str] = None,
143+
location: Optional[Location] = None,
144+
path: Optional[tuple[str, ...]] = None,
145+
) -> "Self":
146+
"""
147+
Create an error diagnostics.
148+
"""
149+
150+
return cls(
151+
items=(
152+
Diagnostic(
153+
severity=Severity.ERROR,
154+
summary=msg,
155+
detail=detail,
156+
location=location,
157+
path=path,
158+
),
159+
),
160+
)
161+
162+
@classmethod
163+
def create_warning(
164+
cls,
165+
msg: str,
166+
*,
167+
detail: Optional[str] = None,
168+
location: Optional[Location] = None,
169+
path: Optional[tuple[str, ...]] = None,
170+
) -> "Self":
171+
"""
172+
Create a warning diagnostics.
173+
"""
174+
175+
return cls(
176+
items=(
177+
Diagnostic(
178+
severity=Severity.WARNING,
179+
summary=msg,
180+
detail=detail,
181+
location=location,
182+
path=path,
183+
),
184+
)
185+
)
186+
187+
@classmethod
188+
def from_exception(
189+
cls,
190+
exc: Exception,
191+
*,
192+
summary: str,
193+
location: Optional[Location] = None,
194+
path: Optional[tuple[str, ...]] = None,
195+
explanation: Optional[str] = None,
196+
) -> "Self":
197+
"""
198+
Create diagnostics from an exception.
199+
200+
:param exc: exception to create diagnostics from
201+
:param summary: short summary of the error
202+
:param location: optional location in the source code where the error occurred
203+
:param path: optional path to relevant property in databricks.yml
204+
:param explanation: optional explanation to add to the details
205+
"""
206+
207+
detail_io = StringIO()
208+
traceback.print_exception(exc, file=detail_io)
209+
210+
detail = detail_io.getvalue()
211+
if explanation:
212+
detail = f"{detail}\n\n\033[0;36mExplanation:\033[0m {explanation}"
213+
214+
diagnostic = Diagnostic(
215+
severity=Severity.ERROR,
216+
summary=summary,
217+
location=location,
218+
path=path,
219+
detail=detail,
220+
)
221+
222+
return cls(items=(diagnostic,))

0 commit comments

Comments
 (0)