wandb 0.19.9__py3-none-win_amd64.whl → 0.19.11__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wandb/__init__.py +1 -1
- wandb/__init__.pyi +6 -3
- wandb/_pydantic/__init__.py +14 -8
- wandb/_pydantic/base.py +51 -36
- wandb/_pydantic/utils.py +73 -0
- wandb/_pydantic/v1_compat.py +79 -57
- wandb/apis/public/__init__.py +2 -2
- wandb/apis/public/api.py +684 -4
- wandb/apis/public/artifacts.py +377 -677
- wandb/apis/public/automations.py +69 -0
- wandb/apis/public/integrations.py +180 -0
- wandb/apis/public/projects.py +29 -0
- wandb/apis/public/registries/__init__.py +0 -0
- wandb/apis/public/registries/_freezable_list.py +179 -0
- wandb/apis/public/{registries.py → registries/registries_search.py} +22 -129
- wandb/apis/public/registries/registry.py +357 -0
- wandb/apis/public/registries/utils.py +140 -0
- wandb/apis/public/runs.py +58 -56
- wandb/apis/public/utils.py +107 -1
- wandb/automations/__init__.py +73 -0
- wandb/automations/_filters/__init__.py +40 -0
- wandb/automations/_filters/expressions.py +181 -0
- wandb/automations/_filters/operators.py +258 -0
- wandb/automations/_filters/run_metrics.py +332 -0
- wandb/automations/_generated/__init__.py +177 -0
- wandb/automations/_generated/create_automation.py +17 -0
- wandb/automations/_generated/create_generic_webhook_integration.py +43 -0
- wandb/automations/_generated/delete_automation.py +17 -0
- wandb/automations/_generated/enums.py +33 -0
- wandb/automations/_generated/fragments.py +358 -0
- wandb/automations/_generated/generic_webhook_integrations_by_entity.py +22 -0
- wandb/automations/_generated/get_automations.py +24 -0
- wandb/automations/_generated/get_automations_by_entity.py +26 -0
- wandb/automations/_generated/input_types.py +104 -0
- wandb/automations/_generated/integrations_by_entity.py +22 -0
- wandb/automations/_generated/operations.py +647 -0
- wandb/automations/_generated/slack_integrations_by_entity.py +22 -0
- wandb/automations/_generated/update_automation.py +17 -0
- wandb/automations/_utils.py +237 -0
- wandb/automations/_validators.py +165 -0
- wandb/automations/actions.py +220 -0
- wandb/automations/automations.py +87 -0
- wandb/automations/events.py +287 -0
- wandb/automations/integrations.py +45 -0
- wandb/automations/scopes.py +78 -0
- wandb/beta/workflows.py +9 -10
- wandb/bin/gpu_stats.exe +0 -0
- wandb/bin/wandb-core +0 -0
- wandb/cli/cli.py +3 -3
- wandb/env.py +11 -0
- wandb/integration/keras/keras.py +2 -1
- wandb/integration/langchain/wandb_tracer.py +2 -1
- wandb/jupyter.py +137 -118
- wandb/old/settings.py +4 -1
- wandb/old/summary.py +0 -2
- wandb/proto/v3/wandb_internal_pb2.py +297 -292
- wandb/proto/v3/wandb_settings_pb2.py +2 -2
- wandb/proto/v3/wandb_telemetry_pb2.py +10 -10
- wandb/proto/v4/wandb_internal_pb2.py +292 -292
- wandb/proto/v4/wandb_settings_pb2.py +2 -2
- wandb/proto/v4/wandb_telemetry_pb2.py +10 -10
- wandb/proto/v5/wandb_internal_pb2.py +292 -292
- wandb/proto/v5/wandb_settings_pb2.py +2 -2
- wandb/proto/v5/wandb_telemetry_pb2.py +10 -10
- wandb/proto/v6/wandb_base_pb2.py +41 -0
- wandb/proto/v6/wandb_internal_pb2.py +393 -0
- wandb/proto/v6/wandb_server_pb2.py +78 -0
- wandb/proto/v6/wandb_settings_pb2.py +58 -0
- wandb/proto/v6/wandb_telemetry_pb2.py +52 -0
- wandb/proto/wandb_base_pb2.py +2 -0
- wandb/proto/wandb_deprecated.py +8 -0
- wandb/proto/wandb_internal_pb2.py +3 -1
- wandb/proto/wandb_server_pb2.py +2 -0
- wandb/proto/wandb_settings_pb2.py +2 -0
- wandb/proto/wandb_telemetry_pb2.py +2 -0
- wandb/sdk/artifacts/_generated/__init__.py +289 -0
- wandb/sdk/artifacts/_generated/add_aliases.py +21 -0
- wandb/sdk/artifacts/_generated/artifact_collection_membership_files.py +43 -0
- wandb/sdk/artifacts/_generated/artifact_version_files.py +36 -0
- wandb/sdk/artifacts/_generated/create_artifact_collection_tag_assignments.py +36 -0
- wandb/sdk/artifacts/_generated/delete_aliases.py +21 -0
- wandb/sdk/artifacts/_generated/delete_artifact_collection_tag_assignments.py +25 -0
- wandb/sdk/artifacts/_generated/delete_artifact_portfolio.py +35 -0
- wandb/sdk/artifacts/_generated/delete_artifact_sequence.py +35 -0
- wandb/sdk/artifacts/_generated/enums.py +17 -0
- wandb/sdk/artifacts/_generated/fetch_linked_artifacts.py +67 -0
- wandb/sdk/artifacts/_generated/fragments.py +221 -0
- wandb/sdk/artifacts/_generated/input_types.py +28 -0
- wandb/sdk/artifacts/_generated/move_artifact_collection.py +35 -0
- wandb/sdk/artifacts/_generated/operations.py +611 -0
- wandb/sdk/artifacts/_generated/project_artifact_collection.py +101 -0
- wandb/sdk/artifacts/_generated/project_artifact_collections.py +33 -0
- wandb/sdk/artifacts/_generated/project_artifact_type.py +24 -0
- wandb/sdk/artifacts/_generated/project_artifact_types.py +24 -0
- wandb/sdk/artifacts/_generated/project_artifacts.py +42 -0
- wandb/sdk/artifacts/_generated/run_input_artifacts.py +51 -0
- wandb/sdk/artifacts/_generated/run_output_artifacts.py +51 -0
- wandb/sdk/artifacts/_generated/update_artifact.py +26 -0
- wandb/sdk/artifacts/_generated/update_artifact_portfolio.py +35 -0
- wandb/sdk/artifacts/_generated/update_artifact_sequence.py +35 -0
- wandb/sdk/artifacts/_graphql_fragments.py +57 -79
- wandb/sdk/artifacts/_validators.py +120 -1
- wandb/sdk/artifacts/artifact.py +419 -215
- wandb/sdk/artifacts/artifact_file_cache.py +4 -6
- wandb/sdk/artifacts/artifact_manifest_entry.py +13 -3
- wandb/sdk/artifacts/storage_handlers/azure_handler.py +1 -0
- wandb/sdk/artifacts/storage_policies/wandb_storage_policy.py +182 -1
- wandb/sdk/artifacts/storage_policy.py +3 -0
- wandb/sdk/data_types/base_types/media.py +2 -3
- wandb/sdk/data_types/base_types/wb_value.py +34 -11
- wandb/sdk/data_types/html.py +36 -9
- wandb/sdk/data_types/image.py +12 -12
- wandb/sdk/data_types/table.py +5 -0
- wandb/sdk/data_types/trace_tree.py +2 -0
- wandb/sdk/data_types/utils.py +1 -1
- wandb/sdk/data_types/video.py +59 -57
- wandb/sdk/interface/interface.py +4 -3
- wandb/sdk/internal/internal_api.py +21 -31
- wandb/sdk/internal/profiler.py +6 -5
- wandb/sdk/internal/run.py +13 -6
- wandb/sdk/internal/sender.py +5 -2
- wandb/sdk/launch/sweeps/utils.py +8 -0
- wandb/sdk/lib/apikey.py +25 -4
- wandb/sdk/lib/asyncio_compat.py +1 -1
- wandb/sdk/lib/deprecate.py +13 -22
- wandb/sdk/lib/disabled.py +2 -1
- wandb/sdk/lib/printer.py +37 -8
- wandb/sdk/lib/printer_asyncio.py +46 -0
- wandb/sdk/lib/redirect.py +10 -5
- wandb/sdk/projects/_generated/__init__.py +47 -0
- wandb/sdk/projects/_generated/delete_project.py +22 -0
- wandb/sdk/projects/_generated/enums.py +4 -0
- wandb/sdk/projects/_generated/fetch_registry.py +22 -0
- wandb/sdk/projects/_generated/fragments.py +41 -0
- wandb/sdk/projects/_generated/input_types.py +13 -0
- wandb/sdk/projects/_generated/operations.py +88 -0
- wandb/sdk/projects/_generated/rename_project.py +27 -0
- wandb/sdk/projects/_generated/upsert_registry_project.py +27 -0
- wandb/sdk/service/server_sock.py +19 -14
- wandb/sdk/service/service.py +18 -8
- wandb/sdk/service/streams.py +5 -0
- wandb/sdk/verify/verify.py +6 -3
- wandb/sdk/wandb_init.py +217 -70
- wandb/sdk/wandb_login.py +13 -4
- wandb/sdk/wandb_run.py +419 -295
- wandb/sdk/wandb_settings.py +27 -10
- wandb/sdk/wandb_setup.py +61 -0
- wandb/util.py +33 -29
- {wandb-0.19.9.dist-info → wandb-0.19.11.dist-info}/METADATA +5 -5
- {wandb-0.19.9.dist-info → wandb-0.19.11.dist-info}/RECORD +153 -83
- wandb/_globals.py +0 -19
- wandb/sdk/internal/_generated/base.py +0 -226
- wandb/sdk/internal/_generated/typing_compat.py +0 -14
- {wandb-0.19.9.dist-info → wandb-0.19.11.dist-info}/WHEEL +0 -0
- {wandb-0.19.9.dist-info → wandb-0.19.11.dist-info}/entry_points.txt +0 -0
- {wandb-0.19.9.dist-info → wandb-0.19.11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,237 @@
|
|
1
|
+
# ruff: noqa: UP007 # Avoid using `X | Y` for union fields, as this can cause issues with pydantic < 2.6
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Any, Collection, Final, Optional, Protocol, TypedDict
|
6
|
+
|
7
|
+
from pydantic import Field
|
8
|
+
from typing_extensions import Annotated, Self, Unpack
|
9
|
+
|
10
|
+
from wandb._pydantic import GQLBase, GQLId, computed_field, model_validator, to_json
|
11
|
+
|
12
|
+
from ._filters import MongoLikeFilter
|
13
|
+
from ._generated import (
|
14
|
+
CreateFilterTriggerInput,
|
15
|
+
QueueJobActionInput,
|
16
|
+
TriggeredActionConfig,
|
17
|
+
UpdateFilterTriggerInput,
|
18
|
+
)
|
19
|
+
from ._validators import to_input_action
|
20
|
+
from .actions import (
|
21
|
+
ActionType,
|
22
|
+
DoNothing,
|
23
|
+
InputAction,
|
24
|
+
SavedAction,
|
25
|
+
SendNotification,
|
26
|
+
SendWebhook,
|
27
|
+
)
|
28
|
+
from .automations import Automation, NewAutomation
|
29
|
+
from .events import EventType, InputEvent, RunMetricFilter, _WrappedSavedEventFilter
|
30
|
+
from .scopes import AutomationScope, ScopeType
|
31
|
+
|
32
|
+
EXCLUDED_INPUT_EVENTS: Final[Collection[EventType]] = frozenset(
|
33
|
+
{
|
34
|
+
EventType.UPDATE_ARTIFACT_ALIAS,
|
35
|
+
}
|
36
|
+
)
|
37
|
+
"""Event types that should not be assigned when creating/updating automations."""
|
38
|
+
|
39
|
+
EXCLUDED_INPUT_ACTIONS: Final[Collection[ActionType]] = frozenset(
|
40
|
+
{
|
41
|
+
ActionType.QUEUE_JOB,
|
42
|
+
}
|
43
|
+
)
|
44
|
+
"""Action types that should not be assigned when creating/updating automations."""
|
45
|
+
|
46
|
+
ALWAYS_SUPPORTED_EVENTS: Final[Collection[EventType]] = frozenset(
|
47
|
+
{
|
48
|
+
EventType.CREATE_ARTIFACT,
|
49
|
+
EventType.LINK_ARTIFACT,
|
50
|
+
EventType.ADD_ARTIFACT_ALIAS,
|
51
|
+
}
|
52
|
+
)
|
53
|
+
"""Event types that we can safely assume all contemporary server versions support."""
|
54
|
+
|
55
|
+
ALWAYS_SUPPORTED_ACTIONS: Final[Collection[ActionType]] = frozenset(
|
56
|
+
{
|
57
|
+
ActionType.NOTIFICATION,
|
58
|
+
ActionType.GENERIC_WEBHOOK,
|
59
|
+
}
|
60
|
+
)
|
61
|
+
"""Action types that we can safely assume all contemporary server versions support."""
|
62
|
+
|
63
|
+
|
64
|
+
class HasId(Protocol):
|
65
|
+
id: str
|
66
|
+
|
67
|
+
|
68
|
+
def extract_id(obj: HasId | str) -> str:
|
69
|
+
return obj.id if hasattr(obj, "id") else obj
|
70
|
+
|
71
|
+
|
72
|
+
# ---------------------------------------------------------------------------
|
73
|
+
ACTION_CONFIG_KEYS: dict[ActionType, str] = {
|
74
|
+
ActionType.NOTIFICATION: "notification_action_input",
|
75
|
+
ActionType.GENERIC_WEBHOOK: "generic_webhook_action_input",
|
76
|
+
ActionType.NO_OP: "no_op_action_input",
|
77
|
+
ActionType.QUEUE_JOB: "queue_job_action_input",
|
78
|
+
}
|
79
|
+
|
80
|
+
|
81
|
+
class InputActionConfig(TriggeredActionConfig):
|
82
|
+
"""A `TriggeredActionConfig` that prepares the action config for saving an automation."""
|
83
|
+
|
84
|
+
# NOTE: `QueueJobActionInput` for defining a Launch job is deprecated,
|
85
|
+
# so while it's allowed here to update EXISTING mutations, we don't
|
86
|
+
# currently expose it through the public API to create NEW automations.
|
87
|
+
queue_job_action_input: Optional[QueueJobActionInput] = None
|
88
|
+
|
89
|
+
notification_action_input: Optional[SendNotification] = None
|
90
|
+
generic_webhook_action_input: Optional[SendWebhook] = None
|
91
|
+
no_op_action_input: Optional[DoNothing] = None
|
92
|
+
|
93
|
+
|
94
|
+
def prepare_action_config_input(obj: SavedAction | InputAction) -> dict[str, Any]:
|
95
|
+
"""Prepare the `TriggeredActionConfig` input, nesting the action input inside the appropriate key.
|
96
|
+
|
97
|
+
This is necessary to conform to the schemas for:
|
98
|
+
- CreateFilterTriggerInput
|
99
|
+
- UpdateFilterTriggerInput
|
100
|
+
"""
|
101
|
+
# Delegate to inner validators to convert SavedAction -> InputAction types, if needed.
|
102
|
+
obj = to_input_action(obj)
|
103
|
+
return InputActionConfig(**{ACTION_CONFIG_KEYS[obj.action_type]: obj}).model_dump()
|
104
|
+
|
105
|
+
|
106
|
+
def prepare_event_filter_input(
|
107
|
+
obj: _WrappedSavedEventFilter | MongoLikeFilter | RunMetricFilter,
|
108
|
+
) -> str:
|
109
|
+
"""Prepare the `EventFilter` input, unnesting the filter if needed and serializing to JSON.
|
110
|
+
|
111
|
+
This is necessary to conform to the schemas for:
|
112
|
+
- CreateFilterTriggerInput
|
113
|
+
- UpdateFilterTriggerInput
|
114
|
+
"""
|
115
|
+
# Input event filters are nested one level deeper than saved event filters.
|
116
|
+
# Note that this is NOT the case for run/run metric filters.
|
117
|
+
#
|
118
|
+
# Yes, this is confusing. It's also necessary to conform to under-the-hood
|
119
|
+
# schemas and logic in the backend.
|
120
|
+
filter_to_serialize = (
|
121
|
+
obj.filter if isinstance(obj, _WrappedSavedEventFilter) else obj
|
122
|
+
)
|
123
|
+
return to_json(filter_to_serialize)
|
124
|
+
|
125
|
+
|
126
|
+
class WriteAutomationsKwargs(TypedDict, total=False):
|
127
|
+
"""Keyword arguments that can be passed to create or update an automation."""
|
128
|
+
|
129
|
+
name: str
|
130
|
+
description: str
|
131
|
+
enabled: bool
|
132
|
+
scope: AutomationScope
|
133
|
+
event: InputEvent
|
134
|
+
action: InputAction
|
135
|
+
|
136
|
+
|
137
|
+
class ValidatedCreateInput(GQLBase, extra="forbid", frozen=True):
|
138
|
+
"""Validated automation parameters, prepared for creating a new automation.
|
139
|
+
|
140
|
+
Note: Users should never need to instantiate this class directly.
|
141
|
+
"""
|
142
|
+
|
143
|
+
name: str
|
144
|
+
description: Optional[str] = None
|
145
|
+
enabled: bool = True
|
146
|
+
|
147
|
+
# ------------------------------------------------------------------------------
|
148
|
+
# Set on instantiation, but used to derive other fields and deliberately
|
149
|
+
# EXCLUDED from the final GraphQL request vars
|
150
|
+
event: Annotated[InputEvent, Field(exclude=True)]
|
151
|
+
action: Annotated[InputAction, Field(exclude=True)]
|
152
|
+
|
153
|
+
# ------------------------------------------------------------------------------
|
154
|
+
# Derived fields to match the input schemas
|
155
|
+
@computed_field
|
156
|
+
def scope_type(self) -> ScopeType:
|
157
|
+
return self.event.scope.scope_type
|
158
|
+
|
159
|
+
@computed_field
|
160
|
+
def scope_id(self) -> GQLId:
|
161
|
+
return self.event.scope.id
|
162
|
+
|
163
|
+
@computed_field
|
164
|
+
def triggering_event_type(self) -> EventType:
|
165
|
+
return self.event.event_type
|
166
|
+
|
167
|
+
@computed_field
|
168
|
+
def event_filter(self) -> str:
|
169
|
+
return prepare_event_filter_input(self.event.filter)
|
170
|
+
|
171
|
+
@computed_field
|
172
|
+
def triggered_action_type(self) -> ActionType:
|
173
|
+
return self.action.action_type
|
174
|
+
|
175
|
+
@computed_field
|
176
|
+
def triggered_action_config(self) -> dict[str, Any]:
|
177
|
+
return prepare_action_config_input(self.action)
|
178
|
+
|
179
|
+
# ------------------------------------------------------------------------------
|
180
|
+
# Custom validation
|
181
|
+
@model_validator(mode="after")
|
182
|
+
def _forbid_legacy_event_types(self) -> Self:
|
183
|
+
if (type_ := self.event.event_type) in EXCLUDED_INPUT_EVENTS:
|
184
|
+
raise ValueError(f"{type_!r} events cannot be assigned to automations.")
|
185
|
+
return self
|
186
|
+
|
187
|
+
@model_validator(mode="after")
|
188
|
+
def _forbid_legacy_action_types(self) -> Self:
|
189
|
+
if (type_ := self.action.action_type) in EXCLUDED_INPUT_ACTIONS:
|
190
|
+
raise ValueError(f"{type_!r} actions cannot be assigned to automations.")
|
191
|
+
return self
|
192
|
+
|
193
|
+
|
194
|
+
def prepare_to_create(
|
195
|
+
obj: NewAutomation | None = None,
|
196
|
+
/,
|
197
|
+
**kwargs: Unpack[WriteAutomationsKwargs],
|
198
|
+
) -> CreateFilterTriggerInput:
|
199
|
+
"""Prepares the payload to create an automation in a GraphQL request."""
|
200
|
+
# Validate all input variables, and prepare as expected by the GraphQL request.
|
201
|
+
# - if an object is provided, override its fields with any keyword args
|
202
|
+
# - otherwise, instantiate from the keyword args
|
203
|
+
|
204
|
+
# NOTE: `exclude_none=True` drops fields that are still `None`.
|
205
|
+
#
|
206
|
+
# This assumes that `None` is good enough for now as a sentinel
|
207
|
+
# "unset" value. If this proves insufficient, revisit in the future,
|
208
|
+
# as it should be reasonably easy to implement a custom sentinel
|
209
|
+
# type later on.
|
210
|
+
obj_dict = {**obj.model_dump(exclude_none=True), **kwargs} if obj else kwargs
|
211
|
+
validated = ValidatedCreateInput(**obj_dict)
|
212
|
+
return CreateFilterTriggerInput.model_validate(validated)
|
213
|
+
|
214
|
+
|
215
|
+
def prepare_to_update(
|
216
|
+
obj: Automation | None = None,
|
217
|
+
/,
|
218
|
+
**kwargs: Unpack[WriteAutomationsKwargs],
|
219
|
+
) -> UpdateFilterTriggerInput:
|
220
|
+
"""Prepares the payload to update an automation in a GraphQL request."""
|
221
|
+
# Validate all values:
|
222
|
+
# - if an object is provided, override its fields with any keyword args
|
223
|
+
# - otherwise, instantiate from the keyword args
|
224
|
+
v_obj = Automation(**{**dict(obj or {}), **kwargs})
|
225
|
+
|
226
|
+
return UpdateFilterTriggerInput(
|
227
|
+
id=v_obj.id,
|
228
|
+
name=v_obj.name,
|
229
|
+
description=v_obj.description,
|
230
|
+
enabled=v_obj.enabled,
|
231
|
+
scope_type=v_obj.scope.scope_type,
|
232
|
+
scope_id=v_obj.scope.id,
|
233
|
+
triggering_event_type=v_obj.event.event_type,
|
234
|
+
event_filter=prepare_event_filter_input(v_obj.event.filter),
|
235
|
+
triggered_action_type=v_obj.action.action_type,
|
236
|
+
triggered_action_config=prepare_action_config_input(v_obj.action),
|
237
|
+
)
|
@@ -0,0 +1,165 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from enum import Enum
|
4
|
+
from functools import singledispatch
|
5
|
+
from itertools import chain
|
6
|
+
from typing import Any, TypeVar
|
7
|
+
|
8
|
+
from pydantic_core import PydanticUseDefault
|
9
|
+
|
10
|
+
from ._filters import And, FilterExpr, In, Nor, Not, NotIn, Op, Or
|
11
|
+
|
12
|
+
T = TypeVar("T")
|
13
|
+
|
14
|
+
|
15
|
+
class LenientStrEnum(str, Enum):
|
16
|
+
"""A string enum allowing for case-insensitive lookups by value.
|
17
|
+
|
18
|
+
May include other internal customizations if needed.
|
19
|
+
|
20
|
+
Note: This is a bespoke, internal implementation and NOT intended as a
|
21
|
+
backport of `enum.StrEnum` from Python 3.11+.
|
22
|
+
"""
|
23
|
+
|
24
|
+
def __repr__(self) -> str:
|
25
|
+
return self.name
|
26
|
+
|
27
|
+
@classmethod
|
28
|
+
def _missing_(cls, value: object) -> Any:
|
29
|
+
# Accept case-insensitive enum values
|
30
|
+
if isinstance(value, str):
|
31
|
+
v = value.lower()
|
32
|
+
return next((e for e in cls if e.value.lower() == v), None)
|
33
|
+
return None
|
34
|
+
|
35
|
+
|
36
|
+
def default_if_none(v: Any) -> Any:
|
37
|
+
"""A before-validator validator that coerces `None` to the default field value instead."""
|
38
|
+
# https://docs.pydantic.dev/2.11/api/pydantic_core/#pydantic_core.PydanticUseDefault
|
39
|
+
if v is None:
|
40
|
+
raise PydanticUseDefault
|
41
|
+
return v
|
42
|
+
|
43
|
+
|
44
|
+
def upper_if_str(v: Any) -> Any:
|
45
|
+
return v.strip().upper() if isinstance(v, str) else v
|
46
|
+
|
47
|
+
|
48
|
+
# ----------------------------------------------------------------------------
|
49
|
+
def to_scope(v: Any) -> Any:
|
50
|
+
"""Convert eligible objects, including pre-existing `wandb` types, to an automation scope."""
|
51
|
+
from wandb.apis.public import ArtifactCollection, Project
|
52
|
+
|
53
|
+
from .scopes import ProjectScope, _ArtifactPortfolioScope, _ArtifactSequenceScope
|
54
|
+
|
55
|
+
if isinstance(v, Project):
|
56
|
+
return ProjectScope(id=v.id, name=v.name)
|
57
|
+
if isinstance(v, ArtifactCollection):
|
58
|
+
cls = _ArtifactSequenceScope if v.is_sequence() else _ArtifactPortfolioScope
|
59
|
+
return cls(id=v.id, name=v.name)
|
60
|
+
return v
|
61
|
+
|
62
|
+
|
63
|
+
def to_saved_action(v: Any) -> Any:
|
64
|
+
"""If necessary (and possible), convert the object to a saved action."""
|
65
|
+
from .actions import (
|
66
|
+
DoNothing,
|
67
|
+
SavedNoOpAction,
|
68
|
+
SavedNotificationAction,
|
69
|
+
SavedWebhookAction,
|
70
|
+
SendNotification,
|
71
|
+
SendWebhook,
|
72
|
+
)
|
73
|
+
|
74
|
+
if isinstance(v, SendNotification):
|
75
|
+
return SavedNotificationAction(
|
76
|
+
integration={"id": v.integration_id},
|
77
|
+
**v.model_dump(exclude={"integration_id"}),
|
78
|
+
)
|
79
|
+
if isinstance(v, SendWebhook):
|
80
|
+
return SavedWebhookAction(
|
81
|
+
integration={"id": v.integration_id},
|
82
|
+
**v.model_dump(exclude={"integration_id"}),
|
83
|
+
)
|
84
|
+
if isinstance(v, DoNothing):
|
85
|
+
return SavedNoOpAction.model_validate(v)
|
86
|
+
|
87
|
+
return v
|
88
|
+
|
89
|
+
|
90
|
+
def to_input_action(v: Any) -> Any:
|
91
|
+
"""If necessary (and possible), convert the object to an input action."""
|
92
|
+
from .actions import (
|
93
|
+
DoNothing,
|
94
|
+
SavedNoOpAction,
|
95
|
+
SavedNotificationAction,
|
96
|
+
SavedWebhookAction,
|
97
|
+
SendNotification,
|
98
|
+
SendWebhook,
|
99
|
+
)
|
100
|
+
|
101
|
+
if isinstance(v, SavedNotificationAction):
|
102
|
+
return SendNotification(
|
103
|
+
integration_id=v.integration.id,
|
104
|
+
**v.model_dump(exclude={"integration"}),
|
105
|
+
)
|
106
|
+
if isinstance(v, SavedWebhookAction):
|
107
|
+
return SendWebhook(
|
108
|
+
integration_id=v.integration.id,
|
109
|
+
**v.model_dump(exclude={"integration"}),
|
110
|
+
)
|
111
|
+
if isinstance(v, SavedNoOpAction):
|
112
|
+
return DoNothing.model_validate(v)
|
113
|
+
|
114
|
+
return v
|
115
|
+
|
116
|
+
|
117
|
+
# ----------------------------------------------------------------------------
|
118
|
+
@singledispatch
|
119
|
+
def simplify_op(op: Op | FilterExpr) -> Op | FilterExpr:
|
120
|
+
"""Simplify a MongoDB filter by removing and unnesting redundant operators."""
|
121
|
+
return op
|
122
|
+
|
123
|
+
|
124
|
+
@simplify_op.register
|
125
|
+
def _(op: And) -> Op:
|
126
|
+
# {"$and": []} -> {"$and": []}
|
127
|
+
if not (args := op.and_):
|
128
|
+
return op
|
129
|
+
|
130
|
+
# {"$and": [op]} -> op
|
131
|
+
if len(args) == 1:
|
132
|
+
return simplify_op(args[0])
|
133
|
+
|
134
|
+
# {"$and": [op, {"$and": [op2, ...]}]} -> {"$and": [op, op2, ...]}
|
135
|
+
flattened = chain.from_iterable(x.and_ if isinstance(x, And) else [x] for x in args)
|
136
|
+
return And(and_=map(simplify_op, flattened))
|
137
|
+
|
138
|
+
|
139
|
+
@simplify_op.register
|
140
|
+
def _(op: Or) -> Op:
|
141
|
+
# {"$or": []} -> {"$or": []}
|
142
|
+
if not (args := op.or_):
|
143
|
+
return op
|
144
|
+
|
145
|
+
# {"$or": [op]} -> op
|
146
|
+
if len(args) == 1:
|
147
|
+
return simplify_op(args[0])
|
148
|
+
|
149
|
+
# {"$or": [op, {"$or": [op2, ...]}]} -> {"$or": [op, op2, ...]}
|
150
|
+
flattened = chain.from_iterable(x.or_ if isinstance(x, Or) else [x] for x in args)
|
151
|
+
return Or(or_=map(simplify_op, flattened))
|
152
|
+
|
153
|
+
|
154
|
+
@simplify_op.register
|
155
|
+
def _(op: Not) -> Op:
|
156
|
+
inner = op.not_
|
157
|
+
|
158
|
+
# {"$not": {"$not": op}} -> op
|
159
|
+
# {"$not": {"$or": [op, ...]}} -> {"$nor": [op, ...]}
|
160
|
+
# {"$not": {"$nor": [op, ...]}} -> {"$or": [op, ...]}
|
161
|
+
# {"$not": {"$in": [op, ...]}} -> {"$nin": [op, ...]}
|
162
|
+
# {"$not": {"$nin": [op, ...]}} -> {"$in": [op, ...]}
|
163
|
+
if isinstance(inner, (Not, Or, Nor, In, NotIn)):
|
164
|
+
return simplify_op(~inner)
|
165
|
+
return Not(not_=simplify_op(inner))
|
@@ -0,0 +1,220 @@
|
|
1
|
+
"""Actions that are triggered by W&B Automations."""
|
2
|
+
|
3
|
+
# ruff: noqa: UP007 # Avoid using `X | Y` for union fields, as this can cause issues with pydantic < 2.6
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
from typing import Any, Literal, Optional, Union
|
8
|
+
|
9
|
+
from pydantic import BeforeValidator, Field
|
10
|
+
from typing_extensions import Annotated, Self, get_args
|
11
|
+
|
12
|
+
from wandb._pydantic import GQLBase, GQLId, SerializedToJson, Typename
|
13
|
+
|
14
|
+
from ._generated import (
|
15
|
+
AlertSeverity,
|
16
|
+
GenericWebhookActionFields,
|
17
|
+
GenericWebhookActionInput,
|
18
|
+
NoOpActionFields,
|
19
|
+
NoOpTriggeredActionInput,
|
20
|
+
NotificationActionFields,
|
21
|
+
NotificationActionInput,
|
22
|
+
QueueJobActionFields,
|
23
|
+
)
|
24
|
+
from ._validators import (
|
25
|
+
LenientStrEnum,
|
26
|
+
default_if_none,
|
27
|
+
to_input_action,
|
28
|
+
to_saved_action,
|
29
|
+
upper_if_str,
|
30
|
+
)
|
31
|
+
from .integrations import SlackIntegration, WebhookIntegration
|
32
|
+
|
33
|
+
|
34
|
+
# NOTE: Name shortened for readability and defined publicly for easier access
|
35
|
+
class ActionType(LenientStrEnum):
|
36
|
+
"""The type of action triggered by an automation."""
|
37
|
+
|
38
|
+
QUEUE_JOB = "QUEUE_JOB" # NOTE: Deprecated for creation
|
39
|
+
NOTIFICATION = "NOTIFICATION"
|
40
|
+
GENERIC_WEBHOOK = "GENERIC_WEBHOOK"
|
41
|
+
NO_OP = "NO_OP"
|
42
|
+
|
43
|
+
|
44
|
+
# ------------------------------------------------------------------------------
|
45
|
+
# Saved types: for parsing response data from saved automations
|
46
|
+
|
47
|
+
|
48
|
+
# NOTE: `QueueJobActionInput` for defining a Launch job is deprecated,
|
49
|
+
# so while we allow parsing it from previously saved Automations, we deliberately
|
50
|
+
# don't currently expose it in the API for creating automations.
|
51
|
+
class SavedLaunchJobAction(QueueJobActionFields):
|
52
|
+
action_type: Literal[ActionType.QUEUE_JOB] = ActionType.QUEUE_JOB
|
53
|
+
|
54
|
+
|
55
|
+
# FIXME: Find a better place to put these OR a better way to handle the
|
56
|
+
# conversion from `InputAction` -> `SavedAction`.
|
57
|
+
#
|
58
|
+
# Necessary placeholder class defs for converting:
|
59
|
+
# - `SendNotification -> SavedNotificationAction`
|
60
|
+
# - `SendWebhook -> SavedWebhookAction`
|
61
|
+
#
|
62
|
+
# The "input" types (`Send{Notification,Webhook}`) will only have an `integration_id`,
|
63
|
+
# and we don't want/need to fetch the other `{Slack,Webhook}Integration` fields if
|
64
|
+
# we can avoid it.
|
65
|
+
class _SavedActionSlackIntegration(GQLBase, extra="allow"):
|
66
|
+
typename__: Typename[Literal["SlackIntegration"]] = "SlackIntegration"
|
67
|
+
id: GQLId
|
68
|
+
|
69
|
+
|
70
|
+
class _SavedActionWebhookIntegration(GQLBase, extra="allow"):
|
71
|
+
typename__: Typename[Literal["GenericWebhookIntegration"]] = (
|
72
|
+
"GenericWebhookIntegration"
|
73
|
+
)
|
74
|
+
id: GQLId
|
75
|
+
|
76
|
+
|
77
|
+
class SavedNotificationAction(NotificationActionFields):
|
78
|
+
action_type: Literal[ActionType.NOTIFICATION] = ActionType.NOTIFICATION
|
79
|
+
integration: _SavedActionSlackIntegration
|
80
|
+
|
81
|
+
|
82
|
+
class SavedWebhookAction(GenericWebhookActionFields):
|
83
|
+
action_type: Literal[ActionType.GENERIC_WEBHOOK] = ActionType.GENERIC_WEBHOOK
|
84
|
+
integration: _SavedActionWebhookIntegration
|
85
|
+
|
86
|
+
# We override the type of the `requestPayload` field since the original GraphQL
|
87
|
+
# schema (and generated class) effectively defines it as a string, when we know
|
88
|
+
# and need to anticipate the expected structure of the JSON-serialized data.
|
89
|
+
request_payload: Annotated[
|
90
|
+
Optional[SerializedToJson[dict[str, Any]]],
|
91
|
+
Field(alias="requestPayload"),
|
92
|
+
] = None # type: ignore[assignment]
|
93
|
+
|
94
|
+
|
95
|
+
class SavedNoOpAction(NoOpActionFields, frozen=True):
|
96
|
+
action_type: Literal[ActionType.NO_OP] = ActionType.NO_OP
|
97
|
+
|
98
|
+
no_op: Annotated[bool, BeforeValidator(default_if_none)] = True
|
99
|
+
"""Placeholder field, only needed to conform to schema requirements.
|
100
|
+
|
101
|
+
There should never be a need to set this field explicitly, as its value is ignored.
|
102
|
+
"""
|
103
|
+
|
104
|
+
|
105
|
+
# for type annotations
|
106
|
+
SavedAction = Annotated[
|
107
|
+
Union[
|
108
|
+
SavedLaunchJobAction,
|
109
|
+
SavedNotificationAction,
|
110
|
+
SavedWebhookAction,
|
111
|
+
SavedNoOpAction,
|
112
|
+
],
|
113
|
+
BeforeValidator(to_saved_action),
|
114
|
+
Field(discriminator="typename__"),
|
115
|
+
]
|
116
|
+
# for runtime type checks
|
117
|
+
SavedActionTypes: tuple[type, ...] = get_args(SavedAction.__origin__) # type: ignore[attr-defined]
|
118
|
+
|
119
|
+
|
120
|
+
# ------------------------------------------------------------------------------
|
121
|
+
# Input types: for creating or updating automations
|
122
|
+
class _BaseActionInput(GQLBase):
|
123
|
+
action_type: Annotated[ActionType, Field(frozen=True)]
|
124
|
+
"""The kind of action to be triggered."""
|
125
|
+
|
126
|
+
|
127
|
+
class SendNotification(_BaseActionInput, NotificationActionInput):
|
128
|
+
"""Defines an automation action that sends a (Slack) notification."""
|
129
|
+
|
130
|
+
action_type: Literal[ActionType.NOTIFICATION] = ActionType.NOTIFICATION
|
131
|
+
|
132
|
+
integration_id: GQLId
|
133
|
+
"""The ID of the Slack integration that will be used to send the notification."""
|
134
|
+
|
135
|
+
# Note: Validation aliases are meant to provide continuity with prior `wandb.alert()` API.
|
136
|
+
title: str = ""
|
137
|
+
"""The title of the sent notification."""
|
138
|
+
|
139
|
+
message: Annotated[str, Field(validation_alias="text")] = ""
|
140
|
+
"""The message body of the sent notification."""
|
141
|
+
|
142
|
+
severity: Annotated[
|
143
|
+
AlertSeverity,
|
144
|
+
BeforeValidator(upper_if_str), # Be helpful by ensuring uppercase strings
|
145
|
+
Field(validation_alias="level"),
|
146
|
+
] = AlertSeverity.INFO
|
147
|
+
"""The severity (`INFO`, `WARN`, `ERROR`) of the sent notification."""
|
148
|
+
|
149
|
+
@classmethod
|
150
|
+
def from_integration(
|
151
|
+
cls,
|
152
|
+
integration: SlackIntegration,
|
153
|
+
*,
|
154
|
+
title: str = "",
|
155
|
+
text: str = "",
|
156
|
+
level: AlertSeverity = AlertSeverity.INFO,
|
157
|
+
) -> Self:
|
158
|
+
"""Define a notification action that sends to the given (Slack) integration."""
|
159
|
+
return cls(
|
160
|
+
integration_id=integration.id,
|
161
|
+
title=title,
|
162
|
+
message=text,
|
163
|
+
severity=level,
|
164
|
+
)
|
165
|
+
|
166
|
+
|
167
|
+
class SendWebhook(_BaseActionInput, GenericWebhookActionInput):
|
168
|
+
"""Defines an automation action that sends a webhook request."""
|
169
|
+
|
170
|
+
action_type: Literal[ActionType.GENERIC_WEBHOOK] = ActionType.GENERIC_WEBHOOK
|
171
|
+
|
172
|
+
integration_id: GQLId
|
173
|
+
"""The ID of the webhook integration that will be used to send the request."""
|
174
|
+
|
175
|
+
# overrides the generated field type to parse/serialize JSON strings
|
176
|
+
request_payload: Optional[SerializedToJson[dict[str, Any]]] = Field( # type: ignore[assignment]
|
177
|
+
default=None, alias="requestPayload"
|
178
|
+
)
|
179
|
+
"""The payload, possibly with template variables, to send in the webhook request."""
|
180
|
+
|
181
|
+
@classmethod
|
182
|
+
def from_integration(
|
183
|
+
cls,
|
184
|
+
integration: WebhookIntegration,
|
185
|
+
*,
|
186
|
+
payload: Optional[SerializedToJson[dict[str, Any]]] = None,
|
187
|
+
) -> Self:
|
188
|
+
"""Define a webhook action that sends to the given (webhook) integration."""
|
189
|
+
return cls(integration_id=integration.id, request_payload=payload)
|
190
|
+
|
191
|
+
|
192
|
+
class DoNothing(_BaseActionInput, NoOpTriggeredActionInput, frozen=True):
|
193
|
+
"""Defines an automation action that intentionally does nothing."""
|
194
|
+
|
195
|
+
action_type: Literal[ActionType.NO_OP] = ActionType.NO_OP
|
196
|
+
|
197
|
+
no_op: Annotated[bool, BeforeValidator(default_if_none)] = True
|
198
|
+
"""Placeholder field which exists only to satisfy backend schema requirements.
|
199
|
+
|
200
|
+
There should never be a need to set this field explicitly, as its value is ignored.
|
201
|
+
"""
|
202
|
+
|
203
|
+
|
204
|
+
# for type annotations
|
205
|
+
InputAction = Annotated[
|
206
|
+
Union[
|
207
|
+
SendNotification,
|
208
|
+
SendWebhook,
|
209
|
+
DoNothing,
|
210
|
+
],
|
211
|
+
BeforeValidator(to_input_action),
|
212
|
+
Field(discriminator="action_type"),
|
213
|
+
]
|
214
|
+
# for runtime type checks
|
215
|
+
InputActionTypes: tuple[type, ...] = get_args(InputAction.__origin__) # type: ignore[attr-defined]
|
216
|
+
|
217
|
+
__all__ = [
|
218
|
+
"ActionType",
|
219
|
+
*(cls.__name__ for cls in InputActionTypes),
|
220
|
+
]
|