diff --git a/python/Taskfile.yml b/python/Taskfile.yml index 30e2db62ae..602e92028a 100644 --- a/python/Taskfile.yml +++ b/python/Taskfile.yml @@ -76,6 +76,8 @@ tasks: ! -path databricks/bundles/core \ ! -path databricks/bundles/resources \ -exec rm -rf {} \; + # core/ is hand-written except for the generated wiring under _generated/. + - rm -rf databricks/bundles/core/_generated - cd codegen && uv run -m pytest codegen_tests - cd codegen && uv run -m codegen.main --output .. # Generated code is fixed and formatted by the global ruff (see ../ruff.toml). diff --git a/python/codegen/codegen/generated_wiring.py b/python/codegen/codegen/generated_wiring.py new file mode 100644 index 0000000000..7811992be9 --- /dev/null +++ b/python/codegen/codegen/generated_wiring.py @@ -0,0 +1,199 @@ +""" +Generates the per-resource wiring in databricks.bundles.core that used to be +hand-written. + +Each wired resource gets its own file _generated/.py (rendered from +wiring_resource.py.tmpl): the add_* method + collection property mixin, the +*_mutator decorator, and the _ResourceType entry. The generated +_generated/__init__.py collects them into _GeneratedResources (mixed into +Resources), _all_resource_types(), and the mutator re-exports. The core package +__init__ is generated too (static exports plus the generated mutators). +""" + +from dataclasses import dataclass +from pathlib import Path +from string import Template + +import codegen.packages as packages + +HEADER = "# Code generated by pydabs-codegen. DO NOT EDIT.\n\n" + +_RESOURCE_TEMPLATE = Template( + (Path(__file__).parent / "wiring_resource.py.tmpl").read_text() +) + + +@dataclass(frozen=True) +class _WiredResource: + class_name: str + """Resource dataclass name, e.g. "Job".""" + + singular_name: str + """Singular name used in methods and messages, e.g. "job".""" + + plural_name: str + """Plural name, the same as the "resources" bundle section, e.g. "jobs".""" + + model_module: str + """Module the resource dataclass lives in, e.g. "databricks.bundles.jobs._models.job".""" + + +def _wired_resources() -> list[_WiredResource]: + # Every namespaced resource is wired. RESOURCE_NAMESPACE is the single source + # of truth for both model generation and wiring. + resources = [] + + for ref, namespace in packages.RESOURCE_NAMESPACE.items(): + class_name = packages.get_class_name(ref) + + resources.append( + _WiredResource( + class_name=class_name, + singular_name=class_name.lower(), + plural_name=namespace, + model_module=packages.get_package(namespace, ref), + ) + ) + + resources.sort(key=lambda r: r.plural_name) + + return resources + + +def write_wiring(output: str): + resources = _wired_resources() + + core_path = Path(output) / "databricks" / "bundles" / "core" + generated_path = core_path / "_generated" + generated_path.mkdir(parents=True, exist_ok=True) + + for r in resources: + code = _RESOURCE_TEMPLATE.substitute( + { + "class": r.class_name, + "singular": r.singular_name, + "plural": r.plural_name, + "model_module": r.model_module, + } + ) + (generated_path / f"{r.plural_name}.py").write_text(HEADER + code) + + (generated_path / "__init__.py").write_text(HEADER + _collector_code(resources)) + (core_path / "__init__.py").write_text(HEADER + _core_init_code(resources)) + + print(f"Writing wiring into {generated_path}") + + +def _collector_code(resources: list[_WiredResource]) -> str: + imports = "\n".join( + f"from databricks.bundles.core._generated.{r.plural_name} import " + f"_{r.class_name}Resources, {r.singular_name}_mutator" + for r in resources + ) + + mutator_names = [f"{r.singular_name}_mutator" for r in resources] + exports = sorted(["_GeneratedResources", "_all_resource_types", *mutator_names]) + all_block = "\n".join(f' "{name}",' for name in exports) + + mixins = "\n".join(f" _{r.class_name}Resources," for r in resources) + module_imports = "\n".join(f" {r.plural_name}," for r in resources) + type_entries = "\n".join( + f" {r.plural_name}._resource_type()," for r in resources + ) + + return f"""from typing import TYPE_CHECKING + +{imports} + +if TYPE_CHECKING: + from databricks.bundles.core._resource_type import _ResourceType + +__all__ = [ +{all_block} +] + + +class _GeneratedResources( +{mixins} +): + pass + + +def _all_resource_types() -> "tuple[_ResourceType, ...]": + from databricks.bundles.core._generated import ( +{module_imports} + ) + + return ( +{type_entries} + ) +""" + + +# Static, non-resource exports of databricks.bundles.core. Kept here because the +# package __init__ is generated; the resource mutators are appended and the whole +# list is sorted. +_CORE_INIT_STATIC_EXPORTS = [ + "Bundle", + "Diagnostic", + "Diagnostics", + "Location", + "Resource", + "ResourceMutator", + "Resources", + "Severity", + "Variable", + "VariableOr", + "VariableOrDict", + "VariableOrList", + "VariableOrOptional", + "load_resources_from_current_package_module", + "load_resources_from_module", + "load_resources_from_modules", + "load_resources_from_package_module", + "variables", +] + +_CORE_INIT_STATIC_IMPORTS = """from databricks.bundles.core._bundle import Bundle +from databricks.bundles.core._diagnostics import ( + Diagnostic, + Diagnostics, + Severity, +) +from databricks.bundles.core._load import ( + load_resources_from_current_package_module, + load_resources_from_module, + load_resources_from_modules, + load_resources_from_package_module, +) +from databricks.bundles.core._location import Location +from databricks.bundles.core._resource import Resource +from databricks.bundles.core._resource_mutator import ResourceMutator +from databricks.bundles.core._resources import Resources +from databricks.bundles.core._variable import ( + Variable, + VariableOr, + VariableOrDict, + VariableOrList, + VariableOrOptional, + variables, +)""" + + +def _core_init_code(resources: list[_WiredResource]) -> str: + mutator_names = [f"{r.singular_name}_mutator" for r in resources] + + all_exports = sorted(_CORE_INIT_STATIC_EXPORTS + mutator_names) + all_block = "\n".join(f' "{name}",' for name in all_exports) + + mutator_imports = ",\n".join(f" {name}" for name in sorted(mutator_names)) + + return f"""__all__ = [ +{all_block} +] + +{_CORE_INIT_STATIC_IMPORTS} +from databricks.bundles.core._generated import ( +{mutator_imports}, +) +""" diff --git a/python/codegen/codegen/main.py b/python/codegen/codegen/main.py index 2cd3fcd91a..7927da8596 100644 --- a/python/codegen/codegen/main.py +++ b/python/codegen/codegen/main.py @@ -8,6 +8,7 @@ import codegen.generated_dataclass_patch as generated_dataclass_patch import codegen.generated_enum as generated_enum import codegen.generated_imports as generated_imports +import codegen.generated_wiring as generated_wiring import codegen.jsonschema as openapi import codegen.jsonschema_patch as openapi_patch import codegen.packages as packages @@ -46,6 +47,11 @@ def main(output: str): _write_exports(namespace, dataclasses, enums, output) + # Generate the per-resource wiring in databricks.bundles.core (the + # _ResourceType registry, Resources add_*/property methods, *_mutator + # decorators, and the core package __init__). + generated_wiring.write_wiring(output) + def _transitively_mark_deprecated_and_private( roots: list[str], diff --git a/python/codegen/codegen/wiring_resource.py.tmpl b/python/codegen/codegen/wiring_resource.py.tmpl new file mode 100644 index 0000000000..90a2ea0f15 --- /dev/null +++ b/python/codegen/codegen/wiring_resource.py.tmpl @@ -0,0 +1,113 @@ +from collections.abc import Callable +from typing import TYPE_CHECKING, Optional, overload + +from databricks.bundles.core._bundle import Bundle +from databricks.bundles.core._location import Location +from databricks.bundles.core._resource_mutator import ResourceMutator +from databricks.bundles.core._transform import _transform + +if TYPE_CHECKING: + from databricks.bundles.core._resource_type import _ResourceType + from ${model_module} import ${class}, ${class}Param + + +def _resource_type() -> "_ResourceType": + from databricks.bundles.core._resource_type import _ResourceType + from ${model_module} import ${class} + + return _ResourceType( + resource_type=${class}, + singular_name="${singular}", + plural_name="${plural}", + ) + + +class _${class}Resources: + """ + Generated ${singular} accessors, mixed into Resources. + """ + + # Provided by the Resources subclass; declared here so the generated methods + # below type-check. + _resources: dict[str, dict] + + if TYPE_CHECKING: + + def add_location(self, path: tuple[str, ...], location: Location) -> None: ... + + def add_diagnostic_error( + self, + msg: str, + *, + detail: Optional[str] = None, + path: Optional[tuple[str, ...]] = None, + location: Optional[Location] = None, + ) -> None: ... + + @property + def ${plural}(self) -> dict[str, "${class}"]: + return self._resources["${plural}"] + + def add_${singular}( + self, + resource_name: str, + ${singular}: "${class}Param", + *, + location: Optional[Location] = None, + ) -> None: + """ + Adds the resource ${singular} to the collection of resources. Resource name must be unique across all ${plural}. + + :param resource_name: unique identifier for the ${singular} + :param ${singular}: the ${singular} to add, can be ${class} or dict + :param location: optional location of the ${singular} in the source code + """ + from ${model_module} import ${class} + + ${singular} = _transform(${class}, ${singular}) + path = ("resources", "${plural}", resource_name) + location = location or Location.from_stack_frame(depth=1) + + if self._resources["${plural}"].get(resource_name): + self.add_diagnostic_error( + msg=f"Duplicate resource name '{resource_name}' for resource '${singular}'. Resource names must be unique.", + location=location, + path=path, + ) + else: + if location: + self.add_location(path, location) + + self._resources["${plural}"][resource_name] = ${singular} + + +@overload +def ${singular}_mutator( + function: Callable[[Bundle, "${class}"], "${class}"], +) -> ResourceMutator["${class}"]: ... + + +@overload +def ${singular}_mutator( + function: Callable[["${class}"], "${class}"], +) -> ResourceMutator["${class}"]: ... + + +def ${singular}_mutator(function: Callable) -> ResourceMutator["${class}"]: + """ + Decorator for defining mutator for ${plural}. Function should return a new instance of the ${singular} + with the desired changes, instead of mutating the input ${singular}. + + Example: + + .. code-block:: python + + @${singular}_mutator + def my_${singular}_mutator(bundle: Bundle, ${singular}: ${class}) -> ${class}: + return replace(${singular}, ...) + + :param function: Function that mutates ${plural}. + """ + from ${model_module} import ${class} + + return ResourceMutator(resource_type=${class}, function=function) diff --git a/python/databricks/bundles/.gitattributes b/python/databricks/bundles/.gitattributes index 747d44f164..658274265f 100644 --- a/python/databricks/bundles/.gitattributes +++ b/python/databricks/bundles/.gitattributes @@ -1,5 +1,6 @@ # Generated by pydabs-codegen (see python/codegen). Each generated namespace -# has a _models/ tree and an __init__.py; core/ is hand-written, so unset it. +# has a _models/ tree and an __init__.py. In core/, only the _generated/ tree +# and the package __init__.py are generated; the rest is hand-written. */_models/** linguist-generated=true */__init__.py linguist-generated=true -core/__init__.py linguist-generated=false +core/_generated/** linguist-generated=true diff --git a/python/databricks/bundles/core/__init__.py b/python/databricks/bundles/core/__init__.py index 98abbaaf45..cbf4661aed 100644 --- a/python/databricks/bundles/core/__init__.py +++ b/python/databricks/bundles/core/__init__.py @@ -1,3 +1,5 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + __all__ = [ "Bundle", "Diagnostic", @@ -31,6 +33,14 @@ Diagnostics, Severity, ) +from databricks.bundles.core._generated import ( + alert_mutator, + catalog_mutator, + job_mutator, + pipeline_mutator, + schema_mutator, + volume_mutator, +) from databricks.bundles.core._load import ( load_resources_from_current_package_module, load_resources_from_module, @@ -39,15 +49,7 @@ ) from databricks.bundles.core._location import Location from databricks.bundles.core._resource import Resource -from databricks.bundles.core._resource_mutator import ( - ResourceMutator, - alert_mutator, - catalog_mutator, - job_mutator, - pipeline_mutator, - schema_mutator, - volume_mutator, -) +from databricks.bundles.core._resource_mutator import ResourceMutator from databricks.bundles.core._resources import Resources from databricks.bundles.core._variable import ( Variable, diff --git a/python/databricks/bundles/core/_generated/__init__.py b/python/databricks/bundles/core/_generated/__init__.py new file mode 100644 index 0000000000..ade9313238 --- /dev/null +++ b/python/databricks/bundles/core/_generated/__init__.py @@ -0,0 +1,61 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from typing import TYPE_CHECKING + +from databricks.bundles.core._generated.alerts import _AlertResources, alert_mutator +from databricks.bundles.core._generated.catalogs import ( + _CatalogResources, + catalog_mutator, +) +from databricks.bundles.core._generated.jobs import _JobResources, job_mutator +from databricks.bundles.core._generated.pipelines import ( + _PipelineResources, + pipeline_mutator, +) +from databricks.bundles.core._generated.schemas import _SchemaResources, schema_mutator +from databricks.bundles.core._generated.volumes import _VolumeResources, volume_mutator + +if TYPE_CHECKING: + from databricks.bundles.core._resource_type import _ResourceType + +__all__ = [ + "_GeneratedResources", + "_all_resource_types", + "alert_mutator", + "catalog_mutator", + "job_mutator", + "pipeline_mutator", + "schema_mutator", + "volume_mutator", +] + + +class _GeneratedResources( + _AlertResources, + _CatalogResources, + _JobResources, + _PipelineResources, + _SchemaResources, + _VolumeResources, +): + pass + + +def _all_resource_types() -> "tuple[_ResourceType, ...]": + from databricks.bundles.core._generated import ( + alerts, + catalogs, + jobs, + pipelines, + schemas, + volumes, + ) + + return ( + alerts._resource_type(), + catalogs._resource_type(), + jobs._resource_type(), + pipelines._resource_type(), + schemas._resource_type(), + volumes._resource_type(), + ) diff --git a/python/databricks/bundles/core/_generated/alerts.py b/python/databricks/bundles/core/_generated/alerts.py new file mode 100644 index 0000000000..94e4af9a13 --- /dev/null +++ b/python/databricks/bundles/core/_generated/alerts.py @@ -0,0 +1,115 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from collections.abc import Callable +from typing import TYPE_CHECKING, Optional, overload + +from databricks.bundles.core._bundle import Bundle +from databricks.bundles.core._location import Location +from databricks.bundles.core._resource_mutator import ResourceMutator +from databricks.bundles.core._transform import _transform + +if TYPE_CHECKING: + from databricks.bundles.alerts._models.alert import Alert, AlertParam + from databricks.bundles.core._resource_type import _ResourceType + + +def _resource_type() -> "_ResourceType": + from databricks.bundles.alerts._models.alert import Alert + from databricks.bundles.core._resource_type import _ResourceType + + return _ResourceType( + resource_type=Alert, + singular_name="alert", + plural_name="alerts", + ) + + +class _AlertResources: + """ + Generated alert accessors, mixed into Resources. + """ + + # Provided by the Resources subclass; declared here so the generated methods + # below type-check. + _resources: dict[str, dict] + + if TYPE_CHECKING: + + def add_location(self, path: tuple[str, ...], location: Location) -> None: ... + + def add_diagnostic_error( + self, + msg: str, + *, + detail: Optional[str] = None, + path: Optional[tuple[str, ...]] = None, + location: Optional[Location] = None, + ) -> None: ... + + @property + def alerts(self) -> dict[str, "Alert"]: + return self._resources["alerts"] + + def add_alert( + self, + resource_name: str, + alert: "AlertParam", + *, + location: Optional[Location] = None, + ) -> None: + """ + Adds the resource alert to the collection of resources. Resource name must be unique across all alerts. + + :param resource_name: unique identifier for the alert + :param alert: the alert to add, can be Alert or dict + :param location: optional location of the alert in the source code + """ + from databricks.bundles.alerts._models.alert import Alert + + alert = _transform(Alert, alert) + path = ("resources", "alerts", resource_name) + location = location or Location.from_stack_frame(depth=1) + + if self._resources["alerts"].get(resource_name): + self.add_diagnostic_error( + msg=f"Duplicate resource name '{resource_name}' for resource 'alert'. Resource names must be unique.", + location=location, + path=path, + ) + else: + if location: + self.add_location(path, location) + + self._resources["alerts"][resource_name] = alert + + +@overload +def alert_mutator( + function: Callable[[Bundle, "Alert"], "Alert"], +) -> ResourceMutator["Alert"]: ... + + +@overload +def alert_mutator( + function: Callable[["Alert"], "Alert"], +) -> ResourceMutator["Alert"]: ... + + +def alert_mutator(function: Callable) -> ResourceMutator["Alert"]: + """ + Decorator for defining mutator for alerts. Function should return a new instance of the alert + with the desired changes, instead of mutating the input alert. + + Example: + + .. code-block:: python + + @alert_mutator + def my_alert_mutator(bundle: Bundle, alert: Alert) -> Alert: + return replace(alert, ...) + + :param function: Function that mutates alerts. + """ + from databricks.bundles.alerts._models.alert import Alert + + return ResourceMutator(resource_type=Alert, function=function) diff --git a/python/databricks/bundles/core/_generated/catalogs.py b/python/databricks/bundles/core/_generated/catalogs.py new file mode 100644 index 0000000000..55d6bbcd31 --- /dev/null +++ b/python/databricks/bundles/core/_generated/catalogs.py @@ -0,0 +1,115 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from collections.abc import Callable +from typing import TYPE_CHECKING, Optional, overload + +from databricks.bundles.core._bundle import Bundle +from databricks.bundles.core._location import Location +from databricks.bundles.core._resource_mutator import ResourceMutator +from databricks.bundles.core._transform import _transform + +if TYPE_CHECKING: + from databricks.bundles.catalogs._models.catalog import Catalog, CatalogParam + from databricks.bundles.core._resource_type import _ResourceType + + +def _resource_type() -> "_ResourceType": + from databricks.bundles.catalogs._models.catalog import Catalog + from databricks.bundles.core._resource_type import _ResourceType + + return _ResourceType( + resource_type=Catalog, + singular_name="catalog", + plural_name="catalogs", + ) + + +class _CatalogResources: + """ + Generated catalog accessors, mixed into Resources. + """ + + # Provided by the Resources subclass; declared here so the generated methods + # below type-check. + _resources: dict[str, dict] + + if TYPE_CHECKING: + + def add_location(self, path: tuple[str, ...], location: Location) -> None: ... + + def add_diagnostic_error( + self, + msg: str, + *, + detail: Optional[str] = None, + path: Optional[tuple[str, ...]] = None, + location: Optional[Location] = None, + ) -> None: ... + + @property + def catalogs(self) -> dict[str, "Catalog"]: + return self._resources["catalogs"] + + def add_catalog( + self, + resource_name: str, + catalog: "CatalogParam", + *, + location: Optional[Location] = None, + ) -> None: + """ + Adds the resource catalog to the collection of resources. Resource name must be unique across all catalogs. + + :param resource_name: unique identifier for the catalog + :param catalog: the catalog to add, can be Catalog or dict + :param location: optional location of the catalog in the source code + """ + from databricks.bundles.catalogs._models.catalog import Catalog + + catalog = _transform(Catalog, catalog) + path = ("resources", "catalogs", resource_name) + location = location or Location.from_stack_frame(depth=1) + + if self._resources["catalogs"].get(resource_name): + self.add_diagnostic_error( + msg=f"Duplicate resource name '{resource_name}' for resource 'catalog'. Resource names must be unique.", + location=location, + path=path, + ) + else: + if location: + self.add_location(path, location) + + self._resources["catalogs"][resource_name] = catalog + + +@overload +def catalog_mutator( + function: Callable[[Bundle, "Catalog"], "Catalog"], +) -> ResourceMutator["Catalog"]: ... + + +@overload +def catalog_mutator( + function: Callable[["Catalog"], "Catalog"], +) -> ResourceMutator["Catalog"]: ... + + +def catalog_mutator(function: Callable) -> ResourceMutator["Catalog"]: + """ + Decorator for defining mutator for catalogs. Function should return a new instance of the catalog + with the desired changes, instead of mutating the input catalog. + + Example: + + .. code-block:: python + + @catalog_mutator + def my_catalog_mutator(bundle: Bundle, catalog: Catalog) -> Catalog: + return replace(catalog, ...) + + :param function: Function that mutates catalogs. + """ + from databricks.bundles.catalogs._models.catalog import Catalog + + return ResourceMutator(resource_type=Catalog, function=function) diff --git a/python/databricks/bundles/core/_generated/jobs.py b/python/databricks/bundles/core/_generated/jobs.py new file mode 100644 index 0000000000..8653c55055 --- /dev/null +++ b/python/databricks/bundles/core/_generated/jobs.py @@ -0,0 +1,115 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from collections.abc import Callable +from typing import TYPE_CHECKING, Optional, overload + +from databricks.bundles.core._bundle import Bundle +from databricks.bundles.core._location import Location +from databricks.bundles.core._resource_mutator import ResourceMutator +from databricks.bundles.core._transform import _transform + +if TYPE_CHECKING: + from databricks.bundles.core._resource_type import _ResourceType + from databricks.bundles.jobs._models.job import Job, JobParam + + +def _resource_type() -> "_ResourceType": + from databricks.bundles.core._resource_type import _ResourceType + from databricks.bundles.jobs._models.job import Job + + return _ResourceType( + resource_type=Job, + singular_name="job", + plural_name="jobs", + ) + + +class _JobResources: + """ + Generated job accessors, mixed into Resources. + """ + + # Provided by the Resources subclass; declared here so the generated methods + # below type-check. + _resources: dict[str, dict] + + if TYPE_CHECKING: + + def add_location(self, path: tuple[str, ...], location: Location) -> None: ... + + def add_diagnostic_error( + self, + msg: str, + *, + detail: Optional[str] = None, + path: Optional[tuple[str, ...]] = None, + location: Optional[Location] = None, + ) -> None: ... + + @property + def jobs(self) -> dict[str, "Job"]: + return self._resources["jobs"] + + def add_job( + self, + resource_name: str, + job: "JobParam", + *, + location: Optional[Location] = None, + ) -> None: + """ + Adds the resource job to the collection of resources. Resource name must be unique across all jobs. + + :param resource_name: unique identifier for the job + :param job: the job to add, can be Job or dict + :param location: optional location of the job in the source code + """ + from databricks.bundles.jobs._models.job import Job + + job = _transform(Job, job) + path = ("resources", "jobs", resource_name) + location = location or Location.from_stack_frame(depth=1) + + if self._resources["jobs"].get(resource_name): + self.add_diagnostic_error( + msg=f"Duplicate resource name '{resource_name}' for resource 'job'. Resource names must be unique.", + location=location, + path=path, + ) + else: + if location: + self.add_location(path, location) + + self._resources["jobs"][resource_name] = job + + +@overload +def job_mutator( + function: Callable[[Bundle, "Job"], "Job"], +) -> ResourceMutator["Job"]: ... + + +@overload +def job_mutator( + function: Callable[["Job"], "Job"], +) -> ResourceMutator["Job"]: ... + + +def job_mutator(function: Callable) -> ResourceMutator["Job"]: + """ + Decorator for defining mutator for jobs. Function should return a new instance of the job + with the desired changes, instead of mutating the input job. + + Example: + + .. code-block:: python + + @job_mutator + def my_job_mutator(bundle: Bundle, job: Job) -> Job: + return replace(job, ...) + + :param function: Function that mutates jobs. + """ + from databricks.bundles.jobs._models.job import Job + + return ResourceMutator(resource_type=Job, function=function) diff --git a/python/databricks/bundles/core/_generated/pipelines.py b/python/databricks/bundles/core/_generated/pipelines.py new file mode 100644 index 0000000000..961070a6e4 --- /dev/null +++ b/python/databricks/bundles/core/_generated/pipelines.py @@ -0,0 +1,115 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from collections.abc import Callable +from typing import TYPE_CHECKING, Optional, overload + +from databricks.bundles.core._bundle import Bundle +from databricks.bundles.core._location import Location +from databricks.bundles.core._resource_mutator import ResourceMutator +from databricks.bundles.core._transform import _transform + +if TYPE_CHECKING: + from databricks.bundles.core._resource_type import _ResourceType + from databricks.bundles.pipelines._models.pipeline import Pipeline, PipelineParam + + +def _resource_type() -> "_ResourceType": + from databricks.bundles.core._resource_type import _ResourceType + from databricks.bundles.pipelines._models.pipeline import Pipeline + + return _ResourceType( + resource_type=Pipeline, + singular_name="pipeline", + plural_name="pipelines", + ) + + +class _PipelineResources: + """ + Generated pipeline accessors, mixed into Resources. + """ + + # Provided by the Resources subclass; declared here so the generated methods + # below type-check. + _resources: dict[str, dict] + + if TYPE_CHECKING: + + def add_location(self, path: tuple[str, ...], location: Location) -> None: ... + + def add_diagnostic_error( + self, + msg: str, + *, + detail: Optional[str] = None, + path: Optional[tuple[str, ...]] = None, + location: Optional[Location] = None, + ) -> None: ... + + @property + def pipelines(self) -> dict[str, "Pipeline"]: + return self._resources["pipelines"] + + def add_pipeline( + self, + resource_name: str, + pipeline: "PipelineParam", + *, + location: Optional[Location] = None, + ) -> None: + """ + Adds the resource pipeline to the collection of resources. Resource name must be unique across all pipelines. + + :param resource_name: unique identifier for the pipeline + :param pipeline: the pipeline to add, can be Pipeline or dict + :param location: optional location of the pipeline in the source code + """ + from databricks.bundles.pipelines._models.pipeline import Pipeline + + pipeline = _transform(Pipeline, pipeline) + path = ("resources", "pipelines", resource_name) + location = location or Location.from_stack_frame(depth=1) + + if self._resources["pipelines"].get(resource_name): + self.add_diagnostic_error( + msg=f"Duplicate resource name '{resource_name}' for resource 'pipeline'. Resource names must be unique.", + location=location, + path=path, + ) + else: + if location: + self.add_location(path, location) + + self._resources["pipelines"][resource_name] = pipeline + + +@overload +def pipeline_mutator( + function: Callable[[Bundle, "Pipeline"], "Pipeline"], +) -> ResourceMutator["Pipeline"]: ... + + +@overload +def pipeline_mutator( + function: Callable[["Pipeline"], "Pipeline"], +) -> ResourceMutator["Pipeline"]: ... + + +def pipeline_mutator(function: Callable) -> ResourceMutator["Pipeline"]: + """ + Decorator for defining mutator for pipelines. Function should return a new instance of the pipeline + with the desired changes, instead of mutating the input pipeline. + + Example: + + .. code-block:: python + + @pipeline_mutator + def my_pipeline_mutator(bundle: Bundle, pipeline: Pipeline) -> Pipeline: + return replace(pipeline, ...) + + :param function: Function that mutates pipelines. + """ + from databricks.bundles.pipelines._models.pipeline import Pipeline + + return ResourceMutator(resource_type=Pipeline, function=function) diff --git a/python/databricks/bundles/core/_generated/schemas.py b/python/databricks/bundles/core/_generated/schemas.py new file mode 100644 index 0000000000..e83fe02c41 --- /dev/null +++ b/python/databricks/bundles/core/_generated/schemas.py @@ -0,0 +1,115 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from collections.abc import Callable +from typing import TYPE_CHECKING, Optional, overload + +from databricks.bundles.core._bundle import Bundle +from databricks.bundles.core._location import Location +from databricks.bundles.core._resource_mutator import ResourceMutator +from databricks.bundles.core._transform import _transform + +if TYPE_CHECKING: + from databricks.bundles.core._resource_type import _ResourceType + from databricks.bundles.schemas._models.schema import Schema, SchemaParam + + +def _resource_type() -> "_ResourceType": + from databricks.bundles.core._resource_type import _ResourceType + from databricks.bundles.schemas._models.schema import Schema + + return _ResourceType( + resource_type=Schema, + singular_name="schema", + plural_name="schemas", + ) + + +class _SchemaResources: + """ + Generated schema accessors, mixed into Resources. + """ + + # Provided by the Resources subclass; declared here so the generated methods + # below type-check. + _resources: dict[str, dict] + + if TYPE_CHECKING: + + def add_location(self, path: tuple[str, ...], location: Location) -> None: ... + + def add_diagnostic_error( + self, + msg: str, + *, + detail: Optional[str] = None, + path: Optional[tuple[str, ...]] = None, + location: Optional[Location] = None, + ) -> None: ... + + @property + def schemas(self) -> dict[str, "Schema"]: + return self._resources["schemas"] + + def add_schema( + self, + resource_name: str, + schema: "SchemaParam", + *, + location: Optional[Location] = None, + ) -> None: + """ + Adds the resource schema to the collection of resources. Resource name must be unique across all schemas. + + :param resource_name: unique identifier for the schema + :param schema: the schema to add, can be Schema or dict + :param location: optional location of the schema in the source code + """ + from databricks.bundles.schemas._models.schema import Schema + + schema = _transform(Schema, schema) + path = ("resources", "schemas", resource_name) + location = location or Location.from_stack_frame(depth=1) + + if self._resources["schemas"].get(resource_name): + self.add_diagnostic_error( + msg=f"Duplicate resource name '{resource_name}' for resource 'schema'. Resource names must be unique.", + location=location, + path=path, + ) + else: + if location: + self.add_location(path, location) + + self._resources["schemas"][resource_name] = schema + + +@overload +def schema_mutator( + function: Callable[[Bundle, "Schema"], "Schema"], +) -> ResourceMutator["Schema"]: ... + + +@overload +def schema_mutator( + function: Callable[["Schema"], "Schema"], +) -> ResourceMutator["Schema"]: ... + + +def schema_mutator(function: Callable) -> ResourceMutator["Schema"]: + """ + Decorator for defining mutator for schemas. Function should return a new instance of the schema + with the desired changes, instead of mutating the input schema. + + Example: + + .. code-block:: python + + @schema_mutator + def my_schema_mutator(bundle: Bundle, schema: Schema) -> Schema: + return replace(schema, ...) + + :param function: Function that mutates schemas. + """ + from databricks.bundles.schemas._models.schema import Schema + + return ResourceMutator(resource_type=Schema, function=function) diff --git a/python/databricks/bundles/core/_generated/volumes.py b/python/databricks/bundles/core/_generated/volumes.py new file mode 100644 index 0000000000..fe56e45ce3 --- /dev/null +++ b/python/databricks/bundles/core/_generated/volumes.py @@ -0,0 +1,115 @@ +# Code generated by pydabs-codegen. DO NOT EDIT. + +from collections.abc import Callable +from typing import TYPE_CHECKING, Optional, overload + +from databricks.bundles.core._bundle import Bundle +from databricks.bundles.core._location import Location +from databricks.bundles.core._resource_mutator import ResourceMutator +from databricks.bundles.core._transform import _transform + +if TYPE_CHECKING: + from databricks.bundles.core._resource_type import _ResourceType + from databricks.bundles.volumes._models.volume import Volume, VolumeParam + + +def _resource_type() -> "_ResourceType": + from databricks.bundles.core._resource_type import _ResourceType + from databricks.bundles.volumes._models.volume import Volume + + return _ResourceType( + resource_type=Volume, + singular_name="volume", + plural_name="volumes", + ) + + +class _VolumeResources: + """ + Generated volume accessors, mixed into Resources. + """ + + # Provided by the Resources subclass; declared here so the generated methods + # below type-check. + _resources: dict[str, dict] + + if TYPE_CHECKING: + + def add_location(self, path: tuple[str, ...], location: Location) -> None: ... + + def add_diagnostic_error( + self, + msg: str, + *, + detail: Optional[str] = None, + path: Optional[tuple[str, ...]] = None, + location: Optional[Location] = None, + ) -> None: ... + + @property + def volumes(self) -> dict[str, "Volume"]: + return self._resources["volumes"] + + def add_volume( + self, + resource_name: str, + volume: "VolumeParam", + *, + location: Optional[Location] = None, + ) -> None: + """ + Adds the resource volume to the collection of resources. Resource name must be unique across all volumes. + + :param resource_name: unique identifier for the volume + :param volume: the volume to add, can be Volume or dict + :param location: optional location of the volume in the source code + """ + from databricks.bundles.volumes._models.volume import Volume + + volume = _transform(Volume, volume) + path = ("resources", "volumes", resource_name) + location = location or Location.from_stack_frame(depth=1) + + if self._resources["volumes"].get(resource_name): + self.add_diagnostic_error( + msg=f"Duplicate resource name '{resource_name}' for resource 'volume'. Resource names must be unique.", + location=location, + path=path, + ) + else: + if location: + self.add_location(path, location) + + self._resources["volumes"][resource_name] = volume + + +@overload +def volume_mutator( + function: Callable[[Bundle, "Volume"], "Volume"], +) -> ResourceMutator["Volume"]: ... + + +@overload +def volume_mutator( + function: Callable[["Volume"], "Volume"], +) -> ResourceMutator["Volume"]: ... + + +def volume_mutator(function: Callable) -> ResourceMutator["Volume"]: + """ + Decorator for defining mutator for volumes. Function should return a new instance of the volume + with the desired changes, instead of mutating the input volume. + + Example: + + .. code-block:: python + + @volume_mutator + def my_volume_mutator(bundle: Bundle, volume: Volume) -> Volume: + return replace(volume, ...) + + :param function: Function that mutates volumes. + """ + from databricks.bundles.volumes._models.volume import Volume + + return ResourceMutator(resource_type=Volume, function=function) diff --git a/python/databricks/bundles/core/_resource_mutator.py b/python/databricks/bundles/core/_resource_mutator.py index fafdcdc5ef..22ea17cfc5 100644 --- a/python/databricks/bundles/core/_resource_mutator.py +++ b/python/databricks/bundles/core/_resource_mutator.py @@ -1,18 +1,9 @@ from collections.abc import Callable from dataclasses import dataclass -from typing import TYPE_CHECKING, Generic, Type, TypeVar, overload +from typing import Generic, Type, TypeVar -from databricks.bundles.core._bundle import Bundle from databricks.bundles.core._resource import Resource -if TYPE_CHECKING: - from databricks.bundles.alerts._models.alert import Alert - from databricks.bundles.catalogs._models.catalog import Catalog - from databricks.bundles.jobs._models.job import Job - from databricks.bundles.pipelines._models.pipeline import Pipeline - from databricks.bundles.schemas._models.schema import Schema - from databricks.bundles.volumes._models.volume import Volume - _T = TypeVar("_T", bound=Resource) @@ -57,8 +48,9 @@ def my_job_mutator(bundle: Bundle, job: Job) -> Job: """ -# Below, we define decorators for each resource type. This approach allows us -# to implement mutators that are only applied for specific resource types. +# A decorator is generated for each resource type (see +# _generated/_resource_mutators.py). This approach allows us to implement +# mutators that are only applied for specific resource types. # # Alternative approaches considered and rejected during design: # @@ -69,193 +61,3 @@ def my_job_mutator(bundle: Bundle, job: Job) -> Job: # - Using a universal @mutator decorator. # Rationale: Determining whether a mutator is invoked based solely on type annotations # was deemed overly implicit and potentially confusing. - - -@overload -def alert_mutator( - function: Callable[[Bundle, "Alert"], "Alert"], -) -> ResourceMutator["Alert"]: ... - - -@overload -def alert_mutator( - function: Callable[["Alert"], "Alert"], -) -> ResourceMutator["Alert"]: ... - - -def alert_mutator(function: Callable) -> ResourceMutator["Alert"]: - """ - Decorator for defining an alert mutator. Function should return a new instance of the alert with the desired changes, - instead of mutating the input alert. - - Example: - - .. code-block:: python - - @alert_mutator - def my_alert_mutator(bundle: Bundle, alert: Alert) -> Alert: - return replace(alert, display_name="my_alert") - - :param function: Function that mutates an alert. - """ - from databricks.bundles.alerts._models.alert import Alert - - return ResourceMutator(resource_type=Alert, function=function) - - -@overload -def catalog_mutator( - function: Callable[[Bundle, "Catalog"], "Catalog"], -) -> ResourceMutator["Catalog"]: ... - - -@overload -def catalog_mutator( - function: Callable[["Catalog"], "Catalog"], -) -> ResourceMutator["Catalog"]: ... - - -def catalog_mutator(function: Callable) -> ResourceMutator["Catalog"]: - """ - Decorator for defining a catalog mutator. Function should return a new instance of the catalog with the desired changes, - instead of mutating the input catalog. - - Example: - - .. code-block:: python - - @catalog_mutator - def my_catalog_mutator(bundle: Bundle, catalog: Catalog) -> Catalog: - return replace(catalog, name="my_catalog") - - :param function: Function that mutates a catalog. - """ - from databricks.bundles.catalogs._models.catalog import Catalog - - return ResourceMutator(resource_type=Catalog, function=function) - - -@overload -def job_mutator( - function: Callable[[Bundle, "Job"], "Job"], -) -> ResourceMutator["Job"]: ... - - -@overload -def job_mutator(function: Callable[["Job"], "Job"]) -> ResourceMutator["Job"]: ... - - -def job_mutator(function: Callable) -> ResourceMutator["Job"]: - """ - Decorator for defining a job mutator. Function should return a new instance of the job with the desired changes, - instead of mutating the input job. - - Example: - - .. code-block:: python - - @job_mutator - def my_job_mutator(bundle: Bundle, job: Job) -> Job: - return replace(job, name="my_job") - - :param function: Function that mutates a job. - """ - from databricks.bundles.jobs._models.job import Job - - return ResourceMutator(resource_type=Job, function=function) - - -@overload -def pipeline_mutator( - function: Callable[[Bundle, "Pipeline"], "Pipeline"], -) -> ResourceMutator["Pipeline"]: ... - - -@overload -def pipeline_mutator( - function: Callable[["Pipeline"], "Pipeline"], -) -> ResourceMutator["Pipeline"]: ... - - -def pipeline_mutator(function: Callable) -> ResourceMutator["Pipeline"]: - """ - Decorator for defining a pipeline mutator. Function should return a new instance of the pipeline with the desired changes, - instead of mutating the input pipeline. - - Example: - - .. code-block:: python - - @pipeline_mutator - def my_pipeline_mutator(bundle: Bundle, pipeline: Pipeline) -> Pipeline: - return replace(pipeline, name="my_job") - - :param function: Function that mutates a pipeline. - """ - from databricks.bundles.pipelines._models.pipeline import Pipeline - - return ResourceMutator(resource_type=Pipeline, function=function) - - -@overload -def schema_mutator( - function: Callable[[Bundle, "Schema"], "Schema"], -) -> ResourceMutator["Schema"]: ... - - -@overload -def schema_mutator( - function: Callable[["Schema"], "Schema"], -) -> ResourceMutator["Schema"]: ... - - -def schema_mutator(function: Callable) -> ResourceMutator["Schema"]: - """ - Decorator for defining a schema mutator. Function should return a new instance of the schema with the desired changes, - instead of mutating the input schema. - - Example: - - .. code-block:: python - - @schema_mutator - def my_schema_mutator(bundle: Bundle, schema: Schema) -> Schema: - return replace(schema, name="my_schema") - - :param function: Function that mutates a schema. - """ - from databricks.bundles.schemas._models.schema import Schema - - return ResourceMutator(resource_type=Schema, function=function) - - -@overload -def volume_mutator( - function: Callable[[Bundle, "Volume"], "Volume"], -) -> ResourceMutator["Volume"]: ... - - -@overload -def volume_mutator( - function: Callable[["Volume"], "Volume"], -) -> ResourceMutator["Volume"]: ... - - -def volume_mutator(function: Callable) -> ResourceMutator["Volume"]: - """ - Decorator for defining a volume mutator. Function should return a new instance of the volume with the desired changes, - instead of mutating the input volume. - - Example: - - .. code-block:: python - - @volume_mutator - def my_volume_mutator(bundle: Bundle, volume: Volume) -> Volume: - return replace(volume, name="my_volume") - - :param function: Function that mutates a volume. - """ - from databricks.bundles.volumes._models.volume import Volume - - return ResourceMutator(resource_type=Volume, function=function) diff --git a/python/databricks/bundles/core/_resource_type.py b/python/databricks/bundles/core/_resource_type.py index 73d30a9207..53fd17c169 100644 --- a/python/databricks/bundles/core/_resource_type.py +++ b/python/databricks/bundles/core/_resource_type.py @@ -27,46 +27,6 @@ def all(cls) -> tuple["_ResourceType", ...]: """ Returns all supported resource types. """ + from databricks.bundles.core._generated import _all_resource_types - # intentionally lazily load all resource types to avoid imports from databricks.bundles.core to - # be imported in databricks.bundles. - - from databricks.bundles.alerts._models.alert import Alert - from databricks.bundles.catalogs._models.catalog import Catalog - from databricks.bundles.jobs._models.job import Job - from databricks.bundles.pipelines._models.pipeline import Pipeline - from databricks.bundles.schemas._models.schema import Schema - from databricks.bundles.volumes._models.volume import Volume - - return ( - _ResourceType( - resource_type=Job, - singular_name="job", - plural_name="jobs", - ), - _ResourceType( - resource_type=Pipeline, - plural_name="pipelines", - singular_name="pipeline", - ), - _ResourceType( - resource_type=Volume, - plural_name="volumes", - singular_name="volume", - ), - _ResourceType( - resource_type=Schema, - plural_name="schemas", - singular_name="schema", - ), - _ResourceType( - resource_type=Alert, - plural_name="alerts", - singular_name="alert", - ), - _ResourceType( - resource_type=Catalog, - plural_name="catalogs", - singular_name="catalog", - ), - ) + return _all_resource_types() diff --git a/python/databricks/bundles/core/_resources.py b/python/databricks/bundles/core/_resources.py index b926f14b75..6818031596 100644 --- a/python/databricks/bundles/core/_resources.py +++ b/python/databricks/bundles/core/_resources.py @@ -1,22 +1,15 @@ -from typing import TYPE_CHECKING, Optional +from typing import Optional from databricks.bundles.core._diagnostics import Diagnostics +from databricks.bundles.core._generated import _GeneratedResources from databricks.bundles.core._location import Location from databricks.bundles.core._resource import Resource -from databricks.bundles.core._transform import _transform - -if TYPE_CHECKING: - from databricks.bundles.alerts._models.alert import Alert, AlertParam - from databricks.bundles.catalogs._models.catalog import Catalog, CatalogParam - from databricks.bundles.jobs._models.job import Job, JobParam - from databricks.bundles.pipelines._models.pipeline import Pipeline, PipelineParam - from databricks.bundles.schemas._models.schema import Schema, SchemaParam - from databricks.bundles.volumes._models.volume import Volume, VolumeParam +from databricks.bundles.core._resource_type import _ResourceType __all__ = ["Resources"] -class Resources: +class Resources(_GeneratedResources): """ Resources is a collection of resources in a bundle. @@ -58,31 +51,12 @@ def load_resources(bundle: Bundle) -> Resources: """ def __init__(self): - self._jobs = dict[str, "Job"]() - self._pipelines = dict[str, "Pipeline"]() - self._schemas = dict[str, "Schema"]() - self._volumes = dict[str, "Volume"]() - self._alerts = dict[str, "Alert"]() - self._catalogs = dict[str, "Catalog"]() + self._resources: dict[str, dict] = { + resource_type.plural_name: {} for resource_type in _ResourceType.all() + } self._locations = dict[tuple[str, ...], Location]() self._diagnostics = Diagnostics() - @property - def jobs(self) -> dict[str, "Job"]: - return self._jobs - - @property - def pipelines(self) -> dict[str, "Pipeline"]: - return self._pipelines - - @property - def schemas(self) -> dict[str, "Schema"]: - return self._schemas - - @property - def volumes(self) -> dict[str, "Volume"]: - return self._volumes - @property def diagnostics(self) -> Diagnostics: """ @@ -90,14 +64,6 @@ def diagnostics(self) -> Diagnostics: """ return self._diagnostics - @property - def alerts(self) -> dict[str, "Alert"]: - return self._alerts - - @property - def catalogs(self) -> dict[str, "Catalog"]: - return self._catalogs - def add_resource( self, resource_name: str, @@ -114,218 +80,15 @@ def add_resource( :param location: optional location of the resource in the source code """ - from databricks.bundles.alerts import Alert - from databricks.bundles.catalogs import Catalog - from databricks.bundles.jobs import Job - from databricks.bundles.pipelines import Pipeline - from databricks.bundles.schemas import Schema - from databricks.bundles.volumes import Volume - - location = location or Location.from_stack_frame(depth=1) - - match resource: - case Job(): - self.add_job(resource_name, resource, location=location) - case Pipeline(): - self.add_pipeline(resource_name, resource, location=location) - case Schema(): - self.add_schema(resource_name, resource, location=location) - case Volume(): - self.add_volume(resource_name, resource, location=location) - case Alert(): - self.add_alert(resource_name, resource, location=location) - case Catalog(): - self.add_catalog(resource_name, resource, location=location) - case _: - raise ValueError(f"Unsupported resource type: {type(resource)}") - - def add_job( - self, - resource_name: str, - job: "JobParam", - *, - location: Optional[Location] = None, - ) -> None: - """ - Adds a job to the collection of resources. Resource name must be unique across all jobs. - - :param resource_name: unique identifier for the job - :param job: the job to add, can be Job or dict - :param location: optional location of the job in the source code - """ - from databricks.bundles.jobs import Job - - job = _transform(Job, job) - path = ("resources", "jobs", resource_name) - location = location or Location.from_stack_frame(depth=1) - - if self._jobs.get(resource_name): - self.add_diagnostic_error( - msg=f"Duplicate resource name '{resource_name}' for a job. Resource names must be unique.", - location=location, - path=path, - ) - else: - if location: - self.add_location(path, location) - - self._jobs[resource_name] = job - - def add_pipeline( - self, - resource_name: str, - pipeline: "PipelineParam", - *, - location: Optional[Location] = None, - ) -> None: - """ - Adds a pipeline to the collection of resources. Resource name must be unique across all pipelines. - - :param resource_name: unique identifier for the pipeline - :param pipeline: the pipeline to add, can be Pipeline or dict - :param location: optional location of the pipeline in the source code - """ - from databricks.bundles.pipelines import Pipeline - - pipeline = _transform(Pipeline, pipeline) - path = ("resources", "pipelines", resource_name) - location = location or Location.from_stack_frame(depth=1) - - if self._pipelines.get(resource_name): - self.add_diagnostic_error( - msg=f"Duplicate resource name '{resource_name}' for a pipeline. Resource names must be unique.", - location=location, - path=path, - ) - else: - if location: - self.add_location(path, location) - - self._pipelines[resource_name] = pipeline - - def add_schema( - self, - resource_name: str, - schema: "SchemaParam", - *, - location: Optional[Location] = None, - ) -> None: - """ - Adds a schema to the collection of resources. Resource name must be unique across all schemas. - - :param resource_name: unique identifier for the schema - :param schema: the schema to add, can be Schema or dict - :param location: optional location of the schema in the source code - """ - from databricks.bundles.schemas import Schema - - schema = _transform(Schema, schema) - path = ("resources", "schemas", resource_name) - location = location or Location.from_stack_frame(depth=1) - - if self._schemas.get(resource_name): - self.add_diagnostic_error( - msg=f"Duplicate resource name '{resource_name}' for a schema. Resource names must be unique.", - location=location, - path=path, - ) - else: - if location: - self.add_location(path, location) - - self._schemas[resource_name] = schema - - def add_volume( - self, - resource_name: str, - volume: "VolumeParam", - *, - location: Optional[Location] = None, - ) -> None: - """ - Adds a volume to the collection of resources. Resource name must be unique across all volumes. - - :param resource_name: unique identifier for the volume - :param volume: the volume to add, can be Volume or dict - :param location: optional location of the volume in the source code - """ - from databricks.bundles.volumes import Volume - - volume = _transform(Volume, volume) - path = ("resources", "volumes", resource_name) - location = location or Location.from_stack_frame(depth=1) - - if self._volumes.get(resource_name): - self.add_diagnostic_error( - msg=f"Duplicate resource name '{resource_name}' for a volume. Resource names must be unique.", - location=location, - path=path, - ) - else: - if location: - self.add_location(path, location) - - self._volumes[resource_name] = volume - - def add_alert( - self, - resource_name: str, - alert: "AlertParam", - *, - location: Optional[Location] = None, - ) -> None: - """ - Adds an alert to the collection of resources. Resource name must be unique across all alerts. - """ - from databricks.bundles.alerts import Alert - - alert = _transform(Alert, alert) - path = ("resources", "alerts", resource_name) - location = location or Location.from_stack_frame(depth=1) - - if self._alerts.get(resource_name): - self.add_diagnostic_error( - msg=f"Duplicate resource name '{resource_name}' for an alert. Resource names must be unique.", - location=location, - path=path, - ) - else: - if location: - self.add_location(path, location) - - self._alerts[resource_name] = alert - - def add_catalog( - self, - resource_name: str, - catalog: "CatalogParam", - *, - location: Optional[Location] = None, - ) -> None: - """ - Adds a catalog to the collection of resources. Resource name must be unique across all catalogs. - - :param resource_name: unique identifier for the catalog - :param catalog: the catalog to add, can be Catalog or dict - :param location: optional location of the catalog in the source code - """ - from databricks.bundles.catalogs import Catalog - - catalog = _transform(Catalog, catalog) - path = ("resources", "catalogs", resource_name) location = location or Location.from_stack_frame(depth=1) - if self._catalogs.get(resource_name): - self.add_diagnostic_error( - msg=f"Duplicate resource name '{resource_name}' for a catalog. Resource names must be unique.", - location=location, - path=path, - ) - else: - if location: - self.add_location(path, location) + for resource_type in _ResourceType.all(): + if isinstance(resource, resource_type.resource_type): + add_method = getattr(self, f"add_{resource_type.singular_name}") + add_method(resource_name, resource, location=location) + return - self._catalogs[resource_name] = catalog + raise ValueError(f"Unsupported resource type: {type(resource)}") def add_location(self, path: tuple[str, ...], location: Location) -> None: """ @@ -397,23 +160,10 @@ def add_resources(self, other: "Resources") -> None: Adds error to diagnostics if there are duplicate resource names. """ - for name, job in other.jobs.items(): - self.add_job(name, job) - - for name, pipeline in other.pipelines.items(): - self.add_pipeline(name, pipeline) - - for name, schema in other.schemas.items(): - self.add_schema(name, schema) - - for name, volume in other.volumes.items(): - self.add_volume(name, volume) - - for name, alert in other.alerts.items(): - self.add_alert(name, alert) - - for name, catalog in other.catalogs.items(): - self.add_catalog(name, catalog) + for resource_type in _ResourceType.all(): + add_method = getattr(self, f"add_{resource_type.singular_name}") + for name, resource in getattr(other, resource_type.plural_name).items(): + add_method(name, resource) for path, location in other._locations.items(): self.add_location(path, location) diff --git a/python/databricks_tests/core/test_resources.py b/python/databricks_tests/core/test_resources.py index d58650d893..ee2ab7ec40 100644 --- a/python/databricks_tests/core/test_resources.py +++ b/python/databricks_tests/core/test_resources.py @@ -11,11 +11,10 @@ from databricks.bundles.alerts._models.comparison_operator import ComparisonOperator from databricks.bundles.alerts._models.cron_schedule import CronSchedule from databricks.bundles.catalogs._models.catalog import Catalog -from databricks.bundles.core import Location, Resources, Severity -from databricks.bundles.core._bundle import Bundle -from databricks.bundles.core._resource import Resource -from databricks.bundles.core._resource_mutator import ( - ResourceMutator, +from databricks.bundles.core import ( + Location, + Resources, + Severity, alert_mutator, catalog_mutator, job_mutator, @@ -23,6 +22,9 @@ schema_mutator, volume_mutator, ) +from databricks.bundles.core._bundle import Bundle +from databricks.bundles.core._resource import Resource +from databricks.bundles.core._resource_mutator import ResourceMutator from databricks.bundles.core._resource_type import _ResourceType from databricks.bundles.jobs._models.job import Job from databricks.bundles.pipelines._models.pipeline import Pipeline @@ -36,7 +38,6 @@ class TestCase: dict_example: dict dataclass_example: Resource mutator: Callable - article: str = "a" # grammatical article in the duplicate-resource error message resource_types = {tpe.resource_type: tpe for tpe in _ResourceType.all()} @@ -115,7 +116,6 @@ class TestCase: ), ), mutator=alert_mutator, - article="an", ), resource_types[Alert], ), @@ -324,7 +324,7 @@ def test_add_duplicate_resource(tc: TestCase, tpe: _ResourceType): assert item.severity == Severity.ERROR assert ( item.summary - == f"Duplicate resource name 'my_resource' for {tc.article} {tpe.singular_name}. Resource names must be unique." + == f"Duplicate resource name 'my_resource' for resource '{tpe.singular_name}'. Resource names must be unique." ) diff --git a/python/databricks_tests/test_build.py b/python/databricks_tests/test_build.py index 65d9683e92..14fce61975 100644 --- a/python/databricks_tests/test_build.py +++ b/python/databricks_tests/test_build.py @@ -28,8 +28,8 @@ Resources, Severity, job_mutator, + pipeline_mutator, ) -from databricks.bundles.core._resource_mutator import pipeline_mutator from databricks.bundles.jobs import Job from databricks.bundles.pipelines._models.pipeline import Pipeline