wandb 0.19.8__py3-none-win32.whl → 0.19.10__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 (154) hide show
  1. wandb/__init__.py +5 -1
  2. wandb/__init__.pyi +15 -8
  3. wandb/_pydantic/__init__.py +30 -0
  4. wandb/_pydantic/base.py +148 -0
  5. wandb/_pydantic/utils.py +66 -0
  6. wandb/_pydantic/v1_compat.py +284 -0
  7. wandb/apis/paginator.py +82 -38
  8. wandb/apis/public/__init__.py +2 -2
  9. wandb/apis/public/api.py +111 -53
  10. wandb/apis/public/artifacts.py +387 -639
  11. wandb/apis/public/automations.py +69 -0
  12. wandb/apis/public/files.py +2 -2
  13. wandb/apis/public/integrations.py +168 -0
  14. wandb/apis/public/projects.py +32 -2
  15. wandb/apis/public/reports.py +2 -2
  16. wandb/apis/public/runs.py +19 -11
  17. wandb/apis/public/utils.py +107 -1
  18. wandb/automations/__init__.py +81 -0
  19. wandb/automations/_filters/__init__.py +40 -0
  20. wandb/automations/_filters/expressions.py +179 -0
  21. wandb/automations/_filters/operators.py +267 -0
  22. wandb/automations/_filters/run_metrics.py +183 -0
  23. wandb/automations/_generated/__init__.py +184 -0
  24. wandb/automations/_generated/create_filter_trigger.py +21 -0
  25. wandb/automations/_generated/create_generic_webhook_integration.py +43 -0
  26. wandb/automations/_generated/delete_trigger.py +19 -0
  27. wandb/automations/_generated/enums.py +33 -0
  28. wandb/automations/_generated/fragments.py +343 -0
  29. wandb/automations/_generated/generic_webhook_integrations_by_entity.py +22 -0
  30. wandb/automations/_generated/get_triggers.py +24 -0
  31. wandb/automations/_generated/get_triggers_by_entity.py +24 -0
  32. wandb/automations/_generated/input_types.py +104 -0
  33. wandb/automations/_generated/integrations_by_entity.py +22 -0
  34. wandb/automations/_generated/operations.py +710 -0
  35. wandb/automations/_generated/slack_integrations_by_entity.py +22 -0
  36. wandb/automations/_generated/update_filter_trigger.py +21 -0
  37. wandb/automations/_utils.py +123 -0
  38. wandb/automations/_validators.py +73 -0
  39. wandb/automations/actions.py +205 -0
  40. wandb/automations/automations.py +109 -0
  41. wandb/automations/events.py +235 -0
  42. wandb/automations/integrations.py +26 -0
  43. wandb/automations/scopes.py +76 -0
  44. wandb/beta/workflows.py +9 -10
  45. wandb/bin/gpu_stats.exe +0 -0
  46. wandb/bin/wandb-core +0 -0
  47. wandb/cli/cli.py +3 -3
  48. wandb/integration/keras/keras.py +2 -1
  49. wandb/integration/langchain/wandb_tracer.py +2 -1
  50. wandb/integration/metaflow/metaflow.py +19 -17
  51. wandb/integration/sacred/__init__.py +1 -1
  52. wandb/jupyter.py +155 -133
  53. wandb/old/summary.py +0 -2
  54. wandb/proto/v3/wandb_internal_pb2.py +297 -292
  55. wandb/proto/v3/wandb_settings_pb2.py +2 -2
  56. wandb/proto/v3/wandb_telemetry_pb2.py +10 -10
  57. wandb/proto/v4/wandb_internal_pb2.py +292 -292
  58. wandb/proto/v4/wandb_settings_pb2.py +2 -2
  59. wandb/proto/v4/wandb_telemetry_pb2.py +10 -10
  60. wandb/proto/v5/wandb_internal_pb2.py +292 -292
  61. wandb/proto/v5/wandb_settings_pb2.py +2 -2
  62. wandb/proto/v5/wandb_telemetry_pb2.py +10 -10
  63. wandb/proto/v6/wandb_base_pb2.py +41 -0
  64. wandb/proto/v6/wandb_internal_pb2.py +393 -0
  65. wandb/proto/v6/wandb_server_pb2.py +78 -0
  66. wandb/proto/v6/wandb_settings_pb2.py +58 -0
  67. wandb/proto/v6/wandb_telemetry_pb2.py +52 -0
  68. wandb/proto/wandb_base_pb2.py +2 -0
  69. wandb/proto/wandb_deprecated.py +10 -0
  70. wandb/proto/wandb_internal_pb2.py +3 -1
  71. wandb/proto/wandb_server_pb2.py +2 -0
  72. wandb/proto/wandb_settings_pb2.py +2 -0
  73. wandb/proto/wandb_telemetry_pb2.py +2 -0
  74. wandb/sdk/artifacts/_generated/__init__.py +248 -0
  75. wandb/sdk/artifacts/_generated/artifact_collection_membership_files.py +43 -0
  76. wandb/sdk/artifacts/_generated/artifact_version_files.py +36 -0
  77. wandb/sdk/artifacts/_generated/create_artifact_collection_tag_assignments.py +36 -0
  78. wandb/sdk/artifacts/_generated/delete_artifact_collection_tag_assignments.py +25 -0
  79. wandb/sdk/artifacts/_generated/delete_artifact_portfolio.py +35 -0
  80. wandb/sdk/artifacts/_generated/delete_artifact_sequence.py +35 -0
  81. wandb/sdk/artifacts/_generated/enums.py +17 -0
  82. wandb/sdk/artifacts/_generated/fragments.py +186 -0
  83. wandb/sdk/artifacts/_generated/input_types.py +16 -0
  84. wandb/sdk/artifacts/_generated/move_artifact_collection.py +35 -0
  85. wandb/sdk/artifacts/_generated/operations.py +510 -0
  86. wandb/sdk/artifacts/_generated/project_artifact_collection.py +101 -0
  87. wandb/sdk/artifacts/_generated/project_artifact_collections.py +33 -0
  88. wandb/sdk/artifacts/_generated/project_artifact_type.py +24 -0
  89. wandb/sdk/artifacts/_generated/project_artifact_types.py +24 -0
  90. wandb/sdk/artifacts/_generated/project_artifacts.py +42 -0
  91. wandb/sdk/artifacts/_generated/run_input_artifacts.py +51 -0
  92. wandb/sdk/artifacts/_generated/run_output_artifacts.py +51 -0
  93. wandb/sdk/artifacts/_generated/update_artifact_portfolio.py +35 -0
  94. wandb/sdk/artifacts/_generated/update_artifact_sequence.py +35 -0
  95. wandb/sdk/artifacts/_graphql_fragments.py +56 -81
  96. wandb/sdk/artifacts/_validators.py +1 -0
  97. wandb/sdk/artifacts/artifact.py +110 -49
  98. wandb/sdk/artifacts/artifact_manifest_entry.py +2 -1
  99. wandb/sdk/artifacts/artifact_saver.py +16 -2
  100. wandb/sdk/artifacts/storage_handlers/azure_handler.py +1 -0
  101. wandb/sdk/artifacts/storage_policies/wandb_storage_policy.py +23 -2
  102. wandb/sdk/data_types/audio.py +1 -3
  103. wandb/sdk/data_types/base_types/media.py +13 -7
  104. wandb/sdk/data_types/base_types/wb_value.py +34 -11
  105. wandb/sdk/data_types/html.py +36 -9
  106. wandb/sdk/data_types/image.py +56 -37
  107. wandb/sdk/data_types/molecule.py +1 -5
  108. wandb/sdk/data_types/object_3d.py +2 -1
  109. wandb/sdk/data_types/saved_model.py +7 -9
  110. wandb/sdk/data_types/table.py +5 -0
  111. wandb/sdk/data_types/trace_tree.py +2 -0
  112. wandb/sdk/data_types/utils.py +1 -1
  113. wandb/sdk/data_types/video.py +15 -30
  114. wandb/sdk/interface/interface.py +2 -0
  115. wandb/{apis/public → sdk/internal}/_generated/__init__.py +0 -6
  116. wandb/{apis/public → sdk/internal}/_generated/server_features_query.py +3 -3
  117. wandb/sdk/internal/internal_api.py +138 -47
  118. wandb/sdk/internal/profiler.py +6 -5
  119. wandb/sdk/internal/run.py +13 -6
  120. wandb/sdk/internal/sender.py +2 -0
  121. wandb/sdk/internal/sender_config.py +8 -11
  122. wandb/sdk/internal/settings_static.py +24 -2
  123. wandb/sdk/lib/apikey.py +40 -20
  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/lib/run_moment.py +4 -6
  131. wandb/sdk/lib/wb_logging.py +161 -0
  132. wandb/sdk/service/server_sock.py +19 -14
  133. wandb/sdk/service/service.py +9 -7
  134. wandb/sdk/service/streams.py +5 -0
  135. wandb/sdk/verify/verify.py +6 -3
  136. wandb/sdk/wandb_config.py +44 -43
  137. wandb/sdk/wandb_init.py +323 -141
  138. wandb/sdk/wandb_login.py +13 -4
  139. wandb/sdk/wandb_metadata.py +107 -91
  140. wandb/sdk/wandb_run.py +529 -325
  141. wandb/sdk/wandb_settings.py +422 -202
  142. wandb/sdk/wandb_setup.py +52 -1
  143. wandb/util.py +29 -29
  144. {wandb-0.19.8.dist-info → wandb-0.19.10.dist-info}/METADATA +7 -7
  145. {wandb-0.19.8.dist-info → wandb-0.19.10.dist-info}/RECORD +151 -94
  146. wandb/_globals.py +0 -19
  147. wandb/apis/public/_generated/base.py +0 -128
  148. wandb/apis/public/_generated/typing_compat.py +0 -14
  149. /wandb/{apis/public → sdk/internal}/_generated/enums.py +0 -0
  150. /wandb/{apis/public → sdk/internal}/_generated/input_types.py +0 -0
  151. /wandb/{apis/public → sdk/internal}/_generated/operations.py +0 -0
  152. {wandb-0.19.8.dist-info → wandb-0.19.10.dist-info}/WHEEL +0 -0
  153. {wandb-0.19.8.dist-info → wandb-0.19.10.dist-info}/entry_points.txt +0 -0
  154. {wandb-0.19.8.dist-info → wandb-0.19.10.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,179 @@
1
+ """Pydantic-compatible representations of MongoDB expressions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from typing import Any, Union
7
+
8
+ from pydantic import ConfigDict, model_serializer
9
+ from typing_extensions import Self, TypeAlias
10
+
11
+ from wandb._pydantic import CompatBaseModel, model_validator
12
+
13
+ from .operators import (
14
+ Contains,
15
+ Eq,
16
+ Exists,
17
+ Gt,
18
+ Gte,
19
+ In,
20
+ Lt,
21
+ Lte,
22
+ Ne,
23
+ NotIn,
24
+ Op,
25
+ Regex,
26
+ RichReprResult,
27
+ Scalar,
28
+ ScalarTypes,
29
+ SupportsLogicalOpSyntax,
30
+ )
31
+
32
+
33
+ class FilterableField:
34
+ """A descriptor that can be used to define a "filterable" field on a class.
35
+
36
+ Internal helper to support syntactic sugar for defining event filters.
37
+ """
38
+
39
+ _python_name: str #: The name of the field this descriptor was assigned to in the Python class.
40
+ _server_name: str | None #: If set, the actual server-side field name to filter on.
41
+
42
+ def __init__(self, server_name: str | None = None):
43
+ self._server_name = server_name
44
+
45
+ def __set_name__(self, owner: type, name: str) -> None:
46
+ self._python_name = name
47
+
48
+ def __get__(self, obj: Any, objtype: type) -> Self:
49
+ # By default, if we didn't explicitly provide a backend name for
50
+ # filtering, assume the field has the same name in the backend as
51
+ # the python attribute.
52
+ return self
53
+
54
+ @property
55
+ def _name(self) -> str:
56
+ return self._server_name or self._python_name
57
+
58
+ def __str__(self) -> str:
59
+ return self._name
60
+
61
+ def __repr__(self) -> str:
62
+ return f"{type(self).__name__}({self._name!r})"
63
+
64
+ # Methods to define filter expressions through chaining
65
+ def matches_regex(self, pattern: str) -> FilterExpr:
66
+ return FilterExpr(field=self._name, op=Regex(regex_=pattern))
67
+
68
+ def contains(self, text: str) -> FilterExpr:
69
+ return FilterExpr(field=self._name, op=Contains(contains_=text))
70
+
71
+ def exists(self, exists: bool = True) -> FilterExpr:
72
+ return FilterExpr(field=self._name, op=Exists(exists_=exists))
73
+
74
+ def lt(self, value: Scalar) -> FilterExpr:
75
+ return FilterExpr(field=self._name, op=Lt(lt_=value))
76
+
77
+ def gt(self, value: Scalar) -> FilterExpr:
78
+ return FilterExpr(field=self._name, op=Gt(gt_=value))
79
+
80
+ def lte(self, value: Scalar) -> FilterExpr:
81
+ return FilterExpr(field=self._name, op=Lte(lte_=value))
82
+
83
+ def gte(self, value: Scalar) -> FilterExpr:
84
+ return FilterExpr(field=self._name, op=Gte(gte_=value))
85
+
86
+ def ne(self, value: Scalar) -> FilterExpr:
87
+ return FilterExpr(field=self._name, op=Ne(ne_=value))
88
+
89
+ def eq(self, value: Scalar) -> FilterExpr:
90
+ return FilterExpr(field=self._name, op=Eq(eq_=value))
91
+
92
+ def in_(self, values: Iterable[Scalar]) -> FilterExpr:
93
+ return FilterExpr(field=self._name, op=In(in_=values))
94
+
95
+ def not_in(self, values: Iterable[Scalar]) -> FilterExpr:
96
+ return FilterExpr(field=self._name, op=NotIn(nin_=values))
97
+
98
+ # Override the default behavior of comparison operators: <, >=, ==, etc
99
+ def __lt__(self, other: Any) -> FilterExpr:
100
+ if isinstance(other, ScalarTypes):
101
+ return self.lt(other)
102
+ raise TypeError(f"Invalid operand type in filter expression: {type(other)!r}")
103
+
104
+ def __gt__(self, other: Any) -> FilterExpr:
105
+ if isinstance(other, ScalarTypes):
106
+ return self.gt(other)
107
+ raise TypeError(f"Invalid operand type in filter expression: {type(other)!r}")
108
+
109
+ def __le__(self, other: Any) -> FilterExpr:
110
+ if isinstance(other, ScalarTypes):
111
+ return self.lte(other)
112
+ raise TypeError(f"Invalid operand type in filter expression: {type(other)!r}")
113
+
114
+ def __ge__(self, other: Any) -> FilterExpr:
115
+ if isinstance(other, ScalarTypes):
116
+ return self.gte(other)
117
+ raise TypeError(f"Invalid operand type in filter expression: {type(other)!r}")
118
+
119
+ # Operator behavior is intentionally overridden to allow defining
120
+ # filter expressions like `field == "value"`. See similar overrides
121
+ # of built-in dunder methods in sqlalchemy, polars, pandas, numpy, etc.
122
+ #
123
+ # sqlalchemy example for illustrative purposes:
124
+ # https://github.com/sqlalchemy/sqlalchemy/blob/f21ae633486380a26dc0b67b70ae1c0efc6b4dc4/lib/sqlalchemy/orm/descriptor_props.py#L808-L812
125
+ def __eq__(self, other: Any) -> FilterExpr:
126
+ if isinstance(other, ScalarTypes):
127
+ return self.eq(other)
128
+ raise TypeError(f"Invalid operand type in filter expression: {type(other)!r}")
129
+
130
+ def __ne__(self, other: Any) -> FilterExpr:
131
+ if isinstance(other, ScalarTypes):
132
+ return self.ne(other)
133
+ raise TypeError(f"Invalid operand type in filter expression: {type(other)!r}")
134
+
135
+
136
+ # ------------------------------------------------------------------------------
137
+ class FilterExpr(CompatBaseModel, SupportsLogicalOpSyntax):
138
+ """A MongoDB filter expression on a specific field."""
139
+
140
+ model_config = ConfigDict(
141
+ arbitrary_types_allowed=True,
142
+ )
143
+
144
+ field: str
145
+ op: Op
146
+
147
+ def __repr__(self) -> str:
148
+ return f"{type(self).__name__}({self.field!s}={self.op!r})"
149
+
150
+ def __rich_repr__(self) -> RichReprResult: # type: ignore[override]
151
+ # https://rich.readthedocs.io/en/stable/pretty.html
152
+ yield self.field, self.op
153
+
154
+ @model_validator(mode="before")
155
+ @classmethod
156
+ def _validate(cls, data: Any) -> Any:
157
+ """Parse a MongoDB dict representation of the filter expression."""
158
+ if (
159
+ isinstance(data, dict)
160
+ and len(data) == 1
161
+ and not any(key.startswith("$") for key in data)
162
+ ):
163
+ # This looks like a MongoDB filter dict. E.g.:
164
+ # - in: `{"display_name": {"$contains": "my-run"}}`
165
+ # - out: `FilterExpr(field="display_name", op=Contains(contains_="my-run"))`
166
+ ((field, op),) = data.items()
167
+ return {"field": field, "op": op}
168
+ return data
169
+
170
+ @model_serializer(mode="plain")
171
+ def _serialize(self) -> dict[str, Any]:
172
+ """Return a MongoDB dict representation of the expression."""
173
+ from pydantic_core import to_jsonable_python # Only valid in pydantic v2
174
+
175
+ op_dict = to_jsonable_python(self.op, by_alias=True, round_trip=True)
176
+ return {self.field: op_dict}
177
+
178
+
179
+ MongoLikeFilter: TypeAlias = Union[Op, FilterExpr]
@@ -0,0 +1,267 @@
1
+ """Types that represent operators in MongoDB filter expressions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, Dict, Iterable, Tuple, TypeVar, Union, overload
6
+
7
+ from pydantic import ConfigDict, Field, StrictBool, StrictFloat, StrictInt, StrictStr
8
+ from typing_extensions import TypeAlias, get_args
9
+
10
+ from wandb._pydantic import Base
11
+
12
+ if TYPE_CHECKING:
13
+ from wandb.automations._filters.run_metrics import MetricThresholdFilter
14
+ from wandb.automations.events import RunMetricFilter
15
+
16
+
17
+ # for type annotations
18
+ Scalar = Union[StrictStr, StrictInt, StrictFloat, StrictBool]
19
+ # for runtime type checks
20
+ ScalarTypes = get_args(Scalar)
21
+
22
+ # See: https://rich.readthedocs.io/en/stable/pretty.html#rich-repr-protocol
23
+ RichReprResult: TypeAlias = Iterable[
24
+ Union[
25
+ Any,
26
+ Tuple[Any],
27
+ Tuple[str, Any],
28
+ Tuple[str, Any, Any],
29
+ ]
30
+ ]
31
+
32
+ T = TypeVar("T")
33
+ TupleOf: TypeAlias = Tuple[T, ...]
34
+
35
+
36
+ # NOTE: Wherever class descriptions that are not docstrings, this is deliberate.
37
+ # This is done to ensure the descriptions are omitted from generated API docs.
38
+
39
+
40
+ # Mixin to support syntactic sugar for MongoDB expressions with (bitwise) logical operators,
41
+ # e.g. `a | b` -> `{"$or": [a, b]}` or `~a` -> `{"$not": a}`.
42
+ class SupportsLogicalOpSyntax:
43
+ def __or__(self, other: Any) -> Or:
44
+ """Syntactic sugar for: `a | b` -> `Or(a, b)`."""
45
+ return Or(or_=[self, other])
46
+
47
+ @overload
48
+ def __and__(self, other: MetricThresholdFilter) -> RunMetricFilter: ...
49
+ @overload
50
+ def __and__(self, other: Any) -> And: ...
51
+ def __and__(self, other: Any) -> Any:
52
+ """Syntactic sugar for: `a & b` -> `And(a, b)`."""
53
+ from wandb.automations._filters.run_metrics import MetricThresholdFilter
54
+
55
+ # Special handling `run_filter & metric_filter`
56
+ if isinstance(other, MetricThresholdFilter):
57
+ return other.__and__(self)
58
+ return And(and_=[self, other])
59
+
60
+ def __invert__(self) -> Not:
61
+ """Syntactic sugar for: `~a` -> `Not(a)`."""
62
+ return Not(not_=self)
63
+
64
+
65
+ # Base class for parsed MongoDB filter/query operators, e.g. `{"$and": [...]}`.
66
+ class BaseOp(Base, SupportsLogicalOpSyntax):
67
+ model_config = ConfigDict(
68
+ frozen=True, # Make pseudo-immutable for easier comparison and hashing
69
+ )
70
+
71
+ def __repr__(self) -> str:
72
+ # Display operand as a positional arg
73
+ values_repr = ", ".join(map(repr, self.model_dump().values()))
74
+ return f"{type(self).__name__}({values_repr})"
75
+
76
+ def __rich_repr__(self) -> RichReprResult: # type: ignore[override]
77
+ # Display field values as positional args:
78
+ # https://rich.readthedocs.io/en/stable/pretty.html
79
+ yield from ((None, v) for v in self.model_dump().values())
80
+
81
+
82
+ # Logical operator(s)
83
+ # https://www.mongodb.com/docs/manual/reference/operator/query/and/
84
+ # https://www.mongodb.com/docs/manual/reference/operator/query/or/
85
+ # https://www.mongodb.com/docs/manual/reference/operator/query/nor/
86
+ # https://www.mongodb.com/docs/manual/reference/operator/query/not/
87
+ class And(BaseOp):
88
+ and_: TupleOf[Any] = Field(default=(), alias="$and")
89
+
90
+
91
+ class Or(BaseOp):
92
+ or_: TupleOf[Any] = Field(default=(), alias="$or")
93
+
94
+ def __invert__(self) -> Nor:
95
+ """Syntactic sugar for: `~Or(a, b)` -> `Nor(a, b)`."""
96
+ return Nor(nor_=self.or_)
97
+
98
+
99
+ class Nor(BaseOp):
100
+ nor_: TupleOf[Any] = Field(default=(), alias="$nor")
101
+
102
+ def __invert__(self) -> Or:
103
+ """Syntactic sugar for: `~Nor(a, b)` -> `Or(a, b)`."""
104
+ return Or(or_=self.nor_)
105
+
106
+
107
+ class Not(BaseOp):
108
+ not_: Any = Field(alias="$not")
109
+
110
+ def __invert__(self) -> Any:
111
+ """Syntactic sugar for: `~Not(a)` -> `a`."""
112
+ return self.not_
113
+
114
+
115
+ # Comparison operator(s)
116
+ # https://www.mongodb.com/docs/manual/reference/operator/query/lt/
117
+ # https://www.mongodb.com/docs/manual/reference/operator/query/gt/
118
+ # https://www.mongodb.com/docs/manual/reference/operator/query/lte/
119
+ # https://www.mongodb.com/docs/manual/reference/operator/query/gte/
120
+ # https://www.mongodb.com/docs/manual/reference/operator/query/eq/
121
+ # https://www.mongodb.com/docs/manual/reference/operator/query/ne/
122
+ # https://www.mongodb.com/docs/manual/reference/operator/query/in/
123
+ # https://www.mongodb.com/docs/manual/reference/operator/query/nin/
124
+ class Lt(BaseOp):
125
+ lt_: Scalar = Field(alias="$lt")
126
+
127
+ def __invert__(self) -> Gte:
128
+ """Syntactic sugar for: `~Lt(a)` -> `Gte(a)`."""
129
+ return Gte(gte_=self.lt_)
130
+
131
+
132
+ class Gt(BaseOp):
133
+ gt_: Scalar = Field(alias="$gt")
134
+
135
+ def __invert__(self) -> Lte:
136
+ """Syntactic sugar for: `~Gt(a)` -> `Lte(a)`."""
137
+ return Lte(lte_=self.gt_)
138
+
139
+
140
+ class Lte(BaseOp):
141
+ lte_: Scalar = Field(alias="$lte")
142
+
143
+ def __invert__(self) -> Gt:
144
+ """Syntactic sugar for: `~Lte(a)` -> `Gt(a)`."""
145
+ return Gt(gt_=self.lte_)
146
+
147
+
148
+ class Gte(BaseOp):
149
+ gte_: Scalar = Field(alias="$gte")
150
+
151
+ def __invert__(self) -> Lt:
152
+ """Syntactic sugar for: `~Gte(a)` -> `Lt(a)`."""
153
+ return Lt(lt_=self.gte_)
154
+
155
+
156
+ class Eq(BaseOp):
157
+ eq_: Scalar = Field(alias="$eq")
158
+
159
+ def __invert__(self) -> Ne:
160
+ """Syntactic sugar for: `~Eq(a)` -> `Ne(a)`."""
161
+ return Ne(ne_=self.eq_)
162
+
163
+
164
+ class Ne(BaseOp):
165
+ ne_: Scalar = Field(alias="$ne")
166
+
167
+ def __invert__(self) -> Eq:
168
+ """Syntactic sugar for: `~Ne(a)` -> `Eq(a)`."""
169
+ return Eq(eq_=self.ne_)
170
+
171
+
172
+ class In(BaseOp):
173
+ in_: TupleOf[Scalar] = Field(default=(), alias="$in")
174
+
175
+ def __invert__(self) -> NotIn:
176
+ """Syntactic sugar for: `~In(a)` -> `NotIn(a)`."""
177
+ return NotIn(nin_=self.in_)
178
+
179
+
180
+ class NotIn(BaseOp):
181
+ nin_: TupleOf[Scalar] = Field(default=(), alias="$nin")
182
+
183
+ def __invert__(self) -> In:
184
+ """Syntactic sugar for: `~NotIn(a)` -> `In(a)`."""
185
+ return In(in_=self.nin_)
186
+
187
+
188
+ # Element operator(s)
189
+ # https://www.mongodb.com/docs/manual/reference/operator/query/exists/
190
+ class Exists(BaseOp):
191
+ exists_: bool = Field(alias="$exists")
192
+
193
+
194
+ # Evaluation operator(s)
195
+ # https://www.mongodb.com/docs/manual/reference/operator/query/regex/
196
+ #
197
+ # Note: "$contains" is NOT a formal MongoDB operator, but the backend recognizes and
198
+ # executes it as a substring-match filter.
199
+ class Regex(BaseOp):
200
+ regex_: str = Field(alias="$regex") #: The regex expression to match against.
201
+
202
+
203
+ class Contains(BaseOp):
204
+ contains_: str = Field(alias="$contains") #: The substring to match against.
205
+
206
+
207
+ And.model_rebuild()
208
+ Or.model_rebuild()
209
+ Not.model_rebuild()
210
+ Lt.model_rebuild()
211
+ Gt.model_rebuild()
212
+ Lte.model_rebuild()
213
+ Gte.model_rebuild()
214
+ Eq.model_rebuild()
215
+ Ne.model_rebuild()
216
+ In.model_rebuild()
217
+ NotIn.model_rebuild()
218
+ Exists.model_rebuild()
219
+ Regex.model_rebuild()
220
+ Contains.model_rebuild()
221
+
222
+
223
+ # ------------------------------------------------------------------------------
224
+ # Convenience helpers, constants, and utils for supported MongoDB operators
225
+ # ------------------------------------------------------------------------------
226
+ KEY_TO_OP: dict[str, type[BaseOp]] = {
227
+ "$and": And,
228
+ "$or": Or,
229
+ "$nor": Nor,
230
+ "$not": Not,
231
+ "$lt": Lt,
232
+ "$gt": Gt,
233
+ "$lte": Lte,
234
+ "$gte": Gte,
235
+ "$eq": Eq,
236
+ "$ne": Ne,
237
+ "$in": In,
238
+ "$nin": NotIn,
239
+ "$exists": Exists,
240
+ "$regex": Regex,
241
+ "$contains": Contains,
242
+ }
243
+
244
+
245
+ KnownOp = Union[
246
+ And,
247
+ Or,
248
+ Nor,
249
+ Not,
250
+ Lt,
251
+ Gt,
252
+ Lte,
253
+ Gte,
254
+ Eq,
255
+ Ne,
256
+ In,
257
+ NotIn,
258
+ Exists,
259
+ Regex,
260
+ Contains,
261
+ ]
262
+ UnknownOp = Dict[str, Any]
263
+
264
+ # for type annotations
265
+ Op = Union[KnownOp, UnknownOp]
266
+ # for runtime type checks
267
+ OpTypes: tuple[type, ...] = (*get_args(KnownOp), dict)
@@ -0,0 +1,183 @@
1
+ # ruff: noqa: UP007
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+ from typing import TYPE_CHECKING, Any, Final, Literal, Optional, Union, overload
7
+
8
+ from pydantic import Field, PositiveInt, StrictFloat, StrictInt, field_validator
9
+ from typing_extensions import Self, override
10
+
11
+ from wandb._pydantic.base import Base, GQLBase
12
+
13
+ from .expressions import FilterExpr
14
+ from .operators import BaseOp, RichReprResult
15
+
16
+ if TYPE_CHECKING:
17
+ from wandb.automations.events import RunMetricFilter
18
+
19
+ # Maps MongoDB comparison operators -> Python literal (str) representations
20
+ MONGO2PY_OPS: Final[dict[str, str]] = {
21
+ "$eq": "==",
22
+ "$ne": "!=",
23
+ "$gt": ">",
24
+ "$lt": "<",
25
+ "$gte": ">=",
26
+ "$lte": "<=",
27
+ }
28
+ # Reverse mapping from Python literal (str) -> MongoDB operator key
29
+ PY2MONGO_OPS: Final[dict[str, str]] = {v: k for k, v in MONGO2PY_OPS.items()}
30
+
31
+
32
+ class Agg(str, Enum): # from `Aggregation`
33
+ """Supported run metric aggregation operations."""
34
+
35
+ MAX = "MAX"
36
+ MIN = "MIN"
37
+ AVERAGE = "AVERAGE"
38
+
39
+
40
+ class ChangeType(str, Enum): # from `RunMetricChangeType`
41
+ """Describes the metric change as absolute (arithmetic difference) or relative (decimal percentage)."""
42
+
43
+ ABSOLUTE = "ABSOLUTE"
44
+ RELATIVE = "RELATIVE"
45
+
46
+
47
+ class ChangeDirection(str, Enum): # from `RunMetricChangeDirection`
48
+ """Describes the direction of the metric change."""
49
+
50
+ INCREASE = "INCREASE"
51
+ DECREASE = "DECREASE"
52
+ ANY = "ANY"
53
+
54
+
55
+ class _BaseMetricFilter(GQLBase):
56
+ name: str
57
+ """Name of the observed metric."""
58
+
59
+ agg: Optional[Agg]
60
+ """Aggregation operation, if any, to apply over the window size."""
61
+
62
+ window: PositiveInt
63
+ """Size of the window over which the metric is aggregated."""
64
+
65
+ # ------------------------------------------------------------------------------
66
+
67
+ threshold: Union[StrictInt, StrictFloat]
68
+ """Threshold value to compare against."""
69
+
70
+ @field_validator("agg", mode="before")
71
+ @classmethod
72
+ def _validate_agg(cls, v: Any) -> Any:
73
+ # Be helpful: e.g. "min" -> "MIN"
74
+ return v.strip().upper() if isinstance(v, str) else v
75
+
76
+ @overload
77
+ def __and__(self, other: BaseOp | FilterExpr) -> RunMetricFilter: ...
78
+ @overload
79
+ def __and__(self, other: Any) -> Any: ...
80
+ def __and__(self, other: BaseOp | FilterExpr | Any) -> RunMetricFilter | Any:
81
+ """Supports syntactic sugar for defining a triggering RunMetricEvent from `run_metric_filter & run_filter`."""
82
+ from wandb.automations.events import RunMetricFilter, _InnerRunMetricFilter
83
+
84
+ if isinstance(run_filter := other, (BaseOp, FilterExpr)):
85
+ # Assume `other` is a run filter, and we are building a RunMetricEvent.
86
+ # For the metric filter, delegate to the inner validator(s) to further wrap/nest as appropriate.
87
+ metric_filter = _InnerRunMetricFilter.model_validate(self)
88
+ return RunMetricFilter(
89
+ run_metric_filter=metric_filter, run_filter=run_filter
90
+ )
91
+ return other.__and__(self) # Try switching the order of operands
92
+
93
+
94
+ class MetricThresholdFilter(_BaseMetricFilter): # from `RunMetricThresholdFilter`
95
+ """For run events, defines a metric filter comparing a metric against a user-defined threshold value."""
96
+
97
+ name: str
98
+ agg: Optional[Agg] = Field(default=None, alias="agg_op")
99
+ window: PositiveInt = Field(default=1, alias="window_size")
100
+
101
+ cmp: Literal["$gte", "$gt", "$lt", "$lte"] = Field(alias="cmp_op")
102
+ """Comparison operator used to compare the metric value (left) vs. the threshold value (right)."""
103
+
104
+ threshold: Union[StrictInt, StrictFloat]
105
+
106
+ @field_validator("cmp", mode="before")
107
+ @classmethod
108
+ def _validate_cmp(cls, v: Any) -> Any:
109
+ # Be helpful: e.g. ">" -> "$gt"
110
+ return PY2MONGO_OPS.get(v.strip(), v) if isinstance(v, str) else v
111
+
112
+ def __repr__(self) -> str:
113
+ metric = f"{self.agg.value}({self.name})" if self.agg else self.name
114
+ op = MONGO2PY_OPS.get(self.cmp, self.cmp)
115
+ expr = rf"{metric} {op} {self.threshold}"
116
+ return repr(expr)
117
+
118
+ @override
119
+ def __rich_repr__(self) -> RichReprResult: # type: ignore[override]
120
+ yield None, repr(self)
121
+
122
+
123
+ class MetricChangeFilter(_BaseMetricFilter): # from `RunMetricChangeFilter`
124
+ # FIXME:
125
+ # - `prior_window` should be optional and default to `window` if not provided.
126
+ # - implement declarative syntax for `MetricChangeFilter` similar to `MetricThresholdFilter`.
127
+ # - split this into tagged union of relative/absolute change filters.
128
+
129
+ name: str
130
+ agg: Optional[Agg] = Field(default=None, alias="agg_op")
131
+
132
+ # FIXME: Set the `prior_window` to `window` if it's not provided, for convenience.
133
+ window: PositiveInt = Field(alias="current_window_size")
134
+ prior_window: PositiveInt = Field(alias="prior_window_size")
135
+ """Size of the preceding window over which the metric is aggregated."""
136
+
137
+ # NOTE: `cmp_op` isn't a field here. In the backend, it's effectively `cmp_op` = "$gte"
138
+
139
+ change_type: ChangeType = Field(alias="change_type")
140
+ change_direction: ChangeDirection = Field(alias="change_dir")
141
+
142
+ threshold: Union[StrictInt, StrictFloat] = Field(alias="change_amount")
143
+
144
+
145
+ class MetricOperand(Base):
146
+ name: str
147
+ agg: Optional[Agg] = Field(default=None, alias="agg_op")
148
+ window: PositiveInt = Field(default=1, alias="window_size")
149
+
150
+ def _agg(self, op: Agg, window: int) -> Self:
151
+ if self.agg is None: # Prevent overwriting an existing aggregation operator
152
+ return self.model_copy(update={"agg": op, "window": window})
153
+ raise ValueError(f"Aggregation operator already set as: {self.agg!r}")
154
+
155
+ def max(self, window: int) -> Self:
156
+ return self._agg(Agg.MAX, window)
157
+
158
+ def min(self, window: int) -> Self:
159
+ return self._agg(Agg.MIN, window)
160
+
161
+ def average(self, window: int) -> Self:
162
+ return self._agg(Agg.AVERAGE, window)
163
+
164
+ # Aliased method for users familiar with e.g. torch/tf/numpy/pandas/polars/etc.
165
+ def mean(self, window: int) -> Self:
166
+ return self.average(window=window)
167
+
168
+ def gt(self, other: int | float) -> MetricThresholdFilter:
169
+ return MetricThresholdFilter(**dict(self), cmp="$gt", threshold=other)
170
+
171
+ def lt(self, other: int | float) -> MetricThresholdFilter:
172
+ return MetricThresholdFilter(**dict(self), cmp="$lt", threshold=other)
173
+
174
+ def gte(self, other: int | float) -> MetricThresholdFilter:
175
+ return MetricThresholdFilter(**dict(self), cmp="$gte", threshold=other)
176
+
177
+ def lte(self, other: int | float) -> MetricThresholdFilter:
178
+ return MetricThresholdFilter(**dict(self), cmp="$lte", threshold=other)
179
+
180
+ __gt__ = gt
181
+ __lt__ = lt
182
+ __ge__ = gte
183
+ __le__ = lte