jamtur01's picture
Upload folder using huggingface_hub
9c6594c verified
"""Actions that are triggered by W&B Automations."""
# ruff: noqa: UP007 # Avoid using `X | Y` for union fields, as this can cause issues with pydantic < 2.6
from __future__ import annotations
from typing import Any, Literal, Optional, Union
from pydantic import BeforeValidator, Field
from typing_extensions import Annotated, Self, get_args
from wandb._pydantic import GQLBase, GQLId, SerializedToJson, Typename
from ._generated import (
AlertSeverity,
GenericWebhookActionFields,
GenericWebhookActionInput,
NoOpActionFields,
NoOpTriggeredActionInput,
NotificationActionFields,
NotificationActionInput,
QueueJobActionFields,
)
from ._validators import (
LenientStrEnum,
default_if_none,
to_input_action,
to_saved_action,
upper_if_str,
)
from .integrations import SlackIntegration, WebhookIntegration
# NOTE: Name shortened for readability and defined publicly for easier access
class ActionType(LenientStrEnum):
"""The type of action triggered by an automation."""
QUEUE_JOB = "QUEUE_JOB" # NOTE: Deprecated for creation
NOTIFICATION = "NOTIFICATION"
GENERIC_WEBHOOK = "GENERIC_WEBHOOK"
NO_OP = "NO_OP"
# ------------------------------------------------------------------------------
# Saved types: for parsing response data from saved automations
# NOTE: `QueueJobActionInput` for defining a Launch job is deprecated,
# so while we allow parsing it from previously saved Automations, we deliberately
# don't currently expose it in the API for creating automations.
class SavedLaunchJobAction(QueueJobActionFields):
action_type: Literal[ActionType.QUEUE_JOB] = ActionType.QUEUE_JOB
# FIXME: Find a better place to put these OR a better way to handle the
# conversion from `InputAction` -> `SavedAction`.
#
# Necessary placeholder class defs for converting:
# - `SendNotification -> SavedNotificationAction`
# - `SendWebhook -> SavedWebhookAction`
#
# The "input" types (`Send{Notification,Webhook}`) will only have an `integration_id`,
# and we don't want/need to fetch the other `{Slack,Webhook}Integration` fields if
# we can avoid it.
class _SavedActionSlackIntegration(GQLBase, extra="allow"):
typename__: Typename[Literal["SlackIntegration"]] = "SlackIntegration"
id: GQLId
class _SavedActionWebhookIntegration(GQLBase, extra="allow"):
typename__: Typename[Literal["GenericWebhookIntegration"]] = (
"GenericWebhookIntegration"
)
id: GQLId
class SavedNotificationAction(NotificationActionFields):
action_type: Literal[ActionType.NOTIFICATION] = ActionType.NOTIFICATION
integration: _SavedActionSlackIntegration
class SavedWebhookAction(GenericWebhookActionFields):
action_type: Literal[ActionType.GENERIC_WEBHOOK] = ActionType.GENERIC_WEBHOOK
integration: _SavedActionWebhookIntegration
# We override the type of the `requestPayload` field since the original GraphQL
# schema (and generated class) effectively defines it as a string, when we know
# and need to anticipate the expected structure of the JSON-serialized data.
request_payload: Annotated[
Optional[SerializedToJson[dict[str, Any]]],
Field(alias="requestPayload"),
] = None # type: ignore[assignment]
class SavedNoOpAction(NoOpActionFields, frozen=True):
action_type: Literal[ActionType.NO_OP] = ActionType.NO_OP
no_op: Annotated[bool, BeforeValidator(default_if_none)] = True
"""Placeholder field, only needed to conform to schema requirements.
There should never be a need to set this field explicitly, as its value is ignored.
"""
# for type annotations
SavedAction = Annotated[
Union[
SavedLaunchJobAction,
SavedNotificationAction,
SavedWebhookAction,
SavedNoOpAction,
],
BeforeValidator(to_saved_action),
Field(discriminator="typename__"),
]
# for runtime type checks
SavedActionTypes: tuple[type, ...] = get_args(SavedAction.__origin__) # type: ignore[attr-defined]
# ------------------------------------------------------------------------------
# Input types: for creating or updating automations
class _BaseActionInput(GQLBase):
action_type: Annotated[ActionType, Field(frozen=True)]
"""The kind of action to be triggered."""
class SendNotification(_BaseActionInput, NotificationActionInput):
"""Defines an automation action that sends a (Slack) notification."""
action_type: Literal[ActionType.NOTIFICATION] = ActionType.NOTIFICATION
integration_id: GQLId
"""The ID of the Slack integration that will be used to send the notification."""
# Note: Validation aliases are meant to provide continuity with prior `wandb.alert()` API.
title: str = ""
"""The title of the sent notification."""
message: Annotated[str, Field(validation_alias="text")] = ""
"""The message body of the sent notification."""
severity: Annotated[
AlertSeverity,
BeforeValidator(upper_if_str), # Be helpful by ensuring uppercase strings
Field(validation_alias="level"),
] = AlertSeverity.INFO
"""The severity (`INFO`, `WARN`, `ERROR`) of the sent notification."""
@classmethod
def from_integration(
cls,
integration: SlackIntegration,
*,
title: str = "",
text: str = "",
level: AlertSeverity = AlertSeverity.INFO,
) -> Self:
"""Define a notification action that sends to the given (Slack) integration."""
return cls(
integration_id=integration.id,
title=title,
message=text,
severity=level,
)
class SendWebhook(_BaseActionInput, GenericWebhookActionInput):
"""Defines an automation action that sends a webhook request."""
action_type: Literal[ActionType.GENERIC_WEBHOOK] = ActionType.GENERIC_WEBHOOK
integration_id: GQLId
"""The ID of the webhook integration that will be used to send the request."""
# overrides the generated field type to parse/serialize JSON strings
request_payload: Optional[SerializedToJson[dict[str, Any]]] = Field( # type: ignore[assignment]
default=None, alias="requestPayload"
)
"""The payload, possibly with template variables, to send in the webhook request."""
@classmethod
def from_integration(
cls,
integration: WebhookIntegration,
*,
payload: Optional[SerializedToJson[dict[str, Any]]] = None,
) -> Self:
"""Define a webhook action that sends to the given (webhook) integration."""
return cls(integration_id=integration.id, request_payload=payload)
class DoNothing(_BaseActionInput, NoOpTriggeredActionInput, frozen=True):
"""Defines an automation action that intentionally does nothing."""
action_type: Literal[ActionType.NO_OP] = ActionType.NO_OP
no_op: Annotated[bool, BeforeValidator(default_if_none)] = True
"""Placeholder field which exists only to satisfy backend schema requirements.
There should never be a need to set this field explicitly, as its value is ignored.
"""
# for type annotations
InputAction = Annotated[
Union[
SendNotification,
SendWebhook,
DoNothing,
],
BeforeValidator(to_input_action),
Field(discriminator="action_type"),
]
# for runtime type checks
InputActionTypes: tuple[type, ...] = get_args(InputAction.__origin__) # type: ignore[attr-defined]
__all__ = [
"ActionType",
*(cls.__name__ for cls in InputActionTypes),
]