wandb 0.21.3__py3-none-win32.whl → 0.22.0__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 (76) 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/__init__.py +42 -0
  11. wandb/apis/public/api.py +17 -4
  12. wandb/apis/public/artifacts.py +5 -4
  13. wandb/apis/public/automations.py +2 -1
  14. wandb/apis/public/registries/_freezable_list.py +6 -6
  15. wandb/apis/public/registries/_utils.py +2 -1
  16. wandb/apis/public/registries/registries_search.py +4 -0
  17. wandb/apis/public/registries/registry.py +7 -0
  18. wandb/apis/public/runs.py +24 -6
  19. wandb/automations/_filters/expressions.py +3 -2
  20. wandb/automations/_filters/operators.py +2 -1
  21. wandb/automations/_validators.py +20 -0
  22. wandb/automations/actions.py +4 -2
  23. wandb/automations/events.py +4 -5
  24. wandb/bin/gpu_stats.exe +0 -0
  25. wandb/bin/wandb-core +0 -0
  26. wandb/cli/beta.py +48 -130
  27. wandb/cli/beta_sync.py +226 -0
  28. wandb/integration/dspy/__init__.py +5 -0
  29. wandb/integration/dspy/dspy.py +422 -0
  30. wandb/integration/weave/weave.py +55 -0
  31. wandb/proto/v3/wandb_internal_pb2.py +234 -224
  32. wandb/proto/v3/wandb_server_pb2.py +38 -57
  33. wandb/proto/v3/wandb_sync_pb2.py +87 -0
  34. wandb/proto/v3/wandb_telemetry_pb2.py +12 -12
  35. wandb/proto/v4/wandb_internal_pb2.py +226 -224
  36. wandb/proto/v4/wandb_server_pb2.py +38 -41
  37. wandb/proto/v4/wandb_sync_pb2.py +38 -0
  38. wandb/proto/v4/wandb_telemetry_pb2.py +12 -12
  39. wandb/proto/v5/wandb_internal_pb2.py +226 -224
  40. wandb/proto/v5/wandb_server_pb2.py +38 -41
  41. wandb/proto/v5/wandb_sync_pb2.py +39 -0
  42. wandb/proto/v5/wandb_telemetry_pb2.py +12 -12
  43. wandb/proto/v6/wandb_base_pb2.py +3 -3
  44. wandb/proto/v6/wandb_internal_pb2.py +229 -227
  45. wandb/proto/v6/wandb_server_pb2.py +41 -44
  46. wandb/proto/v6/wandb_settings_pb2.py +3 -3
  47. wandb/proto/v6/wandb_sync_pb2.py +49 -0
  48. wandb/proto/v6/wandb_telemetry_pb2.py +15 -15
  49. wandb/proto/wandb_generate_proto.py +1 -0
  50. wandb/proto/wandb_sync_pb2.py +12 -0
  51. wandb/sdk/artifacts/_validators.py +50 -49
  52. wandb/sdk/artifacts/artifact.py +7 -7
  53. wandb/sdk/artifacts/exceptions.py +2 -1
  54. wandb/sdk/artifacts/storage_handlers/gcs_handler.py +1 -1
  55. wandb/sdk/artifacts/storage_handlers/http_handler.py +1 -3
  56. wandb/sdk/artifacts/storage_handlers/local_file_handler.py +1 -1
  57. wandb/sdk/artifacts/storage_handlers/s3_handler.py +3 -2
  58. wandb/sdk/artifacts/storage_policies/_factories.py +63 -0
  59. wandb/sdk/artifacts/storage_policies/wandb_storage_policy.py +59 -124
  60. wandb/sdk/interface/interface.py +10 -0
  61. wandb/sdk/interface/interface_shared.py +9 -0
  62. wandb/sdk/lib/asyncio_compat.py +88 -23
  63. wandb/sdk/lib/gql_request.py +18 -7
  64. wandb/sdk/lib/printer.py +9 -13
  65. wandb/sdk/lib/progress.py +8 -6
  66. wandb/sdk/lib/service/service_connection.py +42 -12
  67. wandb/sdk/mailbox/wait_with_progress.py +1 -1
  68. wandb/sdk/wandb_init.py +9 -9
  69. wandb/sdk/wandb_run.py +13 -1
  70. wandb/sdk/wandb_settings.py +55 -0
  71. wandb/wandb_agent.py +35 -4
  72. {wandb-0.21.3.dist-info → wandb-0.22.0.dist-info}/METADATA +1 -1
  73. {wandb-0.21.3.dist-info → wandb-0.22.0.dist-info}/RECORD +76 -64
  74. {wandb-0.21.3.dist-info → wandb-0.22.0.dist-info}/WHEEL +0 -0
  75. {wandb-0.21.3.dist-info → wandb-0.22.0.dist-info}/entry_points.txt +0 -0
  76. {wandb-0.21.3.dist-info → wandb-0.22.0.dist-info}/licenses/LICENSE +0 -0
@@ -4,6 +4,7 @@ from enum import Enum
4
4
  from functools import lru_cache
5
5
  from typing import TYPE_CHECKING, Any, Literal, Mapping, Sequence
6
6
 
7
+ from wandb._strutils import ensureprefix
7
8
  from wandb.sdk.artifacts._validators import (
8
9
  REGISTRY_PREFIX,
9
10
  validate_artifact_types_list,
@@ -81,7 +82,7 @@ def ensure_registry_prefix_on_names(query: Any, in_name: bool = False) -> Any:
81
82
  """
82
83
  if isinstance((txt := query), str):
83
84
  if in_name:
84
- return txt if txt.startswith(REGISTRY_PREFIX) else f"{REGISTRY_PREFIX}{txt}"
85
+ return ensureprefix(txt, REGISTRY_PREFIX)
85
86
  return txt
86
87
  if isinstance((dct := query), Mapping):
87
88
  new_dict = {}
@@ -9,6 +9,7 @@ from pydantic import ValidationError
9
9
  from typing_extensions import override
10
10
  from wandb_gql import gql
11
11
 
12
+ from wandb._analytics import tracked
12
13
  from wandb.apis.paginator import Paginator
13
14
  from wandb.apis.public.utils import gql_compat
14
15
  from wandb.sdk.artifacts._generated import (
@@ -69,6 +70,7 @@ class Registries(Paginator):
69
70
  raise StopIteration
70
71
  return self.objects[self.index]
71
72
 
73
+ @tracked
72
74
  def collections(self, filter: dict[str, Any] | None = None) -> Collections:
73
75
  return Collections(
74
76
  client=self.client,
@@ -77,6 +79,7 @@ class Registries(Paginator):
77
79
  collection_filter=filter,
78
80
  )
79
81
 
82
+ @tracked
80
83
  def versions(self, filter: dict[str, Any] | None = None) -> Versions:
81
84
  return Versions(
82
85
  client=self.client,
@@ -177,6 +180,7 @@ class Collections(Paginator["ArtifactCollection"]):
177
180
  raise StopIteration
178
181
  return self.objects[self.index]
179
182
 
183
+ @tracked
180
184
  def versions(self, filter: dict[str, Any] | None = None) -> Versions:
181
185
  return Versions(
182
186
  client=self.client,
@@ -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(
wandb/apis/public/runs.py CHANGED
@@ -88,10 +88,28 @@ RUN_FRAGMENT = """fragment RunFragment on Run {
88
88
 
89
89
  @normalize_exceptions
90
90
  def _server_provides_internal_id_for_project(client) -> bool:
91
- """Returns True if the server allows us to query the internalId field for a project.
92
-
93
- This check is done by utilizing GraphQL introspection in the available fields on the Project type.
91
+ """Returns True if the server allows us to query the internalId field for a project."""
92
+ query_string = """
93
+ query ProbeProjectInput {
94
+ ProjectType: __type(name:"Project") {
95
+ fields {
96
+ name
97
+ }
98
+ }
99
+ }
94
100
  """
101
+
102
+ # Only perform the query once to avoid extra network calls
103
+ query = gql(query_string)
104
+ res = client.execute(query)
105
+ return "internalId" in [
106
+ x["name"] for x in (res.get("ProjectType", {}).get("fields", [{}]))
107
+ ]
108
+
109
+
110
+ @normalize_exceptions
111
+ def _server_provides_project_id_for_run(client) -> bool:
112
+ """Returns True if the server allows us to query the projectId field for a run."""
95
113
  query_string = """
96
114
  query ProbeRunInput {
97
115
  RunType: __type(name:"Run") {
@@ -209,13 +227,13 @@ class Runs(SizedPaginator["Run"]):
209
227
  f"""#graphql
210
228
  query Runs($project: String!, $entity: String!, $cursor: String, $perPage: Int = 50, $order: String, $filters: JSONString) {{
211
229
  project(name: $project, entityName: $entity) {{
212
- internalId
230
+ {"internalId" if _server_provides_internal_id_for_project(client) else ""}
213
231
  runCount(filters: $filters)
214
232
  readOnly
215
233
  runs(filters: $filters, after: $cursor, first: $perPage, order: $order) {{
216
234
  edges {{
217
235
  node {{
218
- {"projectId" if _server_provides_internal_id_for_project(client) else ""}
236
+ {"projectId" if _server_provides_project_id_for_run(client) else ""}
219
237
  ...RunFragment
220
238
  }}
221
239
  cursor
@@ -603,7 +621,7 @@ class Run(Attrs):
603
621
  query Run($project: String!, $entity: String!, $name: String!) {{
604
622
  project(name: $project, entityName: $entity) {{
605
623
  run(name: $name) {{
606
- {"projectId" if _server_provides_internal_id_for_project(self.client) else ""}
624
+ {"projectId" if _server_provides_project_id_for_run(self.client) else ""}
607
625
  ...RunFragment
608
626
  }}
609
627
  }}
@@ -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
+ )