wandb 0.15.9__py3-none-any.whl → 0.15.11__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. wandb/__init__.py +5 -1
  2. wandb/apis/public.py +137 -17
  3. wandb/apis/reports/_panels.py +1 -1
  4. wandb/apis/reports/blocks.py +1 -0
  5. wandb/apis/reports/report.py +27 -5
  6. wandb/cli/cli.py +52 -41
  7. wandb/docker/__init__.py +17 -0
  8. wandb/docker/auth.py +1 -1
  9. wandb/env.py +24 -4
  10. wandb/filesync/step_checksum.py +3 -3
  11. wandb/integration/openai/openai.py +3 -0
  12. wandb/integration/ultralytics/__init__.py +9 -0
  13. wandb/integration/ultralytics/bbox_utils.py +196 -0
  14. wandb/integration/ultralytics/callback.py +458 -0
  15. wandb/integration/ultralytics/classification_utils.py +66 -0
  16. wandb/integration/ultralytics/mask_utils.py +141 -0
  17. wandb/integration/ultralytics/pose_utils.py +92 -0
  18. wandb/integration/xgboost/xgboost.py +3 -3
  19. wandb/integration/yolov8/__init__.py +0 -7
  20. wandb/integration/yolov8/yolov8.py +22 -3
  21. wandb/old/settings.py +7 -0
  22. wandb/plot/line_series.py +0 -1
  23. wandb/proto/v3/wandb_internal_pb2.py +353 -300
  24. wandb/proto/v3/wandb_server_pb2.py +37 -41
  25. wandb/proto/v3/wandb_settings_pb2.py +2 -2
  26. wandb/proto/v3/wandb_telemetry_pb2.py +16 -16
  27. wandb/proto/v4/wandb_internal_pb2.py +272 -260
  28. wandb/proto/v4/wandb_server_pb2.py +37 -40
  29. wandb/proto/v4/wandb_settings_pb2.py +2 -2
  30. wandb/proto/v4/wandb_telemetry_pb2.py +16 -16
  31. wandb/proto/wandb_internal_codegen.py +7 -31
  32. wandb/sdk/artifacts/artifact.py +321 -189
  33. wandb/sdk/artifacts/artifact_cache.py +14 -0
  34. wandb/sdk/artifacts/artifact_manifest.py +5 -4
  35. wandb/sdk/artifacts/artifact_manifest_entry.py +37 -9
  36. wandb/sdk/artifacts/artifact_manifests/artifact_manifest_v1.py +1 -9
  37. wandb/sdk/artifacts/artifact_saver.py +13 -50
  38. wandb/sdk/artifacts/artifact_ttl.py +6 -0
  39. wandb/sdk/artifacts/artifacts_cache.py +119 -93
  40. wandb/sdk/artifacts/staging.py +25 -0
  41. wandb/sdk/artifacts/storage_handlers/s3_handler.py +12 -7
  42. wandb/sdk/artifacts/storage_handlers/wb_local_artifact_handler.py +2 -3
  43. wandb/sdk/artifacts/storage_policies/__init__.py +4 -0
  44. wandb/sdk/artifacts/storage_policies/register.py +1 -0
  45. wandb/sdk/artifacts/storage_policies/wandb_storage_policy.py +4 -3
  46. wandb/sdk/artifacts/storage_policy.py +4 -2
  47. wandb/sdk/backend/backend.py +0 -16
  48. wandb/sdk/data_types/image.py +3 -1
  49. wandb/sdk/integration_utils/auto_logging.py +38 -13
  50. wandb/sdk/interface/interface.py +16 -135
  51. wandb/sdk/interface/interface_shared.py +9 -147
  52. wandb/sdk/interface/interface_sock.py +0 -26
  53. wandb/sdk/internal/file_pusher.py +20 -3
  54. wandb/sdk/internal/file_stream.py +3 -1
  55. wandb/sdk/internal/handler.py +53 -70
  56. wandb/sdk/internal/internal_api.py +220 -130
  57. wandb/sdk/internal/job_builder.py +41 -37
  58. wandb/sdk/internal/sender.py +7 -25
  59. wandb/sdk/internal/system/assets/disk.py +144 -11
  60. wandb/sdk/internal/system/system_info.py +6 -2
  61. wandb/sdk/launch/__init__.py +5 -0
  62. wandb/sdk/launch/{launch.py → _launch.py} +53 -54
  63. wandb/sdk/launch/{launch_add.py → _launch_add.py} +34 -31
  64. wandb/sdk/launch/_project_spec.py +13 -2
  65. wandb/sdk/launch/agent/agent.py +103 -59
  66. wandb/sdk/launch/agent/run_queue_item_file_saver.py +6 -4
  67. wandb/sdk/launch/builder/build.py +19 -1
  68. wandb/sdk/launch/builder/docker_builder.py +5 -1
  69. wandb/sdk/launch/builder/kaniko_builder.py +5 -1
  70. wandb/sdk/launch/create_job.py +20 -5
  71. wandb/sdk/launch/loader.py +14 -5
  72. wandb/sdk/launch/runner/abstract.py +0 -2
  73. wandb/sdk/launch/runner/kubernetes_monitor.py +329 -0
  74. wandb/sdk/launch/runner/kubernetes_runner.py +66 -209
  75. wandb/sdk/launch/runner/local_container.py +5 -2
  76. wandb/sdk/launch/runner/local_process.py +4 -1
  77. wandb/sdk/launch/sweeps/scheduler.py +43 -25
  78. wandb/sdk/launch/sweeps/utils.py +5 -3
  79. wandb/sdk/launch/utils.py +3 -1
  80. wandb/sdk/lib/_settings_toposort_generate.py +3 -9
  81. wandb/sdk/lib/_settings_toposort_generated.py +27 -3
  82. wandb/sdk/lib/_wburls_generated.py +1 -0
  83. wandb/sdk/lib/filenames.py +27 -6
  84. wandb/sdk/lib/filesystem.py +181 -7
  85. wandb/sdk/lib/fsm.py +5 -3
  86. wandb/sdk/lib/gql_request.py +3 -0
  87. wandb/sdk/lib/ipython.py +7 -0
  88. wandb/sdk/lib/wburls.py +1 -0
  89. wandb/sdk/service/port_file.py +2 -15
  90. wandb/sdk/service/server.py +7 -55
  91. wandb/sdk/service/service.py +56 -26
  92. wandb/sdk/service/service_base.py +1 -1
  93. wandb/sdk/service/streams.py +11 -5
  94. wandb/sdk/verify/verify.py +2 -2
  95. wandb/sdk/wandb_init.py +8 -2
  96. wandb/sdk/wandb_manager.py +4 -14
  97. wandb/sdk/wandb_run.py +143 -53
  98. wandb/sdk/wandb_settings.py +148 -35
  99. wandb/testing/relay.py +85 -38
  100. wandb/util.py +87 -4
  101. wandb/wandb_torch.py +24 -38
  102. {wandb-0.15.9.dist-info → wandb-0.15.11.dist-info}/METADATA +48 -23
  103. {wandb-0.15.9.dist-info → wandb-0.15.11.dist-info}/RECORD +107 -103
  104. {wandb-0.15.9.dist-info → wandb-0.15.11.dist-info}/WHEEL +1 -1
  105. wandb/proto/v3/wandb_server_pb2_grpc.py +0 -1422
  106. wandb/proto/v4/wandb_server_pb2_grpc.py +0 -1422
  107. wandb/proto/wandb_server_pb2_grpc.py +0 -8
  108. wandb/sdk/artifacts/storage_policies/s3_bucket_policy.py +0 -61
  109. wandb/sdk/interface/interface_grpc.py +0 -460
  110. wandb/sdk/service/server_grpc.py +0 -444
  111. wandb/sdk/service/service_grpc.py +0 -73
  112. {wandb-0.15.9.dist-info → wandb-0.15.11.dist-info}/LICENSE +0 -0
  113. {wandb-0.15.9.dist-info → wandb-0.15.11.dist-info}/entry_points.txt +0 -0
  114. {wandb-0.15.9.dist-info → wandb-0.15.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,14 @@
1
+ """Recent Artifact storage.
2
+
3
+ Artifacts are registered in the cache to ensure they won't be immediately garbage
4
+ collected and can be retrieved by their ID.
5
+ """
6
+ from typing import TYPE_CHECKING, Dict
7
+
8
+ from wandb.sdk.lib.capped_dict import CappedDict
9
+
10
+ if TYPE_CHECKING:
11
+ from wandb.sdk.artifacts.artifact import Artifact
12
+
13
+ # There is nothing special about the artifact cache, it's just a global capped dict.
14
+ artifact_cache: Dict[str, "Artifact"] = CappedDict(100)
@@ -5,9 +5,7 @@ from wandb.sdk.lib.hashutil import HexMD5
5
5
 
6
6
  if TYPE_CHECKING:
7
7
  from wandb.sdk.artifacts.artifact_manifest_entry import ArtifactManifestEntry
8
- from wandb.sdk.artifacts.storage_policies.wandb_storage_policy import (
9
- WandbStoragePolicy,
10
- )
8
+ from wandb.sdk.artifacts.storage_policy import StoragePolicy
11
9
 
12
10
 
13
11
  class ArtifactManifest:
@@ -29,12 +27,15 @@ class ArtifactManifest:
29
27
 
30
28
  def __init__(
31
29
  self,
32
- storage_policy: "WandbStoragePolicy",
30
+ storage_policy: "StoragePolicy",
33
31
  entries: Optional[Mapping[str, "ArtifactManifestEntry"]] = None,
34
32
  ) -> None:
35
33
  self.storage_policy = storage_policy
36
34
  self.entries = dict(entries) if entries else {}
37
35
 
36
+ def __len__(self) -> int:
37
+ return len(self.entries)
38
+
38
39
  def to_manifest_json(self) -> Dict:
39
40
  raise NotImplementedError
40
41
 
@@ -1,11 +1,10 @@
1
1
  """Artifact manifest entry."""
2
+ import json
2
3
  import os
3
4
  from pathlib import Path
4
5
  from typing import TYPE_CHECKING, Dict, Optional, Union
5
6
  from urllib.parse import urlparse
6
7
 
7
- import wandb
8
- from wandb import util
9
8
  from wandb.errors.term import termwarn
10
9
  from wandb.sdk.lib import filesystem
11
10
  from wandb.sdk.lib.hashutil import (
@@ -18,7 +17,6 @@ from wandb.sdk.lib.hashutil import (
18
17
  from wandb.sdk.lib.paths import FilePathStr, LogicalPath, StrPath, URIStr
19
18
 
20
19
  if TYPE_CHECKING:
21
- from wandb.apis.public import RetryingClient
22
20
  from wandb.sdk.artifacts.artifact import Artifact
23
21
 
24
22
 
@@ -56,6 +54,38 @@ class ArtifactManifestEntry:
56
54
  if self.local_path and self.size is None:
57
55
  self.size = Path(self.local_path).stat().st_size
58
56
 
57
+ def __repr__(self) -> str:
58
+ cls = self.__class__.__name__
59
+ ref = f", ref={self.ref!r}" if self.ref is not None else ""
60
+ birth_artifact_id = (
61
+ f", birth_artifact_id={self.birth_artifact_id!r}"
62
+ if self.birth_artifact_id is not None
63
+ else ""
64
+ )
65
+ size = f", size={self.size}" if self.size is not None else ""
66
+ extra = f", extra={json.dumps(self.extra)}" if self.extra else ""
67
+ local_path = f", local_path={self.local_path!r}" if self.local_path else ""
68
+ others = ref + birth_artifact_id + size + extra + local_path
69
+ return f"{cls}(path={self.path!r}, digest={self.digest!r}{others})"
70
+
71
+ def __eq__(self, other: object) -> bool:
72
+ """Strict equality, comparing all public fields.
73
+
74
+ ArtifactManifestEntries for the same file may not compare equal if they were
75
+ added in different ways or created for different parent artifacts.
76
+ """
77
+ if not isinstance(other, ArtifactManifestEntry):
78
+ return False
79
+ return (
80
+ self.path == other.path
81
+ and self.digest == other.digest
82
+ and self.ref == other.ref
83
+ and self.birth_artifact_id == other.birth_artifact_id
84
+ and self.size == other.size
85
+ and self.extra == other.extra
86
+ and self.local_path == other.local_path
87
+ )
88
+
59
89
  @property
60
90
  def name(self) -> LogicalPath:
61
91
  # TODO(hugh): add telemetry to see if anyone is still using this.
@@ -151,9 +181,7 @@ class ArtifactManifestEntry:
151
181
  def _is_artifact_reference(self) -> bool:
152
182
  return self.ref is not None and urlparse(self.ref).scheme == "wandb-artifact"
153
183
 
154
- def _get_referenced_artifact(self, client: "RetryingClient") -> "Artifact":
155
- artifact: Artifact = wandb.Artifact._from_id(
156
- hex_to_b64_id(util.host_from_path(self.ref)), client
157
- )
158
- assert artifact is not None
159
- return artifact
184
+ def _referenced_artifact_id(self) -> Optional[str]:
185
+ if not self._is_artifact_reference():
186
+ return None
187
+ return hex_to_b64_id(urlparse(self.ref).netloc)
@@ -3,7 +3,6 @@ from typing import Any, Dict, Mapping, Optional
3
3
 
4
4
  from wandb.sdk.artifacts.artifact_manifest import ArtifactManifest
5
5
  from wandb.sdk.artifacts.artifact_manifest_entry import ArtifactManifestEntry
6
- from wandb.sdk.artifacts.storage_policies.wandb_storage_policy import WandbStoragePolicy
7
6
  from wandb.sdk.artifacts.storage_policy import StoragePolicy
8
7
  from wandb.sdk.lib.hashutil import HexMD5, _md5
9
8
 
@@ -23,13 +22,6 @@ class ArtifactManifestV1(ArtifactManifest):
23
22
  storage_policy_name = manifest_json["storagePolicy"]
24
23
  storage_policy_config = manifest_json.get("storagePolicyConfig", {})
25
24
  storage_policy_cls = StoragePolicy.lookup_by_name(storage_policy_name)
26
- if storage_policy_cls is None:
27
- raise ValueError('Failed to find storage policy "%s"' % storage_policy_name)
28
- if not issubclass(storage_policy_cls, WandbStoragePolicy):
29
- raise ValueError(
30
- "No handler found for storage handler of type '%s'"
31
- % storage_policy_name
32
- )
33
25
 
34
26
  entries: Mapping[str, ArtifactManifestEntry]
35
27
  entries = {
@@ -49,7 +41,7 @@ class ArtifactManifestV1(ArtifactManifest):
49
41
 
50
42
  def __init__(
51
43
  self,
52
- storage_policy: "WandbStoragePolicy",
44
+ storage_policy: "StoragePolicy",
53
45
  entries: Optional[Mapping[str, ArtifactManifestEntry]] = None,
54
46
  ) -> None:
55
47
  super().__init__(storage_policy, entries=entries)
@@ -4,15 +4,15 @@ import json
4
4
  import os
5
5
  import sys
6
6
  import tempfile
7
- from typing import TYPE_CHECKING, Awaitable, Dict, List, Optional, Sequence
7
+ from typing import TYPE_CHECKING, Awaitable, Dict, Optional, Sequence
8
8
 
9
9
  import wandb
10
10
  import wandb.filesync.step_prepare
11
- from wandb import env, util
11
+ from wandb import util
12
12
  from wandb.sdk.artifacts.artifact_manifest import ArtifactManifest
13
- from wandb.sdk.lib.filesystem import mkdir_exists_ok
13
+ from wandb.sdk.artifacts.staging import get_staging_dir
14
14
  from wandb.sdk.lib.hashutil import B64MD5, b64_to_hex_id, md5_file_b64
15
- from wandb.sdk.lib.paths import FilePathStr, URIStr
15
+ from wandb.sdk.lib.paths import URIStr
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from wandb.sdk.artifacts.artifact_manifest_entry import ArtifactManifestEntry
@@ -65,9 +65,9 @@ class ArtifactSaver:
65
65
  distributed_id: Optional[str] = None,
66
66
  finalize: bool = True,
67
67
  metadata: Optional[Dict] = None,
68
+ ttl_duration_seconds: Optional[int] = None,
68
69
  description: Optional[str] = None,
69
70
  aliases: Optional[Sequence[str]] = None,
70
- labels: Optional[List[str]] = None,
71
71
  use_after_commit: bool = False,
72
72
  incremental: bool = False,
73
73
  history_step: Optional[int] = None,
@@ -82,9 +82,9 @@ class ArtifactSaver:
82
82
  distributed_id,
83
83
  finalize,
84
84
  metadata,
85
+ ttl_duration_seconds,
85
86
  description,
86
87
  aliases,
87
- labels,
88
88
  use_after_commit,
89
89
  incremental,
90
90
  history_step,
@@ -102,33 +102,17 @@ class ArtifactSaver:
102
102
  distributed_id: Optional[str] = None,
103
103
  finalize: bool = True,
104
104
  metadata: Optional[Dict] = None,
105
+ ttl_duration_seconds: Optional[int] = None,
105
106
  description: Optional[str] = None,
106
107
  aliases: Optional[Sequence[str]] = None,
107
- labels: Optional[List[str]] = None,
108
108
  use_after_commit: bool = False,
109
109
  incremental: bool = False,
110
110
  history_step: Optional[int] = None,
111
111
  base_id: Optional[str] = None,
112
112
  ) -> Optional[Dict]:
113
- aliases = aliases or []
114
113
  alias_specs = []
115
- for alias in aliases:
116
- if ":" in alias:
117
- # Users can explicitly alias this artifact to names
118
- # other than the primary one passed in by using the
119
- # 'secondaryName:alias' notation.
120
- idx = alias.index(":")
121
- artifact_collection_name = alias[: idx - 1]
122
- tag = alias[idx + 1 :]
123
- else:
124
- artifact_collection_name = name
125
- tag = alias
126
- alias_specs.append(
127
- {
128
- "artifactCollectionName": artifact_collection_name,
129
- "alias": tag,
130
- }
131
- )
114
+ for alias in aliases or []:
115
+ alias_specs.append({"artifactCollectionName": name, "alias": alias})
132
116
 
133
117
  """Returns the server artifact."""
134
118
  self._server_artifact, latest = self._api.create_artifact(
@@ -136,35 +120,27 @@ class ArtifactSaver:
136
120
  name,
137
121
  self._digest,
138
122
  metadata=metadata,
123
+ ttl_duration_seconds=ttl_duration_seconds,
139
124
  aliases=alias_specs,
140
- labels=labels,
141
125
  description=description,
142
126
  is_user_created=self._is_user_created,
143
127
  distributed_id=distributed_id,
144
128
  client_id=client_id,
145
129
  sequence_client_id=sequence_client_id,
146
- enable_digest_deduplication=use_after_commit, # Reuse logical duplicates in the `use_artifact` flow
147
130
  history_step=history_step,
148
131
  )
149
132
 
150
- # TODO(artifacts):
151
- # if it's committed, all is good. If it's committing, just moving ahead isn't necessarily
152
- # correct. It may be better to poll until it's committed or failed, and then decided what to
153
- # do
154
133
  assert self._server_artifact is not None # mypy optionality unwrapper
155
134
  artifact_id = self._server_artifact["id"]
156
135
  if base_id is None and latest:
157
136
  base_id = latest["id"]
158
- if (
159
- self._server_artifact["state"] == "COMMITTED"
160
- or self._server_artifact["state"] == "COMMITTING"
161
- ):
162
- # TODO: update aliases, labels, description etc?
137
+ if self._server_artifact["state"] == "COMMITTED":
163
138
  if use_after_commit:
164
139
  self._api.use_artifact(artifact_id)
165
140
  return self._server_artifact
166
- elif (
141
+ if (
167
142
  self._server_artifact["state"] != "PENDING"
143
+ # For old servers, see https://github.com/wandb/wandb/pull/6190
168
144
  and self._server_artifact["state"] != "DELETED"
169
145
  ):
170
146
  raise Exception(
@@ -306,16 +282,3 @@ class ArtifactSaver:
306
282
  os.remove(entry.local_path)
307
283
  except OSError:
308
284
  pass
309
-
310
-
311
- def get_staging_dir() -> FilePathStr:
312
- path = os.path.join(env.get_data_dir(), "artifacts", "staging")
313
- try:
314
- mkdir_exists_ok(path)
315
- except OSError as e:
316
- raise PermissionError(
317
- f"Unable to write staging files to {path}. To fix this problem, please set "
318
- f"{env.DATA_DIR} to a directory where you have the necessary write access."
319
- ) from e
320
-
321
- return FilePathStr(os.path.abspath(os.path.expanduser(path)))
@@ -0,0 +1,6 @@
1
+ """Artifact TTL."""
2
+ from enum import Enum
3
+
4
+
5
+ class ArtifactTTL(Enum):
6
+ INHERIT = 0
@@ -1,23 +1,23 @@
1
1
  """Artifact cache."""
2
2
  import contextlib
3
+ import errno
3
4
  import hashlib
4
5
  import os
5
- import secrets
6
- from typing import IO, TYPE_CHECKING, ContextManager, Dict, Generator, Optional, Tuple
6
+ import shutil
7
+ from pathlib import Path
8
+ from tempfile import NamedTemporaryFile
9
+ from typing import IO, TYPE_CHECKING, ContextManager, Generator, Optional, Tuple
7
10
 
8
11
  import wandb
9
12
  from wandb import env, util
10
- from wandb.sdk.artifacts.exceptions import ArtifactNotLoggedError
11
- from wandb.sdk.lib.capped_dict import CappedDict
12
- from wandb.sdk.lib.filesystem import mkdir_exists_ok
13
+ from wandb.errors import term
14
+ from wandb.sdk.lib.filesystem import files_in
13
15
  from wandb.sdk.lib.hashutil import B64MD5, ETag, b64_to_hex_id
14
16
  from wandb.sdk.lib.paths import FilePathStr, StrPath, URIStr
15
17
 
16
18
  if TYPE_CHECKING:
17
19
  import sys
18
20
 
19
- from wandb.sdk.artifacts.artifact import Artifact
20
-
21
21
  if sys.version_info >= (3, 8):
22
22
  from typing import Protocol
23
23
  else:
@@ -29,26 +29,18 @@ if TYPE_CHECKING:
29
29
 
30
30
 
31
31
  class ArtifactsCache:
32
- _TMP_PREFIX = "tmp"
33
-
34
32
  def __init__(self, cache_dir: StrPath) -> None:
35
- self._cache_dir = cache_dir
36
- mkdir_exists_ok(self._cache_dir)
37
- self._md5_obj_dir = os.path.join(self._cache_dir, "obj", "md5")
38
- self._etag_obj_dir = os.path.join(self._cache_dir, "obj", "etag")
39
- self._artifacts_by_id: Dict[str, Artifact] = CappedDict()
40
- self._artifacts_by_client_id: Dict[str, Artifact] = CappedDict()
33
+ self._cache_dir = Path(cache_dir)
34
+ self._obj_dir = self._cache_dir / "obj"
35
+ self._temp_dir = self._cache_dir / "tmp"
36
+ self._temp_dir.mkdir(parents=True, exist_ok=True)
41
37
 
42
38
  def check_md5_obj_path(
43
39
  self, b64_md5: B64MD5, size: int
44
40
  ) -> Tuple[FilePathStr, bool, "Opener"]:
45
41
  hex_md5 = b64_to_hex_id(b64_md5)
46
- path = os.path.join(self._cache_dir, "obj", "md5", hex_md5[:2], hex_md5[2:])
47
- opener = self._cache_opener(path)
48
- if os.path.isfile(path) and os.path.getsize(path) == size:
49
- return FilePathStr(path), True, opener
50
- mkdir_exists_ok(os.path.dirname(path))
51
- return FilePathStr(path), False, opener
42
+ path = self._obj_dir / "md5" / hex_md5[:2] / hex_md5[2:]
43
+ return self._check_or_create(path, size)
52
44
 
53
45
  # TODO(spencerpearson): this method at least needs its signature changed.
54
46
  # An ETag is not (necessarily) a checksum.
@@ -62,103 +54,138 @@ class ArtifactsCache:
62
54
  hashlib.sha256(url.encode("utf-8")).digest()
63
55
  + hashlib.sha256(etag.encode("utf-8")).digest()
64
56
  ).hexdigest()
65
- path = os.path.join(self._cache_dir, "obj", "etag", hexhash[:2], hexhash[2:])
66
- opener = self._cache_opener(path)
67
- if os.path.isfile(path) and os.path.getsize(path) == size:
68
- return FilePathStr(path), True, opener
69
- mkdir_exists_ok(os.path.dirname(path))
70
- return FilePathStr(path), False, opener
71
-
72
- def get_artifact(self, artifact_id: str) -> Optional["Artifact"]:
73
- return self._artifacts_by_id.get(artifact_id)
57
+ path = self._obj_dir / "etag" / hexhash[:2] / hexhash[2:]
58
+ return self._check_or_create(path, size)
74
59
 
75
- def store_artifact(self, artifact: "Artifact") -> None:
76
- if not artifact.id:
77
- raise ArtifactNotLoggedError(artifact, "store_artifact")
78
- self._artifacts_by_id[artifact.id] = artifact
79
-
80
- def get_client_artifact(self, client_id: str) -> Optional["Artifact"]:
81
- return self._artifacts_by_client_id.get(client_id)
60
+ def _check_or_create(
61
+ self, path: Path, size: int
62
+ ) -> Tuple[FilePathStr, bool, "Opener"]:
63
+ opener = self._cache_opener(path, size)
64
+ hit = path.is_file() and path.stat().st_size == size
65
+ return FilePathStr(str(path)), hit, opener
82
66
 
83
- def store_client_artifact(self, artifact: "Artifact") -> None:
84
- self._artifacts_by_client_id[artifact._client_id] = artifact
67
+ def cleanup(
68
+ self,
69
+ target_size: Optional[int] = None,
70
+ target_fraction: Optional[float] = None,
71
+ remove_temp: bool = False,
72
+ ) -> int:
73
+ """Clean up the cache, removing the least recently used files first.
74
+
75
+ Args:
76
+ target_size: The target size of the cache in bytes. If the cache is larger
77
+ than this, we will remove the least recently used files until the cache
78
+ is smaller than this size.
79
+ target_fraction: The target fraction of the cache to reclaim. If the cache
80
+ is larger than this, we will remove the least recently used files until
81
+ the cache is smaller than this fraction of its current size. It is an
82
+ error to specify both target_size and target_fraction.
83
+ remove_temp: Whether to remove temporary files. Temporary files are files
84
+ that are currently being written to the cache. If remove_temp is True,
85
+ all temp files will be removed, regardless of the target_size or
86
+ target_fraction.
87
+
88
+ Returns:
89
+ The number of bytes reclaimed.
90
+ """
91
+ if target_size is None and target_fraction is None:
92
+ # Default to clearing the entire cache.
93
+ target_size = 0
94
+ if target_size is not None and target_fraction is not None:
95
+ raise ValueError("Cannot specify both target_size and target_fraction")
96
+ if target_size and target_size < 0:
97
+ raise ValueError("target_size must be non-negative")
98
+ if target_fraction and (target_fraction < 0 or target_fraction > 1):
99
+ raise ValueError("target_fraction must be between 0 and 1")
85
100
 
86
- def cleanup(self, target_size: int, remove_temp: bool = False) -> int:
87
101
  bytes_reclaimed = 0
88
- paths = {}
89
102
  total_size = 0
90
103
  temp_size = 0
91
- for root, _, files in os.walk(self._cache_dir):
92
- for file in files:
104
+
105
+ # Remove all temporary files if requested. Otherwise sum their size.
106
+ for entry in files_in(self._temp_dir):
107
+ size = entry.stat().st_size
108
+ total_size += size
109
+ if remove_temp:
93
110
  try:
94
- path = os.path.join(root, file)
95
- stat = os.stat(path)
96
-
97
- if file.startswith(ArtifactsCache._TMP_PREFIX):
98
- if remove_temp:
99
- os.remove(path)
100
- bytes_reclaimed += stat.st_size
101
- else:
102
- temp_size += stat.st_size
103
- continue
111
+ os.remove(entry.path)
112
+ bytes_reclaimed += size
104
113
  except OSError:
105
- continue
106
- paths[path] = stat
107
- total_size += stat.st_size
108
-
114
+ pass
115
+ else:
116
+ temp_size += size
109
117
  if temp_size:
110
118
  wandb.termwarn(
111
119
  f"Cache contains {util.to_human_size(temp_size)} of temporary files. "
112
120
  "Run `wandb artifact cleanup --remove-temp` to remove them."
113
121
  )
114
122
 
115
- sorted_paths = sorted(paths.items(), key=lambda x: x[1].st_atime)
116
- for path, stat in sorted_paths:
117
- if total_size < target_size:
118
- return bytes_reclaimed
123
+ entries = []
124
+ for file_entry in files_in(self._obj_dir):
125
+ total_size += file_entry.stat().st_size
126
+ entries.append(file_entry)
127
+
128
+ if target_fraction is not None:
129
+ target_size = int(total_size * target_fraction)
130
+ assert target_size is not None
119
131
 
132
+ for entry in sorted(entries, key=lambda x: x.stat().st_atime):
133
+ if total_size <= target_size:
134
+ return bytes_reclaimed
120
135
  try:
121
- os.remove(path)
136
+ os.remove(entry.path)
122
137
  except OSError:
123
138
  pass
139
+ total_size -= entry.stat().st_size
140
+ bytes_reclaimed += entry.stat().st_size
141
+
142
+ if total_size > target_size:
143
+ wandb.termerror(
144
+ f"Failed to reclaim enough space in {self._cache_dir}. Try running"
145
+ " `wandb artifact cleanup --remove-temp` to remove temporary files."
146
+ )
124
147
 
125
- total_size -= stat.st_size
126
- bytes_reclaimed += stat.st_size
127
148
  return bytes_reclaimed
128
149
 
129
- def _cache_opener(self, path: StrPath) -> "Opener":
150
+ def _free_space(self) -> int:
151
+ """Return the number of bytes of free space in the cache directory."""
152
+ return shutil.disk_usage(self._cache_dir)[2]
153
+
154
+ def _reserve_space(self, size: int) -> None:
155
+ """If a `size` write would exceed disk space, remove cached items to make space.
156
+
157
+ Raises:
158
+ OSError: If there is not enough space to write `size` bytes, even after
159
+ removing cached items.
160
+ """
161
+ if size <= self._free_space():
162
+ return
163
+
164
+ term.termwarn("Cache size exceeded. Attempting to reclaim space...")
165
+ self.cleanup(target_fraction=0.5)
166
+ if size <= self._free_space():
167
+ return
168
+
169
+ self.cleanup(target_size=0)
170
+ if size > self._free_space():
171
+ raise OSError(errno.ENOSPC, f"Insufficient free space in {self._cache_dir}")
172
+
173
+ def _cache_opener(self, path: Path, size: int) -> "Opener":
130
174
  @contextlib.contextmanager
131
175
  def helper(mode: str = "w") -> Generator[IO, None, None]:
132
176
  if "a" in mode:
133
177
  raise ValueError("Appending to cache files is not supported")
134
178
 
135
- dirname = os.path.dirname(path)
136
- tmp_file = os.path.join(
137
- dirname, f"{ArtifactsCache._TMP_PREFIX}_{secrets.token_hex(8)}"
138
- )
139
- with util.fsync_open(tmp_file, mode=mode) as f:
140
- yield f
141
-
179
+ self._reserve_space(size)
180
+ temp_file = NamedTemporaryFile(dir=self._temp_dir, mode=mode, delete=False)
142
181
  try:
143
- # Use replace where we can, as it implements an atomic
144
- # move on most platforms. If it doesn't exist, we have
145
- # to use rename which isn't atomic in all cases but there
146
- # isn't a better option.
147
- #
148
- # The atomic replace is important in the event multiple processes
149
- # attempt to write to / read from the cache at the same time. Each
150
- # writer firsts stages its writes to a temporary file in the cache.
151
- # Once it is finished, we issue an atomic replace operation to update
152
- # the cache. Although this can result in redundant downloads, this
153
- # guarantees that readers can NEVER read incomplete files from the
154
- # cache.
155
- #
156
- # IMPORTANT: Replace is NOT atomic across different filesystems. This why
157
- # it is critical that the temporary files sit directly in the cache --
158
- # they need to be on the same filesystem!
159
- os.replace(tmp_file, path)
160
- except AttributeError:
161
- os.rename(tmp_file, path)
182
+ yield temp_file
183
+ temp_file.close()
184
+ path.parent.mkdir(parents=True, exist_ok=True)
185
+ os.replace(temp_file.name, path)
186
+ except Exception:
187
+ os.remove(temp_file.name)
188
+ raise
162
189
 
163
190
  return helper
164
191
 
@@ -169,6 +196,5 @@ _artifacts_cache = None
169
196
  def get_artifacts_cache() -> ArtifactsCache:
170
197
  global _artifacts_cache
171
198
  if _artifacts_cache is None:
172
- cache_dir = os.path.join(env.get_cache_dir(), "artifacts")
173
- _artifacts_cache = ArtifactsCache(cache_dir)
199
+ _artifacts_cache = ArtifactsCache(env.get_cache_dir() / "artifacts")
174
200
  return _artifacts_cache
@@ -0,0 +1,25 @@
1
+ """Manages artifact file staging.
2
+
3
+ Artifact files are copied to the staging area as soon as they are added to an artifact
4
+ in order to avoid file changes corrupting the artifact. Once the upload is complete, the
5
+ file should be moved to the artifact cache.
6
+ """
7
+
8
+ import os
9
+
10
+ from wandb import env
11
+ from wandb.sdk.lib.filesystem import mkdir_exists_ok
12
+ from wandb.sdk.lib.paths import FilePathStr
13
+
14
+
15
+ def get_staging_dir() -> FilePathStr:
16
+ path = os.path.join(env.get_data_dir(), "artifacts", "staging")
17
+ try:
18
+ mkdir_exists_ok(path)
19
+ except OSError as e:
20
+ raise PermissionError(
21
+ f"Unable to write staging files to {path}. To fix this problem, please set "
22
+ f"{env.DATA_DIR} to a directory where you have the necessary write access."
23
+ ) from e
24
+
25
+ return FilePathStr(os.path.abspath(os.path.expanduser(path)))
@@ -204,7 +204,9 @@ class S3Handler(StorageHandler):
204
204
  )
205
205
  return entries
206
206
 
207
- def _size_from_obj(self, obj: "boto3.s3.Object") -> int:
207
+ def _size_from_obj(
208
+ self, obj: Union["boto3.s3.Object", "boto3.s3.ObjectSummary"]
209
+ ) -> int:
208
210
  # ObjectSummary has size, Object has content_length
209
211
  size: int
210
212
  if hasattr(obj, "size"):
@@ -215,7 +217,7 @@ class S3Handler(StorageHandler):
215
217
 
216
218
  def _entry_from_obj(
217
219
  self,
218
- obj: "boto3.s3.Object",
220
+ obj: Union["boto3.s3.Object", "boto3.s3.ObjectSummary"],
219
221
  path: str,
220
222
  name: Optional[StrPath] = None,
221
223
  prefix: str = "",
@@ -261,18 +263,21 @@ class S3Handler(StorageHandler):
261
263
  )
262
264
 
263
265
  @staticmethod
264
- def _etag_from_obj(obj: "boto3.s3.Object") -> ETag:
266
+ def _etag_from_obj(obj: Union["boto3.s3.Object", "boto3.s3.ObjectSummary"]) -> ETag:
265
267
  etag: ETag
266
268
  etag = obj.e_tag[1:-1] # escape leading and trailing quote
267
269
  return etag
268
270
 
269
- @staticmethod
270
- def _extra_from_obj(obj: "boto3.s3.Object") -> Dict[str, str]:
271
+ def _extra_from_obj(
272
+ self, obj: Union["boto3.s3.Object", "boto3.s3.ObjectSummary"]
273
+ ) -> Dict[str, str]:
271
274
  extra = {
272
275
  "etag": obj.e_tag[1:-1], # escape leading and trailing quote
273
276
  }
274
- # ObjectSummary will never have version_id
275
- if hasattr(obj, "version_id") and obj.version_id != "null":
277
+ if not hasattr(obj, "version_id"):
278
+ # Convert ObjectSummary to Object to get the version_id.
279
+ obj = self._s3.Object(obj.bucket_name, obj.key) # type: ignore[union-attr]
280
+ if hasattr(obj, "version_id") and obj.version_id and obj.version_id != "null":
276
281
  extra["versionID"] = obj.version_id
277
282
  return extra
278
283
 
@@ -4,8 +4,8 @@ from typing import TYPE_CHECKING, Optional, Sequence, Union
4
4
 
5
5
  import wandb
6
6
  from wandb import util
7
+ from wandb.sdk.artifacts.artifact_cache import artifact_cache
7
8
  from wandb.sdk.artifacts.artifact_manifest_entry import ArtifactManifestEntry
8
- from wandb.sdk.artifacts.artifacts_cache import get_artifacts_cache
9
9
  from wandb.sdk.artifacts.storage_handler import StorageHandler
10
10
  from wandb.sdk.lib.paths import FilePathStr, StrPath, URIStr
11
11
 
@@ -20,7 +20,6 @@ class WBLocalArtifactHandler(StorageHandler):
20
20
 
21
21
  def __init__(self) -> None:
22
22
  self._scheme = "wandb-client-artifact"
23
- self._cache = get_artifacts_cache()
24
23
 
25
24
  def can_handle(self, parsed_url: "ParseResult") -> bool:
26
25
  return parsed_url.scheme == self._scheme
@@ -54,7 +53,7 @@ class WBLocalArtifactHandler(StorageHandler):
54
53
  """
55
54
  client_id = util.host_from_path(path)
56
55
  target_path = util.uri_from_path(path)
57
- target_artifact = self._cache.get_client_artifact(client_id)
56
+ target_artifact = artifact_cache.get(client_id)
58
57
  if not isinstance(target_artifact, wandb.Artifact):
59
58
  raise RuntimeError("Local Artifact not found - invalid reference")
60
59
  target_entry = target_artifact._manifest.entries[target_path]