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
wandb/apis/public/utils.py
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
import re
|
2
2
|
from enum import Enum
|
3
|
-
from typing import Optional
|
3
|
+
from typing import Any, Dict, Iterable, Mapping, Optional, Set
|
4
4
|
from urllib.parse import urlparse
|
5
5
|
|
6
|
+
from wandb_gql import gql
|
7
|
+
from wandb_graphql.language import ast, visitor
|
8
|
+
|
6
9
|
from wandb._iterutils import one
|
7
10
|
from wandb.sdk.artifacts._validators import is_artifact_registry_project
|
8
11
|
from wandb.sdk.internal.internal_api import Api as InternalApi
|
@@ -102,3 +105,106 @@ def fetch_org_from_settings_or_entity(
|
|
102
105
|
)
|
103
106
|
organization = entity_org.display_name
|
104
107
|
return organization
|
108
|
+
|
109
|
+
|
110
|
+
class _GQLCompatRewriter(visitor.Visitor):
|
111
|
+
"""GraphQL AST visitor to rewrite queries/mutations to be compatible with older server versions."""
|
112
|
+
|
113
|
+
omit_variables: Set[str]
|
114
|
+
omit_fragments: Set[str]
|
115
|
+
omit_fields: Set[str]
|
116
|
+
rename_fields: Dict[str, str]
|
117
|
+
|
118
|
+
def __init__(
|
119
|
+
self,
|
120
|
+
omit_variables: Optional[Iterable[str]] = None,
|
121
|
+
omit_fragments: Optional[Iterable[str]] = None,
|
122
|
+
omit_fields: Optional[Iterable[str]] = None,
|
123
|
+
rename_fields: Optional[Mapping[str, str]] = None,
|
124
|
+
):
|
125
|
+
self.omit_variables = set(omit_variables or ())
|
126
|
+
self.omit_fragments = set(omit_fragments or ())
|
127
|
+
self.omit_fields = set(omit_fields or ())
|
128
|
+
self.rename_fields = dict(rename_fields or {})
|
129
|
+
|
130
|
+
def enter_VariableDefinition(self, node: ast.VariableDefinition, *_, **__) -> Any: # noqa: N802
|
131
|
+
if node.variable.name.value in self.omit_variables:
|
132
|
+
return visitor.REMOVE
|
133
|
+
# return node
|
134
|
+
|
135
|
+
def enter_ObjectField(self, node: ast.ObjectField, *_, **__) -> Any: # noqa: N802
|
136
|
+
# For context, note that e.g.:
|
137
|
+
#
|
138
|
+
# {description: $description
|
139
|
+
# ...}
|
140
|
+
#
|
141
|
+
# Is parsed as:
|
142
|
+
#
|
143
|
+
# ObjectValue(fields=[
|
144
|
+
# ObjectField(name=Name(value='description'), value=Variable(name=Name(value='description'))),
|
145
|
+
# ...])
|
146
|
+
if (
|
147
|
+
isinstance(var := node.value, ast.Variable)
|
148
|
+
and var.name.value in self.omit_variables
|
149
|
+
):
|
150
|
+
return visitor.REMOVE
|
151
|
+
|
152
|
+
def enter_Argument(self, node: ast.Argument, *_, **__) -> Any: # noqa: N802
|
153
|
+
if node.name.value in self.omit_variables:
|
154
|
+
return visitor.REMOVE
|
155
|
+
|
156
|
+
def enter_FragmentDefinition(self, node: ast.FragmentDefinition, *_, **__) -> Any: # noqa: N802
|
157
|
+
if node.name.value in self.omit_fragments:
|
158
|
+
return visitor.REMOVE
|
159
|
+
|
160
|
+
def enter_FragmentSpread(self, node: ast.FragmentSpread, *_, **__) -> Any: # noqa: N802
|
161
|
+
if node.name.value in self.omit_fragments:
|
162
|
+
return visitor.REMOVE
|
163
|
+
|
164
|
+
def enter_Field(self, node: ast.Field, *_, **__) -> Any: # noqa: N802
|
165
|
+
if node.name.value in self.omit_fields:
|
166
|
+
return visitor.REMOVE
|
167
|
+
if new_name := self.rename_fields.get(node.name.value):
|
168
|
+
node.name.value = new_name
|
169
|
+
return node
|
170
|
+
|
171
|
+
def leave_Field(self, node: ast.Field, *_, **__) -> Any: # noqa: N802
|
172
|
+
# If the field had a selection set, but now it's empty, remove the field entirely
|
173
|
+
if (node.selection_set is not None) and (not node.selection_set.selections):
|
174
|
+
return visitor.REMOVE
|
175
|
+
|
176
|
+
|
177
|
+
def gql_compat(
|
178
|
+
request_string: str,
|
179
|
+
omit_variables: Optional[Iterable[str]] = None,
|
180
|
+
omit_fragments: Optional[Iterable[str]] = None,
|
181
|
+
omit_fields: Optional[Iterable[str]] = None,
|
182
|
+
rename_fields: Optional[Mapping[str, str]] = None,
|
183
|
+
) -> ast.Document:
|
184
|
+
"""Rewrite a GraphQL request string to ensure compatibility with older server versions.
|
185
|
+
|
186
|
+
Args:
|
187
|
+
request_string (str): The GraphQL request string to rewrite.
|
188
|
+
omit_variables (Iterable[str] | None): Names of variables to remove from the request string.
|
189
|
+
omit_fragments (Iterable[str] | None): Names of fragments to remove from the request string.
|
190
|
+
omit_fields (Iterable[str] | None): Names of fields to remove from the request string.
|
191
|
+
rename_fields (Mapping[str, str] | None):
|
192
|
+
A mapping of fields to rename in the request string, given as `{old_name -> new_name}`.
|
193
|
+
|
194
|
+
Returns:
|
195
|
+
str: Modified GraphQL request string with fragments on omitted types removed.
|
196
|
+
"""
|
197
|
+
# Parse the request into a GraphQL AST
|
198
|
+
doc = gql(request_string)
|
199
|
+
|
200
|
+
if not (omit_variables or omit_fragments or omit_fields or rename_fields):
|
201
|
+
return doc
|
202
|
+
|
203
|
+
# Visit the AST with our visitor to filter out unwanted fragments
|
204
|
+
rewriter = _GQLCompatRewriter(
|
205
|
+
omit_variables=omit_variables,
|
206
|
+
omit_fragments=omit_fragments,
|
207
|
+
omit_fields=omit_fields,
|
208
|
+
rename_fields=rename_fields,
|
209
|
+
)
|
210
|
+
return visitor.visit(doc, rewriter)
|
@@ -0,0 +1,73 @@
|
|
1
|
+
import wandb
|
2
|
+
from wandb._pydantic import IS_PYDANTIC_V2
|
3
|
+
|
4
|
+
from .actions import ActionType, DoNothing, SendNotification, SendWebhook
|
5
|
+
from .automations import Automation, NewAutomation
|
6
|
+
from .events import (
|
7
|
+
ArtifactEvent,
|
8
|
+
EventType,
|
9
|
+
MetricChangeFilter,
|
10
|
+
MetricThresholdFilter,
|
11
|
+
OnAddArtifactAlias,
|
12
|
+
OnCreateArtifact,
|
13
|
+
OnLinkArtifact,
|
14
|
+
OnRunMetric,
|
15
|
+
RunEvent,
|
16
|
+
)
|
17
|
+
from .integrations import Integration, SlackIntegration, WebhookIntegration
|
18
|
+
from .scopes import ArtifactCollectionScope, ProjectScope, ScopeType
|
19
|
+
|
20
|
+
# ----------------------------------------------------------------------------
|
21
|
+
# WARNINGS on import
|
22
|
+
if not IS_PYDANTIC_V2:
|
23
|
+
# Raises an error in Pydantic v1 environments, where the Automations API
|
24
|
+
# has not been tested and is unlikely to work as expected.
|
25
|
+
#
|
26
|
+
# Remove this when we either:
|
27
|
+
# - Drop support for Pydantic v1
|
28
|
+
# - Are able to implement (limited) Pydantic v1 support
|
29
|
+
raise ImportError(
|
30
|
+
"The W&B Automations API requires Pydantic v2. "
|
31
|
+
"We recommend upgrading `pydantic` to use this feature."
|
32
|
+
)
|
33
|
+
|
34
|
+
else:
|
35
|
+
# If Pydantic v2 is available, we can use the full Automations API
|
36
|
+
# but communicate to users that the API is still experimental and
|
37
|
+
# may change rapidly.
|
38
|
+
wandb.termwarn(
|
39
|
+
"The W&B Automations API is experimental and the implementation is subject to change."
|
40
|
+
"Review the release notes before upgrading. We recommend pinning your "
|
41
|
+
f"package version to `{wandb.__package__}=={wandb.__version__}` to reduce the risk of disruption.",
|
42
|
+
repeat=False,
|
43
|
+
)
|
44
|
+
# ----------------------------------------------------------------------------
|
45
|
+
|
46
|
+
__all__ = [
|
47
|
+
# Scopes
|
48
|
+
"ScopeType",
|
49
|
+
"ArtifactCollectionScope",
|
50
|
+
"ProjectScope",
|
51
|
+
# Events
|
52
|
+
"EventType",
|
53
|
+
"OnAddArtifactAlias",
|
54
|
+
"OnCreateArtifact",
|
55
|
+
"OnLinkArtifact",
|
56
|
+
"OnRunMetric",
|
57
|
+
"ArtifactEvent",
|
58
|
+
"RunEvent",
|
59
|
+
"MetricThresholdFilter",
|
60
|
+
"MetricChangeFilter",
|
61
|
+
# Actions
|
62
|
+
"ActionType",
|
63
|
+
"SendNotification",
|
64
|
+
"SendWebhook",
|
65
|
+
"DoNothing",
|
66
|
+
# Automations
|
67
|
+
"Automation",
|
68
|
+
"NewAutomation",
|
69
|
+
# Integrations
|
70
|
+
"Integration",
|
71
|
+
"SlackIntegration",
|
72
|
+
"WebhookIntegration",
|
73
|
+
]
|
@@ -0,0 +1,40 @@
|
|
1
|
+
from .expressions import FilterExpr, MongoLikeFilter
|
2
|
+
from .operators import (
|
3
|
+
And,
|
4
|
+
Contains,
|
5
|
+
Eq,
|
6
|
+
Exists,
|
7
|
+
Gt,
|
8
|
+
Gte,
|
9
|
+
In,
|
10
|
+
Lt,
|
11
|
+
Lte,
|
12
|
+
Ne,
|
13
|
+
Nor,
|
14
|
+
Not,
|
15
|
+
NotIn,
|
16
|
+
Op,
|
17
|
+
Or,
|
18
|
+
Regex,
|
19
|
+
)
|
20
|
+
|
21
|
+
__all__ = [
|
22
|
+
"And",
|
23
|
+
"Or",
|
24
|
+
"Nor",
|
25
|
+
"Not",
|
26
|
+
"Op",
|
27
|
+
"Gt",
|
28
|
+
"Lt",
|
29
|
+
"Gte",
|
30
|
+
"Lte",
|
31
|
+
"Eq",
|
32
|
+
"Ne",
|
33
|
+
"In",
|
34
|
+
"NotIn",
|
35
|
+
"Contains",
|
36
|
+
"Exists",
|
37
|
+
"Regex",
|
38
|
+
"FilterExpr",
|
39
|
+
"MongoLikeFilter",
|
40
|
+
]
|
@@ -0,0 +1,181 @@
|
|
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, get_args
|
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) # type: ignore[arg-type]
|
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) # type: ignore[arg-type]
|
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) # type: ignore[arg-type]
|
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) # type: ignore[arg-type]
|
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) # type: ignore[arg-type]
|
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) # type: ignore[arg-type]
|
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
|
+
return {self.field: to_jsonable_python(self.op, by_alias=True, round_trip=True)}
|
176
|
+
|
177
|
+
|
178
|
+
# for type annotations
|
179
|
+
MongoLikeFilter: TypeAlias = Union[Op, FilterExpr]
|
180
|
+
# for runtime type checks
|
181
|
+
MongoLikeFilterTypes: tuple[type, ...] = get_args(MongoLikeFilter)
|
@@ -0,0 +1,258 @@
|
|
1
|
+
"""Types that represent operators in MongoDB filter expressions."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Any, Dict, Iterable, Tuple, TypeVar, Union
|
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 GQLBase
|
11
|
+
|
12
|
+
# for type annotations
|
13
|
+
Scalar = Union[StrictStr, StrictInt, StrictFloat, StrictBool]
|
14
|
+
# for runtime type checks
|
15
|
+
ScalarTypes: tuple[type, ...] = tuple(t.__origin__ for t in get_args(Scalar))
|
16
|
+
|
17
|
+
# See: https://rich.readthedocs.io/en/stable/pretty.html#rich-repr-protocol
|
18
|
+
RichReprResult: TypeAlias = Iterable[
|
19
|
+
Union[
|
20
|
+
Any,
|
21
|
+
Tuple[Any],
|
22
|
+
Tuple[str, Any],
|
23
|
+
Tuple[str, Any, Any],
|
24
|
+
]
|
25
|
+
]
|
26
|
+
|
27
|
+
T = TypeVar("T")
|
28
|
+
TupleOf: TypeAlias = Tuple[T, ...]
|
29
|
+
|
30
|
+
|
31
|
+
# NOTE: Wherever class descriptions that are not docstrings, this is deliberate.
|
32
|
+
# This is done to ensure the descriptions are omitted from generated API docs.
|
33
|
+
|
34
|
+
|
35
|
+
# Mixin to support syntactic sugar for MongoDB expressions with (bitwise) logical operators,
|
36
|
+
# e.g. `a | b` -> `{"$or": [a, b]}` or `~a` -> `{"$not": a}`.
|
37
|
+
class SupportsLogicalOpSyntax:
|
38
|
+
def __or__(self, other: Any) -> Or:
|
39
|
+
"""Syntactic sugar for: `a | b` -> `Or(a, b)`."""
|
40
|
+
return Or(or_=[self, other])
|
41
|
+
|
42
|
+
def __and__(self, other: Any) -> And:
|
43
|
+
"""Syntactic sugar for: `a & b` -> `And(a, b)`."""
|
44
|
+
from .expressions import FilterExpr
|
45
|
+
|
46
|
+
if isinstance(other, (BaseOp, FilterExpr)):
|
47
|
+
return And(and_=[self, other])
|
48
|
+
return NotImplemented
|
49
|
+
|
50
|
+
def __invert__(self) -> Not:
|
51
|
+
"""Syntactic sugar for: `~a` -> `Not(a)`."""
|
52
|
+
return Not(not_=self)
|
53
|
+
|
54
|
+
|
55
|
+
# Base class for parsed MongoDB filter/query operators, e.g. `{"$and": [...]}`.
|
56
|
+
class BaseOp(GQLBase, SupportsLogicalOpSyntax):
|
57
|
+
model_config = ConfigDict(
|
58
|
+
extra="forbid",
|
59
|
+
frozen=True, # Make pseudo-immutable for easier comparison and hashing
|
60
|
+
)
|
61
|
+
|
62
|
+
def __repr__(self) -> str:
|
63
|
+
# Display operand as a positional arg
|
64
|
+
values_repr = ", ".join(map(repr, self.model_dump().values()))
|
65
|
+
return f"{type(self).__name__}({values_repr})"
|
66
|
+
|
67
|
+
def __rich_repr__(self) -> RichReprResult: # type: ignore[override]
|
68
|
+
# Display field values as positional args:
|
69
|
+
# https://rich.readthedocs.io/en/stable/pretty.html
|
70
|
+
yield from ((None, v) for v in self.model_dump().values())
|
71
|
+
|
72
|
+
|
73
|
+
# Logical operator(s)
|
74
|
+
# https://www.mongodb.com/docs/manual/reference/operator/query/and/
|
75
|
+
# https://www.mongodb.com/docs/manual/reference/operator/query/or/
|
76
|
+
# https://www.mongodb.com/docs/manual/reference/operator/query/nor/
|
77
|
+
# https://www.mongodb.com/docs/manual/reference/operator/query/not/
|
78
|
+
class And(BaseOp):
|
79
|
+
and_: TupleOf[Any] = Field(default=(), alias="$and")
|
80
|
+
|
81
|
+
|
82
|
+
class Or(BaseOp):
|
83
|
+
or_: TupleOf[Any] = Field(default=(), alias="$or")
|
84
|
+
|
85
|
+
def __invert__(self) -> Nor:
|
86
|
+
"""Syntactic sugar for: `~Or(a, b)` -> `Nor(a, b)`."""
|
87
|
+
return Nor(nor_=self.or_)
|
88
|
+
|
89
|
+
|
90
|
+
class Nor(BaseOp):
|
91
|
+
nor_: TupleOf[Any] = Field(default=(), alias="$nor")
|
92
|
+
|
93
|
+
def __invert__(self) -> Or:
|
94
|
+
"""Syntactic sugar for: `~Nor(a, b)` -> `Or(a, b)`."""
|
95
|
+
return Or(or_=self.nor_)
|
96
|
+
|
97
|
+
|
98
|
+
class Not(BaseOp):
|
99
|
+
not_: Any = Field(alias="$not")
|
100
|
+
|
101
|
+
def __invert__(self) -> Any:
|
102
|
+
"""Syntactic sugar for: `~Not(a)` -> `a`."""
|
103
|
+
return self.not_
|
104
|
+
|
105
|
+
|
106
|
+
# Comparison operator(s)
|
107
|
+
# https://www.mongodb.com/docs/manual/reference/operator/query/lt/
|
108
|
+
# https://www.mongodb.com/docs/manual/reference/operator/query/gt/
|
109
|
+
# https://www.mongodb.com/docs/manual/reference/operator/query/lte/
|
110
|
+
# https://www.mongodb.com/docs/manual/reference/operator/query/gte/
|
111
|
+
# https://www.mongodb.com/docs/manual/reference/operator/query/eq/
|
112
|
+
# https://www.mongodb.com/docs/manual/reference/operator/query/ne/
|
113
|
+
# https://www.mongodb.com/docs/manual/reference/operator/query/in/
|
114
|
+
# https://www.mongodb.com/docs/manual/reference/operator/query/nin/
|
115
|
+
class Lt(BaseOp):
|
116
|
+
lt_: Scalar = Field(alias="$lt")
|
117
|
+
|
118
|
+
def __invert__(self) -> Gte:
|
119
|
+
"""Syntactic sugar for: `~Lt(a)` -> `Gte(a)`."""
|
120
|
+
return Gte(gte_=self.lt_)
|
121
|
+
|
122
|
+
|
123
|
+
class Gt(BaseOp):
|
124
|
+
gt_: Scalar = Field(alias="$gt")
|
125
|
+
|
126
|
+
def __invert__(self) -> Lte:
|
127
|
+
"""Syntactic sugar for: `~Gt(a)` -> `Lte(a)`."""
|
128
|
+
return Lte(lte_=self.gt_)
|
129
|
+
|
130
|
+
|
131
|
+
class Lte(BaseOp):
|
132
|
+
lte_: Scalar = Field(alias="$lte")
|
133
|
+
|
134
|
+
def __invert__(self) -> Gt:
|
135
|
+
"""Syntactic sugar for: `~Lte(a)` -> `Gt(a)`."""
|
136
|
+
return Gt(gt_=self.lte_)
|
137
|
+
|
138
|
+
|
139
|
+
class Gte(BaseOp):
|
140
|
+
gte_: Scalar = Field(alias="$gte")
|
141
|
+
|
142
|
+
def __invert__(self) -> Lt:
|
143
|
+
"""Syntactic sugar for: `~Gte(a)` -> `Lt(a)`."""
|
144
|
+
return Lt(lt_=self.gte_)
|
145
|
+
|
146
|
+
|
147
|
+
class Eq(BaseOp):
|
148
|
+
eq_: Scalar = Field(alias="$eq")
|
149
|
+
|
150
|
+
def __invert__(self) -> Ne:
|
151
|
+
"""Syntactic sugar for: `~Eq(a)` -> `Ne(a)`."""
|
152
|
+
return Ne(ne_=self.eq_)
|
153
|
+
|
154
|
+
|
155
|
+
class Ne(BaseOp):
|
156
|
+
ne_: Scalar = Field(alias="$ne")
|
157
|
+
|
158
|
+
def __invert__(self) -> Eq:
|
159
|
+
"""Syntactic sugar for: `~Ne(a)` -> `Eq(a)`."""
|
160
|
+
return Eq(eq_=self.ne_)
|
161
|
+
|
162
|
+
|
163
|
+
class In(BaseOp):
|
164
|
+
in_: TupleOf[Scalar] = Field(default=(), alias="$in")
|
165
|
+
|
166
|
+
def __invert__(self) -> NotIn:
|
167
|
+
"""Syntactic sugar for: `~In(a)` -> `NotIn(a)`."""
|
168
|
+
return NotIn(nin_=self.in_)
|
169
|
+
|
170
|
+
|
171
|
+
class NotIn(BaseOp):
|
172
|
+
nin_: TupleOf[Scalar] = Field(default=(), alias="$nin")
|
173
|
+
|
174
|
+
def __invert__(self) -> In:
|
175
|
+
"""Syntactic sugar for: `~NotIn(a)` -> `In(a)`."""
|
176
|
+
return In(in_=self.nin_)
|
177
|
+
|
178
|
+
|
179
|
+
# Element operator(s)
|
180
|
+
# https://www.mongodb.com/docs/manual/reference/operator/query/exists/
|
181
|
+
class Exists(BaseOp):
|
182
|
+
exists_: bool = Field(alias="$exists")
|
183
|
+
|
184
|
+
|
185
|
+
# Evaluation operator(s)
|
186
|
+
# https://www.mongodb.com/docs/manual/reference/operator/query/regex/
|
187
|
+
#
|
188
|
+
# Note: "$contains" is NOT a formal MongoDB operator, but the backend recognizes and
|
189
|
+
# executes it as a substring-match filter.
|
190
|
+
class Regex(BaseOp):
|
191
|
+
regex_: str = Field(alias="$regex") #: The regex expression to match against.
|
192
|
+
|
193
|
+
|
194
|
+
class Contains(BaseOp):
|
195
|
+
contains_: str = Field(alias="$contains") #: The substring to match against.
|
196
|
+
|
197
|
+
|
198
|
+
And.model_rebuild()
|
199
|
+
Or.model_rebuild()
|
200
|
+
Not.model_rebuild()
|
201
|
+
Lt.model_rebuild()
|
202
|
+
Gt.model_rebuild()
|
203
|
+
Lte.model_rebuild()
|
204
|
+
Gte.model_rebuild()
|
205
|
+
Eq.model_rebuild()
|
206
|
+
Ne.model_rebuild()
|
207
|
+
In.model_rebuild()
|
208
|
+
NotIn.model_rebuild()
|
209
|
+
Exists.model_rebuild()
|
210
|
+
Regex.model_rebuild()
|
211
|
+
Contains.model_rebuild()
|
212
|
+
|
213
|
+
|
214
|
+
# ------------------------------------------------------------------------------
|
215
|
+
# Convenience helpers, constants, and utils for supported MongoDB operators
|
216
|
+
# ------------------------------------------------------------------------------
|
217
|
+
KEY_TO_OP: dict[str, type[BaseOp]] = {
|
218
|
+
"$and": And,
|
219
|
+
"$or": Or,
|
220
|
+
"$nor": Nor,
|
221
|
+
"$not": Not,
|
222
|
+
"$lt": Lt,
|
223
|
+
"$gt": Gt,
|
224
|
+
"$lte": Lte,
|
225
|
+
"$gte": Gte,
|
226
|
+
"$eq": Eq,
|
227
|
+
"$ne": Ne,
|
228
|
+
"$in": In,
|
229
|
+
"$nin": NotIn,
|
230
|
+
"$exists": Exists,
|
231
|
+
"$regex": Regex,
|
232
|
+
"$contains": Contains,
|
233
|
+
}
|
234
|
+
|
235
|
+
|
236
|
+
KnownOp = Union[
|
237
|
+
And,
|
238
|
+
Or,
|
239
|
+
Nor,
|
240
|
+
Not,
|
241
|
+
Lt,
|
242
|
+
Gt,
|
243
|
+
Lte,
|
244
|
+
Gte,
|
245
|
+
Eq,
|
246
|
+
Ne,
|
247
|
+
In,
|
248
|
+
NotIn,
|
249
|
+
Exists,
|
250
|
+
Regex,
|
251
|
+
Contains,
|
252
|
+
]
|
253
|
+
UnknownOp = Dict[str, Any]
|
254
|
+
|
255
|
+
# for type annotations
|
256
|
+
Op = Union[KnownOp, UnknownOp]
|
257
|
+
# for runtime type checks
|
258
|
+
OpTypes: tuple[type, ...] = (*get_args(KnownOp), dict)
|