diff --git a/python/codegen/codegen/packages.py b/python/codegen/codegen/packages.py index 9774911feb9..e230fd134ca 100644 --- a/python/codegen/codegen/packages.py +++ b/python/codegen/codegen/packages.py @@ -8,6 +8,7 @@ "resources.Catalog": "catalogs", "resources.Schema": "schemas", "resources.Volume": "volumes", + "resources.Alert": "alerts", } RESOURCE_TYPES = list(RESOURCE_NAMESPACE.keys()) @@ -21,6 +22,8 @@ "pipelines", "resources", "catalog", + "sql", + "iam", ] RENAMES = { diff --git a/python/databricks/bundles/alerts/__init__.py b/python/databricks/bundles/alerts/__init__.py new file mode 100644 index 00000000000..b948119b07b --- /dev/null +++ b/python/databricks/bundles/alerts/__init__.py @@ -0,0 +1,123 @@ +__all__ = [ + "Aggregation", + "AggregationParam", + "Alert", + "AlertDict", + "AlertEvaluationState", + "AlertEvaluationStateParam", + "AlertParam", + "AlertStatementParameter", + "AlertStatementParameterDict", + "AlertStatementParameterParam", + "AlertV2Evaluation", + "AlertV2EvaluationDict", + "AlertV2EvaluationParam", + "AlertV2Notification", + "AlertV2NotificationDict", + "AlertV2NotificationParam", + "AlertV2Operand", + "AlertV2OperandColumn", + "AlertV2OperandColumnDict", + "AlertV2OperandColumnParam", + "AlertV2OperandDict", + "AlertV2OperandParam", + "AlertV2OperandValue", + "AlertV2OperandValueDict", + "AlertV2OperandValueParam", + "AlertV2RunAs", + "AlertV2RunAsDict", + "AlertV2RunAsParam", + "AlertV2Subscription", + "AlertV2SubscriptionDict", + "AlertV2SubscriptionParam", + "ComparisonOperator", + "ComparisonOperatorParam", + "CronSchedule", + "CronScheduleDict", + "CronScheduleParam", + "Lifecycle", + "LifecycleDict", + "LifecycleParam", + "Permission", + "PermissionDict", + "PermissionLevel", + "PermissionLevelParam", + "PermissionParam", + "SchedulePauseStatus", + "SchedulePauseStatusParam", +] + + +from databricks.bundles.alerts._models.aggregation import Aggregation, AggregationParam +from databricks.bundles.alerts._models.alert import Alert, AlertDict, AlertParam +from databricks.bundles.alerts._models.alert_evaluation_state import ( + AlertEvaluationState, + AlertEvaluationStateParam, +) +from databricks.bundles.alerts._models.alert_statement_parameter import ( + AlertStatementParameter, + AlertStatementParameterDict, + AlertStatementParameterParam, +) +from databricks.bundles.alerts._models.alert_v2_evaluation import ( + AlertV2Evaluation, + AlertV2EvaluationDict, + AlertV2EvaluationParam, +) +from databricks.bundles.alerts._models.alert_v2_notification import ( + AlertV2Notification, + AlertV2NotificationDict, + AlertV2NotificationParam, +) +from databricks.bundles.alerts._models.alert_v2_operand import ( + AlertV2Operand, + AlertV2OperandDict, + AlertV2OperandParam, +) +from databricks.bundles.alerts._models.alert_v2_operand_column import ( + AlertV2OperandColumn, + AlertV2OperandColumnDict, + AlertV2OperandColumnParam, +) +from databricks.bundles.alerts._models.alert_v2_operand_value import ( + AlertV2OperandValue, + AlertV2OperandValueDict, + AlertV2OperandValueParam, +) +from databricks.bundles.alerts._models.alert_v2_run_as import ( + AlertV2RunAs, + AlertV2RunAsDict, + AlertV2RunAsParam, +) +from databricks.bundles.alerts._models.alert_v2_subscription import ( + AlertV2Subscription, + AlertV2SubscriptionDict, + AlertV2SubscriptionParam, +) +from databricks.bundles.alerts._models.comparison_operator import ( + ComparisonOperator, + ComparisonOperatorParam, +) +from databricks.bundles.alerts._models.cron_schedule import ( + CronSchedule, + CronScheduleDict, + CronScheduleParam, +) +from databricks.bundles.alerts._models.lifecycle import ( + Lifecycle, + LifecycleDict, + LifecycleParam, +) +from databricks.bundles.alerts._models.permission import ( + Permission, + PermissionDict, + PermissionParam, +) +from databricks.bundles.alerts._models.permission_level import ( + PermissionLevel, + PermissionLevelParam, +) +from databricks.bundles.alerts._models.schedule_pause_status import ( + SchedulePauseStatus, + SchedulePauseStatusParam, +) diff --git a/python/databricks/bundles/alerts/_models/aggregation.py b/python/databricks/bundles/alerts/_models/aggregation.py new file mode 100644 index 00000000000..461a439f1bc --- /dev/null +++ b/python/databricks/bundles/alerts/_models/aggregation.py @@ -0,0 +1,19 @@ +from enum import Enum +from typing import Literal + + +class Aggregation(Enum): + SUM = "SUM" + COUNT = "COUNT" + COUNT_DISTINCT = "COUNT_DISTINCT" + AVG = "AVG" + MEDIAN = "MEDIAN" + MIN = "MIN" + MAX = "MAX" + STDDEV = "STDDEV" + + +AggregationParam = ( + Literal["SUM", "COUNT", "COUNT_DISTINCT", "AVG", "MEDIAN", "MIN", "MAX", "STDDEV"] + | Aggregation +) diff --git a/python/databricks/bundles/alerts/_models/alert.py b/python/databricks/bundles/alerts/_models/alert.py new file mode 100644 index 00000000000..44c3894f5b1 --- /dev/null +++ b/python/databricks/bundles/alerts/_models/alert.py @@ -0,0 +1,191 @@ +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.alerts._models.alert_statement_parameter import ( + AlertStatementParameter, + AlertStatementParameterParam, +) +from databricks.bundles.alerts._models.alert_v2_evaluation import ( + AlertV2Evaluation, + AlertV2EvaluationParam, +) +from databricks.bundles.alerts._models.alert_v2_run_as import ( + AlertV2RunAs, + AlertV2RunAsParam, +) +from databricks.bundles.alerts._models.cron_schedule import ( + CronSchedule, + CronScheduleParam, +) +from databricks.bundles.alerts._models.lifecycle import Lifecycle, LifecycleParam +from databricks.bundles.alerts._models.permission import Permission, PermissionParam +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, + VariableOrList, + VariableOrOptional, +) + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Alert(Resource): + """""" + + display_name: VariableOr[str] + """ + The display name of the alert. + """ + + evaluation: VariableOr[AlertV2Evaluation] + + query_text: VariableOr[str] + """ + Text of the query to be run. + """ + + schedule: VariableOr[CronSchedule] + + warehouse_id: VariableOr[str] + """ + ID of the SQL warehouse attached to the alert. + """ + + custom_description: VariableOrOptional[str] = None + """ + Custom description for the alert. support mustache template. + """ + + custom_summary: VariableOrOptional[str] = None + """ + Custom summary for the alert. support mustache template. + """ + + file_path: VariableOrOptional[str] = None + + lifecycle: VariableOrOptional[Lifecycle] = None + """ + Settings that control the deployment lifecycle of the resource, such as preventing it from being destroyed. + """ + + parameters: VariableOrList[AlertStatementParameter] = field(default_factory=list) + """ + :meta private: [EXPERIMENTAL] + + [Private Preview] Query parameters bound when executing the alert query, referenced in the + query text with `:name` syntax. Static values only. + """ + + parent_path: VariableOrOptional[str] = None + """ + The workspace path of the folder containing the alert. Can only be set on create, and cannot be updated. + """ + + permissions: VariableOrList[Permission] = field(default_factory=list) + """ + The permissions to apply to this resource. + """ + + run_as: VariableOrOptional[AlertV2RunAs] = None + """ + Specifies the identity that will be used to run the alert. + This field allows you to configure alerts to run as a specific user or service principal. + - For user identity: Set `user_name` to the email of an active workspace user. Users can only set this to their own email. + - For service principal: Set `service_principal_name` to the application ID. Requires the `servicePrincipal/user` role. + If not specified, the alert will run as the request user. + """ + + run_as_user_name: VariableOrOptional[str] = None + """ + [DEPRECATED] The run as username or application ID of service principal. + On Create and Update, this field can be set to application ID of an active service principal. Setting this field requires the servicePrincipal/user role. + Deprecated: Use `run_as` field instead. This field will be removed in a future release. + """ + + @classmethod + def from_dict(cls, value: "AlertDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "AlertDict": + return _transform_to_json_value(self) # type:ignore + + +class AlertDict(TypedDict, total=False): + """""" + + display_name: VariableOr[str] + """ + The display name of the alert. + """ + + evaluation: VariableOr[AlertV2EvaluationParam] + + query_text: VariableOr[str] + """ + Text of the query to be run. + """ + + schedule: VariableOr[CronScheduleParam] + + warehouse_id: VariableOr[str] + """ + ID of the SQL warehouse attached to the alert. + """ + + custom_description: VariableOrOptional[str] + """ + Custom description for the alert. support mustache template. + """ + + custom_summary: VariableOrOptional[str] + """ + Custom summary for the alert. support mustache template. + """ + + file_path: VariableOrOptional[str] + + lifecycle: VariableOrOptional[LifecycleParam] + """ + Settings that control the deployment lifecycle of the resource, such as preventing it from being destroyed. + """ + + parameters: VariableOrList[AlertStatementParameterParam] + """ + :meta private: [EXPERIMENTAL] + + [Private Preview] Query parameters bound when executing the alert query, referenced in the + query text with `:name` syntax. Static values only. + """ + + parent_path: VariableOrOptional[str] + """ + The workspace path of the folder containing the alert. Can only be set on create, and cannot be updated. + """ + + permissions: VariableOrList[PermissionParam] + """ + The permissions to apply to this resource. + """ + + run_as: VariableOrOptional[AlertV2RunAsParam] + """ + Specifies the identity that will be used to run the alert. + This field allows you to configure alerts to run as a specific user or service principal. + - For user identity: Set `user_name` to the email of an active workspace user. Users can only set this to their own email. + - For service principal: Set `service_principal_name` to the application ID. Requires the `servicePrincipal/user` role. + If not specified, the alert will run as the request user. + """ + + run_as_user_name: VariableOrOptional[str] + """ + [DEPRECATED] The run as username or application ID of service principal. + On Create and Update, this field can be set to application ID of an active service principal. Setting this field requires the servicePrincipal/user role. + Deprecated: Use `run_as` field instead. This field will be removed in a future release. + """ + + +AlertParam = AlertDict | Alert diff --git a/python/databricks/bundles/alerts/_models/alert_evaluation_state.py b/python/databricks/bundles/alerts/_models/alert_evaluation_state.py new file mode 100644 index 00000000000..1ca5018bdcc --- /dev/null +++ b/python/databricks/bundles/alerts/_models/alert_evaluation_state.py @@ -0,0 +1,22 @@ +from enum import Enum +from typing import Literal + + +class AlertEvaluationState(Enum): + """ + UNSPECIFIED - default unspecify value for proto enum, do not use it in the code + UNKNOWN - alert not yet evaluated + TRIGGERED - alert is triggered + OK - alert is not triggered + ERROR - alert evaluation failed + """ + + UNKNOWN = "UNKNOWN" + TRIGGERED = "TRIGGERED" + OK = "OK" + ERROR = "ERROR" + + +AlertEvaluationStateParam = ( + Literal["UNKNOWN", "TRIGGERED", "OK", "ERROR"] | AlertEvaluationState +) diff --git a/python/databricks/bundles/alerts/_models/alert_statement_parameter.py b/python/databricks/bundles/alerts/_models/alert_statement_parameter.py new file mode 100644 index 00000000000..22676634dfd --- /dev/null +++ b/python/databricks/bundles/alerts/_models/alert_statement_parameter.py @@ -0,0 +1,82 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, 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 + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class AlertStatementParameter: + """ + :meta private: [EXPERIMENTAL] + + Redash-owned copy of the internal StatementParameter for the external AlertV2 API. + The internal `ordinal` and `args` fields are intentionally omitted: the public API + supports only flat, named scalar parameters; complex types (ARRAY, MAP, STRUCT) are + not supported. This mirrors SEA's public StatementParameter schema, see: + cmdexec/sql-exec-api/proto/sql_exec_api_service.proto:763-779 + """ + + name: VariableOr[str] + """ + :meta private: [EXPERIMENTAL] + + [Private Preview] The name of the parameter, referenced in the query as `:name`. + """ + + type: VariableOrOptional[str] = None + """ + :meta private: [EXPERIMENTAL] + + [Private Preview] The SQL data type of the parameter, e.g. STRING, INT, or DATE. Defaults to STRING. This is a + string rather than an enum because scalar subtypes such as DECIMAL(10, 4) cannot be enumerated. + Complex types such as ARRAY, MAP, and STRUCT are not supported. + """ + + value: VariableOrOptional[str] = None + """ + :meta private: [EXPERIMENTAL] + + [Private Preview] The bound value for the parameter, given as a string. If omitted, the value is interpreted as NULL. + """ + + @classmethod + def from_dict(cls, value: "AlertStatementParameterDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "AlertStatementParameterDict": + return _transform_to_json_value(self) # type:ignore + + +class AlertStatementParameterDict(TypedDict, total=False): + """""" + + name: VariableOr[str] + """ + :meta private: [EXPERIMENTAL] + + [Private Preview] The name of the parameter, referenced in the query as `:name`. + """ + + type: VariableOrOptional[str] + """ + :meta private: [EXPERIMENTAL] + + [Private Preview] The SQL data type of the parameter, e.g. STRING, INT, or DATE. Defaults to STRING. This is a + string rather than an enum because scalar subtypes such as DECIMAL(10, 4) cannot be enumerated. + Complex types such as ARRAY, MAP, and STRUCT are not supported. + """ + + value: VariableOrOptional[str] + """ + :meta private: [EXPERIMENTAL] + + [Private Preview] The bound value for the parameter, given as a string. If omitted, the value is interpreted as NULL. + """ + + +AlertStatementParameterParam = AlertStatementParameterDict | AlertStatementParameter diff --git a/python/databricks/bundles/alerts/_models/alert_v2_evaluation.py b/python/databricks/bundles/alerts/_models/alert_v2_evaluation.py new file mode 100644 index 00000000000..90cc3037b3c --- /dev/null +++ b/python/databricks/bundles/alerts/_models/alert_v2_evaluation.py @@ -0,0 +1,98 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.alerts._models.alert_evaluation_state import ( + AlertEvaluationState, + AlertEvaluationStateParam, +) +from databricks.bundles.alerts._models.alert_v2_notification import ( + AlertV2Notification, + AlertV2NotificationParam, +) +from databricks.bundles.alerts._models.alert_v2_operand import ( + AlertV2Operand, + AlertV2OperandParam, +) +from databricks.bundles.alerts._models.alert_v2_operand_column import ( + AlertV2OperandColumn, + AlertV2OperandColumnParam, +) +from databricks.bundles.alerts._models.comparison_operator import ( + ComparisonOperator, + ComparisonOperatorParam, +) +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 + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class AlertV2Evaluation: + """""" + + comparison_operator: VariableOr[ComparisonOperator] + """ + Operator used for comparison in alert evaluation. + """ + + source: VariableOr[AlertV2OperandColumn] + """ + Source column from result to use to evaluate alert + """ + + empty_result_state: VariableOrOptional[AlertEvaluationState] = None + """ + Alert state if result is empty. Please avoid setting this field to be `UNKNOWN` because `UNKNOWN` state is planned to be deprecated. + """ + + notification: VariableOrOptional[AlertV2Notification] = None + """ + User or Notification Destination to notify when alert is triggered. + """ + + threshold: VariableOrOptional[AlertV2Operand] = None + """ + Threshold to user for alert evaluation, can be a column or a value. + """ + + @classmethod + def from_dict(cls, value: "AlertV2EvaluationDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "AlertV2EvaluationDict": + return _transform_to_json_value(self) # type:ignore + + +class AlertV2EvaluationDict(TypedDict, total=False): + """""" + + comparison_operator: VariableOr[ComparisonOperatorParam] + """ + Operator used for comparison in alert evaluation. + """ + + source: VariableOr[AlertV2OperandColumnParam] + """ + Source column from result to use to evaluate alert + """ + + empty_result_state: VariableOrOptional[AlertEvaluationStateParam] + """ + Alert state if result is empty. Please avoid setting this field to be `UNKNOWN` because `UNKNOWN` state is planned to be deprecated. + """ + + notification: VariableOrOptional[AlertV2NotificationParam] + """ + User or Notification Destination to notify when alert is triggered. + """ + + threshold: VariableOrOptional[AlertV2OperandParam] + """ + Threshold to user for alert evaluation, can be a column or a value. + """ + + +AlertV2EvaluationParam = AlertV2EvaluationDict | AlertV2Evaluation diff --git a/python/databricks/bundles/alerts/_models/alert_v2_notification.py b/python/databricks/bundles/alerts/_models/alert_v2_notification.py new file mode 100644 index 00000000000..5687a6c5cc7 --- /dev/null +++ b/python/databricks/bundles/alerts/_models/alert_v2_notification.py @@ -0,0 +1,60 @@ +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.alerts._models.alert_v2_subscription import ( + AlertV2Subscription, + AlertV2SubscriptionParam, +) +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 VariableOrList, VariableOrOptional + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class AlertV2Notification: + """""" + + notify_on_ok: VariableOrOptional[bool] = None + """ + Whether to notify alert subscribers when alert returns back to normal. + """ + + retrigger_seconds: VariableOrOptional[int] = None + """ + Number of seconds an alert waits after being triggered before it is allowed to send another notification. + If set to 0 or omitted, the alert will not send any further notifications after the first trigger + Setting this value to 1 allows the alert to send a notification on every evaluation where the condition is met, effectively making it always retrigger for notification purposes. + """ + + subscriptions: VariableOrList[AlertV2Subscription] = field(default_factory=list) + + @classmethod + def from_dict(cls, value: "AlertV2NotificationDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "AlertV2NotificationDict": + return _transform_to_json_value(self) # type:ignore + + +class AlertV2NotificationDict(TypedDict, total=False): + """""" + + notify_on_ok: VariableOrOptional[bool] + """ + Whether to notify alert subscribers when alert returns back to normal. + """ + + retrigger_seconds: VariableOrOptional[int] + """ + Number of seconds an alert waits after being triggered before it is allowed to send another notification. + If set to 0 or omitted, the alert will not send any further notifications after the first trigger + Setting this value to 1 allows the alert to send a notification on every evaluation where the condition is met, effectively making it always retrigger for notification purposes. + """ + + subscriptions: VariableOrList[AlertV2SubscriptionParam] + + +AlertV2NotificationParam = AlertV2NotificationDict | AlertV2Notification diff --git a/python/databricks/bundles/alerts/_models/alert_v2_operand.py b/python/databricks/bundles/alerts/_models/alert_v2_operand.py new file mode 100644 index 00000000000..8e0268d5895 --- /dev/null +++ b/python/databricks/bundles/alerts/_models/alert_v2_operand.py @@ -0,0 +1,44 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.alerts._models.alert_v2_operand_column import ( + AlertV2OperandColumn, + AlertV2OperandColumnParam, +) +from databricks.bundles.alerts._models.alert_v2_operand_value import ( + AlertV2OperandValue, + AlertV2OperandValueParam, +) +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 + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class AlertV2Operand: + """""" + + column: VariableOrOptional[AlertV2OperandColumn] = None + + value: VariableOrOptional[AlertV2OperandValue] = None + + @classmethod + def from_dict(cls, value: "AlertV2OperandDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "AlertV2OperandDict": + return _transform_to_json_value(self) # type:ignore + + +class AlertV2OperandDict(TypedDict, total=False): + """""" + + column: VariableOrOptional[AlertV2OperandColumnParam] + + value: VariableOrOptional[AlertV2OperandValueParam] + + +AlertV2OperandParam = AlertV2OperandDict | AlertV2Operand diff --git a/python/databricks/bundles/alerts/_models/alert_v2_operand_column.py b/python/databricks/bundles/alerts/_models/alert_v2_operand_column.py new file mode 100644 index 00000000000..db9feecb796 --- /dev/null +++ b/python/databricks/bundles/alerts/_models/alert_v2_operand_column.py @@ -0,0 +1,47 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.alerts._models.aggregation import Aggregation, AggregationParam +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 + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class AlertV2OperandColumn: + """""" + + name: VariableOr[str] + + aggregation: VariableOrOptional[Aggregation] = None + """ + If not set, the behavior is equivalent to using `First row` in the UI. + """ + + display: VariableOrOptional[str] = None + + @classmethod + def from_dict(cls, value: "AlertV2OperandColumnDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "AlertV2OperandColumnDict": + return _transform_to_json_value(self) # type:ignore + + +class AlertV2OperandColumnDict(TypedDict, total=False): + """""" + + name: VariableOr[str] + + aggregation: VariableOrOptional[AggregationParam] + """ + If not set, the behavior is equivalent to using `First row` in the UI. + """ + + display: VariableOrOptional[str] + + +AlertV2OperandColumnParam = AlertV2OperandColumnDict | AlertV2OperandColumn diff --git a/python/databricks/bundles/alerts/_models/alert_v2_operand_value.py b/python/databricks/bundles/alerts/_models/alert_v2_operand_value.py new file mode 100644 index 00000000000..4ffdd9cfb1d --- /dev/null +++ b/python/databricks/bundles/alerts/_models/alert_v2_operand_value.py @@ -0,0 +1,40 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, 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 + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class AlertV2OperandValue: + """""" + + bool_value: VariableOrOptional[bool] = None + + double_value: VariableOrOptional[float] = None + + string_value: VariableOrOptional[str] = None + + @classmethod + def from_dict(cls, value: "AlertV2OperandValueDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "AlertV2OperandValueDict": + return _transform_to_json_value(self) # type:ignore + + +class AlertV2OperandValueDict(TypedDict, total=False): + """""" + + bool_value: VariableOrOptional[bool] + + double_value: VariableOrOptional[float] + + string_value: VariableOrOptional[str] + + +AlertV2OperandValueParam = AlertV2OperandValueDict | AlertV2OperandValue diff --git a/python/databricks/bundles/alerts/_models/alert_v2_run_as.py b/python/databricks/bundles/alerts/_models/alert_v2_run_as.py new file mode 100644 index 00000000000..4f01f1e5434 --- /dev/null +++ b/python/databricks/bundles/alerts/_models/alert_v2_run_as.py @@ -0,0 +1,48 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, 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 + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class AlertV2RunAs: + """""" + + service_principal_name: VariableOrOptional[str] = None + """ + Application ID of an active service principal. Setting this field requires the `servicePrincipal/user` role. + """ + + user_name: VariableOrOptional[str] = None + """ + The email of an active workspace user. Can only set this field to their own email. + """ + + @classmethod + def from_dict(cls, value: "AlertV2RunAsDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "AlertV2RunAsDict": + return _transform_to_json_value(self) # type:ignore + + +class AlertV2RunAsDict(TypedDict, total=False): + """""" + + service_principal_name: VariableOrOptional[str] + """ + Application ID of an active service principal. Setting this field requires the `servicePrincipal/user` role. + """ + + user_name: VariableOrOptional[str] + """ + The email of an active workspace user. Can only set this field to their own email. + """ + + +AlertV2RunAsParam = AlertV2RunAsDict | AlertV2RunAs diff --git a/python/databricks/bundles/alerts/_models/alert_v2_subscription.py b/python/databricks/bundles/alerts/_models/alert_v2_subscription.py new file mode 100644 index 00000000000..dcd2269cd02 --- /dev/null +++ b/python/databricks/bundles/alerts/_models/alert_v2_subscription.py @@ -0,0 +1,36 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, 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 + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class AlertV2Subscription: + """""" + + destination_id: VariableOrOptional[str] = None + + user_email: VariableOrOptional[str] = None + + @classmethod + def from_dict(cls, value: "AlertV2SubscriptionDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "AlertV2SubscriptionDict": + return _transform_to_json_value(self) # type:ignore + + +class AlertV2SubscriptionDict(TypedDict, total=False): + """""" + + destination_id: VariableOrOptional[str] + + user_email: VariableOrOptional[str] + + +AlertV2SubscriptionParam = AlertV2SubscriptionDict | AlertV2Subscription diff --git a/python/databricks/bundles/alerts/_models/comparison_operator.py b/python/databricks/bundles/alerts/_models/comparison_operator.py new file mode 100644 index 00000000000..49e1736e433 --- /dev/null +++ b/python/databricks/bundles/alerts/_models/comparison_operator.py @@ -0,0 +1,28 @@ +from enum import Enum +from typing import Literal + + +class ComparisonOperator(Enum): + LESS_THAN = "LESS_THAN" + GREATER_THAN = "GREATER_THAN" + EQUAL = "EQUAL" + NOT_EQUAL = "NOT_EQUAL" + GREATER_THAN_OR_EQUAL = "GREATER_THAN_OR_EQUAL" + LESS_THAN_OR_EQUAL = "LESS_THAN_OR_EQUAL" + IS_NULL = "IS_NULL" + IS_NOT_NULL = "IS_NOT_NULL" + + +ComparisonOperatorParam = ( + Literal[ + "LESS_THAN", + "GREATER_THAN", + "EQUAL", + "NOT_EQUAL", + "GREATER_THAN_OR_EQUAL", + "LESS_THAN_OR_EQUAL", + "IS_NULL", + "IS_NOT_NULL", + ] + | ComparisonOperator +) diff --git a/python/databricks/bundles/alerts/_models/cron_schedule.py b/python/databricks/bundles/alerts/_models/cron_schedule.py new file mode 100644 index 00000000000..3c05f727876 --- /dev/null +++ b/python/databricks/bundles/alerts/_models/cron_schedule.py @@ -0,0 +1,68 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.alerts._models.schedule_pause_status import ( + SchedulePauseStatus, + SchedulePauseStatusParam, +) +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 + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class CronSchedule: + """""" + + quartz_cron_schedule: VariableOr[str] + """ + A cron expression using quartz syntax that specifies the schedule for this pipeline. + Should use the quartz format described here: http://www.quartz-scheduler.org/documentation/quartz-2.1.7/tutorials/tutorial-lesson-06.html + """ + + timezone_id: VariableOr[str] + """ + A Java timezone id. The schedule will be resolved using this timezone. + This will be combined with the quartz_cron_schedule to determine the schedule. + See https://docs.databricks.com/sql/language-manual/sql-ref-syntax-aux-conf-mgmt-set-timezone.html for details. + """ + + pause_status: VariableOrOptional[SchedulePauseStatus] = None + """ + Indicate whether this schedule is paused or not. + """ + + @classmethod + def from_dict(cls, value: "CronScheduleDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "CronScheduleDict": + return _transform_to_json_value(self) # type:ignore + + +class CronScheduleDict(TypedDict, total=False): + """""" + + quartz_cron_schedule: VariableOr[str] + """ + A cron expression using quartz syntax that specifies the schedule for this pipeline. + Should use the quartz format described here: http://www.quartz-scheduler.org/documentation/quartz-2.1.7/tutorials/tutorial-lesson-06.html + """ + + timezone_id: VariableOr[str] + """ + A Java timezone id. The schedule will be resolved using this timezone. + This will be combined with the quartz_cron_schedule to determine the schedule. + See https://docs.databricks.com/sql/language-manual/sql-ref-syntax-aux-conf-mgmt-set-timezone.html for details. + """ + + pause_status: VariableOrOptional[SchedulePauseStatusParam] + """ + Indicate whether this schedule is paused or not. + """ + + +CronScheduleParam = CronScheduleDict | CronSchedule diff --git a/python/databricks/bundles/alerts/_models/lifecycle.py b/python/databricks/bundles/alerts/_models/lifecycle.py new file mode 100644 index 00000000000..c934967f37e --- /dev/null +++ b/python/databricks/bundles/alerts/_models/lifecycle.py @@ -0,0 +1,38 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, 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 + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Lifecycle: + """""" + + prevent_destroy: VariableOrOptional[bool] = None + """ + Lifecycle setting to prevent the resource from being destroyed. + """ + + @classmethod + def from_dict(cls, value: "LifecycleDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "LifecycleDict": + return _transform_to_json_value(self) # type:ignore + + +class LifecycleDict(TypedDict, total=False): + """""" + + prevent_destroy: VariableOrOptional[bool] + """ + Lifecycle setting to prevent the resource from being destroyed. + """ + + +LifecycleParam = LifecycleDict | Lifecycle diff --git a/python/databricks/bundles/alerts/_models/permission.py b/python/databricks/bundles/alerts/_models/permission.py new file mode 100644 index 00000000000..a6f8fd0cadb --- /dev/null +++ b/python/databricks/bundles/alerts/_models/permission.py @@ -0,0 +1,72 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict + +from databricks.bundles.alerts._models.permission_level import ( + PermissionLevel, + PermissionLevelParam, +) +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 + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass(kw_only=True) +class Permission: + """""" + + level: VariableOr[PermissionLevel] + """ + The permission level to apply. The allowed levels depend on the resource type. + """ + + group_name: VariableOrOptional[str] = None + """ + The name of the group granted the permission level. + """ + + service_principal_name: VariableOrOptional[str] = None + """ + The name of the service principal granted the permission level. + """ + + user_name: VariableOrOptional[str] = None + """ + The name of the user granted the permission level. + """ + + @classmethod + def from_dict(cls, value: "PermissionDict") -> "Self": + return _transform(cls, value) + + def as_dict(self) -> "PermissionDict": + return _transform_to_json_value(self) # type:ignore + + +class PermissionDict(TypedDict, total=False): + """""" + + level: VariableOr[PermissionLevelParam] + """ + The permission level to apply. The allowed levels depend on the resource type. + """ + + group_name: VariableOrOptional[str] + """ + The name of the group granted the permission level. + """ + + service_principal_name: VariableOrOptional[str] + """ + The name of the service principal granted the permission level. + """ + + user_name: VariableOrOptional[str] + """ + The name of the user granted the permission level. + """ + + +PermissionParam = PermissionDict | Permission diff --git a/python/databricks/bundles/alerts/_models/permission_level.py b/python/databricks/bundles/alerts/_models/permission_level.py new file mode 100644 index 00000000000..88649a15070 --- /dev/null +++ b/python/databricks/bundles/alerts/_models/permission_level.py @@ -0,0 +1,56 @@ +from enum import Enum +from typing import Literal + + +class PermissionLevel(Enum): + """ + Permission level + """ + + CAN_MANAGE = "CAN_MANAGE" + CAN_RESTART = "CAN_RESTART" + CAN_ATTACH_TO = "CAN_ATTACH_TO" + IS_OWNER = "IS_OWNER" + CAN_MANAGE_RUN = "CAN_MANAGE_RUN" + CAN_VIEW = "CAN_VIEW" + CAN_READ = "CAN_READ" + CAN_RUN = "CAN_RUN" + CAN_EDIT = "CAN_EDIT" + CAN_USE = "CAN_USE" + CAN_MANAGE_STAGING_VERSIONS = "CAN_MANAGE_STAGING_VERSIONS" + CAN_MANAGE_PRODUCTION_VERSIONS = "CAN_MANAGE_PRODUCTION_VERSIONS" + CAN_EDIT_METADATA = "CAN_EDIT_METADATA" + CAN_VIEW_METADATA = "CAN_VIEW_METADATA" + CAN_BIND = "CAN_BIND" + CAN_QUERY = "CAN_QUERY" + CAN_MONITOR = "CAN_MONITOR" + CAN_CREATE = "CAN_CREATE" + CAN_MONITOR_ONLY = "CAN_MONITOR_ONLY" + CAN_CREATE_APP = "CAN_CREATE_APP" + + +PermissionLevelParam = ( + Literal[ + "CAN_MANAGE", + "CAN_RESTART", + "CAN_ATTACH_TO", + "IS_OWNER", + "CAN_MANAGE_RUN", + "CAN_VIEW", + "CAN_READ", + "CAN_RUN", + "CAN_EDIT", + "CAN_USE", + "CAN_MANAGE_STAGING_VERSIONS", + "CAN_MANAGE_PRODUCTION_VERSIONS", + "CAN_EDIT_METADATA", + "CAN_VIEW_METADATA", + "CAN_BIND", + "CAN_QUERY", + "CAN_MONITOR", + "CAN_CREATE", + "CAN_MONITOR_ONLY", + "CAN_CREATE_APP", + ] + | PermissionLevel +) diff --git a/python/databricks/bundles/alerts/_models/schedule_pause_status.py b/python/databricks/bundles/alerts/_models/schedule_pause_status.py new file mode 100644 index 00000000000..e0f8856ae35 --- /dev/null +++ b/python/databricks/bundles/alerts/_models/schedule_pause_status.py @@ -0,0 +1,10 @@ +from enum import Enum +from typing import Literal + + +class SchedulePauseStatus(Enum): + UNPAUSED = "UNPAUSED" + PAUSED = "PAUSED" + + +SchedulePauseStatusParam = Literal["UNPAUSED", "PAUSED"] | SchedulePauseStatus diff --git a/python/databricks/bundles/core/__init__.py b/python/databricks/bundles/core/__init__.py index 5c525861ac8..09ebbd37ccf 100644 --- a/python/databricks/bundles/core/__init__.py +++ b/python/databricks/bundles/core/__init__.py @@ -12,6 +12,7 @@ "VariableOrDict", "VariableOrList", "VariableOrOptional", + "alert_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, + alert_mutator, job_mutator, pipeline_mutator, schema_mutator, diff --git a/python/databricks/bundles/core/_resource_mutator.py b/python/databricks/bundles/core/_resource_mutator.py index 90e8987216f..4cf7eb0a47c 100644 --- a/python/databricks/bundles/core/_resource_mutator.py +++ b/python/databricks/bundles/core/_resource_mutator.py @@ -6,6 +6,7 @@ from databricks.bundles.core._resource import Resource if TYPE_CHECKING: + from databricks.bundles.alerts._models.alert import Alert from databricks.bundles.jobs._models.job import Job from databricks.bundles.pipelines._models.pipeline import Pipeline from databricks.bundles.schemas._models.schema import Schema @@ -69,6 +70,38 @@ def my_job_mutator(bundle: Bundle, job: Job) -> Job: # 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 job_mutator( function: Callable[[Bundle, "Job"], "Job"], diff --git a/python/databricks/bundles/core/_resource_type.py b/python/databricks/bundles/core/_resource_type.py index 9e9bb1bdf89..c5930f02beb 100644 --- a/python/databricks/bundles/core/_resource_type.py +++ b/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.alerts._models.alert import Alert 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=Alert, + plural_name="alerts", + singular_name="alert", + ), ) diff --git a/python/databricks/bundles/core/_resources.py b/python/databricks/bundles/core/_resources.py index 9be121718e0..2db10ebac2f 100644 --- a/python/databricks/bundles/core/_resources.py +++ b/python/databricks/bundles/core/_resources.py @@ -6,6 +6,7 @@ from databricks.bundles.core._transform import _transform if TYPE_CHECKING: + from databricks.bundles.alerts._models.alert import Alert, AlertParam 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 @@ -60,6 +61,7 @@ def __init__(self): self._pipelines = dict[str, "Pipeline"]() self._schemas = dict[str, "Schema"]() self._volumes = dict[str, "Volume"]() + self._alerts = dict[str, "Alert"]() self._locations = dict[tuple[str, ...], Location]() self._diagnostics = Diagnostics() @@ -86,6 +88,10 @@ def diagnostics(self) -> Diagnostics: """ return self._diagnostics + @property + def alerts(self) -> dict[str, "Alert"]: + return self._alerts + def add_resource( self, resource_name: str, @@ -102,6 +108,7 @@ def add_resource( :param location: optional location of the resource in the source code """ + from databricks.bundles.alerts import Alert from databricks.bundles.jobs import Job from databricks.bundles.pipelines import Pipeline from databricks.bundles.schemas import Schema @@ -118,6 +125,8 @@ def add_resource( 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 _: raise ValueError(f"Unsupported resource type: {type(resource)}") @@ -249,6 +258,34 @@ def add_volume( 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_location(self, path: tuple[str, ...], location: Location) -> None: """ Associate source code location with a path in the bundle configuration. @@ -331,6 +368,9 @@ def add_resources(self, other: "Resources") -> None: 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 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 ccdd7f1d864..421a7e9dbe0 100644 --- a/python/databricks_tests/core/test_resources.py +++ b/python/databricks_tests/core/test_resources.py @@ -3,11 +3,19 @@ import pytest +from databricks.bundles.alerts._models.alert import Alert +from databricks.bundles.alerts._models.alert_v2_evaluation import AlertV2Evaluation +from databricks.bundles.alerts._models.alert_v2_operand_column import ( + AlertV2OperandColumn, +) +from databricks.bundles.alerts._models.comparison_operator import ComparisonOperator +from databricks.bundles.alerts._models.cron_schedule import CronSchedule 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, + alert_mutator, job_mutator, pipeline_mutator, schema_mutator, @@ -26,6 +34,7 @@ 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()} @@ -74,6 +83,40 @@ class TestCase: ), resource_types[Schema], ), + ( + TestCase( + add_resource=Resources.add_alert, + dict_example={ + "display_name": "My Alert", + "query_text": "SELECT 1", + "warehouse_id": "my_warehouse", + "evaluation": { + "comparison_operator": "GREATER_THAN", + "source": {"name": "column_1"}, + }, + "schedule": { + "quartz_cron_schedule": "0 0 0 * * ?", + "timezone_id": "UTC", + }, + }, + dataclass_example=Alert( + display_name="My Alert", + query_text="SELECT 1", + warehouse_id="my_warehouse", + evaluation=AlertV2Evaluation( + comparison_operator=ComparisonOperator.GREATER_THAN, + source=AlertV2OperandColumn(name="column_1"), + ), + schedule=CronSchedule( + quartz_cron_schedule="0 0 0 * * ?", + timezone_id="UTC", + ), + ), + mutator=alert_mutator, + article="an", + ), + resource_types[Alert], + ), ] test_case_ids = [tpe.plural_name for _, tpe in test_cases] @@ -270,7 +313,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 a {tpe.singular_name}. Resource names must be unique." + == f"Duplicate resource name 'my_resource' for {tc.article} {tpe.singular_name}. Resource names must be unique." )