wandb 0.21.3__py3-none-musllinux_1_2_aarch64.whl → 0.21.4__py3-none-musllinux_1_2_aarch64.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 +1 -1
- wandb/_analytics.py +65 -0
- wandb/_iterutils.py +8 -0
- wandb/_pydantic/__init__.py +10 -11
- wandb/_pydantic/base.py +3 -53
- wandb/_pydantic/field_types.py +29 -0
- wandb/_pydantic/v1_compat.py +47 -30
- wandb/_strutils.py +40 -0
- wandb/apis/public/api.py +17 -4
- wandb/apis/public/artifacts.py +5 -4
- wandb/apis/public/automations.py +2 -1
- wandb/apis/public/registries/_freezable_list.py +6 -6
- wandb/apis/public/registries/_utils.py +2 -1
- wandb/apis/public/registries/registries_search.py +4 -0
- wandb/apis/public/registries/registry.py +7 -0
- wandb/automations/_filters/expressions.py +3 -2
- wandb/automations/_filters/operators.py +2 -1
- wandb/automations/_validators.py +20 -0
- wandb/automations/actions.py +4 -2
- wandb/automations/events.py +4 -5
- wandb/bin/gpu_stats +0 -0
- wandb/bin/wandb-core +0 -0
- wandb/cli/beta.py +48 -130
- wandb/cli/beta_sync.py +226 -0
- wandb/integration/dspy/__init__.py +5 -0
- wandb/integration/dspy/dspy.py +422 -0
- wandb/integration/weave/weave.py +55 -0
- wandb/proto/v3/wandb_server_pb2.py +38 -57
- wandb/proto/v3/wandb_sync_pb2.py +87 -0
- wandb/proto/v3/wandb_telemetry_pb2.py +12 -12
- wandb/proto/v4/wandb_server_pb2.py +38 -41
- wandb/proto/v4/wandb_sync_pb2.py +38 -0
- wandb/proto/v4/wandb_telemetry_pb2.py +12 -12
- wandb/proto/v5/wandb_server_pb2.py +38 -41
- wandb/proto/v5/wandb_sync_pb2.py +39 -0
- wandb/proto/v5/wandb_telemetry_pb2.py +12 -12
- wandb/proto/v6/wandb_server_pb2.py +38 -41
- wandb/proto/v6/wandb_sync_pb2.py +49 -0
- wandb/proto/v6/wandb_telemetry_pb2.py +12 -12
- wandb/proto/wandb_generate_proto.py +1 -0
- wandb/proto/wandb_sync_pb2.py +12 -0
- wandb/sdk/artifacts/_validators.py +50 -49
- wandb/sdk/artifacts/artifact.py +7 -7
- wandb/sdk/artifacts/exceptions.py +2 -1
- wandb/sdk/artifacts/storage_handlers/s3_handler.py +2 -1
- wandb/sdk/lib/asyncio_compat.py +88 -23
- wandb/sdk/lib/gql_request.py +18 -7
- wandb/sdk/lib/printer.py +9 -13
- wandb/sdk/lib/progress.py +8 -6
- wandb/sdk/lib/service/service_connection.py +42 -12
- wandb/sdk/mailbox/wait_with_progress.py +1 -1
- wandb/sdk/wandb_init.py +0 -8
- wandb/sdk/wandb_run.py +13 -1
- wandb/sdk/wandb_settings.py +55 -0
- {wandb-0.21.3.dist-info → wandb-0.21.4.dist-info}/METADATA +1 -1
- {wandb-0.21.3.dist-info → wandb-0.21.4.dist-info}/RECORD +60 -49
- {wandb-0.21.3.dist-info → wandb-0.21.4.dist-info}/WHEEL +0 -0
- {wandb-0.21.3.dist-info → wandb-0.21.4.dist-info}/entry_points.txt +0 -0
- {wandb-0.21.3.dist-info → wandb-0.21.4.dist-info}/licenses/LICENSE +0 -0
@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional
|
|
3
3
|
from wandb_gql import gql
|
4
4
|
|
5
5
|
import wandb
|
6
|
+
from wandb._analytics import tracked
|
6
7
|
from wandb.proto.wandb_internal_pb2 import ServerFeature
|
7
8
|
from wandb.sdk.artifacts._validators import REGISTRY_PREFIX, validate_project_name
|
8
9
|
from wandb.sdk.internal.internal_api import Api as InternalApi
|
@@ -177,6 +178,7 @@ class Registry:
|
|
177
178
|
"""
|
178
179
|
self._visibility = value
|
179
180
|
|
181
|
+
@tracked
|
180
182
|
def collections(self, filter: Optional[Dict[str, Any]] = None) -> Collections:
|
181
183
|
"""Returns the collections belonging to the registry."""
|
182
184
|
registry_filter = {
|
@@ -184,6 +186,7 @@ class Registry:
|
|
184
186
|
}
|
185
187
|
return Collections(self.client, self.organization, registry_filter, filter)
|
186
188
|
|
189
|
+
@tracked
|
187
190
|
def versions(self, filter: Optional[Dict[str, Any]] = None) -> Versions:
|
188
191
|
"""Returns the versions belonging to the registry."""
|
189
192
|
registry_filter = {
|
@@ -192,6 +195,7 @@ class Registry:
|
|
192
195
|
return Versions(self.client, self.organization, registry_filter, None, filter)
|
193
196
|
|
194
197
|
@classmethod
|
198
|
+
@tracked
|
195
199
|
def create(
|
196
200
|
cls,
|
197
201
|
client: "Client",
|
@@ -256,6 +260,7 @@ class Registry:
|
|
256
260
|
response["upsertModel"]["project"],
|
257
261
|
)
|
258
262
|
|
263
|
+
@tracked
|
259
264
|
def delete(self) -> None:
|
260
265
|
"""Delete the registry. This is irreversible."""
|
261
266
|
try:
|
@@ -272,6 +277,7 @@ class Registry:
|
|
272
277
|
f"Failed to delete registry: {self.name!r} in organization: {self.organization!r}"
|
273
278
|
)
|
274
279
|
|
280
|
+
@tracked
|
275
281
|
def load(self) -> None:
|
276
282
|
"""Load the registry attributes from the backend to reflect the latest saved state."""
|
277
283
|
load_failure_message = (
|
@@ -295,6 +301,7 @@ class Registry:
|
|
295
301
|
raise ValueError(load_failure_message)
|
296
302
|
self._update_attributes(self.attrs)
|
297
303
|
|
304
|
+
@tracked
|
298
305
|
def save(self) -> None:
|
299
306
|
"""Save registry attributes to the backend."""
|
300
307
|
if not InternalApi()._server_supports(
|
@@ -9,6 +9,7 @@ from pydantic import ConfigDict, model_serializer
|
|
9
9
|
from typing_extensions import Self, TypeAlias, get_args
|
10
10
|
|
11
11
|
from wandb._pydantic import CompatBaseModel, model_validator
|
12
|
+
from wandb._strutils import nameof
|
12
13
|
|
13
14
|
from .operators import (
|
14
15
|
Contains,
|
@@ -59,7 +60,7 @@ class FilterableField:
|
|
59
60
|
return self._name
|
60
61
|
|
61
62
|
def __repr__(self) -> str:
|
62
|
-
return f"{type(self)
|
63
|
+
return f"{nameof(type(self))}({self._name!r})"
|
63
64
|
|
64
65
|
# Methods to define filter expressions through chaining
|
65
66
|
def matches_regex(self, pattern: str) -> FilterExpr:
|
@@ -145,7 +146,7 @@ class FilterExpr(CompatBaseModel, SupportsLogicalOpSyntax):
|
|
145
146
|
op: Op
|
146
147
|
|
147
148
|
def __repr__(self) -> str:
|
148
|
-
return f"{type(self)
|
149
|
+
return f"{nameof(type(self))}({self.field!s}: {self.op!r})"
|
149
150
|
|
150
151
|
def __rich_repr__(self) -> RichReprResult:
|
151
152
|
# https://rich.readthedocs.io/en/stable/pretty.html
|
@@ -8,6 +8,7 @@ from pydantic import ConfigDict, Field, StrictBool, StrictFloat, StrictInt, Stri
|
|
8
8
|
from typing_extensions import TypeAlias, get_args
|
9
9
|
|
10
10
|
from wandb._pydantic import GQLBase
|
11
|
+
from wandb._strutils import nameof
|
11
12
|
|
12
13
|
# for type annotations
|
13
14
|
Scalar = Union[StrictStr, StrictInt, StrictFloat, StrictBool]
|
@@ -62,7 +63,7 @@ class BaseOp(GQLBase, SupportsLogicalOpSyntax):
|
|
62
63
|
def __repr__(self) -> str:
|
63
64
|
# Display operand as a positional arg
|
64
65
|
values_repr = ", ".join(map(repr, self.model_dump().values()))
|
65
|
-
return f"{type(self)
|
66
|
+
return f"{nameof(type(self))}({values_repr})"
|
66
67
|
|
67
68
|
def __rich_repr__(self) -> RichReprResult:
|
68
69
|
# Display field values as positional args:
|
wandb/automations/_validators.py
CHANGED
@@ -5,13 +5,33 @@ from functools import singledispatch
|
|
5
5
|
from itertools import chain
|
6
6
|
from typing import Any, TypeVar
|
7
7
|
|
8
|
+
from pydantic import BeforeValidator, Json, PlainSerializer
|
8
9
|
from pydantic_core import PydanticUseDefault
|
10
|
+
from typing_extensions import Annotated
|
11
|
+
|
12
|
+
from wandb._pydantic import to_json
|
9
13
|
|
10
14
|
from ._filters import And, FilterExpr, In, Nor, Not, NotIn, Op, Or
|
11
15
|
|
12
16
|
T = TypeVar("T")
|
13
17
|
|
14
18
|
|
19
|
+
def ensure_json(v: Any) -> Any:
|
20
|
+
"""In case the incoming value isn't serialized JSON, reserialize it.
|
21
|
+
|
22
|
+
This lets us use `Json[...]` fields with values that are already deserialized.
|
23
|
+
"""
|
24
|
+
# NOTE: Assumes that the deserialized type is not itself a string.
|
25
|
+
# Revisit this if we need to support deserialized types that are str/bytes.
|
26
|
+
return v if isinstance(v, (str, bytes)) else to_json(v)
|
27
|
+
|
28
|
+
|
29
|
+
# Allow lenient instantiation/validation: incoming data may already be deserialized.
|
30
|
+
SerializedToJson = Annotated[
|
31
|
+
Json[T], BeforeValidator(ensure_json), PlainSerializer(to_json)
|
32
|
+
]
|
33
|
+
|
34
|
+
|
15
35
|
class LenientStrEnum(str, Enum):
|
16
36
|
"""A string enum allowing for case-insensitive lookups by value.
|
17
37
|
|
wandb/automations/actions.py
CHANGED
@@ -7,7 +7,8 @@ from typing import Any, Literal, Optional, Union
|
|
7
7
|
from pydantic import BeforeValidator, Field
|
8
8
|
from typing_extensions import Annotated, Self, get_args
|
9
9
|
|
10
|
-
from wandb._pydantic import GQLBase, GQLId,
|
10
|
+
from wandb._pydantic import GQLBase, GQLId, Typename
|
11
|
+
from wandb._strutils import nameof
|
11
12
|
|
12
13
|
from ._generated import (
|
13
14
|
AlertSeverity,
|
@@ -21,6 +22,7 @@ from ._generated import (
|
|
21
22
|
)
|
22
23
|
from ._validators import (
|
23
24
|
LenientStrEnum,
|
25
|
+
SerializedToJson,
|
24
26
|
default_if_none,
|
25
27
|
to_input_action,
|
26
28
|
to_saved_action,
|
@@ -214,5 +216,5 @@ InputActionTypes: tuple[type, ...] = get_args(InputAction.__origin__) # type: i
|
|
214
216
|
|
215
217
|
__all__ = [
|
216
218
|
"ActionType",
|
217
|
-
*(cls
|
219
|
+
*(nameof(cls) for cls in InputActionTypes),
|
218
220
|
]
|
wandb/automations/events.py
CHANGED
@@ -9,18 +9,17 @@ from typing_extensions import Annotated, Self, get_args
|
|
9
9
|
|
10
10
|
from wandb._pydantic import (
|
11
11
|
GQLBase,
|
12
|
-
SerializedToJson,
|
13
|
-
ensure_json,
|
14
12
|
field_validator,
|
15
13
|
model_validator,
|
16
14
|
pydantic_isinstance,
|
17
15
|
)
|
16
|
+
from wandb._strutils import nameof
|
18
17
|
|
19
18
|
from ._filters import And, MongoLikeFilter, Or
|
20
19
|
from ._filters.expressions import FilterableField
|
21
20
|
from ._filters.run_metrics import MetricChangeFilter, MetricThresholdFilter, MetricVal
|
22
21
|
from ._generated import FilterEventFields
|
23
|
-
from ._validators import LenientStrEnum, simplify_op
|
22
|
+
from ._validators import LenientStrEnum, SerializedToJson, ensure_json, simplify_op
|
24
23
|
from .actions import InputAction, InputActionTypes, SavedActionTypes
|
25
24
|
from .scopes import ArtifactCollectionScope, AutomationScope, ProjectScope
|
26
25
|
|
@@ -156,7 +155,7 @@ class _BaseEventInput(GQLBase):
|
|
156
155
|
if isinstance(action, (InputActionTypes, SavedActionTypes)):
|
157
156
|
return NewAutomation(event=self, action=action)
|
158
157
|
|
159
|
-
raise TypeError(f"Expected a valid action, got: {type(action)
|
158
|
+
raise TypeError(f"Expected a valid action, got: {nameof(type(action))!r}")
|
160
159
|
|
161
160
|
def __rshift__(self, other: InputAction) -> NewAutomation:
|
162
161
|
"""Implements `event >> action` to define an Automation with this event and action."""
|
@@ -277,7 +276,7 @@ OnRunMetric.model_rebuild()
|
|
277
276
|
|
278
277
|
__all__ = [
|
279
278
|
"EventType",
|
280
|
-
*(cls
|
279
|
+
*(nameof(cls) for cls in InputEventTypes),
|
281
280
|
"RunEvent",
|
282
281
|
"ArtifactEvent",
|
283
282
|
"MetricThresholdFilter",
|
wandb/bin/gpu_stats
CHANGED
Binary file
|
wandb/bin/wandb-core
CHANGED
Binary file
|
wandb/cli/beta.py
CHANGED
@@ -6,13 +6,10 @@ These commands are experimental and may change or be removed in future versions.
|
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
8
|
import pathlib
|
9
|
-
import sys
|
10
9
|
|
11
10
|
import click
|
12
11
|
|
13
|
-
import wandb
|
14
12
|
from wandb.errors import WandbCoreNotAvailableError
|
15
|
-
from wandb.sdk.wandb_sync import _sync
|
16
13
|
from wandb.util import get_core_path
|
17
14
|
|
18
15
|
|
@@ -35,141 +32,62 @@ def beta():
|
|
35
32
|
)
|
36
33
|
|
37
34
|
|
38
|
-
@beta.command(
|
39
|
-
|
40
|
-
context_settings={"default_map": {}},
|
41
|
-
help="Upload a training run to W&B",
|
42
|
-
)
|
43
|
-
@click.pass_context
|
44
|
-
@click.argument("wandb_dir", nargs=1, type=click.Path(exists=True))
|
45
|
-
@click.option("--id", "run_id", help="The run you want to upload to.")
|
46
|
-
@click.option("--project", "-p", help="The project you want to upload to.")
|
47
|
-
@click.option("--entity", "-e", help="The entity to scope to.")
|
48
|
-
@click.option("--skip-console", is_flag=True, default=False, help="Skip console logs")
|
49
|
-
@click.option("--append", is_flag=True, default=False, help="Append run")
|
50
|
-
@click.option(
|
51
|
-
"--include",
|
52
|
-
"-i",
|
53
|
-
help="Glob to include. Can be used multiple times.",
|
54
|
-
multiple=True,
|
55
|
-
)
|
35
|
+
@beta.command()
|
36
|
+
@click.argument("paths", type=click.Path(exists=True), nargs=-1)
|
56
37
|
@click.option(
|
57
|
-
"--
|
58
|
-
|
59
|
-
|
60
|
-
|
38
|
+
"--skip-synced/--no-skip-synced",
|
39
|
+
is_flag=True,
|
40
|
+
default=True,
|
41
|
+
help="Skip runs that have already been synced with this command.",
|
61
42
|
)
|
62
43
|
@click.option(
|
63
|
-
"--
|
44
|
+
"--dry-run",
|
64
45
|
is_flag=True,
|
65
|
-
default=
|
66
|
-
help="
|
46
|
+
default=False,
|
47
|
+
help="Print what would happen without uploading anything.",
|
67
48
|
)
|
68
49
|
@click.option(
|
69
|
-
"
|
50
|
+
"-v",
|
51
|
+
"--verbose",
|
70
52
|
is_flag=True,
|
71
|
-
default=
|
72
|
-
help="
|
53
|
+
default=False,
|
54
|
+
help="Print more information.",
|
73
55
|
)
|
74
56
|
@click.option(
|
75
|
-
"
|
57
|
+
"-n",
|
58
|
+
default=5,
|
59
|
+
help="Max number of runs to sync at a time.",
|
76
60
|
)
|
77
|
-
def
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
skip_console: bool = False,
|
84
|
-
append: bool = False,
|
85
|
-
include: str | None = None,
|
86
|
-
exclude: str | None = None,
|
87
|
-
skip_synced: bool = True,
|
88
|
-
mark_synced: bool = True,
|
89
|
-
dry_run: bool = False,
|
61
|
+
def sync(
|
62
|
+
paths: tuple[str, ...],
|
63
|
+
skip_synced: bool,
|
64
|
+
dry_run: bool,
|
65
|
+
verbose: bool,
|
66
|
+
n: int,
|
90
67
|
) -> None:
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
if not d.is_dir():
|
118
|
-
continue
|
119
|
-
if d in paths:
|
120
|
-
paths.remove(d)
|
121
|
-
|
122
|
-
# remove paths that are already synced, if requested
|
123
|
-
if skip_synced:
|
124
|
-
synced_paths = set()
|
125
|
-
for path in paths:
|
126
|
-
wandb_synced_files = [p for p in path.glob("*.wandb.synced") if p.is_file()]
|
127
|
-
if len(wandb_synced_files) > 1:
|
128
|
-
wandb.termwarn(
|
129
|
-
f"Multiple wandb.synced files found in directory {path}, skipping"
|
130
|
-
)
|
131
|
-
elif len(wandb_synced_files) == 1:
|
132
|
-
synced_paths.add(path)
|
133
|
-
paths -= synced_paths
|
134
|
-
|
135
|
-
if run_id and len(paths) > 1:
|
136
|
-
# TODO: handle this more gracefully
|
137
|
-
click.echo("id can only be set for a single run.", err=True)
|
138
|
-
sys.exit(1)
|
139
|
-
|
140
|
-
if not paths:
|
141
|
-
click.echo("No runs to sync.")
|
142
|
-
return
|
143
|
-
|
144
|
-
click.echo("Found runs:")
|
145
|
-
for path in paths:
|
146
|
-
click.echo(f" {path}")
|
147
|
-
|
148
|
-
if dry_run:
|
149
|
-
return
|
150
|
-
|
151
|
-
wandb.setup()
|
152
|
-
|
153
|
-
# TODO: make it thread-safe in the Rust code
|
154
|
-
with concurrent.futures.ProcessPoolExecutor(
|
155
|
-
max_workers=min(len(paths), cpu_count())
|
156
|
-
) as executor:
|
157
|
-
futures = []
|
158
|
-
for path in paths:
|
159
|
-
# we already know there is only one wandb file in the directory
|
160
|
-
wandb_file = [p for p in path.glob("*.wandb") if p.is_file()][0]
|
161
|
-
future = executor.submit(
|
162
|
-
_sync,
|
163
|
-
wandb_file,
|
164
|
-
run_id=run_id,
|
165
|
-
project=project,
|
166
|
-
entity=entity,
|
167
|
-
skip_console=skip_console,
|
168
|
-
append=append,
|
169
|
-
mark_synced=mark_synced,
|
170
|
-
)
|
171
|
-
futures.append(future)
|
172
|
-
|
173
|
-
# Wait for tasks to complete
|
174
|
-
for _ in concurrent.futures.as_completed(futures):
|
175
|
-
pass
|
68
|
+
"""Upload .wandb files specified by PATHS.
|
69
|
+
|
70
|
+
PATHS can include .wandb files, run directories containing .wandb files,
|
71
|
+
and "wandb" directories containing run directories.
|
72
|
+
|
73
|
+
For example, to sync all runs in a directory:
|
74
|
+
|
75
|
+
wandb beta sync ./wandb
|
76
|
+
|
77
|
+
To sync a specific run:
|
78
|
+
|
79
|
+
wandb beta sync ./wandb/run-20250813_124246-n67z9ude
|
80
|
+
|
81
|
+
Or equivalently:
|
82
|
+
|
83
|
+
wandb beta sync ./wandb/run-20250813_124246-n67z9ude/run-n67z9ude.wandb
|
84
|
+
"""
|
85
|
+
from . import beta_sync
|
86
|
+
|
87
|
+
beta_sync.sync(
|
88
|
+
[pathlib.Path(path) for path in paths],
|
89
|
+
dry_run=dry_run,
|
90
|
+
skip_synced=skip_synced,
|
91
|
+
verbose=verbose,
|
92
|
+
parallelism=n,
|
93
|
+
)
|
wandb/cli/beta_sync.py
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
"""Implements `wandb sync` using wandb-core."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
import pathlib
|
7
|
+
import time
|
8
|
+
from itertools import filterfalse
|
9
|
+
from typing import Iterable, Iterator
|
10
|
+
|
11
|
+
import click
|
12
|
+
|
13
|
+
import wandb
|
14
|
+
from wandb.proto.wandb_sync_pb2 import ServerSyncResponse
|
15
|
+
from wandb.sdk import wandb_setup
|
16
|
+
from wandb.sdk.lib import asyncio_compat
|
17
|
+
from wandb.sdk.lib.printer import ERROR, Printer, new_printer
|
18
|
+
from wandb.sdk.lib.progress import progress_printer
|
19
|
+
from wandb.sdk.lib.service.service_connection import ServiceConnection
|
20
|
+
from wandb.sdk.mailbox.mailbox_handle import MailboxHandle
|
21
|
+
|
22
|
+
_MAX_LIST_LINES = 20
|
23
|
+
_POLL_WAIT_SECONDS = 0.1
|
24
|
+
_SLEEP = asyncio.sleep # patched in tests
|
25
|
+
|
26
|
+
|
27
|
+
def sync(
|
28
|
+
paths: list[pathlib.Path],
|
29
|
+
*,
|
30
|
+
dry_run: bool,
|
31
|
+
skip_synced: bool,
|
32
|
+
verbose: bool,
|
33
|
+
parallelism: int,
|
34
|
+
) -> None:
|
35
|
+
"""Replay one or more .wandb files.
|
36
|
+
|
37
|
+
Args:
|
38
|
+
paths: One or more .wandb files, run directories containing
|
39
|
+
.wandb files, and wandb directories containing run directories.
|
40
|
+
dry_run: If true, just prints what it would do and exits.
|
41
|
+
skip_synced: If true, skips files that have already been synced
|
42
|
+
as indicated by a .wandb.synced marker file in the same directory.
|
43
|
+
verbose: Verbose mode for printing more info.
|
44
|
+
parallelism: Max number of runs to sync at a time.
|
45
|
+
"""
|
46
|
+
wandb_files: set[pathlib.Path] = set()
|
47
|
+
for path in paths:
|
48
|
+
for wandb_file in _find_wandb_files(path, skip_synced=skip_synced):
|
49
|
+
wandb_files.add(wandb_file.resolve())
|
50
|
+
|
51
|
+
if not wandb_files:
|
52
|
+
click.echo("No files to sync.")
|
53
|
+
return
|
54
|
+
|
55
|
+
if dry_run:
|
56
|
+
click.echo(f"Would sync {len(wandb_files)} file(s):")
|
57
|
+
_print_sorted_paths(wandb_files, verbose=verbose)
|
58
|
+
return
|
59
|
+
|
60
|
+
click.echo(f"Syncing {len(wandb_files)} file(s):")
|
61
|
+
_print_sorted_paths(wandb_files, verbose=verbose)
|
62
|
+
|
63
|
+
singleton = wandb_setup.singleton()
|
64
|
+
service = singleton.ensure_service()
|
65
|
+
printer = new_printer()
|
66
|
+
singleton.asyncer.run(
|
67
|
+
lambda: _do_sync(
|
68
|
+
wandb_files,
|
69
|
+
service=service,
|
70
|
+
settings=singleton.settings,
|
71
|
+
printer=printer,
|
72
|
+
parallelism=parallelism,
|
73
|
+
)
|
74
|
+
)
|
75
|
+
|
76
|
+
|
77
|
+
async def _do_sync(
|
78
|
+
wandb_files: set[pathlib.Path],
|
79
|
+
*,
|
80
|
+
service: ServiceConnection,
|
81
|
+
settings: wandb.Settings,
|
82
|
+
printer: Printer,
|
83
|
+
parallelism: int,
|
84
|
+
) -> None:
|
85
|
+
"""Sync the specified files.
|
86
|
+
|
87
|
+
This is factored out to make the progress animation testable.
|
88
|
+
"""
|
89
|
+
init_result = await service.init_sync(
|
90
|
+
wandb_files,
|
91
|
+
settings,
|
92
|
+
).wait_async(timeout=5)
|
93
|
+
|
94
|
+
sync_handle = service.sync(init_result.id, parallelism=parallelism)
|
95
|
+
|
96
|
+
await _SyncStatusLoop(
|
97
|
+
init_result.id,
|
98
|
+
service,
|
99
|
+
printer,
|
100
|
+
).wait_with_progress(sync_handle)
|
101
|
+
|
102
|
+
|
103
|
+
class _SyncStatusLoop:
|
104
|
+
"""Displays a sync operation's status until it completes."""
|
105
|
+
|
106
|
+
def __init__(
|
107
|
+
self,
|
108
|
+
id: str,
|
109
|
+
service: ServiceConnection,
|
110
|
+
printer: Printer,
|
111
|
+
) -> None:
|
112
|
+
self._id = id
|
113
|
+
self._service = service
|
114
|
+
self._printer = printer
|
115
|
+
|
116
|
+
self._rate_limit_last_time: float | None = None
|
117
|
+
self._done = asyncio.Event()
|
118
|
+
|
119
|
+
async def wait_with_progress(
|
120
|
+
self,
|
121
|
+
handle: MailboxHandle[ServerSyncResponse],
|
122
|
+
) -> None:
|
123
|
+
"""Display status updates until the handle completes."""
|
124
|
+
async with asyncio_compat.open_task_group() as group:
|
125
|
+
group.start_soon(self._wait_then_mark_done(handle))
|
126
|
+
group.start_soon(self._show_progress_until_done())
|
127
|
+
|
128
|
+
async def _wait_then_mark_done(
|
129
|
+
self,
|
130
|
+
handle: MailboxHandle[ServerSyncResponse],
|
131
|
+
) -> None:
|
132
|
+
response = await handle.wait_async(timeout=None)
|
133
|
+
if messages := list(response.errors):
|
134
|
+
self._printer.display(messages, level=ERROR)
|
135
|
+
self._done.set()
|
136
|
+
|
137
|
+
async def _show_progress_until_done(self) -> None:
|
138
|
+
"""Show rate-limited status updates until _done is set."""
|
139
|
+
with progress_printer(self._printer, "Syncing...") as progress:
|
140
|
+
while not await self._rate_limit_check_done():
|
141
|
+
handle = self._service.sync_status(self._id)
|
142
|
+
response = await handle.wait_async(timeout=None)
|
143
|
+
|
144
|
+
if messages := list(response.new_errors):
|
145
|
+
self._printer.display(messages, level=ERROR)
|
146
|
+
progress.update(response.stats)
|
147
|
+
|
148
|
+
async def _rate_limit_check_done(self) -> bool:
|
149
|
+
"""Wait for rate limit and return whether _done is set."""
|
150
|
+
now = time.monotonic()
|
151
|
+
last_time = self._rate_limit_last_time
|
152
|
+
self._rate_limit_last_time = now
|
153
|
+
|
154
|
+
if last_time and (time_since_last := now - last_time) < _POLL_WAIT_SECONDS:
|
155
|
+
await asyncio_compat.race(
|
156
|
+
_SLEEP(_POLL_WAIT_SECONDS - time_since_last),
|
157
|
+
self._done.wait(),
|
158
|
+
)
|
159
|
+
|
160
|
+
return self._done.is_set()
|
161
|
+
|
162
|
+
|
163
|
+
def _find_wandb_files(
|
164
|
+
path: pathlib.Path,
|
165
|
+
*,
|
166
|
+
skip_synced: bool,
|
167
|
+
) -> Iterator[pathlib.Path]:
|
168
|
+
"""Returns paths to the .wandb files to sync."""
|
169
|
+
if skip_synced:
|
170
|
+
yield from filterfalse(_is_synced, _expand_wandb_files(path))
|
171
|
+
else:
|
172
|
+
yield from _expand_wandb_files(path)
|
173
|
+
|
174
|
+
|
175
|
+
def _expand_wandb_files(
|
176
|
+
path: pathlib.Path,
|
177
|
+
) -> Iterator[pathlib.Path]:
|
178
|
+
"""Iterate over .wandb files selected by the path."""
|
179
|
+
if path.suffix == ".wandb":
|
180
|
+
yield path
|
181
|
+
return
|
182
|
+
|
183
|
+
files_in_run_directory = path.glob("*.wandb")
|
184
|
+
try:
|
185
|
+
first_file = next(files_in_run_directory)
|
186
|
+
except StopIteration:
|
187
|
+
pass
|
188
|
+
else:
|
189
|
+
yield first_file
|
190
|
+
yield from files_in_run_directory
|
191
|
+
return
|
192
|
+
|
193
|
+
yield from path.glob("*/*.wandb")
|
194
|
+
|
195
|
+
|
196
|
+
def _is_synced(path: pathlib.Path) -> bool:
|
197
|
+
"""Returns whether the .wandb file is synced."""
|
198
|
+
return path.with_suffix(".wandb.synced").exists()
|
199
|
+
|
200
|
+
|
201
|
+
def _print_sorted_paths(paths: Iterable[pathlib.Path], verbose: bool) -> None:
|
202
|
+
"""Print file paths, sorting them and truncating the list if needed.
|
203
|
+
|
204
|
+
Args:
|
205
|
+
paths: Paths to print. Must be absolute with symlinks resolved.
|
206
|
+
verbose: If true, doesn't truncate paths.
|
207
|
+
"""
|
208
|
+
# Prefer to print paths relative to the current working directory.
|
209
|
+
cwd = pathlib.Path(".").resolve()
|
210
|
+
formatted_paths: list[str] = []
|
211
|
+
for path in paths:
|
212
|
+
try:
|
213
|
+
formatted_path = str(path.relative_to(cwd))
|
214
|
+
except ValueError:
|
215
|
+
formatted_path = str(path)
|
216
|
+
formatted_paths.append(formatted_path)
|
217
|
+
|
218
|
+
sorted_paths = sorted(formatted_paths)
|
219
|
+
max_lines = len(sorted_paths) if verbose else _MAX_LIST_LINES
|
220
|
+
|
221
|
+
for i in range(min(len(sorted_paths), max_lines)):
|
222
|
+
click.echo(f" {sorted_paths[i]}")
|
223
|
+
|
224
|
+
if len(sorted_paths) > max_lines:
|
225
|
+
remaining = len(sorted_paths) - max_lines
|
226
|
+
click.echo(f" +{remaining:,d} more (pass --verbose to see all)")
|