wandb 0.21.3__py3-none-win32.whl → 0.21.4__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 (60) hide show
  1. wandb/__init__.py +1 -1
  2. wandb/__init__.pyi +1 -1
  3. wandb/_analytics.py +65 -0
  4. wandb/_iterutils.py +8 -0
  5. wandb/_pydantic/__init__.py +10 -11
  6. wandb/_pydantic/base.py +3 -53
  7. wandb/_pydantic/field_types.py +29 -0
  8. wandb/_pydantic/v1_compat.py +47 -30
  9. wandb/_strutils.py +40 -0
  10. wandb/apis/public/api.py +17 -4
  11. wandb/apis/public/artifacts.py +5 -4
  12. wandb/apis/public/automations.py +2 -1
  13. wandb/apis/public/registries/_freezable_list.py +6 -6
  14. wandb/apis/public/registries/_utils.py +2 -1
  15. wandb/apis/public/registries/registries_search.py +4 -0
  16. wandb/apis/public/registries/registry.py +7 -0
  17. wandb/automations/_filters/expressions.py +3 -2
  18. wandb/automations/_filters/operators.py +2 -1
  19. wandb/automations/_validators.py +20 -0
  20. wandb/automations/actions.py +4 -2
  21. wandb/automations/events.py +4 -5
  22. wandb/bin/gpu_stats.exe +0 -0
  23. wandb/bin/wandb-core +0 -0
  24. wandb/cli/beta.py +48 -130
  25. wandb/cli/beta_sync.py +226 -0
  26. wandb/integration/dspy/__init__.py +5 -0
  27. wandb/integration/dspy/dspy.py +422 -0
  28. wandb/integration/weave/weave.py +55 -0
  29. wandb/proto/v3/wandb_server_pb2.py +38 -57
  30. wandb/proto/v3/wandb_sync_pb2.py +87 -0
  31. wandb/proto/v3/wandb_telemetry_pb2.py +12 -12
  32. wandb/proto/v4/wandb_server_pb2.py +38 -41
  33. wandb/proto/v4/wandb_sync_pb2.py +38 -0
  34. wandb/proto/v4/wandb_telemetry_pb2.py +12 -12
  35. wandb/proto/v5/wandb_server_pb2.py +38 -41
  36. wandb/proto/v5/wandb_sync_pb2.py +39 -0
  37. wandb/proto/v5/wandb_telemetry_pb2.py +12 -12
  38. wandb/proto/v6/wandb_server_pb2.py +38 -41
  39. wandb/proto/v6/wandb_sync_pb2.py +49 -0
  40. wandb/proto/v6/wandb_telemetry_pb2.py +12 -12
  41. wandb/proto/wandb_generate_proto.py +1 -0
  42. wandb/proto/wandb_sync_pb2.py +12 -0
  43. wandb/sdk/artifacts/_validators.py +50 -49
  44. wandb/sdk/artifacts/artifact.py +7 -7
  45. wandb/sdk/artifacts/exceptions.py +2 -1
  46. wandb/sdk/artifacts/storage_handlers/s3_handler.py +2 -1
  47. wandb/sdk/lib/asyncio_compat.py +88 -23
  48. wandb/sdk/lib/gql_request.py +18 -7
  49. wandb/sdk/lib/printer.py +9 -13
  50. wandb/sdk/lib/progress.py +8 -6
  51. wandb/sdk/lib/service/service_connection.py +42 -12
  52. wandb/sdk/mailbox/wait_with_progress.py +1 -1
  53. wandb/sdk/wandb_init.py +0 -8
  54. wandb/sdk/wandb_run.py +13 -1
  55. wandb/sdk/wandb_settings.py +55 -0
  56. {wandb-0.21.3.dist-info → wandb-0.21.4.dist-info}/METADATA +1 -1
  57. {wandb-0.21.3.dist-info → wandb-0.21.4.dist-info}/RECORD +60 -49
  58. {wandb-0.21.3.dist-info → wandb-0.21.4.dist-info}/WHEEL +0 -0
  59. {wandb-0.21.3.dist-info → wandb-0.21.4.dist-info}/entry_points.txt +0 -0
  60. {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).__name__}({self._name!r})"
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).__name__}({self.field!s}: {self.op!r})"
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).__name__}({values_repr})"
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:
@@ -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
 
@@ -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, SerializedToJson, Typename
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.__name__ for cls in InputActionTypes),
219
+ *(nameof(cls) for cls in InputActionTypes),
218
220
  ]
@@ -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).__qualname__!r}")
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.__name__ for cls in InputEventTypes),
279
+ *(nameof(cls) for cls in InputEventTypes),
281
280
  "RunEvent",
282
281
  "ArtifactEvent",
283
282
  "MetricThresholdFilter",
wandb/bin/gpu_stats.exe 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
- name="sync",
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
- "--exclude",
58
- "-e",
59
- help="Glob to exclude. Can be used multiple times.",
60
- multiple=True,
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
- "--mark-synced/--no-mark-synced",
44
+ "--dry-run",
64
45
  is_flag=True,
65
- default=True,
66
- help="Mark runs as synced",
46
+ default=False,
47
+ help="Print what would happen without uploading anything.",
67
48
  )
68
49
  @click.option(
69
- "--skip-synced/--no-skip-synced",
50
+ "-v",
51
+ "--verbose",
70
52
  is_flag=True,
71
- default=True,
72
- help="Skip synced runs",
53
+ default=False,
54
+ help="Print more information.",
73
55
  )
74
56
  @click.option(
75
- "--dry-run", is_flag=True, help="Perform a dry run without uploading anything."
57
+ "-n",
58
+ default=5,
59
+ help="Max number of runs to sync at a time.",
76
60
  )
77
- def sync_beta( # noqa: C901
78
- ctx,
79
- wandb_dir=None,
80
- run_id: str | None = None,
81
- project: str | None = None,
82
- entity: str | None = None,
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
- import concurrent.futures
92
- from multiprocessing import cpu_count
93
-
94
- paths = set()
95
-
96
- # TODO: test file discovery logic
97
- # include and exclude globs are evaluated relative to the provided base_path
98
- if include:
99
- for pattern in include:
100
- matching_dirs = list(pathlib.Path(wandb_dir).glob(pattern))
101
- for d in matching_dirs:
102
- if not d.is_dir():
103
- continue
104
- wandb_files = [p for p in d.glob("*.wandb") if p.is_file()]
105
- if len(wandb_files) > 1:
106
- wandb.termwarn(
107
- f"Multiple wandb files found in directory {d}, skipping"
108
- )
109
- elif len(wandb_files) == 1:
110
- paths.add(d)
111
- else:
112
- paths.update({p.parent for p in pathlib.Path(wandb_dir).glob("**/*.wandb")})
113
-
114
- for pattern in exclude:
115
- matching_dirs = list(pathlib.Path(wandb_dir).glob(pattern))
116
- for d in matching_dirs:
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)")
@@ -0,0 +1,5 @@
1
+ """W&B DSPy integration package."""
2
+
3
+ from .dspy import WandbDSPyCallback
4
+
5
+ __all__ = ["WandbDSPyCallback"]