wandb 0.19.9__py3-none-win32.whl → 0.19.11__py3-none-win32.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.
Files changed (156) hide show
  1. wandb/__init__.py +1 -1
  2. wandb/__init__.pyi +6 -3
  3. wandb/_pydantic/__init__.py +14 -8
  4. wandb/_pydantic/base.py +51 -36
  5. wandb/_pydantic/utils.py +73 -0
  6. wandb/_pydantic/v1_compat.py +79 -57
  7. wandb/apis/public/__init__.py +2 -2
  8. wandb/apis/public/api.py +684 -4
  9. wandb/apis/public/artifacts.py +377 -677
  10. wandb/apis/public/automations.py +69 -0
  11. wandb/apis/public/integrations.py +180 -0
  12. wandb/apis/public/projects.py +29 -0
  13. wandb/apis/public/registries/__init__.py +0 -0
  14. wandb/apis/public/registries/_freezable_list.py +179 -0
  15. wandb/apis/public/{registries.py → registries/registries_search.py} +22 -129
  16. wandb/apis/public/registries/registry.py +357 -0
  17. wandb/apis/public/registries/utils.py +140 -0
  18. wandb/apis/public/runs.py +58 -56
  19. wandb/apis/public/utils.py +107 -1
  20. wandb/automations/__init__.py +73 -0
  21. wandb/automations/_filters/__init__.py +40 -0
  22. wandb/automations/_filters/expressions.py +181 -0
  23. wandb/automations/_filters/operators.py +258 -0
  24. wandb/automations/_filters/run_metrics.py +332 -0
  25. wandb/automations/_generated/__init__.py +177 -0
  26. wandb/automations/_generated/create_automation.py +17 -0
  27. wandb/automations/_generated/create_generic_webhook_integration.py +43 -0
  28. wandb/automations/_generated/delete_automation.py +17 -0
  29. wandb/automations/_generated/enums.py +33 -0
  30. wandb/automations/_generated/fragments.py +358 -0
  31. wandb/automations/_generated/generic_webhook_integrations_by_entity.py +22 -0
  32. wandb/automations/_generated/get_automations.py +24 -0
  33. wandb/automations/_generated/get_automations_by_entity.py +26 -0
  34. wandb/automations/_generated/input_types.py +104 -0
  35. wandb/automations/_generated/integrations_by_entity.py +22 -0
  36. wandb/automations/_generated/operations.py +647 -0
  37. wandb/automations/_generated/slack_integrations_by_entity.py +22 -0
  38. wandb/automations/_generated/update_automation.py +17 -0
  39. wandb/automations/_utils.py +237 -0
  40. wandb/automations/_validators.py +165 -0
  41. wandb/automations/actions.py +220 -0
  42. wandb/automations/automations.py +87 -0
  43. wandb/automations/events.py +287 -0
  44. wandb/automations/integrations.py +45 -0
  45. wandb/automations/scopes.py +78 -0
  46. wandb/beta/workflows.py +9 -10
  47. wandb/bin/gpu_stats.exe +0 -0
  48. wandb/bin/wandb-core +0 -0
  49. wandb/cli/cli.py +3 -3
  50. wandb/env.py +11 -0
  51. wandb/integration/keras/keras.py +2 -1
  52. wandb/integration/langchain/wandb_tracer.py +2 -1
  53. wandb/jupyter.py +137 -118
  54. wandb/old/settings.py +4 -1
  55. wandb/old/summary.py +0 -2
  56. wandb/proto/v3/wandb_internal_pb2.py +297 -292
  57. wandb/proto/v3/wandb_settings_pb2.py +2 -2
  58. wandb/proto/v3/wandb_telemetry_pb2.py +10 -10
  59. wandb/proto/v4/wandb_internal_pb2.py +292 -292
  60. wandb/proto/v4/wandb_settings_pb2.py +2 -2
  61. wandb/proto/v4/wandb_telemetry_pb2.py +10 -10
  62. wandb/proto/v5/wandb_internal_pb2.py +292 -292
  63. wandb/proto/v5/wandb_settings_pb2.py +2 -2
  64. wandb/proto/v5/wandb_telemetry_pb2.py +10 -10
  65. wandb/proto/v6/wandb_base_pb2.py +41 -0
  66. wandb/proto/v6/wandb_internal_pb2.py +393 -0
  67. wandb/proto/v6/wandb_server_pb2.py +78 -0
  68. wandb/proto/v6/wandb_settings_pb2.py +58 -0
  69. wandb/proto/v6/wandb_telemetry_pb2.py +52 -0
  70. wandb/proto/wandb_base_pb2.py +2 -0
  71. wandb/proto/wandb_deprecated.py +8 -0
  72. wandb/proto/wandb_internal_pb2.py +3 -1
  73. wandb/proto/wandb_server_pb2.py +2 -0
  74. wandb/proto/wandb_settings_pb2.py +2 -0
  75. wandb/proto/wandb_telemetry_pb2.py +2 -0
  76. wandb/sdk/artifacts/_generated/__init__.py +289 -0
  77. wandb/sdk/artifacts/_generated/add_aliases.py +21 -0
  78. wandb/sdk/artifacts/_generated/artifact_collection_membership_files.py +43 -0
  79. wandb/sdk/artifacts/_generated/artifact_version_files.py +36 -0
  80. wandb/sdk/artifacts/_generated/create_artifact_collection_tag_assignments.py +36 -0
  81. wandb/sdk/artifacts/_generated/delete_aliases.py +21 -0
  82. wandb/sdk/artifacts/_generated/delete_artifact_collection_tag_assignments.py +25 -0
  83. wandb/sdk/artifacts/_generated/delete_artifact_portfolio.py +35 -0
  84. wandb/sdk/artifacts/_generated/delete_artifact_sequence.py +35 -0
  85. wandb/sdk/artifacts/_generated/enums.py +17 -0
  86. wandb/sdk/artifacts/_generated/fetch_linked_artifacts.py +67 -0
  87. wandb/sdk/artifacts/_generated/fragments.py +221 -0
  88. wandb/sdk/artifacts/_generated/input_types.py +28 -0
  89. wandb/sdk/artifacts/_generated/move_artifact_collection.py +35 -0
  90. wandb/sdk/artifacts/_generated/operations.py +611 -0
  91. wandb/sdk/artifacts/_generated/project_artifact_collection.py +101 -0
  92. wandb/sdk/artifacts/_generated/project_artifact_collections.py +33 -0
  93. wandb/sdk/artifacts/_generated/project_artifact_type.py +24 -0
  94. wandb/sdk/artifacts/_generated/project_artifact_types.py +24 -0
  95. wandb/sdk/artifacts/_generated/project_artifacts.py +42 -0
  96. wandb/sdk/artifacts/_generated/run_input_artifacts.py +51 -0
  97. wandb/sdk/artifacts/_generated/run_output_artifacts.py +51 -0
  98. wandb/sdk/artifacts/_generated/update_artifact.py +26 -0
  99. wandb/sdk/artifacts/_generated/update_artifact_portfolio.py +35 -0
  100. wandb/sdk/artifacts/_generated/update_artifact_sequence.py +35 -0
  101. wandb/sdk/artifacts/_graphql_fragments.py +57 -79
  102. wandb/sdk/artifacts/_validators.py +120 -1
  103. wandb/sdk/artifacts/artifact.py +419 -215
  104. wandb/sdk/artifacts/artifact_file_cache.py +4 -6
  105. wandb/sdk/artifacts/artifact_manifest_entry.py +13 -3
  106. wandb/sdk/artifacts/storage_handlers/azure_handler.py +1 -0
  107. wandb/sdk/artifacts/storage_policies/wandb_storage_policy.py +182 -1
  108. wandb/sdk/artifacts/storage_policy.py +3 -0
  109. wandb/sdk/data_types/base_types/media.py +2 -3
  110. wandb/sdk/data_types/base_types/wb_value.py +34 -11
  111. wandb/sdk/data_types/html.py +36 -9
  112. wandb/sdk/data_types/image.py +12 -12
  113. wandb/sdk/data_types/table.py +5 -0
  114. wandb/sdk/data_types/trace_tree.py +2 -0
  115. wandb/sdk/data_types/utils.py +1 -1
  116. wandb/sdk/data_types/video.py +59 -57
  117. wandb/sdk/interface/interface.py +4 -3
  118. wandb/sdk/internal/internal_api.py +21 -31
  119. wandb/sdk/internal/profiler.py +6 -5
  120. wandb/sdk/internal/run.py +13 -6
  121. wandb/sdk/internal/sender.py +5 -2
  122. wandb/sdk/launch/sweeps/utils.py +8 -0
  123. wandb/sdk/lib/apikey.py +25 -4
  124. wandb/sdk/lib/asyncio_compat.py +1 -1
  125. wandb/sdk/lib/deprecate.py +13 -22
  126. wandb/sdk/lib/disabled.py +2 -1
  127. wandb/sdk/lib/printer.py +37 -8
  128. wandb/sdk/lib/printer_asyncio.py +46 -0
  129. wandb/sdk/lib/redirect.py +10 -5
  130. wandb/sdk/projects/_generated/__init__.py +47 -0
  131. wandb/sdk/projects/_generated/delete_project.py +22 -0
  132. wandb/sdk/projects/_generated/enums.py +4 -0
  133. wandb/sdk/projects/_generated/fetch_registry.py +22 -0
  134. wandb/sdk/projects/_generated/fragments.py +41 -0
  135. wandb/sdk/projects/_generated/input_types.py +13 -0
  136. wandb/sdk/projects/_generated/operations.py +88 -0
  137. wandb/sdk/projects/_generated/rename_project.py +27 -0
  138. wandb/sdk/projects/_generated/upsert_registry_project.py +27 -0
  139. wandb/sdk/service/server_sock.py +19 -14
  140. wandb/sdk/service/service.py +18 -8
  141. wandb/sdk/service/streams.py +5 -0
  142. wandb/sdk/verify/verify.py +6 -3
  143. wandb/sdk/wandb_init.py +217 -70
  144. wandb/sdk/wandb_login.py +13 -4
  145. wandb/sdk/wandb_run.py +419 -295
  146. wandb/sdk/wandb_settings.py +27 -10
  147. wandb/sdk/wandb_setup.py +61 -0
  148. wandb/util.py +33 -29
  149. {wandb-0.19.9.dist-info → wandb-0.19.11.dist-info}/METADATA +5 -5
  150. {wandb-0.19.9.dist-info → wandb-0.19.11.dist-info}/RECORD +153 -83
  151. wandb/_globals.py +0 -19
  152. wandb/sdk/internal/_generated/base.py +0 -226
  153. wandb/sdk/internal/_generated/typing_compat.py +0 -14
  154. {wandb-0.19.9.dist-info → wandb-0.19.11.dist-info}/WHEEL +0 -0
  155. {wandb-0.19.9.dist-info → wandb-0.19.11.dist-info}/entry_points.txt +0 -0
  156. {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
+ ]