wandb 0.19.8__py3-none-macosx_11_0_arm64.whl → 0.19.9__py3-none-macosx_11_0_arm64.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 +5 -1
- wandb/__init__.pyi +12 -8
- wandb/_pydantic/__init__.py +23 -0
- wandb/_pydantic/base.py +113 -0
- wandb/_pydantic/v1_compat.py +262 -0
- wandb/apis/paginator.py +82 -38
- wandb/apis/public/api.py +10 -64
- wandb/apis/public/artifacts.py +73 -17
- wandb/apis/public/files.py +2 -2
- wandb/apis/public/projects.py +3 -2
- wandb/apis/public/reports.py +2 -2
- wandb/apis/public/runs.py +19 -11
- wandb/bin/gpu_stats +0 -0
- wandb/bin/wandb-core +0 -0
- wandb/integration/metaflow/metaflow.py +19 -17
- wandb/integration/sacred/__init__.py +1 -1
- wandb/jupyter.py +18 -15
- wandb/proto/v3/wandb_internal_pb2.py +7 -3
- wandb/proto/v3/wandb_settings_pb2.py +2 -2
- wandb/proto/v3/wandb_telemetry_pb2.py +4 -4
- wandb/proto/v4/wandb_internal_pb2.py +3 -3
- wandb/proto/v4/wandb_settings_pb2.py +2 -2
- wandb/proto/v4/wandb_telemetry_pb2.py +4 -4
- wandb/proto/v5/wandb_internal_pb2.py +3 -3
- wandb/proto/v5/wandb_settings_pb2.py +2 -2
- wandb/proto/v5/wandb_telemetry_pb2.py +4 -4
- wandb/proto/wandb_deprecated.py +2 -0
- wandb/sdk/artifacts/_graphql_fragments.py +18 -20
- wandb/sdk/artifacts/_validators.py +1 -0
- wandb/sdk/artifacts/artifact.py +70 -36
- wandb/sdk/artifacts/artifact_saver.py +16 -2
- wandb/sdk/artifacts/storage_policies/wandb_storage_policy.py +23 -2
- wandb/sdk/data_types/audio.py +1 -3
- wandb/sdk/data_types/base_types/media.py +11 -4
- wandb/sdk/data_types/image.py +44 -25
- wandb/sdk/data_types/molecule.py +1 -5
- wandb/sdk/data_types/object_3d.py +2 -1
- wandb/sdk/data_types/saved_model.py +7 -9
- wandb/sdk/data_types/video.py +1 -4
- wandb/{apis/public → sdk/internal}/_generated/__init__.py +0 -6
- wandb/sdk/internal/_generated/base.py +226 -0
- wandb/{apis/public → sdk/internal}/_generated/server_features_query.py +3 -3
- wandb/{apis/public → sdk/internal}/_generated/typing_compat.py +1 -1
- wandb/sdk/internal/internal_api.py +138 -47
- wandb/sdk/internal/sender.py +2 -0
- wandb/sdk/internal/sender_config.py +8 -11
- wandb/sdk/internal/settings_static.py +24 -2
- wandb/sdk/lib/apikey.py +15 -16
- wandb/sdk/lib/run_moment.py +4 -6
- wandb/sdk/lib/wb_logging.py +161 -0
- wandb/sdk/wandb_config.py +44 -43
- wandb/sdk/wandb_init.py +141 -79
- wandb/sdk/wandb_metadata.py +107 -91
- wandb/sdk/wandb_run.py +152 -44
- wandb/sdk/wandb_settings.py +403 -201
- wandb/sdk/wandb_setup.py +3 -1
- {wandb-0.19.8.dist-info → wandb-0.19.9.dist-info}/METADATA +3 -3
- {wandb-0.19.8.dist-info → wandb-0.19.9.dist-info}/RECORD +64 -60
- wandb/apis/public/_generated/base.py +0 -128
- /wandb/{apis/public → sdk/internal}/_generated/enums.py +0 -0
- /wandb/{apis/public → sdk/internal}/_generated/input_types.py +0 -0
- /wandb/{apis/public → sdk/internal}/_generated/operations.py +0 -0
- {wandb-0.19.8.dist-info → wandb-0.19.9.dist-info}/WHEEL +0 -0
- {wandb-0.19.8.dist-info → wandb-0.19.9.dist-info}/entry_points.txt +0 -0
- {wandb-0.19.8.dist-info → wandb-0.19.9.dist-info}/licenses/LICENSE +0 -0
wandb/__init__.py
CHANGED
@@ -10,7 +10,7 @@ For reference documentation, see https://docs.wandb.com/ref/python.
|
|
10
10
|
"""
|
11
11
|
from __future__ import annotations
|
12
12
|
|
13
|
-
__version__ = "0.19.
|
13
|
+
__version__ = "0.19.9"
|
14
14
|
|
15
15
|
|
16
16
|
from wandb.errors import Error
|
@@ -18,6 +18,10 @@ from wandb.errors import Error
|
|
18
18
|
# This needs to be early as other modules call it.
|
19
19
|
from wandb.errors.term import termsetup, termlog, termerror, termwarn
|
20
20
|
|
21
|
+
# Configure the logger as early as possible for consistent behavior.
|
22
|
+
from wandb.sdk.lib import wb_logging as _wb_logging
|
23
|
+
_wb_logging.configure_wandb_logger()
|
24
|
+
|
21
25
|
from wandb import sdk as wandb_sdk
|
22
26
|
|
23
27
|
import wandb
|
wandb/__init__.pyi
CHANGED
@@ -106,7 +106,7 @@ if TYPE_CHECKING:
|
|
106
106
|
import wandb
|
107
107
|
from wandb.plot import CustomChart
|
108
108
|
|
109
|
-
__version__: str = "0.19.
|
109
|
+
__version__: str = "0.19.9"
|
110
110
|
|
111
111
|
run: Run | None
|
112
112
|
config: wandb_config.Config
|
@@ -222,7 +222,15 @@ def init(
|
|
222
222
|
mode: Literal["online", "offline", "disabled"] | None = None,
|
223
223
|
force: bool | None = None,
|
224
224
|
anonymous: Literal["never", "allow", "must"] | None = None,
|
225
|
-
reinit:
|
225
|
+
reinit: (
|
226
|
+
bool
|
227
|
+
| Literal[
|
228
|
+
None,
|
229
|
+
"default",
|
230
|
+
"return_previous",
|
231
|
+
"finish_previous",
|
232
|
+
]
|
233
|
+
) = None,
|
226
234
|
resume: bool | Literal["allow", "never", "must", "auto"] | None = None,
|
227
235
|
resume_from: str | None = None,
|
228
236
|
fork_from: str | None = None,
|
@@ -370,12 +378,8 @@ def init(
|
|
370
378
|
to view the charts and data in the UI.
|
371
379
|
- `"must"`: Forces the run to be logged to an anonymous account, even
|
372
380
|
if the user is logged in.
|
373
|
-
reinit:
|
374
|
-
|
375
|
-
exists, calling `wandb.init()` returns the existing run instead of
|
376
|
-
creating a new one. When `reinit=True`, the active run is finished
|
377
|
-
before a new run is initialized. In notebook environments, runs are
|
378
|
-
reinitialized by default unless `reinit` is explicitly set to `False`.
|
381
|
+
reinit: Shorthand for the "reinit" setting. Determines the behavior of
|
382
|
+
`wandb.init()` when a run is active.
|
379
383
|
resume: Controls the behavior when resuming a run with the specified `id`.
|
380
384
|
Available options are:
|
381
385
|
- `"allow"`: If a run with the specified `id` exists, it will resume
|
@@ -0,0 +1,23 @@
|
|
1
|
+
"""Internal utilities for working with pydantic."""
|
2
|
+
|
3
|
+
from .base import Base, GQLBase, GQLId, SerializedToJson, Typename
|
4
|
+
from .v1_compat import (
|
5
|
+
IS_PYDANTIC_V2,
|
6
|
+
AliasChoices,
|
7
|
+
computed_field,
|
8
|
+
field_validator,
|
9
|
+
model_validator,
|
10
|
+
)
|
11
|
+
|
12
|
+
__all__ = [
|
13
|
+
"IS_PYDANTIC_V2",
|
14
|
+
"Base",
|
15
|
+
"GQLBase",
|
16
|
+
"Typename",
|
17
|
+
"GQLId",
|
18
|
+
"SerializedToJson",
|
19
|
+
"AliasChoices",
|
20
|
+
"computed_field",
|
21
|
+
"field_validator",
|
22
|
+
"model_validator",
|
23
|
+
]
|
wandb/_pydantic/base.py
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
"""Base classes and other customizations for generated pydantic types."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar
|
6
|
+
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, Json
|
8
|
+
from typing_extensions import Annotated, TypedDict, Unpack, override
|
9
|
+
|
10
|
+
from .v1_compat import PydanticCompatMixin
|
11
|
+
|
12
|
+
if TYPE_CHECKING:
|
13
|
+
from pydantic.main import IncEx
|
14
|
+
|
15
|
+
|
16
|
+
class ModelDumpKwargs(TypedDict, total=False):
|
17
|
+
"""Shared keyword arguments for `BaseModel.model_{dump,dump_json}`."""
|
18
|
+
|
19
|
+
include: IncEx | None
|
20
|
+
exclude: IncEx | None
|
21
|
+
context: dict[str, Any] | None
|
22
|
+
by_alias: bool | None
|
23
|
+
exclude_unset: bool
|
24
|
+
exclude_defaults: bool
|
25
|
+
exclude_none: bool
|
26
|
+
round_trip: bool
|
27
|
+
warnings: bool | Literal["none", "warn", "error"]
|
28
|
+
fallback: Callable[[Any], Any] | None
|
29
|
+
serialize_as_any: bool
|
30
|
+
|
31
|
+
|
32
|
+
#: Custom overrides of default kwargs for `BaseModel.model_{dump,dump_json}`.
|
33
|
+
MODEL_DUMP_DEFAULTS = ModelDumpKwargs(
|
34
|
+
by_alias=True, # Always serialize with aliases (e.g. camelCase names)
|
35
|
+
round_trip=True, # Ensure serialized values remain valid inputs for deserialization
|
36
|
+
)
|
37
|
+
|
38
|
+
|
39
|
+
# Base class for all generated classes/types.
|
40
|
+
# Omitted from docstring to avoid inclusion in generated docs.
|
41
|
+
class Base(PydanticCompatMixin, BaseModel):
|
42
|
+
model_config = ConfigDict(
|
43
|
+
populate_by_name=True,
|
44
|
+
validate_assignment=True,
|
45
|
+
validate_default=True,
|
46
|
+
extra="forbid",
|
47
|
+
use_attribute_docstrings=True,
|
48
|
+
from_attributes=True,
|
49
|
+
revalidate_instances="always",
|
50
|
+
)
|
51
|
+
|
52
|
+
@override
|
53
|
+
def model_dump(
|
54
|
+
self,
|
55
|
+
*,
|
56
|
+
mode: Literal["json", "python"] | str = "json", # NOTE: changed default
|
57
|
+
**kwargs: Unpack[ModelDumpKwargs],
|
58
|
+
) -> dict[str, Any]:
|
59
|
+
kwargs = {**MODEL_DUMP_DEFAULTS, **kwargs}
|
60
|
+
return super().model_dump(mode=mode, **kwargs)
|
61
|
+
|
62
|
+
@override
|
63
|
+
def model_dump_json(
|
64
|
+
self,
|
65
|
+
*,
|
66
|
+
indent: int | None = None,
|
67
|
+
**kwargs: Unpack[ModelDumpKwargs],
|
68
|
+
) -> str:
|
69
|
+
kwargs = {**MODEL_DUMP_DEFAULTS, **kwargs}
|
70
|
+
return super().model_dump_json(indent=indent, **kwargs)
|
71
|
+
|
72
|
+
|
73
|
+
# Base class with extra customization for GQL generated types.
|
74
|
+
# Omitted from docstring to avoid inclusion in generated docs.
|
75
|
+
class GQLBase(Base):
|
76
|
+
model_config = ConfigDict(
|
77
|
+
extra="ignore",
|
78
|
+
protected_namespaces=(),
|
79
|
+
)
|
80
|
+
|
81
|
+
|
82
|
+
# ------------------------------------------------------------------------------
|
83
|
+
# Reusable annotations for field types
|
84
|
+
T = TypeVar("T")
|
85
|
+
|
86
|
+
GQLId = Annotated[
|
87
|
+
str,
|
88
|
+
Field(repr=False, strict=True, frozen=True),
|
89
|
+
]
|
90
|
+
|
91
|
+
Typename = Annotated[
|
92
|
+
T,
|
93
|
+
Field(repr=False, alias="__typename", frozen=True),
|
94
|
+
]
|
95
|
+
|
96
|
+
|
97
|
+
# FIXME: Restore, modify, or replace this later after ensuring pydantic v1 compatibility.
|
98
|
+
# def validate_maybe_json(v: Any, handler: ValidatorFunctionWrapHandler) -> Any:
|
99
|
+
# """Wraps default Json[...] field validator to allow instantiation with an already-decoded value."""
|
100
|
+
# try:
|
101
|
+
# return handler(v)
|
102
|
+
# except ValidationError:
|
103
|
+
# # Try revalidating after properly jsonifying the value
|
104
|
+
# return handler(to_json(v, by_alias=True, round_trip=True))
|
105
|
+
#
|
106
|
+
#
|
107
|
+
# SerializedToJson = Annotated[
|
108
|
+
# Json[T],
|
109
|
+
# # Allow lenient instantiation/validation: incoming data may already be deserialized.
|
110
|
+
# WrapValidator(validate_maybe_json),
|
111
|
+
# ]
|
112
|
+
|
113
|
+
SerializedToJson = Json[T]
|
@@ -0,0 +1,262 @@
|
|
1
|
+
"""Provides partial support for compatibility with Pydantic v1."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import sys
|
6
|
+
from importlib.metadata import version
|
7
|
+
from typing import (
|
8
|
+
TYPE_CHECKING,
|
9
|
+
Any,
|
10
|
+
Callable,
|
11
|
+
ClassVar,
|
12
|
+
Literal,
|
13
|
+
Mapping,
|
14
|
+
TypeVar,
|
15
|
+
overload,
|
16
|
+
)
|
17
|
+
|
18
|
+
import pydantic
|
19
|
+
from typing_extensions import ParamSpec
|
20
|
+
|
21
|
+
if TYPE_CHECKING:
|
22
|
+
from typing import Protocol
|
23
|
+
|
24
|
+
class V1Model(Protocol):
|
25
|
+
__config__: ClassVar[type]
|
26
|
+
__fields__: ClassVar[dict[str, Any]]
|
27
|
+
__fields_set__: set[str]
|
28
|
+
|
29
|
+
@classmethod
|
30
|
+
def update_forward_refs(cls, *args: Any, **kwargs: Any) -> None: ...
|
31
|
+
@classmethod
|
32
|
+
def construct(cls, *args: Any, **kwargs: Any) -> V1Model: ...
|
33
|
+
@classmethod
|
34
|
+
def parse_obj(cls, *args: Any, **kwargs: Any) -> V1Model: ...
|
35
|
+
@classmethod
|
36
|
+
def parse_raw(cls, *args: Any, **kwargs: Any) -> V1Model: ...
|
37
|
+
def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]: ...
|
38
|
+
def json(self, *args: Any, **kwargs: Any) -> str: ...
|
39
|
+
def copy(self, *args: Any, **kwargs: Any) -> V1Model: ...
|
40
|
+
|
41
|
+
|
42
|
+
PYTHON_VERSION = sys.version_info
|
43
|
+
|
44
|
+
pydantic_major_version, *_ = version(pydantic.__name__).split(".")
|
45
|
+
IS_PYDANTIC_V2: bool = int(pydantic_major_version) >= 2
|
46
|
+
|
47
|
+
|
48
|
+
ModelT = TypeVar("ModelT")
|
49
|
+
RT = TypeVar("RT")
|
50
|
+
P = ParamSpec("P")
|
51
|
+
|
52
|
+
|
53
|
+
# Maps {v2 -> v1} model config keys that were renamed in v2.
|
54
|
+
# See: https://docs.pydantic.dev/latest/migration/#changes-to-config
|
55
|
+
_V1_CONFIG_KEYS = {
|
56
|
+
"populate_by_name": "allow_population_by_field_name",
|
57
|
+
"str_to_lower": "anystr_lower",
|
58
|
+
"str_strip_whitespace": "anystr_strip_whitespace",
|
59
|
+
"str_to_upper": "anystr_upper",
|
60
|
+
"ignored_types": "keep_untouched",
|
61
|
+
"str_max_length": "max_anystr_length",
|
62
|
+
"str_min_length": "min_anystr_length",
|
63
|
+
"from_attributes": "orm_mode",
|
64
|
+
"json_schema_extra": "schema_extra",
|
65
|
+
"validate_default": "validate_all",
|
66
|
+
}
|
67
|
+
|
68
|
+
|
69
|
+
def _convert_v2_config(v2_config: dict[str, Any]) -> dict[str, Any]:
|
70
|
+
"""Return a copy of the v2 ConfigDict with renamed v1 keys."""
|
71
|
+
return {_V1_CONFIG_KEYS.get(k, k): v for k, v in v2_config.items()}
|
72
|
+
|
73
|
+
|
74
|
+
# Pydantic BaseModels are defined with a custom metaclass, but its namespace
|
75
|
+
# has changed between pydantic versions.
|
76
|
+
#
|
77
|
+
# In v1, it can be imported as `from pydantic.main import ModelMetaclass`
|
78
|
+
# In v2, it's defined in an internal module so we avoid directly importing it.
|
79
|
+
PydanticModelMetaclass: type = type(pydantic.BaseModel)
|
80
|
+
|
81
|
+
|
82
|
+
class V1MixinMetaclass(PydanticModelMetaclass):
|
83
|
+
def __new__(
|
84
|
+
cls,
|
85
|
+
name: str,
|
86
|
+
bases: tuple[type, ...],
|
87
|
+
namespace: dict[str, Any],
|
88
|
+
**kwargs: Any,
|
89
|
+
):
|
90
|
+
# Converts a `model_config` dict in a V2 class definition, e.g.:
|
91
|
+
#
|
92
|
+
# class MyModel(BaseModel):
|
93
|
+
# model_config = ConfigDict(populate_by_name=True)
|
94
|
+
#
|
95
|
+
# ...to a `Config` class in a V1 class definition, e.g.:
|
96
|
+
#
|
97
|
+
# class MyModel(BaseModel):
|
98
|
+
# class Config:
|
99
|
+
# allow_population_by_field_name = True
|
100
|
+
#
|
101
|
+
if config_dict := namespace.pop("model_config", None):
|
102
|
+
namespace["Config"] = type("Config", (), _convert_v2_config(config_dict))
|
103
|
+
return super().__new__(cls, name, bases, namespace, **kwargs)
|
104
|
+
|
105
|
+
# note: workarounds to patch "class properties" aren't consistent between python
|
106
|
+
# versions, so this will have to do until changes are needed.
|
107
|
+
if not ((3, 9) <= PYTHON_VERSION < (3, 13)):
|
108
|
+
|
109
|
+
@property
|
110
|
+
def model_fields(self) -> dict[str, Any]:
|
111
|
+
return self.__fields__
|
112
|
+
|
113
|
+
|
114
|
+
# Mixin to ensure compatibility of Pydantic models if Pydantic v1 is detected.
|
115
|
+
# These are "best effort" implementations and cannot guarantee complete
|
116
|
+
# compatibility in v1 environments.
|
117
|
+
#
|
118
|
+
# Whenever possible, users should strongly prefer upgrading to Pydantic v2 to
|
119
|
+
# ensure full compatibility.
|
120
|
+
class V1Mixin(metaclass=V1MixinMetaclass):
|
121
|
+
@classmethod
|
122
|
+
def __try_update_forward_refs__(cls: type[V1Model], **localns: Any) -> None:
|
123
|
+
if hasattr(sup := super(), "__try_update_forward_refs__"):
|
124
|
+
sup.__try_update_forward_refs__(**localns)
|
125
|
+
|
126
|
+
@classmethod
|
127
|
+
def model_rebuild(cls, *args: Any, **kwargs: Any) -> None:
|
128
|
+
return cls.update_forward_refs(*args, **kwargs)
|
129
|
+
|
130
|
+
@classmethod
|
131
|
+
def model_construct(cls, *args: Any, **kwargs: Any) -> V1Model:
|
132
|
+
return cls.construct(*args, **kwargs)
|
133
|
+
|
134
|
+
@classmethod
|
135
|
+
def model_validate(cls, *args: Any, **kwargs: Any) -> V1Model:
|
136
|
+
return cls.parse_obj(*args, **kwargs)
|
137
|
+
|
138
|
+
@classmethod
|
139
|
+
def model_validate_json(cls, *args: Any, **kwargs: Any) -> V1Model:
|
140
|
+
return cls.parse_raw(*args, **kwargs)
|
141
|
+
|
142
|
+
def model_dump(self: V1Model, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
143
|
+
return self.dict(*args, **kwargs)
|
144
|
+
|
145
|
+
def model_dump_json(self: V1Model, *args: Any, **kwargs: Any) -> str:
|
146
|
+
return self.json(*args, **kwargs)
|
147
|
+
|
148
|
+
def model_copy(self: V1Model, *args: Any, **kwargs: Any) -> V1Model:
|
149
|
+
return self.copy(*args, **kwargs)
|
150
|
+
|
151
|
+
# workarounds to patch "class properties" aren't consistent between python
|
152
|
+
# versions, so this will have to do until changes are needed.
|
153
|
+
if (3, 9) <= PYTHON_VERSION < (3, 13):
|
154
|
+
|
155
|
+
@classmethod # type: ignore[misc]
|
156
|
+
@property
|
157
|
+
def model_fields(cls: type[V1Model]) -> Mapping[str, Any]:
|
158
|
+
return cls.__fields__
|
159
|
+
|
160
|
+
@property
|
161
|
+
def model_fields_set(self: V1Model) -> set[str]:
|
162
|
+
return self.__fields_set__
|
163
|
+
|
164
|
+
|
165
|
+
# Placeholder. Pydantic v2 is already compatible with itself, so no need for extra mixins.
|
166
|
+
class V2Mixin:
|
167
|
+
pass
|
168
|
+
|
169
|
+
|
170
|
+
# Pick the mixin type based on the detected Pydantic version.
|
171
|
+
PydanticCompatMixin: type = V2Mixin if IS_PYDANTIC_V2 else V1Mixin
|
172
|
+
|
173
|
+
|
174
|
+
# ----------------------------------------------------------------------------
|
175
|
+
# Decorators and other pydantic helpers
|
176
|
+
# ----------------------------------------------------------------------------
|
177
|
+
if IS_PYDANTIC_V2:
|
178
|
+
field_validator = pydantic.field_validator
|
179
|
+
model_validator = pydantic.model_validator
|
180
|
+
AliasChoices = pydantic.AliasChoices
|
181
|
+
computed_field = pydantic.computed_field
|
182
|
+
|
183
|
+
else:
|
184
|
+
# Redefines `@field_validator` with a v2-like signature
|
185
|
+
# to call `@validator` from v1 instead.
|
186
|
+
def field_validator(
|
187
|
+
field: str,
|
188
|
+
/,
|
189
|
+
*fields: str,
|
190
|
+
mode: Literal["before", "after", "wrap", "plain"] = "after",
|
191
|
+
check_fields: bool | None = None,
|
192
|
+
**_: Any,
|
193
|
+
) -> Callable:
|
194
|
+
return pydantic.validator(
|
195
|
+
field,
|
196
|
+
*fields,
|
197
|
+
pre=(mode == "before"),
|
198
|
+
always=True,
|
199
|
+
check_fields=bool(check_fields),
|
200
|
+
allow_reuse=True,
|
201
|
+
)
|
202
|
+
|
203
|
+
# Redefines `@model_validator` with a v2-like signature
|
204
|
+
# to call `@root_validator` from v1 instead.
|
205
|
+
def model_validator(
|
206
|
+
*,
|
207
|
+
mode: Literal["before", "after", "wrap", "plain"],
|
208
|
+
**_: Any,
|
209
|
+
) -> Callable:
|
210
|
+
if mode == "after":
|
211
|
+
# Patch the behavior for `@model_validator(mode="after")` in v1. This is
|
212
|
+
# necessarily complicated because:
|
213
|
+
# - `@model_validator(mode="after")` decorates an instance method in pydantic v2
|
214
|
+
# - `@root_validator(pre=False)` always decorates a classmethod in pydantic v1
|
215
|
+
def _decorator(v2_method: Callable) -> Any:
|
216
|
+
def v1_method(
|
217
|
+
cls: type[V1Model], values: dict[str, Any]
|
218
|
+
) -> dict[str, Any]:
|
219
|
+
# Note: Since this is an "after" validator, the values should already be
|
220
|
+
# validated, so `.construct()` in v1 (`.model_construct()` in v2)
|
221
|
+
# should create a valid object to pass to the **original** decorated instance method.
|
222
|
+
validated = v2_method(cls.construct(**values))
|
223
|
+
|
224
|
+
# Pydantic v1 expects the validator to return a dict of {field_name -> value}
|
225
|
+
return {
|
226
|
+
name: getattr(validated, name) for name in validated.__fields__
|
227
|
+
}
|
228
|
+
|
229
|
+
return pydantic.root_validator(pre=False, allow_reuse=True)( # type: ignore[call-overload]
|
230
|
+
classmethod(v1_method)
|
231
|
+
)
|
232
|
+
|
233
|
+
return _decorator
|
234
|
+
else:
|
235
|
+
return pydantic.root_validator(pre=(mode == "before"), allow_reuse=True) # type: ignore[call-overload]
|
236
|
+
|
237
|
+
@overload # type: ignore[no-redef]
|
238
|
+
def computed_field(func: Callable | property, /) -> property: ...
|
239
|
+
@overload
|
240
|
+
def computed_field(
|
241
|
+
func: None, /, **_: Any
|
242
|
+
) -> Callable[[Callable | property], property]: ...
|
243
|
+
|
244
|
+
def computed_field(
|
245
|
+
func: Callable | property | None = None, /, **_: Any
|
246
|
+
) -> property | Callable[[Callable | property], property]:
|
247
|
+
"""Compatibility wrapper for Pydantic v2's `computed_field` in v1."""
|
248
|
+
|
249
|
+
def always_property(f: Callable | property) -> property:
|
250
|
+
# Convert the method to a property only if needed
|
251
|
+
return f if isinstance(f, property) else property(f)
|
252
|
+
|
253
|
+
# Handle both decorator styles
|
254
|
+
return always_property if (func is None) else always_property(func)
|
255
|
+
|
256
|
+
class AliasChoices: # type: ignore [no-redef]
|
257
|
+
"""Placeholder class for Pydantic v2's AliasChoices for partial v1 compatibility."""
|
258
|
+
|
259
|
+
aliases: list[str]
|
260
|
+
|
261
|
+
def __init__(self, *aliases: str):
|
262
|
+
self.aliases = list(aliases)
|
wandb/apis/paginator.py
CHANGED
@@ -1,75 +1,103 @@
|
|
1
|
-
from
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from abc import abstractmethod
|
4
|
+
from typing import (
|
5
|
+
TYPE_CHECKING,
|
6
|
+
Any,
|
7
|
+
ClassVar,
|
8
|
+
Iterator,
|
9
|
+
Mapping,
|
10
|
+
Protocol,
|
11
|
+
Sized,
|
12
|
+
TypeVar,
|
13
|
+
overload,
|
14
|
+
)
|
2
15
|
|
3
16
|
if TYPE_CHECKING:
|
4
|
-
from
|
17
|
+
from wandb_graphql.language.ast import Document
|
18
|
+
|
19
|
+
T = TypeVar("T")
|
20
|
+
|
21
|
+
|
22
|
+
# Structural type hint for the client instance
|
23
|
+
class _Client(Protocol):
|
24
|
+
def execute(self, *args: Any, **kwargs: Any) -> dict[str, Any]: ...
|
25
|
+
|
5
26
|
|
27
|
+
class Paginator(Iterator[T]):
|
28
|
+
"""An iterator for paginated objects from GraphQL requests."""
|
6
29
|
|
7
|
-
|
8
|
-
QUERY = None
|
30
|
+
QUERY: ClassVar[Document | None] = None
|
9
31
|
|
10
32
|
def __init__(
|
11
33
|
self,
|
12
|
-
client:
|
13
|
-
variables:
|
14
|
-
per_page:
|
34
|
+
client: _Client,
|
35
|
+
variables: Mapping[str, Any],
|
36
|
+
per_page: int = 50, # We don't allow unbounded paging
|
15
37
|
):
|
16
|
-
self.client = client
|
17
|
-
self.variables = variables
|
18
|
-
# We don't allow unbounded paging
|
19
|
-
self.per_page = per_page
|
20
|
-
if self.per_page is None:
|
21
|
-
self.per_page = 50
|
22
|
-
self.objects = []
|
23
|
-
self.index = -1
|
24
|
-
self.last_response = None
|
38
|
+
self.client: _Client = client
|
25
39
|
|
26
|
-
|
27
|
-
self.
|
28
|
-
return self
|
40
|
+
# shallow copy partly guards against mutating the original input
|
41
|
+
self.variables: dict[str, Any] = dict(variables)
|
29
42
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
raise ValueError("Object doesn't provide length")
|
35
|
-
return self.length
|
43
|
+
self.per_page: int = per_page
|
44
|
+
self.objects: list[T] = []
|
45
|
+
self.index: int = -1
|
46
|
+
self.last_response: object | None = None
|
36
47
|
|
37
|
-
|
38
|
-
|
39
|
-
|
48
|
+
def __iter__(self) -> Iterator[T]:
|
49
|
+
self.index = -1
|
50
|
+
return self
|
40
51
|
|
41
52
|
@property
|
42
|
-
|
53
|
+
@abstractmethod
|
54
|
+
def more(self) -> bool:
|
55
|
+
"""Whether there are more pages to be fetched."""
|
43
56
|
raise NotImplementedError
|
44
57
|
|
45
58
|
@property
|
46
|
-
|
59
|
+
@abstractmethod
|
60
|
+
def cursor(self) -> str | None:
|
61
|
+
"""The start cursor to use for the next fetched page."""
|
47
62
|
raise NotImplementedError
|
48
63
|
|
49
|
-
|
64
|
+
@abstractmethod
|
65
|
+
def convert_objects(self) -> list[T]:
|
66
|
+
"""Convert the last fetched response data into the iterated objects."""
|
50
67
|
raise NotImplementedError
|
51
68
|
|
52
|
-
def update_variables(self):
|
69
|
+
def update_variables(self) -> None:
|
70
|
+
"""Update the query variables for the next page fetch."""
|
53
71
|
self.variables.update({"perPage": self.per_page, "cursor": self.cursor})
|
54
72
|
|
55
|
-
def
|
56
|
-
|
57
|
-
return False
|
58
|
-
self.update_variables()
|
73
|
+
def _update_response(self) -> None:
|
74
|
+
"""Fetch and store the response data for the next page."""
|
59
75
|
self.last_response = self.client.execute(
|
60
76
|
self.QUERY, variable_values=self.variables
|
61
77
|
)
|
78
|
+
|
79
|
+
def _load_page(self) -> bool:
|
80
|
+
"""Fetch the next page, if any, returning True and storing the response if there was one."""
|
81
|
+
if not self.more:
|
82
|
+
return False
|
83
|
+
self.update_variables()
|
84
|
+
self._update_response()
|
62
85
|
self.objects.extend(self.convert_objects())
|
63
86
|
return True
|
64
87
|
|
65
|
-
|
88
|
+
@overload
|
89
|
+
def __getitem__(self, index: int) -> T: ...
|
90
|
+
@overload
|
91
|
+
def __getitem__(self, index: slice) -> list[T]: ...
|
92
|
+
|
93
|
+
def __getitem__(self, index: int | slice) -> T | list[T]:
|
66
94
|
loaded = True
|
67
95
|
stop = index.stop if isinstance(index, slice) else index
|
68
96
|
while loaded and stop > len(self.objects) - 1:
|
69
97
|
loaded = self._load_page()
|
70
98
|
return self.objects[index]
|
71
99
|
|
72
|
-
def __next__(self):
|
100
|
+
def __next__(self) -> T:
|
73
101
|
self.index += 1
|
74
102
|
if len(self.objects) <= self.index:
|
75
103
|
if not self._load_page():
|
@@ -79,3 +107,19 @@ class Paginator:
|
|
79
107
|
return self.objects[self.index]
|
80
108
|
|
81
109
|
next = __next__
|
110
|
+
|
111
|
+
|
112
|
+
class SizedPaginator(Paginator[T], Sized):
|
113
|
+
"""A Paginator for objects with a known total count."""
|
114
|
+
|
115
|
+
def __len__(self) -> int:
|
116
|
+
if self.length is None:
|
117
|
+
self._load_page()
|
118
|
+
if self.length is None:
|
119
|
+
raise ValueError("Object doesn't provide length")
|
120
|
+
return self.length
|
121
|
+
|
122
|
+
@property
|
123
|
+
@abstractmethod
|
124
|
+
def length(self) -> int | None:
|
125
|
+
raise NotImplementedError
|