diff --git a/.gitignore b/.gitignore index 4b9fe855d5..4cc8029386 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ test-output.json # Built by make for 'make fmt' and yamlcheck.py in acceptance tests tools/yamlfmt +tools/golangci-lint tools/yamlfmt.exe # Cache for tools/gh_report.py diff --git a/.vscode/settings.json b/.vscode/settings.json index a0f74f88ac..3b045a5be8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,5 +22,6 @@ "script": "shellscript", "script.prepare": "shellscript", "script.cleanup": "shellscript" - } + }, + "cursorpyright.analysis.stubPath": ".vscode" } diff --git a/acceptance/bundle/python/apps-support/databricks.yml b/acceptance/bundle/python/apps-support/databricks.yml new file mode 100644 index 0000000000..987889a01f --- /dev/null +++ b/acceptance/bundle/python/apps-support/databricks.yml @@ -0,0 +1,17 @@ +bundle: + name: my_project + +sync: {paths: []} # don't need to copy files + +experimental: + python: + resources: + - "resources:load_resources" + mutators: + - "mutators:update_app" + +resources: + apps: + my_app_1: + name: "My App" + description: "My first app" diff --git a/acceptance/bundle/python/apps-support/mutators.py b/acceptance/bundle/python/apps-support/mutators.py new file mode 100644 index 0000000000..5c90abeb8c --- /dev/null +++ b/acceptance/bundle/python/apps-support/mutators.py @@ -0,0 +1,11 @@ +from dataclasses import replace + +from databricks.bundles.core import app_mutator +from databricks.bundles.apps import App + + +@app_mutator +def update_app(app: App) -> App: + assert isinstance(app.name, str) + + return replace(app, name=f"{app.name} (updated)") diff --git a/acceptance/bundle/python/apps-support/resources.py b/acceptance/bundle/python/apps-support/resources.py new file mode 100644 index 0000000000..b10fb2398e --- /dev/null +++ b/acceptance/bundle/python/apps-support/resources.py @@ -0,0 +1,15 @@ +from databricks.bundles.core import Resources + + +def load_resources() -> Resources: + resources = Resources() + + resources.add_app( + "my_app_2", + { + "name": "My App (2)", + "description": "My second app", + }, + ) + + return resources diff --git a/acceptance/bundle/python/apps-support/script b/acceptance/bundle/python/apps-support/script new file mode 100755 index 0000000000..c94fd42a0c --- /dev/null +++ b/acceptance/bundle/python/apps-support/script @@ -0,0 +1,6 @@ +echo "$DATABRICKS_BUNDLES_WHEEL" > "requirements-latest.txt" + +trace uv run $UV_ARGS -q $CLI bundle validate --output json | \ + jq "pick(.experimental.python, .resources)" + +rm -fr .databricks __pycache__ diff --git a/experimental/python/codegen/codegen/packages.py b/experimental/python/codegen/codegen/packages.py index 48fe8270ab..34c4ab792b 100644 --- a/experimental/python/codegen/codegen/packages.py +++ b/experimental/python/codegen/codegen/packages.py @@ -7,6 +7,7 @@ "resources.Pipeline": "pipelines", "resources.Schema": "schemas", "resources.Volume": "volumes", + "resources.App": "apps", } RESOURCE_TYPES = list(RESOURCE_NAMESPACE.keys()) diff --git a/experimental/python/databricks/bundles/apps/__init__.py b/experimental/python/databricks/bundles/apps/__init__.py new file mode 100644 index 0000000000..be875e24f7 --- /dev/null +++ b/experimental/python/databricks/bundles/apps/__init__.py @@ -0,0 +1,81 @@ +from databricks.bundles.apps._models.app import App, AppDict, AppParam +from databricks.bundles.apps._models.app_config import ( + AppConfigDict, + AppConfigParam, + AppEnvVarDict, +) +from databricks.bundles.apps._models.app_permission import ( + AppPermission, + AppPermissionDict, + AppPermissionLevel, + AppPermissionParam, +) +from databricks.bundles.apps._models.app_resource import ( + AppResource, + AppResourceJob, + AppResourceJobDict, + AppResourceJobParam, + AppResourceParam, + AppResourceSecret, + AppResourceSecretDict, + AppResourceSecretParam, + AppResourceServingEndpoint, + AppResourceServingEndpointDict, + AppResourceServingEndpointParam, + AppResourceSqlWarehouse, + AppResourceSqlWarehouseDict, + AppResourceSqlWarehouseParam, + AppResourceUcSecurable, + AppResourceUcSecurableDict, + AppResourceUcSecurableParam, + JobPermission, + SecretPermission, + ServingEndpointPermission, + SqlWarehousePermission, + UcSecurablePermission, + UcSecurableType, +) +from databricks.bundles.apps._models.lifecycle import ( + Lifecycle, + LifecycleDict, + LifecycleParam, +) + +__all__ = [ + "App", + "AppConfigDict", + "AppConfigParam", + "AppDict", + "AppEnvVarDict", + "AppParam", + "AppPermission", + "AppPermissionDict", + "AppPermissionLevel", + "AppPermissionParam", + "AppResource", + "AppResourceJob", + "AppResourceJobDict", + "AppResourceJobParam", + "AppResourceParam", + "AppResourceSecret", + "AppResourceSecretDict", + "AppResourceSecretParam", + "AppResourceServingEndpoint", + "AppResourceServingEndpointDict", + "AppResourceServingEndpointParam", + "AppResourceSqlWarehouse", + "AppResourceSqlWarehouseDict", + "AppResourceSqlWarehouseParam", + "AppResourceUcSecurable", + "AppResourceUcSecurableDict", + "AppResourceUcSecurableParam", + "JobPermission", + "Lifecycle", + "LifecycleDict", + "LifecycleParam", + "SecretPermission", + "ServingEndpointPermission", + "SqlWarehousePermission", + "UcSecurablePermission", + "UcSecurableType", +] diff --git a/experimental/python/databricks/bundles/apps/_models/app.py b/experimental/python/databricks/bundles/apps/_models/app.py new file mode 100644 index 0000000000..eee8ced514 --- /dev/null +++ b/experimental/python/databricks/bundles/apps/_models/app.py @@ -0,0 +1,126 @@ +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, TypedDict + +from databricks.bundles.apps._models.app_permission import ( + AppPermission, + AppPermissionParam, +) +from databricks.bundles.apps._models.app_resource import AppResource, AppResourceParam +from databricks.bundles.apps._models.lifecycle import Lifecycle, LifecycleParam +from databricks.bundles.core._resource import Resource +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import ( + VariableOr, + VariableOrDict, + VariableOrList, + VariableOrOptional, +) + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class App(Resource): + """Databricks App resource""" + + name: VariableOr[str] + """ + The name of the app. The name must be unique within the workspace. + """ + + source_code_path: VariableOrOptional[str] = None + """ + Path to the app source code on local disk. This is used by DABs to deploy the app. + """ + + description: VariableOrOptional[str] = None + """ + The description of the app. + """ + + resources: VariableOrList[AppResource] = field(default_factory=list) + """ + A list of workspace resources associated with the app. + Each resource can be a job, secret, serving endpoint, SQL warehouse, or Unity Catalog securable. + """ + + permissions: VariableOrList[AppPermission] = field(default_factory=list) + """ + Access control list for the app. Multiple permissions can be defined for different principals. + """ + + lifecycle: VariableOrOptional[Lifecycle] = None + """ + Lifecycle is a struct that contains the lifecycle settings for a resource. It controls the behavior of the resource when it is deployed or destroyed. + """ + + config: VariableOrDict[Any] = field(default_factory=dict) + """ + Application-specific configuration. + + This can include various settings such as: + - command: List of strings for the command to run the app + - env: List of environment variable configurations with 'name', 'value', or 'valueFrom' + - Any other custom app-specific settings + + See AppConfigDict for common configuration structure. + """ + + @classmethod + def from_dict(cls, value: "AppDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "AppDict": + return _transform_to_json_value(self) # type:ignore + + +class AppDict(TypedDict, total=False): + """Databricks App resource""" + + name: VariableOr[str] + """ + The name of the app. The name must be unique within the workspace. + """ + + source_code_path: VariableOrOptional[str] + """ + Path to the app source code on local disk. This is used by DABs to deploy the app. + """ + + description: VariableOrOptional[str] + """ + The description of the app. + """ + + resources: VariableOrList[AppResourceParam] + """ + A list of workspace resources associated with the app. + Each resource can be a job, secret, serving endpoint, SQL warehouse, or Unity Catalog securable. + """ + + permissions: VariableOrList[AppPermissionParam] + """ + Access control list for the app. Multiple permissions can be defined for different principals. + """ + + lifecycle: VariableOrOptional[LifecycleParam] + """ + Lifecycle is a struct that contains the lifecycle settings for a resource. It controls the behavior of the resource when it is deployed or destroyed. + """ + + config: VariableOrDict[Any] + """ + Application-specific configuration. + + This can include various settings such as: + - command: List of strings for the command to run the app + - env: List of environment variable configurations with 'name', 'value', or 'valueFrom' + - Any other custom app-specific settings + + See AppConfigDict for common configuration structure. + """ + + +AppParam = AppDict | App diff --git a/experimental/python/databricks/bundles/apps/_models/app_config.py b/experimental/python/databricks/bundles/apps/_models/app_config.py new file mode 100644 index 0000000000..9a09fc67b1 --- /dev/null +++ b/experimental/python/databricks/bundles/apps/_models/app_config.py @@ -0,0 +1,53 @@ +from typing import Any, Dict, List, TypedDict, Union + +from databricks.bundles.core._variable import VariableOr, VariableOrOptional + + +class AppEnvVarDict(TypedDict, total=False): + """Environment variable configuration for an app""" + + name: VariableOr[str] + """The name of the environment variable""" + + value: VariableOrOptional[str] + """ + The value of the environment variable. + Either value or valueFrom must be specified, but not both. + """ + + valueFrom: VariableOrOptional[str] + """ + Reference to another environment variable to get the value from. + Either value or valueFrom must be specified, but not both. + """ + + +class AppConfigDict(TypedDict, total=False): + """ + Configuration for a Databricks app. + + This is a flexible dictionary structure that can contain various app-specific + configuration settings. Common configuration options include: + + - command: List of strings for the command to run the app + - env: List of environment variable configurations + - Any other app-specific settings + """ + + command: Union[List[str], VariableOr[str]] + """ + The command to run the app. This is typically a list of strings + representing the executable and its arguments. + Example: ["python", "app.py"] or ["streamlit", "run", "main.py"] + """ + + env: List[AppEnvVarDict] + """ + Environment variables to set for the app. + Each variable can have a direct value or reference another environment variable. + Example: [{"name": "PORT", "value": "8080"}, {"name": "DB_URL", "valueFrom": "DATABASE_URL"}] + """ + + +# AppConfigParam is a flexible type that accepts various config formats +AppConfigParam = Union[AppConfigDict, Dict[str, Any]] diff --git a/experimental/python/databricks/bundles/apps/_models/app_permission.py b/experimental/python/databricks/bundles/apps/_models/app_permission.py new file mode 100644 index 0000000000..7917b964ec --- /dev/null +++ b/experimental/python/databricks/bundles/apps/_models/app_permission.py @@ -0,0 +1,60 @@ +from dataclasses import dataclass +from enum import Enum +from typing import TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOr, VariableOrOptional + + +class AppPermissionLevel(str, Enum): + """Permission level for an app""" + + CAN_MANAGE = "CAN_MANAGE" + CAN_USE = "CAN_USE" + + +AppPermissionLevelParam = AppPermissionLevel | str + + +@dataclass(kw_only=True) +class AppPermission: + """AppPermission holds the permission level setting for a single principal.""" + + level: VariableOr[AppPermissionLevel] + """Permission level""" + + user_name: VariableOrOptional[str] = None + """User name to grant permission to""" + + service_principal_name: VariableOrOptional[str] = None + """Service principal name to grant permission to""" + + group_name: VariableOrOptional[str] = None + """Group name to grant permission to""" + + @classmethod + def from_dict(cls, value: "AppPermissionDict") -> "AppPermission": + return _transform(cls, value) + + def as_dict(self) -> "AppPermissionDict": + return _transform_to_json_value(self) # type:ignore + + +class AppPermissionDict(TypedDict, total=False): + """AppPermission holds the permission level setting for a single principal.""" + + level: VariableOr[AppPermissionLevelParam] + """Permission level""" + + user_name: VariableOrOptional[str] + """User name to grant permission to""" + + service_principal_name: VariableOrOptional[str] + """Service principal name to grant permission to""" + + group_name: VariableOrOptional[str] + """Group name to grant permission to""" + + +AppPermissionParam = AppPermissionDict | AppPermission diff --git a/experimental/python/databricks/bundles/apps/_models/app_resource.py b/experimental/python/databricks/bundles/apps/_models/app_resource.py new file mode 100644 index 0000000000..9699291f6c --- /dev/null +++ b/experimental/python/databricks/bundles/apps/_models/app_resource.py @@ -0,0 +1,199 @@ +from dataclasses import dataclass +from enum import Enum +from typing import TYPE_CHECKING, TypedDict, Union + +from databricks.bundles.core._variable import VariableOr, VariableOrOptional + +if TYPE_CHECKING: + from typing_extensions import Self + + +class JobPermission(str, Enum): + """Permission for job resources""" + + JOB_RUN = "JOB_RUN" + + +class SecretPermission(str, Enum): + """Permission for secret resources""" + + READ = "READ" + WRITE = "WRITE" + + +class ServingEndpointPermission(str, Enum): + """Permission for serving endpoint resources""" + + CAN_QUERY = "CAN_QUERY" + CAN_MANAGE = "CAN_MANAGE" + + +class SqlWarehousePermission(str, Enum): + """Permission for SQL warehouse resources""" + + CAN_USE = "CAN_USE" + + +class UcSecurableType(str, Enum): + """Type of Unity Catalog securable""" + + VOLUME = "VOLUME" + + +class UcSecurablePermission(str, Enum): + """Permission for Unity Catalog securable resources""" + + READ_VOLUME = "READ_VOLUME" + WRITE_VOLUME = "WRITE_VOLUME" + + +@dataclass(kw_only=True) +class AppResourceJob: + """Reference to a job resource""" + + id: VariableOr[str] + """The ID of the job""" + + permission: VariableOr[JobPermission | str] + """The permission to grant on the job""" + + +class AppResourceJobDict(TypedDict, total=False): + """Reference to a job resource""" + + id: VariableOr[str] + """The ID of the job""" + + permission: VariableOr[JobPermission | str] + """The permission to grant on the job""" + + +AppResourceJobParam = AppResourceJobDict | AppResourceJob + + +@dataclass(kw_only=True) +class AppResourceSecret: + """Reference to a secret resource""" + + key: VariableOr[str] + """The key of the secret""" + + scope: VariableOr[str] + """The scope of the secret""" + + permission: VariableOr[SecretPermission | str] + """The permission to grant on the secret""" + + +class AppResourceSecretDict(TypedDict, total=False): + """Reference to a secret resource""" + + key: VariableOr[str] + """The key of the secret""" + + scope: VariableOr[str] + """The scope of the secret""" + + permission: VariableOr[SecretPermission | str] + """The permission to grant on the secret""" + + +AppResourceSecretParam = AppResourceSecretDict | AppResourceSecret + + +@dataclass(kw_only=True) +class AppResourceServingEndpoint: + """Reference to a serving endpoint resource""" + + name: VariableOr[str] + """The name of the serving endpoint""" + + permission: VariableOr[ServingEndpointPermission | str] + """The permission to grant on the serving endpoint""" + + +class AppResourceServingEndpointDict(TypedDict, total=False): + """Reference to a serving endpoint resource""" + + name: VariableOr[str] + """The name of the serving endpoint""" + + permission: VariableOr[ServingEndpointPermission | str] + """The permission to grant on the serving endpoint""" + + +AppResourceServingEndpointParam = ( + AppResourceServingEndpointDict | AppResourceServingEndpoint +) + + +@dataclass(kw_only=True) +class AppResourceSqlWarehouse: + """Reference to a SQL warehouse resource""" + + id: VariableOr[str] + """The ID of the SQL warehouse""" + + permission: VariableOr[SqlWarehousePermission | str] + """The permission to grant on the SQL warehouse""" + + +class AppResourceSqlWarehouseDict(TypedDict, total=False): + """Reference to a SQL warehouse resource""" + + id: VariableOr[str] + """The ID of the SQL warehouse""" + + permission: VariableOr[SqlWarehousePermission | str] + """The permission to grant on the SQL warehouse""" + + +AppResourceSqlWarehouseParam = AppResourceSqlWarehouseDict | AppResourceSqlWarehouse + + +@dataclass(kw_only=True) +class AppResourceUcSecurable: + """Reference to a Unity Catalog securable resource""" + + name: VariableOr[str] + """The name of the securable""" + + securable_type: VariableOr[UcSecurableType | str] + """The type of the securable""" + + permission: VariableOr[UcSecurablePermission | str] + """The permission to grant on the securable""" + + +class AppResourceUcSecurableDict(TypedDict, total=False): + """Reference to a Unity Catalog securable resource""" + + name: VariableOr[str] + """The name of the securable""" + + securable_type: VariableOr[UcSecurableType | str] + """The type of the securable""" + + permission: VariableOr[UcSecurablePermission | str] + """The permission to grant on the securable""" + + +AppResourceUcSecurableParam = AppResourceUcSecurableDict | AppResourceUcSecurable + + +# Union of all app resource types +AppResource = Union[ + AppResourceJob, + AppResourceSecret, + AppResourceServingEndpoint, + AppResourceSqlWarehouse, + AppResourceUcSecurable, +] + +AppResourceParam = Union[ + AppResourceJobParam, + AppResourceSecretParam, + AppResourceServingEndpointParam, + AppResourceSqlWarehouseParam, + AppResourceUcSecurableParam, +] diff --git a/experimental/python/databricks/bundles/apps/_models/lifecycle.py b/experimental/python/databricks/bundles/apps/_models/lifecycle.py new file mode 100644 index 0000000000..b636a5825b --- /dev/null +++ b/experimental/python/databricks/bundles/apps/_models/lifecycle.py @@ -0,0 +1,39 @@ +from dataclasses import dataclass +from typing import TypedDict + +from databricks.bundles.core._transform import _transform +from databricks.bundles.core._transform_to_json import _transform_to_json_value +from databricks.bundles.core._variable import VariableOrOptional + + +@dataclass(kw_only=True) +class Lifecycle: + """ + Lifecycle is a struct that contains the lifecycle settings for a resource. It controls the behavior of the resource when it is deployed or destroyed. + """ + + prevent_destroy: VariableOrOptional[bool] = None + """ + Whether to prevent the resource from being destroyed. If set to true, the resource will not be destroyed when running `databricks bundle destroy`. + """ + + @classmethod + def from_dict(cls, value: "LifecycleDict") -> "Lifecycle": + return _transform(cls, value) + + def as_dict(self) -> "LifecycleDict": + return _transform_to_json_value(self) # type:ignore + + +class LifecycleDict(TypedDict, total=False): + """ + Lifecycle is a struct that contains the lifecycle settings for a resource. It controls the behavior of the resource when it is deployed or destroyed. + """ + + prevent_destroy: VariableOrOptional[bool] + """ + Whether to prevent the resource from being destroyed. If set to true, the resource will not be destroyed when running `databricks bundle destroy`. + """ + + +LifecycleParam = LifecycleDict | Lifecycle diff --git a/experimental/python/databricks/bundles/core/__init__.py b/experimental/python/databricks/bundles/core/__init__.py index 5c525861ac..7c0e47092a 100644 --- a/experimental/python/databricks/bundles/core/__init__.py +++ b/experimental/python/databricks/bundles/core/__init__.py @@ -12,6 +12,7 @@ "VariableOrDict", "VariableOrList", "VariableOrOptional", + "app_mutator", "job_mutator", "load_resources_from_current_package_module", "load_resources_from_module", @@ -39,6 +40,7 @@ from databricks.bundles.core._resource import Resource from databricks.bundles.core._resource_mutator import ( ResourceMutator, + app_mutator, job_mutator, pipeline_mutator, schema_mutator, diff --git a/experimental/python/databricks/bundles/core/_resource_mutator.py b/experimental/python/databricks/bundles/core/_resource_mutator.py index 90e8987216..30470329d5 100644 --- a/experimental/python/databricks/bundles/core/_resource_mutator.py +++ b/experimental/python/databricks/bundles/core/_resource_mutator.py @@ -6,6 +6,7 @@ from databricks.bundles.core._resource import Resource if TYPE_CHECKING: + from databricks.bundles.apps._models.app import App from databricks.bundles.jobs._models.job import Job from databricks.bundles.pipelines._models.pipeline import Pipeline from databricks.bundles.schemas._models.schema import Schema @@ -193,3 +194,35 @@ def my_volume_mutator(bundle: Bundle, volume: Volume) -> Volume: from databricks.bundles.volumes._models.volume import Volume return ResourceMutator(resource_type=Volume, function=function) + + +@overload +def app_mutator( + function: Callable[[Bundle, "App"], "App"], +) -> ResourceMutator["App"]: ... + + +@overload +def app_mutator( + function: Callable[["App"], "App"], +) -> ResourceMutator["App"]: ... + + +def app_mutator(function: Callable) -> ResourceMutator["App"]: + """ + Decorator for defining an app mutator. Function should return a new instance of the app with the desired changes, + instead of mutating the input app. + + Example: + + .. code-block:: python + + @app_mutator + def my_app_mutator(bundle: Bundle, app: App) -> App: + return replace(app, name="my_app") + + :param function: Function that mutates an app. + """ + from databricks.bundles.apps._models.app import App + + return ResourceMutator(resource_type=App, function=function) diff --git a/experimental/python/databricks/bundles/core/_resource_type.py b/experimental/python/databricks/bundles/core/_resource_type.py index 9e9bb1bdf8..55a3815b33 100644 --- a/experimental/python/databricks/bundles/core/_resource_type.py +++ b/experimental/python/databricks/bundles/core/_resource_type.py @@ -31,6 +31,7 @@ def all(cls) -> tuple["_ResourceType", ...]: # intentionally lazily load all resource types to avoid imports from databricks.bundles.core to # be imported in databricks.bundles. + from databricks.bundles.apps._models.app import App from databricks.bundles.jobs._models.job import Job from databricks.bundles.pipelines._models.pipeline import Pipeline from databricks.bundles.schemas._models.schema import Schema @@ -57,4 +58,9 @@ def all(cls) -> tuple["_ResourceType", ...]: plural_name="schemas", singular_name="schema", ), + _ResourceType( + resource_type=App, + plural_name="apps", + singular_name="app", + ), ) diff --git a/experimental/python/databricks/bundles/core/_resources.py b/experimental/python/databricks/bundles/core/_resources.py index 698f2a5ca4..a9b6c6a7c9 100644 --- a/experimental/python/databricks/bundles/core/_resources.py +++ b/experimental/python/databricks/bundles/core/_resources.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform if TYPE_CHECKING: + from databricks.bundles.apps._models.app import App, AppParam 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 @@ -61,6 +62,7 @@ def __init__(self): self._pipelines = dict[str, "Pipeline"]() self._schemas = dict[str, "Schema"]() self._volumes = dict[str, "Volume"]() + self._apps = dict[str, "App"]() self._locations = dict[tuple[str, ...], Location]() self._diagnostics = Diagnostics() @@ -80,6 +82,10 @@ def schemas(self) -> dict[str, "Schema"]: def volumes(self) -> dict[str, "Volume"]: return self._volumes + @property + def apps(self) -> dict[str, "App"]: + return self._apps + @property def diagnostics(self) -> Diagnostics: """ @@ -103,6 +109,7 @@ def add_resource( :param location: optional location of the resource in the source code """ + from databricks.bundles.apps import App from databricks.bundles.jobs import Job from databricks.bundles.pipelines import Pipeline from databricks.bundles.schemas import Schema @@ -119,6 +126,8 @@ def add_resource( self.add_schema(resource_name, resource, location=location) case Volume(): self.add_volume(resource_name, resource, location=location) + case App(): + self.add_app(resource_name, resource, location=location) case _: raise ValueError(f"Unsupported resource type: {type(resource)}") @@ -250,6 +259,38 @@ def add_volume( self._volumes[resource_name] = volume + def add_app( + self, + resource_name: str, + app: "AppParam", + *, + location: Optional[Location] = None, + ) -> None: + """ + Adds an app to the collection of resources. Resource name must be unique across all apps. + + :param resource_name: unique identifier for the app + :param app: the app to add, can be App or dict + :param location: optional location of the app in the source code + """ + from databricks.bundles.apps import App + + app = _transform(App, app) + path = ("resources", "apps", resource_name) + location = location or Location.from_stack_frame(depth=1) + + if self._apps.get(resource_name): + self.add_diagnostic_error( + msg=f"Duplicate resource name '{resource_name}' for a app. Resource names must be unique.", + location=location, + path=path, + ) + else: + if location: + self.add_location(path, location) + + self._apps[resource_name] = app + def add_location(self, path: tuple[str, ...], location: Location) -> None: """ Associate source code location with a path in the bundle configuration. @@ -332,6 +373,9 @@ def add_resources(self, other: "Resources") -> None: for name, volume in other.volumes.items(): self.add_volume(name, volume) + for name, app in other.apps.items(): + self.add_app(name, app) + for path, location in other._locations.items(): self.add_location(path, location) diff --git a/experimental/python/databricks_tests/core/test_resources.py b/experimental/python/databricks_tests/core/test_resources.py index ccdd7f1d86..d63ccec0df 100644 --- a/experimental/python/databricks_tests/core/test_resources.py +++ b/experimental/python/databricks_tests/core/test_resources.py @@ -3,11 +3,13 @@ import pytest +from databricks.bundles.apps._models.app import App 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, + app_mutator, job_mutator, pipeline_mutator, schema_mutator, @@ -74,6 +76,15 @@ class TestCase: ), resource_types[Schema], ), + ( + TestCase( + add_resource=Resources.add_app, + dict_example={"name": "my_app"}, + dataclass_example=App(name="my_app"), + mutator=app_mutator, + ), + resource_types[App], + ), ] test_case_ids = [tpe.plural_name for _, tpe in test_cases] diff --git a/experimental/python/docs/databricks.bundles.apps.rst b/experimental/python/docs/databricks.bundles.apps.rst new file mode 100644 index 0000000000..59e86adbfb --- /dev/null +++ b/experimental/python/docs/databricks.bundles.apps.rst @@ -0,0 +1,7 @@ +databricks.bundles.apps package +================================ + +.. automodule:: databricks.bundles.apps + :members: + :undoc-members: + :show-inheritance: diff --git a/experimental/python/docs/index.rst b/experimental/python/docs/index.rst index 0ecdd689ff..5b1917c8af 100644 --- a/experimental/python/docs/index.rst +++ b/experimental/python/docs/index.rst @@ -14,3 +14,4 @@ See `What is Python support for Databricks Asset Bundles? (TBD) <#>`_. databricks.bundles.pipelines databricks.bundles.schemas databricks.bundles.volumes + databricks.bundles.apps