wandb 0.21.2__py3-none-macosx_12_0_arm64.whl → 0.21.4__py3-none-macosx_12_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.
Files changed (68) 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 +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/cli/cli.py +1 -1
  27. wandb/integration/dspy/__init__.py +5 -0
  28. wandb/integration/dspy/dspy.py +422 -0
  29. wandb/integration/weave/weave.py +55 -0
  30. wandb/proto/v3/wandb_server_pb2.py +38 -57
  31. wandb/proto/v3/wandb_sync_pb2.py +87 -0
  32. wandb/proto/v3/wandb_telemetry_pb2.py +12 -12
  33. wandb/proto/v4/wandb_server_pb2.py +38 -41
  34. wandb/proto/v4/wandb_sync_pb2.py +38 -0
  35. wandb/proto/v4/wandb_telemetry_pb2.py +12 -12
  36. wandb/proto/v5/wandb_server_pb2.py +38 -41
  37. wandb/proto/v5/wandb_sync_pb2.py +39 -0
  38. wandb/proto/v5/wandb_telemetry_pb2.py +12 -12
  39. wandb/proto/v6/wandb_server_pb2.py +38 -41
  40. wandb/proto/v6/wandb_sync_pb2.py +49 -0
  41. wandb/proto/v6/wandb_telemetry_pb2.py +12 -12
  42. wandb/proto/wandb_generate_proto.py +1 -0
  43. wandb/proto/wandb_sync_pb2.py +12 -0
  44. wandb/sdk/artifacts/_validators.py +50 -49
  45. wandb/sdk/artifacts/artifact.py +11 -11
  46. wandb/sdk/artifacts/artifact_file_cache.py +1 -1
  47. wandb/sdk/artifacts/artifact_manifest_entry.py +6 -8
  48. wandb/sdk/artifacts/exceptions.py +2 -1
  49. wandb/sdk/artifacts/storage_handlers/gcs_handler.py +1 -1
  50. wandb/sdk/artifacts/storage_handlers/s3_handler.py +2 -1
  51. wandb/sdk/launch/inputs/internal.py +25 -24
  52. wandb/sdk/launch/inputs/schema.py +31 -1
  53. wandb/sdk/lib/asyncio_compat.py +88 -23
  54. wandb/sdk/lib/gql_request.py +18 -7
  55. wandb/sdk/lib/paths.py +23 -21
  56. wandb/sdk/lib/printer.py +9 -13
  57. wandb/sdk/lib/progress.py +8 -6
  58. wandb/sdk/lib/service/service_connection.py +42 -12
  59. wandb/sdk/mailbox/wait_with_progress.py +1 -1
  60. wandb/sdk/wandb_init.py +0 -8
  61. wandb/sdk/wandb_run.py +14 -2
  62. wandb/sdk/wandb_settings.py +55 -0
  63. wandb/sdk/wandb_setup.py +2 -2
  64. {wandb-0.21.2.dist-info → wandb-0.21.4.dist-info}/METADATA +2 -2
  65. {wandb-0.21.2.dist-info → wandb-0.21.4.dist-info}/RECORD +68 -57
  66. {wandb-0.21.2.dist-info → wandb-0.21.4.dist-info}/WHEEL +0 -0
  67. {wandb-0.21.2.dist-info → wandb-0.21.4.dist-info}/entry_points.txt +0 -0
  68. {wandb-0.21.2.dist-info → wandb-0.21.4.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.21.2"
13
+ __version__ = "0.21.4"
14
14
 
15
15
 
16
16
  from wandb.errors import Error
wandb/__init__.pyi CHANGED
@@ -107,7 +107,7 @@ if TYPE_CHECKING:
107
107
  import wandb
108
108
  from wandb.plot import CustomChart
109
109
 
110
- __version__: str = "0.21.2"
110
+ __version__: str = "0.21.4"
111
111
 
112
112
  run: Run | None
113
113
  config: wandb_config.Config
wandb/_analytics.py ADDED
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from contextvars import ContextVar
4
+ from dataclasses import dataclass, field
5
+ from functools import wraps
6
+ from typing import Callable, Final, TypeVar
7
+ from uuid import UUID, uuid4
8
+
9
+ from typing_extensions import ParamSpec
10
+
11
+ from wandb._strutils import nameof
12
+
13
+ P = ParamSpec("P")
14
+ R = TypeVar("R")
15
+
16
+ # Header keys for tracking the calling function
17
+ X_WANDB_PYTHON_FUNC: Final[str] = "X-Wandb-Python-Func"
18
+ X_WANDB_PYTHON_CALL_ID: Final[str] = "X-Wandb-Python-Call-Id"
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class TrackedFuncInfo:
23
+ func: str
24
+ """The fully qualified namespace of the tracked function."""
25
+
26
+ call_id: UUID = field(default_factory=uuid4)
27
+ """A unique identifier assigned to each invocation."""
28
+
29
+ def to_headers(self) -> dict[str, str]:
30
+ return {
31
+ X_WANDB_PYTHON_FUNC: self.func,
32
+ X_WANDB_PYTHON_CALL_ID: str(self.call_id),
33
+ }
34
+
35
+
36
+ _current_func: ContextVar[TrackedFuncInfo] = ContextVar("_current_func")
37
+ """An internal, threadsafe context variable to hold the current function being tracked."""
38
+
39
+
40
+ def tracked(func: Callable[P, R]) -> Callable[P, R]:
41
+ """A decorator to inject the calling function name into any GraphQL request headers.
42
+
43
+ If a tracked function calls another tracked function, only the outermost function in
44
+ the call stack will be tracked.
45
+ """
46
+ func_namespace = f"{func.__module__}.{nameof(func)}"
47
+
48
+ @wraps(func)
49
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
50
+ # Don't override the current tracked function if it's already set
51
+ if tracked_func():
52
+ return func(*args, **kwargs)
53
+
54
+ token = _current_func.set(TrackedFuncInfo(func=func_namespace))
55
+ try:
56
+ return func(*args, **kwargs)
57
+ finally:
58
+ _current_func.reset(token)
59
+
60
+ return wrapper
61
+
62
+
63
+ def tracked_func() -> TrackedFuncInfo | None:
64
+ """Returns info on the current tracked function, if any, otherwise None."""
65
+ return _current_func.get(None)
wandb/_iterutils.py CHANGED
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Hashable
3
4
  from typing import TYPE_CHECKING, Any, Iterable, TypeVar, Union, overload
4
5
 
5
6
  if TYPE_CHECKING:
6
7
  T = TypeVar("T")
8
+ HashableT = TypeVar("HashableT", bound=Hashable)
7
9
  ClassInfo = Union[type[T], tuple[type[T], ...]]
8
10
 
9
11
 
@@ -22,6 +24,12 @@ def always_list(obj: Any, base_type: Any = (str, bytes)) -> list[T]:
22
24
  return [obj] if isinstance(obj, base_type) else list(obj)
23
25
 
24
26
 
27
+ def unique_list(iterable: Iterable[HashableT]) -> list[HashableT]:
28
+ """Return a deduplicated list of items from the given iterable, preserving order."""
29
+ # Trick for O(1) uniqueness check that maintains order
30
+ return list(dict.fromkeys(iterable))
31
+
32
+
25
33
  def one(
26
34
  iterable: Iterable[T],
27
35
  too_short: type[Exception] | Exception | None = None,
@@ -1,15 +1,15 @@
1
1
  """Internal utilities for working with pydantic."""
2
2
 
3
- from .base import (
4
- CompatBaseModel,
5
- GQLBase,
6
- GQLId,
7
- SerializedToJson,
8
- Typename,
9
- ensure_json,
10
- )
3
+ from .base import CompatBaseModel, GQLBase
4
+ from .field_types import GQLId, Typename
11
5
  from .utils import IS_PYDANTIC_V2, from_json, gql_typename, pydantic_isinstance, to_json
12
- from .v1_compat import AliasChoices, computed_field, field_validator, model_validator
6
+ from .v1_compat import (
7
+ AliasChoices,
8
+ computed_field,
9
+ field_validator,
10
+ model_validator,
11
+ to_camel,
12
+ )
13
13
 
14
14
  __all__ = [
15
15
  "IS_PYDANTIC_V2",
@@ -17,14 +17,13 @@ __all__ = [
17
17
  "GQLBase",
18
18
  "Typename",
19
19
  "GQLId",
20
- "SerializedToJson",
21
20
  "AliasChoices",
22
21
  "computed_field",
23
22
  "field_validator",
24
23
  "model_validator",
25
24
  "pydantic_isinstance",
25
+ "to_camel",
26
26
  "to_json",
27
27
  "from_json",
28
- "ensure_json",
29
28
  "gql_typename",
30
29
  ]
wandb/_pydantic/base.py CHANGED
@@ -2,12 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar
5
+ from typing import TYPE_CHECKING, Any, Callable, Literal
6
6
 
7
- from pydantic import BaseModel, ConfigDict, Field, Json, StrictStr
8
- from typing_extensions import Annotated, TypedDict, Unpack, override
7
+ from pydantic import BaseModel, ConfigDict
8
+ from typing_extensions import TypedDict, Unpack, override
9
9
 
10
- from .utils import IS_PYDANTIC_V2, to_json
11
10
  from .v1_compat import PydanticCompatMixin
12
11
 
13
12
  if TYPE_CHECKING:
@@ -77,52 +76,3 @@ class GQLBase(CompatBaseModel):
77
76
  ) -> str:
78
77
  kwargs = {**MODEL_DUMP_DEFAULTS, **kwargs}
79
78
  return super().model_dump_json(indent=indent, **kwargs)
80
-
81
-
82
- # ------------------------------------------------------------------------------
83
- # Reusable annotations for field types
84
- T = TypeVar("T")
85
-
86
- if IS_PYDANTIC_V2 or TYPE_CHECKING:
87
- GQLId = Annotated[
88
- StrictStr,
89
- Field(repr=False, frozen=True),
90
- ]
91
- else:
92
- # FIXME: Find a way to fix this for pydantic v1, which doesn't like when
93
- # `Field(...)` used in the field assignment AND `Annotated[...]`.
94
- # This is a problem for codegen, which can currently outputs e.g.
95
- #
96
- # class MyModel(GQLBase):
97
- # my_id: GQLId = Field(alias="myID")
98
- #
99
- GQLId = StrictStr # type: ignore[misc]
100
-
101
- Typename = Annotated[
102
- T,
103
- Field(repr=False, frozen=True, alias="__typename"),
104
- ]
105
-
106
-
107
- def ensure_json(v: Any) -> Any:
108
- """In case the incoming value isn't serialized JSON, reserialize it.
109
-
110
- This lets us use `Json[...]` fields with values that are already deserialized.
111
- """
112
- # NOTE: Assumes that the deserialized type is not itself a string.
113
- # Revisit this if we need to support deserialized types that are str/bytes.
114
- return v if isinstance(v, (str, bytes)) else to_json(v)
115
-
116
-
117
- if IS_PYDANTIC_V2 or TYPE_CHECKING:
118
- from pydantic import BeforeValidator, PlainSerializer
119
-
120
- SerializedToJson = Annotated[
121
- Json[T],
122
- # Allow lenient instantiation/validation: incoming data may already be deserialized.
123
- BeforeValidator(ensure_json),
124
- PlainSerializer(to_json),
125
- ]
126
- else:
127
- # FIXME: Restore, modify, or replace this later after ensuring pydantic v1 compatibility.
128
- SerializedToJson = Json[T] # type: ignore[misc]
@@ -0,0 +1,29 @@
1
+ """Reusable field types and annotations for pydantic fields."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, TypeVar
6
+
7
+ from pydantic import Field, StrictStr
8
+ from typing_extensions import Annotated
9
+
10
+ from .utils import IS_PYDANTIC_V2
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ #: GraphQL `__typename` fields
16
+ Typename = Annotated[T, Field(repr=False, frozen=True, alias="__typename")]
17
+
18
+
19
+ if IS_PYDANTIC_V2 or TYPE_CHECKING:
20
+ GQLId = Annotated[StrictStr, Field(repr=False, frozen=True)]
21
+
22
+ else:
23
+ # FIXME: Find a way to fix this for pydantic v1, which doesn't like when
24
+ # `Field(...)` used in the field assignment AND `Annotated[...]`.
25
+ # This is a problem for codegen, which can currently output e.g.
26
+ #
27
+ # class MyModel(GQLBase):
28
+ # my_id: GQLId = Field(alias="myID")
29
+ GQLId = StrictStr # type: ignore[misc]
@@ -61,7 +61,12 @@ _V1_CONFIG_KEYS = {
61
61
 
62
62
  def convert_v2_config(v2_config: dict[str, Any]) -> dict[str, Any]:
63
63
  """Internal helper: Return a copy of the v2 ConfigDict with renamed v1 keys."""
64
- return {_V1_CONFIG_KEYS.get(k, k): v for k, v in v2_config.items()}
64
+ return {
65
+ # Convert v2 config keys to v1 keys
66
+ **{_V1_CONFIG_KEYS.get(k, k): v for k, v in v2_config.items()},
67
+ # This is a v1-only config key. In v2, it no longer exists and is effectively always True.
68
+ "underscore_attrs_are_private": True,
69
+ }
65
70
 
66
71
 
67
72
  @lru_cache(maxsize=None) # Reduce repeat introspection via `signature()`
@@ -87,12 +92,10 @@ class V1MixinMetaclass(PydanticModelMetaclass):
87
92
  **kwargs: Any,
88
93
  ):
89
94
  # In the class definition, convert the model config, if any:
90
- # # BEFORE
91
- # class MyModel(BaseModel): # v2 model with `ConfigDict`
95
+ # class MyModel(BaseModel): # BEFORE (v2)
92
96
  # model_config = ConfigDict(populate_by_name=True)
93
97
  #
94
- # # AFTER
95
- # class MyModel(BaseModel): # v1 model with inner `Config` class
98
+ # class MyModel(BaseModel): # AFTER (v1)
96
99
  # class Config:
97
100
  # allow_population_by_field_name = True
98
101
  if config_dict := namespace.pop("model_config", None):
@@ -197,24 +200,41 @@ PydanticCompatMixin: type = V2Mixin if IS_PYDANTIC_V2 else V1Mixin
197
200
  # Decorators and other pydantic helpers
198
201
  # ----------------------------------------------------------------------------
199
202
  if IS_PYDANTIC_V2:
203
+ from pydantic import alias_generators
204
+
205
+ # https://docs.pydantic.dev/latest/api/config/#pydantic.alias_generators.to_camel
206
+ to_camel = alias_generators.to_camel # e.g. "foo_bar" -> "fooBar"
207
+
208
+ # https://docs.pydantic.dev/latest/api/functional_validators/#pydantic.functional_validators.field_validator
200
209
  field_validator = pydantic.field_validator
210
+
211
+ # https://docs.pydantic.dev/latest/api/functional_validators/#pydantic.functional_validators.model_validator
201
212
  model_validator = pydantic.model_validator
202
- AliasChoices = pydantic.AliasChoices
213
+
214
+ # https://docs.pydantic.dev/latest/api/fields/#pydantic.fields.computed_field
203
215
  computed_field = pydantic.computed_field
204
216
 
217
+ # https://docs.pydantic.dev/latest/api/aliases/#pydantic.aliases.AliasChoices
218
+ AliasChoices = pydantic.AliasChoices
219
+
205
220
  else:
206
- # Redefines `@field_validator` with a v2-like signature
207
- # to call `@validator` from v1 instead.
221
+ from pydantic.utils import to_lower_camel
222
+
223
+ V2ValidatorMode = Literal["before", "after", "wrap", "plain"]
224
+
225
+ # NOTE:
226
+ # - `to_lower_camel` in v1 is the equivalent of `to_camel` in v2 (i.e. to lowerCamelCase)
227
+ # - `to_camel` in v1 is the equivalent of `to_pascal` in v2 (i.e. to UpperCamelCase)
228
+ to_camel = to_lower_camel
229
+
230
+ # Lets us use `@field_validator` from v2, while calling `@validator` from v1 if needed.
208
231
  def field_validator(
209
- field: str,
210
- /,
211
232
  *fields: str,
212
- mode: Literal["before", "after", "wrap", "plain"] = "after",
233
+ mode: V2ValidatorMode = "after",
213
234
  check_fields: bool | None = None,
214
235
  **_: Any,
215
236
  ) -> Callable:
216
237
  return pydantic.validator( # type: ignore[deprecated]
217
- field,
218
238
  *fields,
219
239
  pre=(mode == "before"),
220
240
  always=True,
@@ -222,31 +242,28 @@ else:
222
242
  allow_reuse=True,
223
243
  )
224
244
 
225
- # Redefines `@model_validator` with a v2-like signature
226
- # to call `@root_validator` from v1 instead.
227
- def model_validator(
228
- *,
229
- mode: Literal["before", "after", "wrap", "plain"],
230
- **_: Any,
231
- ) -> Callable:
245
+ # Lets us use `@model_validator` from v2, while calling `@root_validator` from v1 if needed.
246
+ def model_validator(*, mode: V2ValidatorMode, **_: Any) -> Callable:
232
247
  if mode == "after":
233
- # Patch the behavior for `@model_validator(mode="after")` in v1. This is
234
- # necessarily complicated because:
235
- # - `@model_validator(mode="after")` decorates an instance method in pydantic v2
236
- # - `@root_validator(pre=False)` always decorates a classmethod in pydantic v1
248
+
237
249
  def _decorator(v2_method: Callable) -> Any:
250
+ # Patch the behavior for `@model_validator(mode="after")` in v1.
251
+ #
252
+ # This is necessarily complicated because:
253
+ # - `@model_validator(mode="after")` always decorates an instance method in v2,
254
+ # i.e. the decorated function has `self` as the first arg.
255
+ # - `@root_validator(pre=False)` always decorates a classmethod in v1,
256
+ # i.e. the decorated function has `cls` as the first arg.
257
+
238
258
  def v1_method(
239
259
  cls: type[V1Model], values: dict[str, Any]
240
260
  ) -> dict[str, Any]:
241
- # Note: Since this is an "after" validator, the values should already be
242
- # validated, so `.construct()` in v1 (`.model_construct()` in v2)
243
- # should create a valid object to pass to the **original** decorated instance method.
244
- validated = v2_method(cls.construct(**values))
261
+ # values should already be validated in an "after" validator, so use `construct()`
262
+ # to instantiate without (re-)validating.
263
+ v_self = v2_method(cls.construct(**values))
245
264
 
246
265
  # Pydantic v1 expects the validator to return a dict of {field_name -> value}
247
- return {
248
- name: getattr(validated, name) for name in validated.__fields__
249
- }
266
+ return {f: getattr(v_self, f) for f in v_self.__fields__}
250
267
 
251
268
  return pydantic.root_validator(pre=False, allow_reuse=True)( # type: ignore[call-overload]
252
269
  classmethod(v1_method)
wandb/_strutils.py ADDED
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ def removeprefix(s: str, prefix: str) -> str:
7
+ """Removes a prefix from a string.
8
+
9
+ This roughly backports the built-in `str.removeprefix` function from Python 3.9+.
10
+ Once Python 3.8 support is dropped, just replace this with `str.removeprefix`.
11
+ """
12
+ return s[len(prefix) :] if s.startswith(prefix) else s
13
+
14
+
15
+ def removesuffix(s: str, suffix: str) -> str:
16
+ """Removes a suffix from a string.
17
+
18
+ This roughly backports the built-in `str.removesuffix` function from Python 3.9+.
19
+ Once Python 3.8 support is dropped, just replace this with `str.removesuffix`.
20
+ """
21
+ return s[: -len(suffix)] if s.endswith(suffix) else s
22
+
23
+
24
+ def ensureprefix(s: str, prefix: str) -> str:
25
+ """Ensures the string has the given prefix prepended."""
26
+ return s if s.startswith(prefix) else f"{prefix}{s}"
27
+
28
+
29
+ def ensuresuffix(s: str, suffix: str) -> str:
30
+ """Ensures the string has the given suffix appended."""
31
+ return s if s.endswith(suffix) else f"{s}{suffix}"
32
+
33
+
34
+ def nameof(obj: Any, full: bool = True) -> str:
35
+ """Internal convenience helper that returns the object's `__name__` or `__qualname__`.
36
+
37
+ If `full` is True, attempt to return the object's `__qualname__` attribute,
38
+ falling back on the `__name__` attribute.
39
+ """
40
+ return getattr(obj, "__qualname__", obj.__name__) if full else obj.__name__
wandb/apis/public/api.py CHANGED
@@ -36,7 +36,9 @@ from wandb_gql.client import RetryError
36
36
 
37
37
  import wandb
38
38
  from wandb import env, util
39
+ from wandb._analytics import tracked
39
40
  from wandb._iterutils import one
41
+ from wandb._strutils import nameof
40
42
  from wandb.apis import public
41
43
  from wandb.apis.normalize import normalize_exceptions
42
44
  from wandb.apis.public.const import RETRY_TIMEDELTA
@@ -1730,6 +1732,7 @@ class Api:
1730
1732
 
1731
1733
  return True
1732
1734
 
1735
+ @tracked
1733
1736
  def registries(
1734
1737
  self,
1735
1738
  organization: Optional[str] = None,
@@ -1797,6 +1800,7 @@ class Api:
1797
1800
  )
1798
1801
  return Registries(self.client, organization, filter)
1799
1802
 
1803
+ @tracked
1800
1804
  def registry(self, name: str, organization: Optional[str] = None) -> Registry:
1801
1805
  """Return a registry given a registry name.
1802
1806
 
@@ -1836,6 +1840,7 @@ class Api:
1836
1840
  registry.load()
1837
1841
  return registry
1838
1842
 
1843
+ @tracked
1839
1844
  def create_registry(
1840
1845
  self,
1841
1846
  name: str,
@@ -1910,6 +1915,7 @@ class Api:
1910
1915
  artifact_types,
1911
1916
  )
1912
1917
 
1918
+ @tracked
1913
1919
  def integrations(
1914
1920
  self,
1915
1921
  entity: Optional[str] = None,
@@ -1933,6 +1939,7 @@ class Api:
1933
1939
  params = {"entityName": entity or self.default_entity}
1934
1940
  return Integrations(client=self.client, variables=params, per_page=per_page)
1935
1941
 
1942
+ @tracked
1936
1943
  def webhook_integrations(
1937
1944
  self, entity: Optional[str] = None, *, per_page: int = 50
1938
1945
  ) -> Iterator["WebhookIntegration"]:
@@ -1976,6 +1983,7 @@ class Api:
1976
1983
  client=self.client, variables=params, per_page=per_page
1977
1984
  )
1978
1985
 
1986
+ @tracked
1979
1987
  def slack_integrations(
1980
1988
  self, *, entity: Optional[str] = None, per_page: int = 50
1981
1989
  ) -> Iterator["SlackIntegration"]:
@@ -2079,10 +2087,10 @@ class Api:
2079
2087
  # Note: we can't currently define this as a constant outside the method
2080
2088
  # and still keep it nearby in this module, because it relies on pydantic v2-only imports
2081
2089
  fragment_names: dict[ActionType, str] = {
2082
- ActionType.NO_OP: NoOpActionFields.__name__,
2083
- ActionType.QUEUE_JOB: QueueJobActionFields.__name__,
2084
- ActionType.NOTIFICATION: NotificationActionFields.__name__,
2085
- ActionType.GENERIC_WEBHOOK: GenericWebhookActionFields.__name__,
2090
+ ActionType.NO_OP: nameof(NoOpActionFields),
2091
+ ActionType.QUEUE_JOB: nameof(QueueJobActionFields),
2092
+ ActionType.NOTIFICATION: nameof(NotificationActionFields),
2093
+ ActionType.GENERIC_WEBHOOK: nameof(GenericWebhookActionFields),
2086
2094
  }
2087
2095
 
2088
2096
  return set(
@@ -2092,6 +2100,7 @@ class Api:
2092
2100
  and (name := fragment_names.get(action))
2093
2101
  )
2094
2102
 
2103
+ @tracked
2095
2104
  def automation(
2096
2105
  self,
2097
2106
  name: str,
@@ -2129,6 +2138,7 @@ class Api:
2129
2138
  too_long=ValueError("Multiple automations found"),
2130
2139
  )
2131
2140
 
2141
+ @tracked
2132
2142
  def automations(
2133
2143
  self,
2134
2144
  entity: Optional[str] = None,
@@ -2186,6 +2196,7 @@ class Api:
2186
2196
  yield from iterator
2187
2197
 
2188
2198
  @normalize_exceptions
2199
+ @tracked
2189
2200
  def create_automation(
2190
2201
  self,
2191
2202
  obj: "NewAutomation",
@@ -2293,6 +2304,7 @@ class Api:
2293
2304
  return Automation.model_validate(result.trigger)
2294
2305
 
2295
2306
  @normalize_exceptions
2307
+ @tracked
2296
2308
  def update_automation(
2297
2309
  self,
2298
2310
  obj: "Automation",
@@ -2415,6 +2427,7 @@ class Api:
2415
2427
  return Automation.model_validate(result.trigger)
2416
2428
 
2417
2429
  @normalize_exceptions
2430
+ @tracked
2418
2431
  def delete_automation(self, obj: Union["Automation", str]) -> Literal[True]:
2419
2432
  """Delete an automation.
2420
2433
 
@@ -15,6 +15,7 @@ from typing_extensions import override
15
15
  from wandb_gql import Client, gql
16
16
 
17
17
  import wandb
18
+ from wandb._strutils import nameof
18
19
  from wandb.apis import public
19
20
  from wandb.apis.normalize import normalize_exceptions
20
21
  from wandb.apis.paginator import Paginator, SizedPaginator
@@ -104,7 +105,7 @@ class ArtifactTypes(Paginator["ArtifactType"]):
104
105
 
105
106
  # Extract the inner `*Connection` result for faster/easier access.
106
107
  if not ((proj := result.project) and (conn := proj.artifact_types)):
107
- raise ValueError(f"Unable to parse {type(self).__name__!r} response data")
108
+ raise ValueError(f"Unable to parse {nameof(type(self))!r} response data")
108
109
 
109
110
  self.last_response = ArtifactTypesFragment.model_validate(conn)
110
111
 
@@ -305,7 +306,7 @@ class ArtifactCollections(SizedPaginator["ArtifactCollection"]):
305
306
  and (type_ := proj.artifact_type)
306
307
  and (conn := type_.artifact_collections)
307
308
  ):
308
- raise ValueError(f"Unable to parse {type(self).__name__!r} response data")
309
+ raise ValueError(f"Unable to parse {nameof(type(self))!r} response data")
309
310
 
310
311
  self.last_response = ArtifactCollectionsFragment.model_validate(conn)
311
312
 
@@ -732,7 +733,7 @@ class Artifacts(SizedPaginator["Artifact"]):
732
733
  and (collection := type_.artifact_collection)
733
734
  and (conn := collection.artifacts)
734
735
  ):
735
- raise ValueError(f"Unable to parse {type(self).__name__!r} response data")
736
+ raise ValueError(f"Unable to parse {nameof(type(self))!r} response data")
736
737
 
737
738
  self.last_response = ArtifactsFragment.model_validate(conn)
738
739
 
@@ -959,7 +960,7 @@ class ArtifactFiles(SizedPaginator["public.File"]):
959
960
  conn = result.project.artifact_type.artifact.files
960
961
 
961
962
  if conn is None:
962
- raise ValueError(f"Unable to parse {type(self).__name__!r} response data")
963
+ raise ValueError(f"Unable to parse {nameof(type(self))!r} response data")
963
964
 
964
965
  self.last_response = FilesFragment.model_validate(conn)
965
966
 
@@ -9,6 +9,7 @@ from pydantic import ValidationError
9
9
  from typing_extensions import override
10
10
  from wandb_graphql.language.ast import Document
11
11
 
12
+ from wandb._strutils import nameof
12
13
  from wandb.apis.paginator import Paginator, _Client
13
14
 
14
15
  if TYPE_CHECKING:
@@ -34,7 +35,7 @@ class Automations(Paginator["Automation"]):
34
35
  ):
35
36
  super().__init__(client, variables, per_page=per_page)
36
37
  if _query is None:
37
- raise RuntimeError(f"Query required for {type(self).__qualname__}")
38
+ raise RuntimeError(f"Query required for {nameof(type(self))}")
38
39
  self._query = _query
39
40
 
40
41
  @property
@@ -13,6 +13,8 @@ from typing import (
13
13
  overload,
14
14
  )
15
15
 
16
+ from wandb._strutils import nameof
17
+
16
18
  T = TypeVar("T")
17
19
 
18
20
 
@@ -84,9 +86,7 @@ class FreezableList(MutableSequence[T]):
84
86
  ) -> None:
85
87
  if isinstance(index, slice):
86
88
  # Setting slices might affect saved items, disallow for simplicity
87
- raise TypeError(
88
- f"{type(self).__name__!r} does not support slice assignment"
89
- )
89
+ raise TypeError(f"{nameof(type(self))!r} does not support slice assignment")
90
90
  else:
91
91
  if value in self._frozen or value in self._draft:
92
92
  return
@@ -111,7 +111,7 @@ class FreezableList(MutableSequence[T]):
111
111
 
112
112
  def __delitem__(self, index: Union[int, slice]) -> None:
113
113
  if isinstance(index, slice):
114
- raise TypeError(f"{type(self).__name__!r} does not support slice deletion")
114
+ raise TypeError(f"{nameof(type(self))!r} does not support slice deletion")
115
115
  else:
116
116
  # The frozen items are sequentially first and protected from changes
117
117
  len_frozen = len(self._frozen)
@@ -158,7 +158,7 @@ class FreezableList(MutableSequence[T]):
158
158
  return self._draft.insert(draft_index, value)
159
159
 
160
160
  def __repr__(self) -> str:
161
- return f"{type(self).__name__}(frozen={list(self._frozen)!r}, draft={list(self._draft)!r})"
161
+ return f"{nameof(type(self))}(frozen={list(self._frozen)!r}, draft={list(self._draft)!r})"
162
162
 
163
163
  @property
164
164
  def draft(self) -> Tuple[T, ...]:
@@ -176,4 +176,4 @@ class AddOnlyArtifactTypesList(FreezableList[str]):
176
176
  )
177
177
 
178
178
  def __repr__(self) -> str:
179
- return f"{type(self).__name__}(saved={list(self._frozen)!r}, draft={list(self._draft)!r})"
179
+ return f"{nameof(type(self))}(saved={list(self._frozen)!r}, draft={list(self._draft)!r})"
@@ -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,