ZhengyangZhang commited on
Commit
aad40ee
·
verified ·
1 Parent(s): 182d6c4

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. lib/python3.12/site-packages/wandb/agents/__init__.py +0 -0
  2. lib/python3.12/site-packages/wandb/agents/__pycache__/__init__.cpython-312.pyc +0 -0
  3. lib/python3.12/site-packages/wandb/agents/__pycache__/pyagent.cpython-312.pyc +0 -0
  4. lib/python3.12/site-packages/wandb/agents/pyagent.py +357 -0
  5. lib/python3.12/site-packages/wandb/analytics/__init__.py +3 -0
  6. lib/python3.12/site-packages/wandb/analytics/__pycache__/__init__.cpython-312.pyc +0 -0
  7. lib/python3.12/site-packages/wandb/analytics/__pycache__/sentry.cpython-312.pyc +0 -0
  8. lib/python3.12/site-packages/wandb/analytics/sentry.py +262 -0
  9. lib/python3.12/site-packages/wandb/apis/__init__.py +48 -0
  10. lib/python3.12/site-packages/wandb/apis/__pycache__/__init__.cpython-312.pyc +0 -0
  11. lib/python3.12/site-packages/wandb/apis/__pycache__/attrs.cpython-312.pyc +0 -0
  12. lib/python3.12/site-packages/wandb/apis/__pycache__/internal.cpython-312.pyc +0 -0
  13. lib/python3.12/site-packages/wandb/apis/__pycache__/normalize.cpython-312.pyc +0 -0
  14. lib/python3.12/site-packages/wandb/apis/__pycache__/paginator.cpython-312.pyc +0 -0
  15. lib/python3.12/site-packages/wandb/apis/attrs.py +50 -0
  16. lib/python3.12/site-packages/wandb/apis/importers/__init__.py +1 -0
  17. lib/python3.12/site-packages/wandb/apis/importers/__pycache__/__init__.cpython-312.pyc +0 -0
  18. lib/python3.12/site-packages/wandb/apis/importers/__pycache__/mlflow.cpython-312.pyc +0 -0
  19. lib/python3.12/site-packages/wandb/apis/importers/__pycache__/validation.cpython-312.pyc +0 -0
  20. lib/python3.12/site-packages/wandb/apis/importers/__pycache__/wandb.cpython-312.pyc +0 -0
  21. lib/python3.12/site-packages/wandb/apis/importers/internals/__pycache__/internal.cpython-312.pyc +0 -0
  22. lib/python3.12/site-packages/wandb/apis/importers/internals/__pycache__/protocols.cpython-312.pyc +0 -0
  23. lib/python3.12/site-packages/wandb/apis/importers/internals/__pycache__/util.cpython-312.pyc +0 -0
  24. lib/python3.12/site-packages/wandb/apis/importers/internals/internal.py +382 -0
  25. lib/python3.12/site-packages/wandb/apis/importers/internals/protocols.py +103 -0
  26. lib/python3.12/site-packages/wandb/apis/importers/internals/util.py +78 -0
  27. lib/python3.12/site-packages/wandb/apis/importers/mlflow.py +254 -0
  28. lib/python3.12/site-packages/wandb/apis/importers/validation.py +108 -0
  29. lib/python3.12/site-packages/wandb/apis/importers/wandb.py +1608 -0
  30. lib/python3.12/site-packages/wandb/apis/internal.py +239 -0
  31. lib/python3.12/site-packages/wandb/apis/normalize.py +81 -0
  32. lib/python3.12/site-packages/wandb/apis/paginator.py +138 -0
  33. lib/python3.12/site-packages/wandb/apis/public/__init__.py +35 -0
  34. lib/python3.12/site-packages/wandb/apis/public/__pycache__/__init__.cpython-312.pyc +0 -0
  35. lib/python3.12/site-packages/wandb/apis/public/__pycache__/api.cpython-312.pyc +0 -0
  36. lib/python3.12/site-packages/wandb/apis/public/__pycache__/artifacts.cpython-312.pyc +0 -0
  37. lib/python3.12/site-packages/wandb/apis/public/__pycache__/automations.cpython-312.pyc +0 -0
  38. lib/python3.12/site-packages/wandb/apis/public/__pycache__/const.cpython-312.pyc +0 -0
  39. lib/python3.12/site-packages/wandb/apis/public/__pycache__/files.cpython-312.pyc +0 -0
  40. lib/python3.12/site-packages/wandb/apis/public/__pycache__/history.cpython-312.pyc +0 -0
  41. lib/python3.12/site-packages/wandb/apis/public/__pycache__/integrations.cpython-312.pyc +0 -0
  42. lib/python3.12/site-packages/wandb/apis/public/__pycache__/jobs.cpython-312.pyc +0 -0
  43. lib/python3.12/site-packages/wandb/apis/public/__pycache__/projects.cpython-312.pyc +0 -0
  44. lib/python3.12/site-packages/wandb/apis/public/__pycache__/query_generator.cpython-312.pyc +0 -0
  45. lib/python3.12/site-packages/wandb/apis/public/__pycache__/reports.cpython-312.pyc +0 -0
  46. lib/python3.12/site-packages/wandb/apis/public/__pycache__/runs.cpython-312.pyc +0 -0
  47. lib/python3.12/site-packages/wandb/apis/public/__pycache__/sweeps.cpython-312.pyc +0 -0
  48. lib/python3.12/site-packages/wandb/apis/public/__pycache__/teams.cpython-312.pyc +0 -0
  49. lib/python3.12/site-packages/wandb/apis/public/__pycache__/users.cpython-312.pyc +0 -0
  50. lib/python3.12/site-packages/wandb/apis/public/__pycache__/utils.cpython-312.pyc +0 -0
lib/python3.12/site-packages/wandb/agents/__init__.py ADDED
File without changes
lib/python3.12/site-packages/wandb/agents/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (191 Bytes). View file
 
lib/python3.12/site-packages/wandb/agents/__pycache__/pyagent.cpython-312.pyc ADDED
Binary file (19 kB). View file
 
lib/python3.12/site-packages/wandb/agents/pyagent.py ADDED
@@ -0,0 +1,357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Agent - Agent object.
2
+
3
+ Manage wandb agent.
4
+
5
+ """
6
+
7
+ import ctypes
8
+ import logging
9
+ import os
10
+ import queue
11
+ import socket
12
+ import threading
13
+ import time
14
+ import traceback
15
+
16
+ import wandb
17
+ from wandb.apis import InternalApi
18
+ from wandb.sdk.launch.sweeps import utils as sweep_utils
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def _terminate_thread(thread):
24
+ if not thread.is_alive():
25
+ return
26
+ if hasattr(thread, "_terminated"):
27
+ return
28
+ thread._terminated = True
29
+ tid = getattr(thread, "_thread_id", None)
30
+ if tid is None:
31
+ for k, v in threading._active.items():
32
+ if v is thread:
33
+ tid = k
34
+ if tid is None:
35
+ # This should never happen
36
+ return
37
+ logger.debug(f"Terminating thread: {tid}")
38
+ res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
39
+ ctypes.c_long(tid), ctypes.py_object(Exception)
40
+ )
41
+ if res == 0:
42
+ # This should never happen
43
+ return
44
+ elif res != 1:
45
+ # Revert
46
+ logger.debug(f"Termination failed for thread {tid}")
47
+ ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), None)
48
+
49
+
50
+ class Job:
51
+ def __init__(self, command):
52
+ self.command = command
53
+ job_type = command.get("type")
54
+ self.type = job_type
55
+ self.run_id = command.get("run_id")
56
+ self.config = command.get("args")
57
+
58
+ def __repr__(self):
59
+ if self.type == "run":
60
+ return f"Job({self.run_id},{self.config})"
61
+ elif self.type == "stop":
62
+ return f"stop({self.run_id})"
63
+ else:
64
+ return "exit"
65
+
66
+
67
+ class RunStatus:
68
+ QUEUED = "QUEUED"
69
+ RUNNING = "RUNNING"
70
+ STOPPED = "STOPPED"
71
+ ERRORED = "ERRORED"
72
+ DONE = "DONE"
73
+
74
+
75
+ class Agent:
76
+ FLAPPING_MAX_SECONDS = 60
77
+ FLAPPING_MAX_FAILURES = 3
78
+ MAX_INITIAL_FAILURES = 5
79
+
80
+ def __init__(
81
+ self, sweep_id=None, project=None, entity=None, function=None, count=None
82
+ ):
83
+ self._sweep_path = sweep_id
84
+ self._sweep_id = None
85
+ self._project = project
86
+ self._entity = entity
87
+ self._function = function
88
+ self._count = count
89
+ # glob_config = os.path.expanduser('~/.config/wandb/settings')
90
+ # loc_config = 'wandb/settings'
91
+ # files = (glob_config, loc_config)
92
+ self._api = InternalApi()
93
+ self._agent_id = None
94
+ self._max_initial_failures = wandb.env.get_agent_max_initial_failures(
95
+ self.MAX_INITIAL_FAILURES
96
+ )
97
+ # if the directory to log to is not set, set it
98
+ if os.environ.get(wandb.env.DIR) is None:
99
+ os.environ[wandb.env.DIR] = os.path.abspath(os.getcwd())
100
+
101
+ def _init(self):
102
+ # These are not in constructor so that Agent instance can be rerun
103
+ self._run_threads = {}
104
+ self._run_status = {}
105
+ self._queue = queue.Queue()
106
+ self._exit_flag = False
107
+ self._exceptions = {}
108
+ self._start_time = time.time()
109
+
110
+ def _register(self):
111
+ logger.debug("Agent._register()")
112
+ agent = self._api.register_agent(socket.gethostname(), sweep_id=self._sweep_id)
113
+ self._agent_id = agent["id"]
114
+ logger.debug(f"agent_id = {self._agent_id}")
115
+
116
+ def _setup(self):
117
+ logger.debug("Agent._setup()")
118
+ self._init()
119
+ parts = dict(entity=self._entity, project=self._project, name=self._sweep_path)
120
+ err = sweep_utils.parse_sweep_id(parts)
121
+ if err:
122
+ wandb.termerror(err)
123
+ return
124
+ entity = parts.get("entity") or self._entity
125
+ project = parts.get("project") or self._project
126
+ sweep_id = parts.get("name") or self._sweep_id
127
+ if sweep_id:
128
+ os.environ[wandb.env.SWEEP_ID] = sweep_id
129
+ if entity:
130
+ wandb.env.set_entity(entity)
131
+ if project:
132
+ wandb.env.set_project(project)
133
+ if sweep_id:
134
+ self._sweep_id = sweep_id
135
+ self._register()
136
+
137
+ def _stop_run(self, run_id):
138
+ logger.debug(f"Stopping run {run_id}.")
139
+ self._run_status[run_id] = RunStatus.STOPPED
140
+ thread = self._run_threads.get(run_id)
141
+ if thread:
142
+ _terminate_thread(thread)
143
+
144
+ def _stop_all_runs(self):
145
+ logger.debug("Stopping all runs.")
146
+ for run in list(self._run_threads.keys()):
147
+ self._stop_run(run)
148
+
149
+ def _exit(self):
150
+ self._stop_all_runs()
151
+ self._exit_flag = True
152
+ # _terminate_thread(self._main_thread)
153
+
154
+ def _heartbeat(self):
155
+ while True:
156
+ if self._exit_flag:
157
+ return
158
+ # if not self._main_thread.is_alive():
159
+ # return
160
+ run_status = {
161
+ run: True
162
+ for run, status in self._run_status.items()
163
+ if status in (RunStatus.QUEUED, RunStatus.RUNNING)
164
+ }
165
+ commands = self._api.agent_heartbeat(self._agent_id, {}, run_status)
166
+ if commands:
167
+ job = Job(commands[0])
168
+ logger.debug(f"Job received: {job}")
169
+ if job.type in ["run", "resume"]:
170
+ self._queue.put(job)
171
+ self._run_status[job.run_id] = RunStatus.QUEUED
172
+ elif job.type == "stop":
173
+ self._stop_run(job.run_id)
174
+ elif job.type == "exit":
175
+ self._exit()
176
+ return
177
+ time.sleep(5)
178
+
179
+ def _run_jobs_from_queue(self):
180
+ global _INSTANCES
181
+ _INSTANCES += 1
182
+ try:
183
+ waiting = False
184
+ count = 0
185
+ while True:
186
+ if self._exit_flag:
187
+ return
188
+ try:
189
+ try:
190
+ job = self._queue.get(timeout=5)
191
+ if self._exit_flag:
192
+ logger.debug("Exiting main loop due to exit flag.")
193
+ wandb.termlog("Sweep Agent: Exiting.")
194
+ return
195
+ except queue.Empty:
196
+ if not waiting:
197
+ logger.debug("Paused.")
198
+ wandb.termlog("Sweep Agent: Waiting for job.")
199
+ waiting = True
200
+ time.sleep(5)
201
+ if self._exit_flag:
202
+ logger.debug("Exiting main loop due to exit flag.")
203
+ wandb.termlog("Sweep Agent: Exiting.")
204
+ return
205
+ continue
206
+ if waiting:
207
+ logger.debug("Resumed.")
208
+ wandb.termlog("Job received.")
209
+ waiting = False
210
+ count += 1
211
+ run_id = job.run_id
212
+ if self._run_status[run_id] == RunStatus.STOPPED:
213
+ continue
214
+ logger.debug(f"Spawning new thread for run {run_id}.")
215
+ thread = threading.Thread(target=self._run_job, args=(job,))
216
+ self._run_threads[run_id] = thread
217
+ thread.start()
218
+ self._run_status[run_id] = RunStatus.RUNNING
219
+ thread.join()
220
+ logger.debug(f"Thread joined for run {run_id}.")
221
+ if self._run_status[run_id] == RunStatus.RUNNING:
222
+ self._run_status[run_id] = RunStatus.DONE
223
+ elif self._run_status[run_id] == RunStatus.ERRORED:
224
+ exc = self._exceptions[run_id]
225
+ exc_type, exc_value, exc_traceback = (
226
+ exc.__class__,
227
+ exc,
228
+ exc.__traceback__,
229
+ )
230
+ exc_traceback_formatted = traceback.format_exception(
231
+ exc_type, exc_value, exc_traceback
232
+ )
233
+ exc_repr = "".join(exc_traceback_formatted)
234
+ logger.error(f"Run {run_id} errored:\n{exc_repr}")
235
+ wandb.termerror(f"Run {run_id} errored:\n{exc_repr}")
236
+ if os.getenv(wandb.env.AGENT_DISABLE_FLAPPING) == "true":
237
+ self._exit_flag = True
238
+ return
239
+ elif (
240
+ time.time() - self._start_time < self.FLAPPING_MAX_SECONDS
241
+ ) and (len(self._exceptions) >= self.FLAPPING_MAX_FAILURES):
242
+ msg = f"Detected {self.FLAPPING_MAX_FAILURES} failed runs in the first {self.FLAPPING_MAX_SECONDS} seconds, killing sweep."
243
+ logger.error(msg)
244
+ wandb.termerror(msg)
245
+ wandb.termlog(
246
+ "To disable this check set WANDB_AGENT_DISABLE_FLAPPING=true"
247
+ )
248
+ self._exit_flag = True
249
+ return
250
+ if (
251
+ self._max_initial_failures < len(self._exceptions)
252
+ and len(self._exceptions) >= count
253
+ ):
254
+ msg = f"Detected {self._max_initial_failures} failed runs in a row at start, killing sweep."
255
+ logger.error(msg)
256
+ wandb.termerror(msg)
257
+ wandb.termlog(
258
+ "To change this value set WANDB_AGENT_MAX_INITIAL_FAILURES=val"
259
+ )
260
+ self._exit_flag = True
261
+ return
262
+ if self._count and self._count == count:
263
+ logger.debug("Exiting main loop because max count reached.")
264
+ self._exit_flag = True
265
+ return
266
+ except KeyboardInterrupt:
267
+ logger.debug("Ctrl + C detected. Stopping sweep.")
268
+ wandb.termlog("Ctrl + C detected. Stopping sweep.")
269
+ self._exit()
270
+ return
271
+ except Exception:
272
+ if self._exit_flag:
273
+ logger.debug("Exiting main loop due to exit flag.")
274
+ wandb.termlog("Sweep Agent: Killed.")
275
+ return
276
+ else:
277
+ raise
278
+ finally:
279
+ _INSTANCES -= 1
280
+
281
+ def _run_job(self, job):
282
+ try:
283
+ run_id = job.run_id
284
+
285
+ config_file = os.path.join(
286
+ "wandb", "sweep-" + self._sweep_id, "config-" + run_id + ".yaml"
287
+ )
288
+ os.environ[wandb.env.RUN_ID] = run_id
289
+ base_dir = os.environ.get(wandb.env.DIR, "")
290
+ sweep_param_path = os.path.join(base_dir, config_file)
291
+ os.environ[wandb.env.SWEEP_PARAM_PATH] = sweep_param_path
292
+ wandb.wandb_lib.config_util.save_config_file_from_dict(
293
+ sweep_param_path, job.config
294
+ )
295
+ os.environ[wandb.env.SWEEP_ID] = self._sweep_id
296
+ wandb.teardown()
297
+
298
+ wandb.termlog(f"Agent Starting Run: {run_id} with config:")
299
+ for k, v in job.config.items():
300
+ wandb.termlog("\t{}: {}".format(k, v["value"]))
301
+
302
+ self._function()
303
+ wandb.finish()
304
+ except KeyboardInterrupt:
305
+ raise
306
+ except Exception as e:
307
+ wandb.finish(exit_code=1)
308
+ if self._run_status[run_id] == RunStatus.RUNNING:
309
+ self._run_status[run_id] = RunStatus.ERRORED
310
+ self._exceptions[run_id] = e
311
+ finally:
312
+ # clean up the environment changes made
313
+ os.environ.pop(wandb.env.RUN_ID, None)
314
+ os.environ.pop(wandb.env.SWEEP_ID, None)
315
+ os.environ.pop(wandb.env.SWEEP_PARAM_PATH, None)
316
+
317
+ def run(self):
318
+ logger.info(
319
+ f"Starting sweep agent: entity={self._entity}, project={self._project}, count={self._count}"
320
+ )
321
+ self._setup()
322
+ # self._main_thread = threading.Thread(target=self._run_jobs_from_queue)
323
+ self._heartbeat_thread = threading.Thread(target=self._heartbeat)
324
+ self._heartbeat_thread.daemon = True
325
+ # self._main_thread.start()
326
+ self._heartbeat_thread.start()
327
+ # self._main_thread.join()
328
+ self._run_jobs_from_queue()
329
+
330
+
331
+ def pyagent(sweep_id, function, entity=None, project=None, count=None):
332
+ """Generic agent entrypoint, used for CLI or jupyter.
333
+
334
+ Args:
335
+ sweep_id (dict): Sweep ID generated by CLI or sweep API
336
+ function (func, optional): A function to call instead of the "program"
337
+ entity (str, optional): W&B Entity
338
+ project (str, optional): W&B Project
339
+ count (int, optional): the number of trials to run.
340
+ """
341
+ if not callable(function):
342
+ raise TypeError("function parameter must be callable!")
343
+ agent = Agent(
344
+ sweep_id,
345
+ function=function,
346
+ entity=entity,
347
+ project=project,
348
+ count=count,
349
+ )
350
+ agent.run()
351
+
352
+
353
+ _INSTANCES = 0
354
+
355
+
356
+ def is_running():
357
+ return bool(_INSTANCES)
lib/python3.12/site-packages/wandb/analytics/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ __all__ = ("Sentry",)
2
+
3
+ from .sentry import Sentry
lib/python3.12/site-packages/wandb/analytics/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (252 Bytes). View file
 
lib/python3.12/site-packages/wandb/analytics/__pycache__/sentry.cpython-312.pyc ADDED
Binary file (10.9 kB). View file
 
lib/python3.12/site-packages/wandb/analytics/sentry.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ __all__ = ("Sentry",)
4
+
5
+
6
+ import atexit
7
+ import functools
8
+ import os
9
+ import pathlib
10
+ import sys
11
+ from types import TracebackType
12
+ from typing import TYPE_CHECKING, Any, Callable, Literal
13
+ from urllib.parse import quote
14
+
15
+ import sentry_sdk # type: ignore
16
+ import sentry_sdk.scope # type: ignore
17
+ import sentry_sdk.utils # type: ignore
18
+ from typing_extensions import Never
19
+
20
+ import wandb
21
+ import wandb.env
22
+ import wandb.util
23
+
24
+ if TYPE_CHECKING:
25
+ import wandb.sdk.internal.settings_static
26
+
27
+ SENTRY_DEFAULT_DSN = (
28
+ "https://2592b1968ea94cca9b5ef5e348e094a7@o151352.ingest.sentry.io/4504800232407040"
29
+ )
30
+
31
+ SessionStatus = Literal["ok", "exited", "crashed", "abnormal"]
32
+
33
+
34
+ def _safe_noop(func: Callable) -> Callable:
35
+ """Decorator to ensure that Sentry methods do nothing if disabled and don't raise."""
36
+
37
+ @functools.wraps(func)
38
+ def wrapper(self: type[Sentry], *args: Any, **kwargs: Any) -> Any:
39
+ if self._disabled:
40
+ return None
41
+ try:
42
+ return func(self, *args, **kwargs)
43
+ except Exception as e:
44
+ # do not call self.exception here to avoid infinite recursion
45
+ if func.__name__ != "exception":
46
+ self.exception(f"Error in {func.__name__}: {e}")
47
+ return None
48
+
49
+ return wrapper
50
+
51
+
52
+ class Sentry:
53
+ _disabled: bool
54
+
55
+ def __init__(self) -> None:
56
+ self._disabled = not wandb.env.error_reporting_enabled()
57
+ self._sent_messages: set = set()
58
+
59
+ self.dsn = os.environ.get(wandb.env.SENTRY_DSN, SENTRY_DEFAULT_DSN)
60
+
61
+ self.scope: sentry_sdk.scope.Scope | None = None
62
+
63
+ # ensure we always end the Sentry session
64
+ atexit.register(self.end_session)
65
+
66
+ @property
67
+ def environment(self) -> str:
68
+ """Return the environment we're running in."""
69
+ # check if we're in a git repo
70
+ is_git = pathlib.Path(__file__).parent.parent.parent.joinpath(".git").exists()
71
+
72
+ # these match the environments for gorilla
73
+ return "development" if is_git else "production"
74
+
75
+ @_safe_noop
76
+ def setup(self) -> None:
77
+ """Setup Sentry SDK.
78
+
79
+ We use lower-level APIs (i.e., not sentry_sdk.init) here
80
+ to avoid the possibility of interfering with the user's
81
+ own Sentry SDK setup.
82
+ """
83
+ client = sentry_sdk.Client(
84
+ dsn=self.dsn,
85
+ default_integrations=False,
86
+ environment=self.environment,
87
+ release=wandb.__version__,
88
+ )
89
+ self.scope = sentry_sdk.get_global_scope().fork()
90
+ self.scope.clear()
91
+ self.scope.set_client(client)
92
+
93
+ @_safe_noop
94
+ def message(self, message: str, repeat: bool = True) -> str | None:
95
+ """Send a message to Sentry."""
96
+ if not repeat and message in self._sent_messages:
97
+ return None
98
+ self._sent_messages.add(message)
99
+ with sentry_sdk.scope.use_isolation_scope(self.scope): # type: ignore
100
+ return sentry_sdk.capture_message(message) # type: ignore
101
+
102
+ @_safe_noop
103
+ def exception(
104
+ self,
105
+ exc: str
106
+ | BaseException
107
+ | tuple[
108
+ type[BaseException] | None,
109
+ BaseException | None,
110
+ TracebackType | None,
111
+ ]
112
+ | None,
113
+ handled: bool = False,
114
+ status: SessionStatus | None = None,
115
+ ) -> str | None:
116
+ """Log an exception to Sentry."""
117
+ if isinstance(exc, str):
118
+ exc_info = sentry_sdk.utils.exc_info_from_error(Exception(exc))
119
+ elif isinstance(exc, BaseException):
120
+ exc_info = sentry_sdk.utils.exc_info_from_error(exc)
121
+ else:
122
+ exc_info = sys.exc_info()
123
+
124
+ event, _ = sentry_sdk.utils.event_from_exception(
125
+ exc_info,
126
+ client_options=self.scope.get_client().options, # type: ignore
127
+ mechanism={"type": "generic", "handled": handled},
128
+ )
129
+ event_id = None
130
+ try:
131
+ with sentry_sdk.scope.use_isolation_scope(self.scope): # type: ignore
132
+ event_id = sentry_sdk.capture_event(event) # type: ignore
133
+ except Exception:
134
+ pass
135
+
136
+ # if the status is not explicitly set, we'll set it to "crashed" if the exception
137
+ # was unhandled, or "errored" if it was handled
138
+ status = status or ("crashed" if not handled else "errored") # type: ignore
139
+ self.mark_session(status=status)
140
+
141
+ client = self.scope.get_client() # type: ignore
142
+ if client is not None:
143
+ client.flush()
144
+
145
+ return event_id
146
+
147
+ def reraise(self, exc: Any) -> Never:
148
+ """Re-raise an exception after logging it to Sentry.
149
+
150
+ Use this for top-level exceptions when you want the user to see the traceback.
151
+
152
+ Must be called from within an exception handler.
153
+ """
154
+ self.exception(exc)
155
+ # this will messily add this "reraise" function to the stack trace,
156
+ # but hopefully it's not too bad
157
+ raise exc.with_traceback(sys.exc_info()[2])
158
+
159
+ @_safe_noop
160
+ def start_session(self) -> None:
161
+ """Start a new session."""
162
+ assert self.scope is not None
163
+ # get the current client and scope
164
+ session = self.scope._session
165
+
166
+ # if there's no session, start one
167
+ if session is None:
168
+ self.scope.start_session()
169
+
170
+ @_safe_noop
171
+ def end_session(self) -> None:
172
+ """End the current session."""
173
+ assert self.scope is not None
174
+ # get the current client and scope
175
+ client = self.scope.get_client()
176
+ session = self.scope._session
177
+
178
+ if session is not None and client is not None:
179
+ self.scope.end_session()
180
+ client.flush()
181
+
182
+ @_safe_noop
183
+ def mark_session(self, status: SessionStatus | None = None) -> None:
184
+ """Mark the current session with a status."""
185
+ assert self.scope is not None
186
+ session = self.scope._session
187
+
188
+ if session is not None:
189
+ session.update(status=status)
190
+
191
+ @_safe_noop
192
+ def configure_scope(
193
+ self,
194
+ tags: dict[str, Any] | None = None,
195
+ process_context: str | None = None,
196
+ ) -> None:
197
+ """Configure the Sentry scope for the current thread.
198
+
199
+ This function should be called at the beginning of every thread that
200
+ will send events to Sentry. It sets the tags that will be applied to
201
+ all events sent from this thread. It also tries to start a session
202
+ if one doesn't already exist for this thread.
203
+ """
204
+ assert self.scope is not None
205
+ settings_tags = (
206
+ "entity",
207
+ "project",
208
+ "run_id",
209
+ "run_url",
210
+ "sweep_url",
211
+ "sweep_id",
212
+ "deployment",
213
+ "launch",
214
+ "_platform",
215
+ )
216
+
217
+ # set context
218
+ if process_context:
219
+ self.scope.set_tag("process_context", process_context)
220
+
221
+ # apply settings tags
222
+ if tags is None:
223
+ return None
224
+
225
+ for tag in settings_tags:
226
+ val = tags.get(tag, None)
227
+ if val not in (None, ""):
228
+ self.scope.set_tag(tag, val)
229
+
230
+ if tags.get("_colab", None):
231
+ python_runtime = "colab"
232
+ elif tags.get("_jupyter", None):
233
+ python_runtime = "jupyter"
234
+ elif tags.get("_ipython", None):
235
+ python_runtime = "ipython"
236
+ else:
237
+ python_runtime = "python"
238
+ self.scope.set_tag("python_runtime", python_runtime)
239
+
240
+ # Construct run_url and sweep_url given run_id and sweep_id
241
+ for obj in ("run", "sweep"):
242
+ obj_id, obj_url = f"{obj}_id", f"{obj}_url"
243
+ if tags.get(obj_url, None):
244
+ continue
245
+
246
+ try:
247
+ app_url = wandb.util.app_url(tags["base_url"]) # type: ignore
248
+ entity, project = (quote(tags[k]) for k in ("entity", "project")) # type: ignore
249
+ self.scope.set_tag(
250
+ obj_url,
251
+ f"{app_url}/{entity}/{project}/{obj}s/{tags[obj_id]}",
252
+ )
253
+ except Exception:
254
+ pass
255
+
256
+ email = tags.get("email")
257
+ if email:
258
+ self.scope.user = {"email": email}
259
+
260
+ # todo: add back the option to pass general tags see: c645f625d1c1a3db4a6b0e2aa8e924fee101904c (wandb/util.py)
261
+
262
+ self.start_session()
lib/python3.12/site-packages/wandb/apis/__init__.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """api."""
2
+
3
+ from typing import Callable
4
+
5
+ import requests
6
+ from urllib3.exceptions import InsecureRequestWarning
7
+
8
+ import wandb
9
+ from wandb import env, util
10
+
11
+
12
+ def _disable_ssl() -> Callable[[], None]:
13
+ # Because third party libraries may also use requests, we monkey patch it globally
14
+ # and turn off urllib3 warnings instead printing a global warning to the user.
15
+ wandb.termwarn(
16
+ "Disabling SSL verification. Connections to this server are not verified and may be insecure!"
17
+ )
18
+
19
+ requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
20
+ old_merge_environment_settings = requests.Session.merge_environment_settings
21
+
22
+ def merge_environment_settings(self, url, proxies, stream, verify, cert):
23
+ settings = old_merge_environment_settings(
24
+ self, url, proxies, stream, verify, cert
25
+ )
26
+ settings["verify"] = False
27
+ return settings
28
+
29
+ requests.Session.merge_environment_settings = merge_environment_settings
30
+
31
+ def reset():
32
+ requests.Session.merge_environment_settings = old_merge_environment_settings
33
+
34
+ return reset
35
+
36
+
37
+ if env.ssl_disabled():
38
+ _disable_ssl()
39
+
40
+
41
+ reset_path = util.vendor_setup()
42
+
43
+ from .internal import Api as InternalApi # noqa
44
+ from .public import Api as PublicApi # noqa
45
+
46
+ reset_path()
47
+
48
+ __all__ = ["InternalApi", "PublicApi"]
lib/python3.12/site-packages/wandb/apis/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (1.9 kB). View file
 
lib/python3.12/site-packages/wandb/apis/__pycache__/attrs.cpython-312.pyc ADDED
Binary file (2.82 kB). View file
 
lib/python3.12/site-packages/wandb/apis/__pycache__/internal.cpython-312.pyc ADDED
Binary file (14.8 kB). View file
 
lib/python3.12/site-packages/wandb/apis/__pycache__/normalize.cpython-312.pyc ADDED
Binary file (3.63 kB). View file
 
lib/python3.12/site-packages/wandb/apis/__pycache__/paginator.cpython-312.pyc ADDED
Binary file (6.51 kB). View file
 
lib/python3.12/site-packages/wandb/apis/attrs.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, MutableMapping
2
+
3
+ import wandb
4
+
5
+ from ..sdk.lib import ipython
6
+
7
+
8
+ class Attrs:
9
+ def __init__(self, attrs: MutableMapping[str, Any]):
10
+ self._attrs = attrs
11
+
12
+ def snake_to_camel(self, string):
13
+ camel = "".join([i.title() for i in string.split("_")])
14
+ return camel[0].lower() + camel[1:]
15
+
16
+ def display(self, height=420, hidden=False) -> bool:
17
+ """Display this object in jupyter."""
18
+ if wandb.run and wandb.run._settings.silent:
19
+ return False
20
+
21
+ if not ipython.in_jupyter():
22
+ return False
23
+
24
+ html = self.to_html(height, hidden)
25
+ if html is None:
26
+ wandb.termwarn("This object does not support `.display()`")
27
+ return False
28
+
29
+ try:
30
+ from IPython import display
31
+ except ImportError:
32
+ wandb.termwarn(".display() only works in jupyter environments")
33
+ return False
34
+
35
+ display.display(display.HTML(html))
36
+ return True
37
+
38
+ def to_html(self, *args, **kwargs):
39
+ return None
40
+
41
+ def __getattr__(self, name):
42
+ key = self.snake_to_camel(name)
43
+ if key == "user":
44
+ raise AttributeError
45
+ if key in self._attrs.keys():
46
+ return self._attrs[key]
47
+ elif name in self._attrs.keys():
48
+ return self._attrs[name]
49
+ else:
50
+ raise AttributeError(f"{repr(self)!r} object has no attribute {name!r}")
lib/python3.12/site-packages/wandb/apis/importers/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .internals.util import Namespace
lib/python3.12/site-packages/wandb/apis/importers/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (250 Bytes). View file
 
lib/python3.12/site-packages/wandb/apis/importers/__pycache__/mlflow.cpython-312.pyc ADDED
Binary file (13.7 kB). View file
 
lib/python3.12/site-packages/wandb/apis/importers/__pycache__/validation.cpython-312.pyc ADDED
Binary file (4.95 kB). View file
 
lib/python3.12/site-packages/wandb/apis/importers/__pycache__/wandb.cpython-312.pyc ADDED
Binary file (73.6 kB). View file
 
lib/python3.12/site-packages/wandb/apis/importers/internals/__pycache__/internal.cpython-312.pyc ADDED
Binary file (19.2 kB). View file
 
lib/python3.12/site-packages/wandb/apis/importers/internals/__pycache__/protocols.cpython-312.pyc ADDED
Binary file (5.59 kB). View file
 
lib/python3.12/site-packages/wandb/apis/importers/internals/__pycache__/util.cpython-312.pyc ADDED
Binary file (3.8 kB). View file
 
lib/python3.12/site-packages/wandb/apis/importers/internals/internal.py ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import math
4
+ import os
5
+ import queue
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Iterable, Optional
9
+
10
+ import numpy as np
11
+ from google.protobuf.json_format import ParseDict
12
+ from tenacity import retry, stop_after_attempt, wait_random_exponential
13
+
14
+ from wandb import Artifact
15
+ from wandb.proto import wandb_internal_pb2 as pb
16
+ from wandb.proto import wandb_settings_pb2
17
+ from wandb.proto import wandb_telemetry_pb2 as telem_pb
18
+ from wandb.sdk.interface.interface import file_policy_to_enum
19
+ from wandb.sdk.interface.interface_queue import InterfaceQueue
20
+ from wandb.sdk.internal import context
21
+ from wandb.sdk.internal.sender import SendManager
22
+ from wandb.sdk.internal.settings_static import SettingsStatic
23
+ from wandb.util import coalesce, recursive_cast_dictlike_to_dict
24
+
25
+ from .protocols import ImporterRun
26
+
27
+ ROOT_DIR = "./wandb-importer"
28
+
29
+
30
+ logger = logging.getLogger(__name__)
31
+ logger.setLevel(logging.INFO)
32
+
33
+ if os.getenv("WANDB_IMPORTER_ENABLE_RICH_LOGGING"):
34
+ from rich.logging import RichHandler
35
+
36
+ logger.addHandler(RichHandler(rich_tracebacks=True, tracebacks_show_locals=True))
37
+ else:
38
+ console_handler = logging.StreamHandler()
39
+ console_handler.setLevel(logging.INFO)
40
+
41
+ formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
42
+ console_handler.setFormatter(formatter)
43
+
44
+ logger.addHandler(console_handler)
45
+
46
+
47
+ exp_retry = retry(
48
+ wait=wait_random_exponential(multiplier=1, max=10), stop=stop_after_attempt(3)
49
+ )
50
+
51
+
52
+ class AlternateSendManager(SendManager):
53
+ def __init__(self, *args, **kwargs):
54
+ super().__init__(*args, **kwargs)
55
+ self._send_artifact = exp_retry(self._send_artifact)
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class SendManagerConfig:
60
+ """Configure which parts of SendManager tooling to use."""
61
+
62
+ use_artifacts: bool = False
63
+ log_artifacts: bool = False
64
+ metadata: bool = False
65
+ files: bool = False
66
+ media: bool = False
67
+ code: bool = False
68
+ history: bool = False
69
+ summary: bool = False
70
+ terminal_output: bool = False
71
+
72
+
73
+ @dataclass
74
+ class RecordMaker:
75
+ run: ImporterRun
76
+ interface: InterfaceQueue = InterfaceQueue()
77
+
78
+ @property
79
+ def run_dir(self) -> str:
80
+ p = Path(f"{ROOT_DIR}/{self.run.run_id()}/wandb")
81
+ p.mkdir(parents=True, exist_ok=True)
82
+ return f"{ROOT_DIR}/{self.run.run_id()}"
83
+
84
+ def make_artifacts_only_records(
85
+ self,
86
+ artifacts: Optional[Iterable[Artifact]] = None,
87
+ used_artifacts: Optional[Iterable[Artifact]] = None,
88
+ ) -> Iterable[pb.Record]:
89
+ """Only make records required to upload artifacts.
90
+
91
+ Escape hatch for adding extra artifacts to a run.
92
+ """
93
+ yield self._make_run_record()
94
+
95
+ if used_artifacts:
96
+ for art in used_artifacts:
97
+ yield self._make_artifact_record(art, use_artifact=True)
98
+
99
+ if artifacts:
100
+ for art in artifacts:
101
+ yield self._make_artifact_record(art)
102
+
103
+ def make_records(
104
+ self,
105
+ config: SendManagerConfig,
106
+ ) -> Iterable[pb.Record]:
107
+ """Make all the records that constitute a run."""
108
+ yield self._make_run_record()
109
+ yield self._make_telem_record()
110
+
111
+ include_artifacts = config.log_artifacts or config.use_artifacts
112
+ yield self._make_files_record(
113
+ include_artifacts, config.files, config.media, config.code
114
+ )
115
+
116
+ if config.use_artifacts:
117
+ if (used_artifacts := self.run.used_artifacts()) is not None:
118
+ for artifact in used_artifacts:
119
+ yield self._make_artifact_record(artifact, use_artifact=True)
120
+
121
+ if config.log_artifacts:
122
+ if (artifacts := self.run.artifacts()) is not None:
123
+ for artifact in artifacts:
124
+ yield self._make_artifact_record(artifact)
125
+
126
+ if config.history:
127
+ yield from self._make_history_records()
128
+
129
+ if config.summary:
130
+ yield self._make_summary_record()
131
+
132
+ if config.terminal_output:
133
+ if (lines := self.run.logs()) is not None:
134
+ for line in lines:
135
+ yield self._make_output_record(line)
136
+
137
+ def _make_run_record(self) -> pb.Record:
138
+ run = pb.RunRecord()
139
+ run.run_id = self.run.run_id()
140
+ run.entity = self.run.entity()
141
+ run.project = self.run.project()
142
+ run.display_name = coalesce(self.run.display_name())
143
+ run.notes = coalesce(self.run.notes(), "")
144
+ run.tags.extend(coalesce(self.run.tags(), []))
145
+ run.start_time.FromMilliseconds(self.run.start_time())
146
+
147
+ host = self.run.host()
148
+ if host is not None:
149
+ run.host = host
150
+
151
+ runtime = self.run.runtime()
152
+ if runtime is not None:
153
+ run.runtime = runtime
154
+
155
+ run_group = self.run.run_group()
156
+ if run_group is not None:
157
+ run.run_group = run_group
158
+
159
+ config = self.run.config()
160
+ if "_wandb" not in config:
161
+ config["_wandb"] = {}
162
+
163
+ # how do I get this automatically?
164
+ config["_wandb"]["code_path"] = self.run.code_path()
165
+ config["_wandb"]["python_version"] = self.run.python_version()
166
+ config["_wandb"]["cli_version"] = self.run.cli_version()
167
+
168
+ self.interface._make_config(
169
+ data=config,
170
+ obj=run.config,
171
+ ) # is there a better way?
172
+ return self.interface._make_record(run=run)
173
+
174
+ def _make_output_record(self, line) -> pb.Record:
175
+ output_raw = pb.OutputRawRecord()
176
+ output_raw.output_type = pb.OutputRawRecord.OutputType.STDOUT
177
+ output_raw.line = line
178
+ return self.interface._make_record(output_raw=output_raw)
179
+
180
+ def _make_summary_record(self) -> pb.Record:
181
+ d: dict = {
182
+ **self.run.summary(),
183
+ "_runtime": self.run.runtime(), # quirk of runtime -- it has to be here!
184
+ # '_timestamp': self.run.start_time()/1000,
185
+ }
186
+ d = recursive_cast_dictlike_to_dict(d)
187
+ summary = self.interface._make_summary_from_dict(d)
188
+ return self.interface._make_record(summary=summary)
189
+
190
+ def _make_history_records(self) -> Iterable[pb.Record]:
191
+ for metrics in self.run.metrics():
192
+ history = pb.HistoryRecord()
193
+ for k, v in metrics.items():
194
+ item = history.item.add()
195
+ item.key = k
196
+ # There seems to be some conversion issue to breaks when we try to re-upload.
197
+ # np.NaN gets converted to float("nan"), which is not expected by our system.
198
+ # If this cast to string (!) is not done, the row will be dropped.
199
+ if (isinstance(v, float) and math.isnan(v)) or v == "NaN":
200
+ v = np.NaN
201
+
202
+ if isinstance(v, bytes):
203
+ # it's a json string encoded as bytes
204
+ v = v.decode("utf-8")
205
+ else:
206
+ v = json.dumps(v)
207
+
208
+ item.value_json = v
209
+ rec = self.interface._make_record(history=history)
210
+ yield rec
211
+
212
+ def _make_files_record(
213
+ self, artifacts: bool, files: bool, media: bool, code: bool
214
+ ) -> pb.Record:
215
+ run_files = self.run.files()
216
+ metadata_fname = f"{self.run_dir}/files/wandb-metadata.json"
217
+ if not files or run_files is None:
218
+ # We'll always need a metadata file even if there are no other files to upload
219
+ metadata_fname = self._make_metadata_file()
220
+ run_files = [(metadata_fname, "end")]
221
+ files_record = pb.FilesRecord()
222
+ for path, policy in run_files:
223
+ if not artifacts and path.startswith("artifact/"):
224
+ continue
225
+ if not media and path.startswith("media/"):
226
+ continue
227
+ if not code and path.startswith("code/"):
228
+ continue
229
+
230
+ # DirWatcher requires the path to start with media/ instead of the full path
231
+ if "media" in path:
232
+ p = Path(path)
233
+ path = str(p.relative_to(f"{self.run_dir}/files"))
234
+ f = files_record.files.add()
235
+ f.path = path
236
+ f.policy = file_policy_to_enum(policy)
237
+
238
+ return self.interface._make_record(files=files_record)
239
+
240
+ def _make_artifact_record(
241
+ self, artifact: Artifact, use_artifact=False
242
+ ) -> pb.Record:
243
+ proto = self.interface._make_artifact(artifact)
244
+ proto.run_id = str(self.run.run_id())
245
+ proto.project = str(self.run.project())
246
+ proto.entity = str(self.run.entity())
247
+ proto.user_created = use_artifact
248
+ proto.use_after_commit = use_artifact
249
+ proto.finalize = True
250
+
251
+ aliases = artifact._aliases
252
+ aliases += ["latest", "imported"]
253
+
254
+ for alias in aliases:
255
+ proto.aliases.append(alias)
256
+ return self.interface._make_record(artifact=proto)
257
+
258
+ def _make_telem_record(self) -> pb.Record:
259
+ telem = telem_pb.TelemetryRecord()
260
+
261
+ feature = telem_pb.Feature()
262
+ feature.importer_mlflow = True
263
+ telem.feature.CopyFrom(feature)
264
+
265
+ cli_version = self.run.cli_version()
266
+ if cli_version:
267
+ telem.cli_version = cli_version
268
+
269
+ python_version = self.run.python_version()
270
+ if python_version:
271
+ telem.python_version = python_version
272
+
273
+ return self.interface._make_record(telemetry=telem)
274
+
275
+ def _make_metadata_file(self) -> str:
276
+ missing_text = "This data was not captured"
277
+ files_dir = f"{self.run_dir}/files"
278
+ os.makedirs(files_dir, exist_ok=True)
279
+
280
+ d = {}
281
+ d["os"] = coalesce(self.run.os_version(), missing_text)
282
+ d["python"] = coalesce(self.run.python_version(), missing_text)
283
+ d["program"] = coalesce(self.run.program(), missing_text)
284
+ d["cuda"] = coalesce(self.run.cuda_version(), missing_text)
285
+ d["host"] = coalesce(self.run.host(), missing_text)
286
+ d["username"] = coalesce(self.run.username(), missing_text)
287
+ d["executable"] = coalesce(self.run.executable(), missing_text)
288
+
289
+ gpus_used = self.run.gpus_used()
290
+ if gpus_used is not None:
291
+ d["gpu_devices"] = json.dumps(gpus_used)
292
+ d["gpu_count"] = json.dumps(len(gpus_used))
293
+
294
+ cpus_used = self.run.cpus_used()
295
+ if cpus_used is not None:
296
+ d["cpu_count"] = json.dumps(self.run.cpus_used())
297
+
298
+ mem_used = self.run.memory_used()
299
+ if mem_used is not None:
300
+ d["memory"] = json.dumps({"total": self.run.memory_used()})
301
+
302
+ fname = f"{files_dir}/wandb-metadata.json"
303
+ with open(fname, "w") as f:
304
+ f.write(json.dumps(d))
305
+ return fname
306
+
307
+
308
+ def _make_settings(
309
+ root_dir: str, settings_override: Optional[Dict[str, Any]] = None
310
+ ) -> SettingsStatic:
311
+ _settings_override = coalesce(settings_override, {})
312
+
313
+ default_settings: Dict[str, Any] = {
314
+ "x_files_dir": os.path.join(root_dir, "files"),
315
+ "root_dir": root_dir,
316
+ "sync_file": os.path.join(root_dir, "txlog.wandb"),
317
+ "resume": "false",
318
+ "program": None,
319
+ "ignore_globs": [],
320
+ "disable_job_creation": True,
321
+ "x_start_time": 0,
322
+ "_offline": None,
323
+ "x_sync": True,
324
+ "x_live_policy_rate_limit": 15, # matches dir_watcher
325
+ "x_live_policy_wait_time": 600, # matches dir_watcher
326
+ "x_file_stream_timeout_seconds": 60,
327
+ }
328
+
329
+ combined_settings = {**default_settings, **_settings_override}
330
+ settings_message = wandb_settings_pb2.Settings()
331
+ ParseDict(combined_settings, settings_message)
332
+
333
+ return SettingsStatic(settings_message)
334
+
335
+
336
+ def send_run(
337
+ run: ImporterRun,
338
+ *,
339
+ extra_arts: Optional[Iterable[Artifact]] = None,
340
+ extra_used_arts: Optional[Iterable[Artifact]] = None,
341
+ config: Optional[SendManagerConfig] = None,
342
+ overrides: Optional[Dict[str, Any]] = None,
343
+ settings_override: Optional[Dict[str, Any]] = None,
344
+ ) -> None:
345
+ if config is None:
346
+ config = SendManagerConfig()
347
+
348
+ # does this need to be here for pmap?
349
+ if overrides:
350
+ for k, v in overrides.items():
351
+ # `lambda: v` won't work!
352
+ # https://stackoverflow.com/questions/10802002/why-deepcopy-doesnt-create-new-references-to-lambda-function
353
+ setattr(run, k, lambda v=v: v)
354
+
355
+ rm = RecordMaker(run)
356
+ root_dir = rm.run_dir
357
+
358
+ settings = _make_settings(root_dir, settings_override)
359
+ sm_record_q = queue.Queue()
360
+ # wm_record_q = queue.Queue()
361
+ result_q = queue.Queue()
362
+ interface = InterfaceQueue(record_q=sm_record_q)
363
+ context_keeper = context.ContextKeeper()
364
+ sm = AlternateSendManager(
365
+ settings, sm_record_q, result_q, interface, context_keeper
366
+ )
367
+
368
+ if extra_arts or extra_used_arts:
369
+ records = rm.make_artifacts_only_records(extra_arts, extra_used_arts)
370
+ else:
371
+ records = rm.make_records(config)
372
+
373
+ for r in records:
374
+ logger.debug(f"Sending {r=}")
375
+ # In a future update, it might be good to write to a transaction log and have
376
+ # incremental uploads only send the missing records.
377
+ # wm.write(r)
378
+
379
+ sm.send(r)
380
+
381
+ sm.finish()
382
+ # wm.finish()
lib/python3.12/site-packages/wandb/apis/importers/internals/protocols.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import (
3
+ Any,
4
+ Dict,
5
+ Iterable,
6
+ List,
7
+ Literal,
8
+ Optional,
9
+ Protocol,
10
+ Tuple,
11
+ runtime_checkable,
12
+ )
13
+
14
+ from wandb.sdk.artifacts.artifact import Artifact
15
+
16
+ logger = logging.getLogger("import_logger")
17
+
18
+ PathStr = str
19
+ Policy = Literal["now", "end", "live"]
20
+
21
+
22
+ @runtime_checkable
23
+ class ImporterRun(Protocol):
24
+ def run_id(self) -> str: ... # pragma: no cover
25
+
26
+ def entity(self) -> str: ... # pragma: no cover
27
+
28
+ def project(self) -> str: ... # pragma: no cover
29
+
30
+ def config(self) -> Dict[str, Any]: ... # pragma: no cover
31
+
32
+ def summary(self) -> Dict[str, float]: ... # pragma: no cover
33
+
34
+ def metrics(self) -> Iterable[Dict[str, float]]:
35
+ """Metrics for the run.
36
+
37
+ We expect metrics in this shape:
38
+
39
+ [
40
+ {'metric1': 1, 'metric2': 1, '_step': 0},
41
+ {'metric1': 2, 'metric2': 4, '_step': 1},
42
+ {'metric1': 3, 'metric2': 9, '_step': 2},
43
+ ...
44
+ ]
45
+
46
+ You can also submit metrics in this shape:
47
+ [
48
+ {'metric1': 1, '_step': 0},
49
+ {'metric2': 1, '_step': 0},
50
+ {'metric1': 2, '_step': 1},
51
+ {'metric2': 4, '_step': 1},
52
+ ...
53
+ ]
54
+ """
55
+ ... # pragma: no cover
56
+
57
+ def run_group(self) -> Optional[str]: ... # pragma: no cover
58
+
59
+ def job_type(self) -> Optional[str]: ... # pragma: no cover
60
+
61
+ def display_name(self) -> str: ... # pragma: no cover
62
+
63
+ def notes(self) -> Optional[str]: ... # pragma: no cover
64
+
65
+ def tags(self) -> Optional[List[str]]: ... # pragma: no cover
66
+
67
+ def artifacts(self) -> Optional[Iterable[Artifact]]: ... # pragma: no cover
68
+
69
+ def used_artifacts(self) -> Optional[Iterable[Artifact]]: ... # pragma: no cover
70
+
71
+ def os_version(self) -> Optional[str]: ... # pragma: no cover
72
+
73
+ def python_version(self) -> Optional[str]: ... # pragma: no cover
74
+
75
+ def cuda_version(self) -> Optional[str]: ... # pragma: no cover
76
+
77
+ def program(self) -> Optional[str]: ... # pragma: no cover
78
+
79
+ def host(self) -> Optional[str]: ... # pragma: no cover
80
+
81
+ def username(self) -> Optional[str]: ... # pragma: no cover
82
+
83
+ def executable(self) -> Optional[str]: ... # pragma: no cover
84
+
85
+ def gpus_used(self) -> Optional[str]: ... # pragma: no cover
86
+
87
+ def cpus_used(self) -> Optional[int]: ... # pragma: no cover
88
+
89
+ def memory_used(self) -> Optional[int]: ... # pragma: no cover
90
+
91
+ def runtime(self) -> Optional[int]: ... # pragma: no cover
92
+
93
+ def start_time(self) -> Optional[int]: ... # pragma: no cover
94
+
95
+ def code_path(self) -> Optional[str]: ... # pragma: no cover
96
+
97
+ def cli_version(self) -> Optional[str]: ... # pragma: no cover
98
+
99
+ def files(
100
+ self,
101
+ ) -> Optional[Iterable[Tuple[PathStr, Policy]]]: ... # pragma: no cover
102
+
103
+ def logs(self) -> Optional[Iterable[str]]: ... # pragma: no cover
lib/python3.12/site-packages/wandb/apis/importers/internals/util.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import sys
3
+ import traceback
4
+ from concurrent.futures import ThreadPoolExecutor, as_completed
5
+ from dataclasses import dataclass
6
+ from typing import Iterable, Optional
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class Namespace:
11
+ """Configure an alternate entity/project at the dst server your data will end up in."""
12
+
13
+ entity: str
14
+ project: str
15
+
16
+ @classmethod
17
+ def from_path(cls, path: str):
18
+ entity, project = path.split("/")
19
+ return cls(entity, project)
20
+
21
+ @property
22
+ def path(self):
23
+ return f"{self.entity}/{self.project}"
24
+
25
+ @property
26
+ def send_manager_overrides(self):
27
+ overrides = {}
28
+ if self.entity:
29
+ overrides["entity"] = self.entity
30
+ if self.project:
31
+ overrides["project"] = self.project
32
+ return overrides
33
+
34
+
35
+ logger = logging.getLogger("import_logger")
36
+
37
+
38
+ def parallelize(
39
+ func,
40
+ iterable: Iterable,
41
+ *args,
42
+ max_workers: Optional[int] = None,
43
+ raise_on_error: bool = False,
44
+ **kwargs,
45
+ ):
46
+ def safe_func(*args, **kwargs):
47
+ try:
48
+ return func(*args, **kwargs)
49
+ except Exception as e:
50
+ _, _, exc_traceback = sys.exc_info()
51
+ traceback_details = traceback.extract_tb(exc_traceback)
52
+ filename = traceback_details[-1].filename
53
+ lineno = traceback_details[-1].lineno
54
+ logger.debug(
55
+ f"Exception: {func=} {args=} {kwargs=} {e=} {filename=} {lineno=}. {traceback_details=}"
56
+ )
57
+ if raise_on_error:
58
+ raise
59
+
60
+ results = []
61
+ with ThreadPoolExecutor(max_workers) as exc:
62
+ futures = {exc.submit(safe_func, x, *args, **kwargs): x for x in iterable}
63
+ for future in as_completed(futures):
64
+ results.append(future.result())
65
+ return results
66
+
67
+
68
+ def for_each(
69
+ func, iterable: Iterable, parallel: bool = True, max_workers: Optional[int] = None
70
+ ):
71
+ if parallel:
72
+ return parallelize(
73
+ func,
74
+ iterable,
75
+ max_workers=max_workers,
76
+ )
77
+
78
+ return [func(x) for x in iterable]
lib/python3.12/site-packages/wandb/apis/importers/mlflow.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import itertools
2
+ import logging
3
+ import re
4
+ from collections import defaultdict
5
+ from typing import Any, Dict, Iterable, List, Optional, Tuple
6
+
7
+ import mlflow
8
+ from packaging.version import Version # type: ignore
9
+
10
+ import wandb
11
+ from wandb import Artifact
12
+
13
+ from .internals import internal
14
+ from .internals.util import Namespace, for_each
15
+
16
+ mlflow_version = Version(mlflow.__version__)
17
+
18
+ logger = logging.getLogger("import_logger")
19
+
20
+
21
+ class MlflowRun:
22
+ def __init__(self, run, mlflow_client):
23
+ self.run = run
24
+ self.mlflow_client: mlflow.MlflowClient = mlflow_client
25
+
26
+ def run_id(self) -> str:
27
+ return self.run.info.run_id
28
+
29
+ def entity(self) -> str:
30
+ return self.run.info.user_id
31
+
32
+ def project(self) -> str:
33
+ return "imported-from-mlflow"
34
+
35
+ def config(self) -> Dict[str, Any]:
36
+ conf = self.run.data.params
37
+
38
+ # Add tags here since mlflow supports very long tag names but we only support up to 64 chars
39
+ tags = {
40
+ k: v for k, v in self.run.data.tags.items() if not k.startswith("mlflow.")
41
+ }
42
+ return {**conf, "imported_mlflow_tags": tags}
43
+
44
+ def summary(self) -> Dict[str, float]:
45
+ return self.run.data.metrics
46
+
47
+ def metrics(self) -> Iterable[Dict[str, float]]:
48
+ d: Dict[int, Dict[str, float]] = defaultdict(dict)
49
+ for k in self.run.data.metrics.keys():
50
+ metric = self.mlflow_client.get_metric_history(self.run.info.run_id, k)
51
+ for item in metric:
52
+ d[item.step][item.key] = item.value
53
+
54
+ for k, v in d.items():
55
+ yield {"_step": k, **v}
56
+
57
+ def run_group(self) -> Optional[str]:
58
+ # this is nesting? Parent at `run.info.tags.get("mlflow.parentRunId")`
59
+ return f"Experiment {self.run.info.experiment_id}"
60
+
61
+ def job_type(self) -> Optional[str]:
62
+ # Is this the right approach?
63
+ return f"User {self.run.info.user_id}"
64
+
65
+ def display_name(self) -> str:
66
+ if mlflow_version < Version("1.30.0"):
67
+ return self.run.data.tags["mlflow.runName"]
68
+ return self.run.info.run_name
69
+
70
+ def notes(self) -> Optional[str]:
71
+ return self.run.data.tags.get("mlflow.note.content")
72
+
73
+ def tags(self) -> Optional[List[str]]:
74
+ ...
75
+
76
+ # W&B tags are different than mlflow tags.
77
+ # The full mlflow tags are added to config under key `imported_mlflow_tags` instead
78
+
79
+ def artifacts(self) -> Optional[Iterable[Artifact]]: # type: ignore
80
+ if mlflow_version < Version("2.0.0"):
81
+ dir_path = self.mlflow_client.download_artifacts(
82
+ run_id=self.run.info.run_id,
83
+ path="",
84
+ )
85
+ else:
86
+ dir_path = mlflow.artifacts.download_artifacts(run_id=self.run.info.run_id)
87
+
88
+ # Since mlflow doesn't have extra metadata about the artifacts,
89
+ # we just lump them all together into a single wandb.Artifact
90
+ artifact_name = self._handle_incompatible_strings(self.display_name())
91
+ art = wandb.Artifact(artifact_name, "imported-artifacts")
92
+ art.add_dir(dir_path)
93
+
94
+ return [art]
95
+
96
+ def used_artifacts(self) -> Optional[Iterable[Artifact]]: # type: ignore
97
+ ... # pragma: no cover
98
+
99
+ def os_version(self) -> Optional[str]: ... # pragma: no cover
100
+
101
+ def python_version(self) -> Optional[str]: ... # pragma: no cover
102
+
103
+ def cuda_version(self) -> Optional[str]: ... # pragma: no cover
104
+
105
+ def program(self) -> Optional[str]: ... # pragma: no cover
106
+
107
+ def host(self) -> Optional[str]: ... # pragma: no cover
108
+
109
+ def username(self) -> Optional[str]: ... # pragma: no cover
110
+
111
+ def executable(self) -> Optional[str]: ... # pragma: no cover
112
+
113
+ def gpus_used(self) -> Optional[str]: ... # pragma: no cover
114
+
115
+ def cpus_used(self) -> Optional[int]: # can we get the model?
116
+ ... # pragma: no cover
117
+
118
+ def memory_used(self) -> Optional[int]: ... # pragma: no cover
119
+
120
+ def runtime(self) -> Optional[int]:
121
+ end_time = (
122
+ self.run.info.end_time // 1000
123
+ if self.run.info.end_time is not None
124
+ else self.start_time()
125
+ )
126
+ return end_time - self.start_time()
127
+
128
+ def start_time(self) -> Optional[int]:
129
+ return self.run.info.start_time // 1000
130
+
131
+ def code_path(self) -> Optional[str]: ... # pragma: no cover
132
+
133
+ def cli_version(self) -> Optional[str]: ... # pragma: no cover
134
+
135
+ def files(self) -> Optional[Iterable[Tuple[str, str]]]: ... # pragma: no cover
136
+
137
+ def logs(self) -> Optional[Iterable[str]]: ... # pragma: no cover
138
+
139
+ @staticmethod
140
+ def _handle_incompatible_strings(s: str) -> str:
141
+ valid_chars = r"[^a-zA-Z0-9_\-\.]"
142
+ replacement = "__"
143
+
144
+ return re.sub(valid_chars, replacement, s)
145
+
146
+
147
+ class MlflowImporter:
148
+ def __init__(
149
+ self,
150
+ dst_base_url: str,
151
+ dst_api_key: str,
152
+ mlflow_tracking_uri: str,
153
+ mlflow_registry_uri: Optional[str] = None,
154
+ *,
155
+ custom_api_kwargs: Optional[Dict[str, Any]] = None,
156
+ ) -> None:
157
+ self.dst_base_url = dst_base_url
158
+ self.dst_api_key = dst_api_key
159
+
160
+ if custom_api_kwargs is None:
161
+ custom_api_kwargs = {"timeout": 600}
162
+
163
+ self.dst_api = wandb.Api(
164
+ api_key=dst_api_key,
165
+ overrides={"base_url": dst_base_url},
166
+ **custom_api_kwargs,
167
+ )
168
+ self.mlflow_tracking_uri = mlflow_tracking_uri
169
+ mlflow.set_tracking_uri(self.mlflow_tracking_uri)
170
+
171
+ if mlflow_registry_uri:
172
+ mlflow.set_registry_uri(mlflow_registry_uri)
173
+
174
+ self.mlflow_client = mlflow.tracking.MlflowClient(mlflow_tracking_uri)
175
+
176
+ def __repr__(self):
177
+ return f"<MlflowImporter src={self.mlflow_tracking_uri}>"
178
+
179
+ def collect_runs(self, *, limit: Optional[int] = None) -> Iterable[MlflowRun]:
180
+ if mlflow_version < Version("1.28.0"):
181
+ experiments = self.mlflow_client.list_experiments()
182
+ else:
183
+ experiments = self.mlflow_client.search_experiments()
184
+
185
+ def _runs():
186
+ for exp in experiments:
187
+ for run in self.mlflow_client.search_runs(exp.experiment_id):
188
+ yield MlflowRun(run, self.mlflow_client)
189
+
190
+ runs = itertools.islice(_runs(), limit)
191
+ yield from runs
192
+
193
+ def _import_run(
194
+ self,
195
+ run: MlflowRun,
196
+ *,
197
+ artifacts: bool = True,
198
+ namespace: Optional[Namespace] = None,
199
+ config: Optional[internal.SendManagerConfig] = None,
200
+ ) -> None:
201
+ if namespace is None:
202
+ namespace = Namespace(run.entity(), run.project())
203
+
204
+ if config is None:
205
+ config = internal.SendManagerConfig(
206
+ metadata=True,
207
+ files=True,
208
+ media=True,
209
+ code=True,
210
+ history=True,
211
+ summary=True,
212
+ terminal_output=True,
213
+ )
214
+
215
+ settings_override = {
216
+ "api_key": self.dst_api_key,
217
+ "base_url": self.dst_base_url,
218
+ "resume": "true",
219
+ "resumed": True,
220
+ }
221
+
222
+ mlflow.set_tracking_uri(self.mlflow_tracking_uri)
223
+ internal.send_run(
224
+ run,
225
+ overrides=namespace.send_manager_overrides,
226
+ settings_override=settings_override,
227
+ config=config,
228
+ )
229
+
230
+ # in mlflow, the artifacts come with the runs, so import them together
231
+ if artifacts:
232
+ arts = list(run.artifacts())
233
+ logger.debug(f"Importing history artifacts, {run=}")
234
+ internal.send_run(
235
+ run,
236
+ extra_arts=arts,
237
+ overrides=namespace.send_manager_overrides,
238
+ settings_override=settings_override,
239
+ config=internal.SendManagerConfig(log_artifacts=True),
240
+ )
241
+
242
+ def import_runs(
243
+ self,
244
+ runs: Iterable[MlflowRun],
245
+ *,
246
+ artifacts: bool = True,
247
+ namespace: Optional[Namespace] = None,
248
+ parallel: bool = True,
249
+ max_workers: Optional[int] = None,
250
+ ) -> None:
251
+ def _import_run_wrapped(run):
252
+ self._import_run(run, namespace=namespace, artifacts=artifacts)
253
+
254
+ for_each(_import_run_wrapped, runs, parallel=parallel, max_workers=max_workers)
lib/python3.12/site-packages/wandb/apis/importers/validation.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import filecmp
2
+ import logging
3
+ import os
4
+
5
+ import requests
6
+
7
+ import wandb
8
+
9
+ logger = logging.getLogger(__name__)
10
+ logger.setLevel(logging.INFO)
11
+
12
+
13
+ def _compare_artifact_manifests(
14
+ src_art: wandb.Artifact, dst_art: wandb.Artifact
15
+ ) -> list:
16
+ problems = []
17
+ if isinstance(dst_art, wandb.CommError):
18
+ return ["commError"]
19
+
20
+ if src_art.digest != dst_art.digest:
21
+ problems.append(f"digest mismatch {src_art.digest=}, {dst_art.digest=}")
22
+
23
+ for name, src_entry in src_art.manifest.entries.items():
24
+ dst_entry = dst_art.manifest.entries.get(name)
25
+ if dst_entry is None:
26
+ problems.append(f"missing manifest entry {name=}, {src_entry=}")
27
+ continue
28
+
29
+ for attr in ["path", "digest", "size"]:
30
+ if getattr(src_entry, attr) != getattr(dst_entry, attr):
31
+ problems.append(
32
+ f"manifest entry mismatch {attr=}, {getattr(src_entry, attr)=}, {getattr(dst_entry, attr)=}"
33
+ )
34
+
35
+ return problems
36
+
37
+
38
+ def _compare_artifact_dirs(src_dir, dst_dir) -> list:
39
+ def compare(src_dir, dst_dir):
40
+ comparison = filecmp.dircmp(src_dir, dst_dir)
41
+ differences = {
42
+ "left_only": comparison.left_only,
43
+ "right_only": comparison.right_only,
44
+ "diff_files": comparison.diff_files,
45
+ "subdir_differences": {},
46
+ }
47
+
48
+ # Recursively find differences in subdirectories
49
+ for subdir in comparison.subdirs:
50
+ subdir_src = os.path.join(src_dir, subdir)
51
+ subdir_dst = os.path.join(dst_dir, subdir)
52
+ subdir_differences = compare(subdir_src, subdir_dst)
53
+ # If there are differences, add them to the result
54
+ if subdir_differences and any(subdir_differences.values()):
55
+ differences["subdir_differences"][subdir] = subdir_differences
56
+
57
+ if all(not diff for diff in differences.values()):
58
+ return None
59
+
60
+ return differences
61
+
62
+ return compare(src_dir, dst_dir)
63
+
64
+
65
+ def _check_entries_are_downloadable(art):
66
+ entries = _collect_entries(art)
67
+ for entry in entries:
68
+ if not _check_entry_is_downloable(entry):
69
+ return False
70
+ return True
71
+
72
+
73
+ def _collect_entries(art):
74
+ has_next_page = True
75
+ cursor = None
76
+ entries = []
77
+ while has_next_page:
78
+ attrs = art._fetch_file_urls(cursor)
79
+ has_next_page = attrs["pageInfo"]["hasNextPage"]
80
+ cursor = attrs["pageInfo"]["endCursor"]
81
+ for edge in attrs["edges"]:
82
+ name = edge["node"]["name"]
83
+ entry = art.get_entry(name)
84
+ entry._download_url = edge["node"]["directUrl"]
85
+ entries.append(entry)
86
+ return entries
87
+
88
+
89
+ def _check_entry_is_downloable(entry):
90
+ url = entry._download_url
91
+ expected_size = entry.size
92
+
93
+ try:
94
+ resp = requests.head(url, allow_redirects=True)
95
+ except Exception:
96
+ logger.exception(f"Problem validating {entry=}")
97
+ return False
98
+
99
+ if resp.status_code != 200:
100
+ return False
101
+
102
+ actual_size = resp.headers.get("content-length", -1)
103
+ actual_size = int(actual_size)
104
+
105
+ if expected_size == actual_size:
106
+ return True
107
+
108
+ return False
lib/python3.12/site-packages/wandb/apis/importers/wandb.py ADDED
@@ -0,0 +1,1608 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tooling for the W&B Importer."""
2
+
3
+ import itertools
4
+ import json
5
+ import logging
6
+ import numbers
7
+ import os
8
+ import re
9
+ import shutil
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime as dt
12
+ from pathlib import Path
13
+ from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple
14
+ from unittest.mock import patch
15
+
16
+ import filelock
17
+ import polars as pl
18
+ import requests
19
+ import urllib3
20
+ import wandb_workspaces.reports.v1 as wr
21
+ import yaml
22
+ from wandb_gql import gql
23
+ from wandb_workspaces.reports.v1 import Report
24
+
25
+ import wandb
26
+ from wandb.apis.public import ArtifactCollection, Run
27
+ from wandb.apis.public.files import File
28
+ from wandb.util import coalesce, remove_keys_with_none_values
29
+
30
+ from . import validation
31
+ from .internals import internal
32
+ from .internals.protocols import PathStr, Policy
33
+ from .internals.util import Namespace, for_each
34
+
35
+ Artifact = wandb.Artifact
36
+ Api = wandb.Api
37
+ Project = wandb.apis.public.Project
38
+
39
+ ARTIFACT_ERRORS_FNAME = "artifact_errors.jsonl"
40
+ ARTIFACT_SUCCESSES_FNAME = "artifact_successes.jsonl"
41
+ RUN_ERRORS_FNAME = "run_errors.jsonl"
42
+ RUN_SUCCESSES_FNAME = "run_successes.jsonl"
43
+
44
+ ART_SEQUENCE_DUMMY_PLACEHOLDER = "__ART_SEQUENCE_DUMMY_PLACEHOLDER__"
45
+ RUN_DUMMY_PLACEHOLDER = "__RUN_DUMMY_PLACEHOLDER__"
46
+ ART_DUMMY_PLACEHOLDER_PATH = "__importer_temp__"
47
+ ART_DUMMY_PLACEHOLDER_TYPE = "__temp__"
48
+
49
+ SRC_ART_PATH = "./artifacts/src"
50
+ DST_ART_PATH = "./artifacts/dst"
51
+
52
+
53
+ logger = logging.getLogger(__name__)
54
+ logger.setLevel(logging.INFO)
55
+
56
+ if os.getenv("WANDB_IMPORTER_ENABLE_RICH_LOGGING"):
57
+ from rich.logging import RichHandler
58
+
59
+ logger.addHandler(RichHandler(rich_tracebacks=True, tracebacks_show_locals=True))
60
+ else:
61
+ console_handler = logging.StreamHandler()
62
+ console_handler.setLevel(logging.INFO)
63
+
64
+ formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
65
+ console_handler.setFormatter(formatter)
66
+
67
+ logger.addHandler(console_handler)
68
+
69
+
70
+ @dataclass
71
+ class ArtifactSequence:
72
+ artifacts: Iterable[wandb.Artifact]
73
+ entity: str
74
+ project: str
75
+ type_: str
76
+ name: str
77
+
78
+ def __iter__(self) -> Iterator:
79
+ return iter(self.artifacts)
80
+
81
+ def __repr__(self) -> str:
82
+ return f"ArtifactSequence({self.identifier})"
83
+
84
+ @property
85
+ def identifier(self) -> str:
86
+ return "/".join([self.entity, self.project, self.type_, self.name])
87
+
88
+ @classmethod
89
+ def from_collection(cls, collection: ArtifactCollection):
90
+ arts = collection.artifacts()
91
+ arts = sorted(arts, key=lambda a: int(a.version.lstrip("v")))
92
+ return ArtifactSequence(
93
+ arts,
94
+ collection.entity,
95
+ collection.project,
96
+ collection.type,
97
+ collection.name,
98
+ )
99
+
100
+
101
+ class WandbRun:
102
+ def __init__(
103
+ self,
104
+ run: Run,
105
+ *,
106
+ src_base_url: str,
107
+ src_api_key: str,
108
+ dst_base_url: str,
109
+ dst_api_key: str,
110
+ ) -> None:
111
+ self.run = run
112
+ self.api = wandb.Api(
113
+ api_key=src_api_key,
114
+ overrides={"base_url": src_base_url},
115
+ )
116
+ self.dst_api = wandb.Api(
117
+ api_key=dst_api_key,
118
+ overrides={"base_url": dst_base_url},
119
+ )
120
+
121
+ # For caching
122
+ self._files: Optional[Iterable[Tuple[str, str]]] = None
123
+ self._artifacts: Optional[Iterable[Artifact]] = None
124
+ self._used_artifacts: Optional[Iterable[Artifact]] = None
125
+ self._parquet_history_paths: Optional[Iterable[str]] = None
126
+
127
+ def __repr__(self) -> str:
128
+ s = os.path.join(self.entity(), self.project(), self.run_id())
129
+ return f"WandbRun({s})"
130
+
131
+ def run_id(self) -> str:
132
+ return self.run.id
133
+
134
+ def entity(self) -> str:
135
+ return self.run.entity
136
+
137
+ def project(self) -> str:
138
+ return self.run.project
139
+
140
+ def config(self) -> Dict[str, Any]:
141
+ return self.run.config
142
+
143
+ def summary(self) -> Dict[str, float]:
144
+ s = self.run.summary
145
+ return s
146
+
147
+ def metrics(self) -> Iterable[Dict[str, float]]:
148
+ if self._parquet_history_paths is None:
149
+ self._parquet_history_paths = list(self._get_parquet_history_paths())
150
+
151
+ if self._parquet_history_paths:
152
+ rows = self._get_rows_from_parquet_history_paths()
153
+ else:
154
+ logger.warning(
155
+ "No parquet files detected; using scan history (this may not be reliable)"
156
+ )
157
+ rows = self.run.scan_history()
158
+
159
+ for row in rows:
160
+ row = remove_keys_with_none_values(row)
161
+ yield row
162
+
163
+ def run_group(self) -> Optional[str]:
164
+ return self.run.group
165
+
166
+ def job_type(self) -> Optional[str]:
167
+ return self.run.job_type
168
+
169
+ def display_name(self) -> str:
170
+ return self.run.display_name
171
+
172
+ def notes(self) -> Optional[str]:
173
+ # Notes includes the previous notes and serves as a catch-all for things we missed or can't add back
174
+ previous_link = f"Imported from: {self.run.url}"
175
+ previous_author = f"Author: {self.run.user.username}"
176
+
177
+ header = [previous_link, previous_author]
178
+ previous_notes = self.run.notes or ""
179
+
180
+ return "\n".join(header) + "\n---\n" + previous_notes
181
+
182
+ def tags(self) -> Optional[List[str]]:
183
+ return self.run.tags
184
+
185
+ def artifacts(self) -> Optional[Iterable[Artifact]]:
186
+ if self._artifacts is None:
187
+ _artifacts = []
188
+ for art in self.run.logged_artifacts():
189
+ a = _clone_art(art)
190
+ _artifacts.append(a)
191
+ self._artifacts = _artifacts
192
+
193
+ yield from self._artifacts
194
+
195
+ def used_artifacts(self) -> Optional[Iterable[Artifact]]:
196
+ if self._used_artifacts is None:
197
+ _used_artifacts = []
198
+ for art in self.run.used_artifacts():
199
+ a = _clone_art(art)
200
+ _used_artifacts.append(a)
201
+ self._used_artifacts = _used_artifacts
202
+
203
+ yield from self._used_artifacts
204
+
205
+ def os_version(self) -> Optional[str]: ... # pragma: no cover
206
+
207
+ def python_version(self) -> Optional[str]:
208
+ return self._metadata_file().get("python")
209
+
210
+ def cuda_version(self) -> Optional[str]: ... # pragma: no cover
211
+
212
+ def program(self) -> Optional[str]: ... # pragma: no cover
213
+
214
+ def host(self) -> Optional[str]:
215
+ return self._metadata_file().get("host")
216
+
217
+ def username(self) -> Optional[str]: ... # pragma: no cover
218
+
219
+ def executable(self) -> Optional[str]: ... # pragma: no cover
220
+
221
+ def gpus_used(self) -> Optional[str]: ... # pragma: no cover
222
+
223
+ def cpus_used(self) -> Optional[int]: # can we get the model?
224
+ ... # pragma: no cover
225
+
226
+ def memory_used(self) -> Optional[int]: ... # pragma: no cover
227
+
228
+ def runtime(self) -> Optional[int]:
229
+ wandb_runtime = self.run.summary.get("_wandb", {}).get("runtime")
230
+ base_runtime = self.run.summary.get("_runtime")
231
+
232
+ if (t := coalesce(wandb_runtime, base_runtime)) is None:
233
+ return t
234
+ return int(t)
235
+
236
+ def start_time(self) -> Optional[int]:
237
+ t = dt.fromisoformat(self.run.created_at).timestamp() * 1000
238
+ return int(t)
239
+
240
+ def code_path(self) -> Optional[str]:
241
+ path = self._metadata_file().get("codePath", "")
242
+ return f"code/{path}"
243
+
244
+ def cli_version(self) -> Optional[str]:
245
+ return self._config_file().get("_wandb", {}).get("value", {}).get("cli_version")
246
+
247
+ def files(self) -> Optional[Iterable[Tuple[PathStr, Policy]]]:
248
+ if self._files is None:
249
+ files_dir = f"{internal.ROOT_DIR}/{self.run_id()}/files"
250
+ _files = []
251
+ for f in self.run.files():
252
+ f: File
253
+ # These optimizations are intended to avoid rate limiting when importing many runs in parallel
254
+ # Don't carry over empty files
255
+ if f.size == 0:
256
+ continue
257
+ # Skip deadlist to avoid overloading S3
258
+ if "wandb_manifest.json.deadlist" in f.name:
259
+ continue
260
+
261
+ result = f.download(files_dir, exist_ok=True, api=self.api)
262
+ file_and_policy = (result.name, "end")
263
+ _files.append(file_and_policy)
264
+ self._files = _files
265
+
266
+ yield from self._files
267
+
268
+ def logs(self) -> Optional[Iterable[str]]:
269
+ log_files = self._find_all_in_files_regex(r"^.*output\.log$")
270
+ for path in log_files:
271
+ with open(path) as f:
272
+ yield from f.readlines()
273
+
274
+ def _metadata_file(self) -> Dict[str, Any]:
275
+ if (fname := self._find_in_files("wandb-metadata.json")) is None:
276
+ return {}
277
+
278
+ with open(fname) as f:
279
+ return json.loads(f.read())
280
+
281
+ def _config_file(self) -> Dict[str, Any]:
282
+ if (fname := self._find_in_files("config.yaml")) is None:
283
+ return {}
284
+
285
+ with open(fname) as f:
286
+ return yaml.safe_load(f) or {}
287
+
288
+ def _get_rows_from_parquet_history_paths(self) -> Iterable[Dict[str, Any]]:
289
+ # Unfortunately, it's not feasible to validate non-parquet history
290
+ if not (paths := self._get_parquet_history_paths()):
291
+ yield {}
292
+ return
293
+
294
+ # Collect and merge parquet history
295
+ dfs = [
296
+ pl.read_parquet(p) for path in paths for p in Path(path).glob("*.parquet")
297
+ ]
298
+ if "_step" in (df := _merge_dfs(dfs)):
299
+ df = df.with_columns(pl.col("_step").cast(pl.Int64))
300
+ yield from df.iter_rows(named=True)
301
+
302
+ def _get_parquet_history_paths(self) -> Iterable[str]:
303
+ if self._parquet_history_paths is None:
304
+ paths = []
305
+ # self.artifacts() returns a copy of the artifacts; use this to get raw
306
+ for art in self.run.logged_artifacts():
307
+ if art.type != "wandb-history":
308
+ continue
309
+ if (
310
+ path := _download_art(art, root=f"{SRC_ART_PATH}/{art.name}")
311
+ ) is None:
312
+ continue
313
+ paths.append(path)
314
+ self._parquet_history_paths = paths
315
+
316
+ yield from self._parquet_history_paths
317
+
318
+ def _find_in_files(self, name: str) -> Optional[str]:
319
+ if files := self.files():
320
+ for path, _ in files:
321
+ if name in path:
322
+ return path
323
+ return None
324
+
325
+ def _find_all_in_files_regex(self, regex: str) -> Iterable[str]:
326
+ if files := self.files():
327
+ for path, _ in files:
328
+ if re.match(regex, path):
329
+ yield path
330
+
331
+
332
+ class WandbImporter:
333
+ """Transfers runs, reports, and artifact sequences between W&B instances."""
334
+
335
+ def __init__(
336
+ self,
337
+ src_base_url: str,
338
+ src_api_key: str,
339
+ dst_base_url: str,
340
+ dst_api_key: str,
341
+ *,
342
+ custom_api_kwargs: Optional[Dict[str, Any]] = None,
343
+ ) -> None:
344
+ self.src_base_url = src_base_url
345
+ self.src_api_key = src_api_key
346
+ self.dst_base_url = dst_base_url
347
+ self.dst_api_key = dst_api_key
348
+
349
+ if custom_api_kwargs is None:
350
+ custom_api_kwargs = {"timeout": 600}
351
+
352
+ self.src_api = wandb.Api(
353
+ api_key=src_api_key,
354
+ overrides={"base_url": src_base_url},
355
+ **custom_api_kwargs,
356
+ )
357
+ self.dst_api = wandb.Api(
358
+ api_key=dst_api_key,
359
+ overrides={"base_url": dst_base_url},
360
+ **custom_api_kwargs,
361
+ )
362
+
363
+ self.run_api_kwargs = {
364
+ "src_base_url": src_base_url,
365
+ "src_api_key": src_api_key,
366
+ "dst_base_url": dst_base_url,
367
+ "dst_api_key": dst_api_key,
368
+ }
369
+
370
+ def __repr__(self):
371
+ return f"<WandbImporter src={self.src_base_url}, dst={self.dst_base_url}>" # pragma: no cover
372
+
373
+ def _import_run(
374
+ self,
375
+ run: WandbRun,
376
+ *,
377
+ namespace: Optional[Namespace] = None,
378
+ config: Optional[internal.SendManagerConfig] = None,
379
+ ) -> None:
380
+ """Import one WandbRun.
381
+
382
+ Use `namespace` to specify alternate settings like where the run should be uploaded
383
+ """
384
+ if namespace is None:
385
+ namespace = Namespace(run.entity(), run.project())
386
+
387
+ if config is None:
388
+ config = internal.SendManagerConfig(
389
+ metadata=True,
390
+ files=True,
391
+ media=True,
392
+ code=True,
393
+ history=True,
394
+ summary=True,
395
+ terminal_output=True,
396
+ )
397
+
398
+ settings_override = {
399
+ "api_key": self.dst_api_key,
400
+ "base_url": self.dst_base_url,
401
+ "resume": "true",
402
+ "resumed": True,
403
+ }
404
+
405
+ # Send run with base config
406
+ logger.debug(f"Importing run, {run=}")
407
+ internal.send_run(
408
+ run,
409
+ overrides=namespace.send_manager_overrides,
410
+ settings_override=settings_override,
411
+ config=config,
412
+ )
413
+
414
+ if config.history:
415
+ # Send run again with history artifacts in case config history=True, artifacts=False
416
+ # The history artifact must come with the actual history data
417
+
418
+ logger.debug(f"Collecting history artifacts, {run=}")
419
+ history_arts = []
420
+ for art in run.run.logged_artifacts():
421
+ if art.type != "wandb-history":
422
+ continue
423
+ logger.debug(f"Collecting history artifact {art.name=}")
424
+ new_art = _clone_art(art)
425
+ history_arts.append(new_art)
426
+
427
+ logger.debug(f"Importing history artifacts, {run=}")
428
+ internal.send_run(
429
+ run,
430
+ extra_arts=history_arts,
431
+ overrides=namespace.send_manager_overrides,
432
+ settings_override=settings_override,
433
+ config=config,
434
+ )
435
+
436
+ def _delete_collection_in_dst(
437
+ self,
438
+ seq: ArtifactSequence,
439
+ namespace: Optional[Namespace] = None,
440
+ ):
441
+ """Deletes the equivalent artifact collection in destination.
442
+
443
+ Intended to clear the destination when an uploaded artifact does not pass validation.
444
+ """
445
+ entity = coalesce(namespace.entity, seq.entity)
446
+ project = coalesce(namespace.project, seq.project)
447
+ art_type = f"{entity}/{project}/{seq.type_}"
448
+ art_name = seq.name
449
+
450
+ logger.info(
451
+ f"Deleting collection {entity=}, {project=}, {art_type=}, {art_name=}"
452
+ )
453
+ try:
454
+ dst_collection = self.dst_api.artifact_collection(art_type, art_name)
455
+ except (wandb.CommError, ValueError):
456
+ logger.warning(f"Collection doesn't exist {art_type=}, {art_name=}")
457
+ return
458
+
459
+ try:
460
+ dst_collection.delete()
461
+ except (wandb.CommError, ValueError) as e:
462
+ logger.warning(
463
+ f"Collection can't be deleted, {art_type=}, {art_name=}, {e=}"
464
+ )
465
+ return
466
+
467
+ def _import_artifact_sequence(
468
+ self,
469
+ seq: ArtifactSequence,
470
+ *,
471
+ namespace: Optional[Namespace] = None,
472
+ ) -> None:
473
+ """Import one artifact sequence.
474
+
475
+ Use `namespace` to specify alternate settings like where the artifact sequence should be uploaded
476
+ """
477
+ if not seq.artifacts:
478
+ # The artifact sequence has no versions. This usually means all artifacts versions were deleted intentionally,
479
+ # but it can also happen if the sequence represents run history and that run was deleted.
480
+ logger.warning(f"Artifact {seq=} has no artifacts, skipping.")
481
+ return
482
+
483
+ if namespace is None:
484
+ namespace = Namespace(seq.entity, seq.project)
485
+
486
+ settings_override = {
487
+ "api_key": self.dst_api_key,
488
+ "base_url": self.dst_base_url,
489
+ "resume": "true",
490
+ "resumed": True,
491
+ }
492
+
493
+ send_manager_config = internal.SendManagerConfig(log_artifacts=True)
494
+
495
+ # Delete any existing artifact sequence, otherwise versions will be out of order
496
+ # Unfortunately, you can't delete only part of the sequence because versions are "remembered" even after deletion
497
+ self._delete_collection_in_dst(seq, namespace)
498
+
499
+ # Get a placeholder run for dummy artifacts we'll upload later
500
+ art = seq.artifacts[0]
501
+ run_or_dummy: Optional[Run] = _get_run_or_dummy_from_art(art, self.src_api)
502
+
503
+ # Each `group_of_artifacts` is either:
504
+ # 1. A single "real" artifact in a list; or
505
+ # 2. A list of dummy artifacts that are uploaded together.
506
+ # This guarantees the real artifacts have the correct version numbers while allowing for parallel upload of dummies.
507
+ groups_of_artifacts = list(_make_groups_of_artifacts(seq))
508
+ for i, group in enumerate(groups_of_artifacts, 1):
509
+ art = group[0]
510
+ if art.description == ART_SEQUENCE_DUMMY_PLACEHOLDER:
511
+ run = WandbRun(run_or_dummy, **self.run_api_kwargs)
512
+ else:
513
+ try:
514
+ wandb_run = art.logged_by()
515
+ except ValueError:
516
+ # The run used to exist but has since been deleted
517
+ # wandb_run = None
518
+ pass
519
+
520
+ # Could be logged by None (rare) or ValueError
521
+ if wandb_run is None:
522
+ logger.warning(
523
+ f"Run for {art.name=} does not exist (deleted?), using {run_or_dummy=}"
524
+ )
525
+ wandb_run = run_or_dummy
526
+
527
+ new_art = _clone_art(art)
528
+ group = [new_art]
529
+ run = WandbRun(wandb_run, **self.run_api_kwargs)
530
+
531
+ logger.info(
532
+ f"Uploading partial artifact {seq=}, {i}/{len(groups_of_artifacts)}"
533
+ )
534
+ internal.send_run(
535
+ run,
536
+ extra_arts=group,
537
+ overrides=namespace.send_manager_overrides,
538
+ settings_override=settings_override,
539
+ config=send_manager_config,
540
+ )
541
+ logger.info(f"Finished uploading {seq=}")
542
+
543
+ # query it back and remove placeholders
544
+ self._remove_placeholders(seq)
545
+
546
+ def _remove_placeholders(self, seq: ArtifactSequence) -> None:
547
+ try:
548
+ retry_arts_func = internal.exp_retry(self._dst_api.artifacts)
549
+ dst_arts = list(retry_arts_func(seq.type_, seq.name))
550
+ except wandb.CommError:
551
+ logger.warning(
552
+ f"{seq=} does not exist in dst. Has it already been deleted?"
553
+ )
554
+ return
555
+ except TypeError:
556
+ logger.exception("Problem getting dst versions (try again later).")
557
+ return
558
+
559
+ for art in dst_arts:
560
+ if art.description != ART_SEQUENCE_DUMMY_PLACEHOLDER:
561
+ continue
562
+ if art.type in ("wandb-history", "job"):
563
+ continue
564
+
565
+ try:
566
+ art.delete(delete_aliases=True)
567
+ except wandb.CommError as e:
568
+ if "cannot delete system managed artifact" in str(e):
569
+ logger.warning("Cannot delete system managed artifact")
570
+ else:
571
+ raise
572
+
573
+ def _get_dst_art(
574
+ self, src_art: Run, entity: Optional[str] = None, project: Optional[str] = None
575
+ ) -> Artifact:
576
+ entity = coalesce(entity, src_art.entity)
577
+ project = coalesce(project, src_art.project)
578
+ name = src_art.name
579
+
580
+ return self.dst_api._artifact(f"{entity}/{project}/{name}")
581
+
582
+ def _get_run_problems(
583
+ self, src_run: Run, dst_run: Run, force_retry: bool = False
584
+ ) -> List[dict]:
585
+ problems = []
586
+
587
+ if force_retry:
588
+ problems.append("__force_retry__")
589
+
590
+ if non_matching_metadata := self._compare_run_metadata(src_run, dst_run):
591
+ problems.append("metadata:" + str(non_matching_metadata))
592
+
593
+ if non_matching_summary := self._compare_run_summary(src_run, dst_run):
594
+ problems.append("summary:" + str(non_matching_summary))
595
+
596
+ # TODO: Compare files?
597
+
598
+ return problems
599
+
600
+ def _compare_run_metadata(self, src_run: Run, dst_run: Run) -> dict:
601
+ fname = "wandb-metadata.json"
602
+ # problems = {}
603
+
604
+ src_f = src_run.file(fname)
605
+ if src_f.size == 0:
606
+ # the src was corrupted so no comparisons here will ever work
607
+ return {}
608
+
609
+ dst_f = dst_run.file(fname)
610
+ try:
611
+ contents = wandb.util.download_file_into_memory(
612
+ dst_f.url, self.dst_api.api_key
613
+ )
614
+ except urllib3.exceptions.ReadTimeoutError:
615
+ return {"Error checking": "Timeout"}
616
+ except requests.HTTPError as e:
617
+ if e.response.status_code == 404:
618
+ return {"Bad upload": f"File not found: {fname}"}
619
+ return {"http problem": f"{fname}: ({e})"}
620
+
621
+ dst_meta = wandb.wandb_sdk.lib.json_util.loads(contents)
622
+
623
+ non_matching = {}
624
+ if src_run.metadata:
625
+ for k, src_v in src_run.metadata.items():
626
+ if k not in dst_meta:
627
+ non_matching[k] = {"src": src_v, "dst": "KEY NOT FOUND"}
628
+ continue
629
+ dst_v = dst_meta[k]
630
+ if src_v != dst_v:
631
+ non_matching[k] = {"src": src_v, "dst": dst_v}
632
+
633
+ return non_matching
634
+
635
+ def _compare_run_summary(self, src_run: Run, dst_run: Run) -> dict:
636
+ non_matching = {}
637
+ for k, src_v in src_run.summary.items():
638
+ # These won't match between systems and that's ok
639
+ if isinstance(src_v, str) and src_v.startswith("wandb-client-artifact://"):
640
+ continue
641
+ if k in ("_wandb", "_runtime"):
642
+ continue
643
+
644
+ src_v = _recursive_cast_to_dict(src_v)
645
+
646
+ dst_v = dst_run.summary.get(k)
647
+ dst_v = _recursive_cast_to_dict(dst_v)
648
+
649
+ if isinstance(src_v, dict) and isinstance(dst_v, dict):
650
+ for kk, sv in src_v.items():
651
+ # These won't match between systems and that's ok
652
+ if isinstance(sv, str) and sv.startswith(
653
+ "wandb-client-artifact://"
654
+ ):
655
+ continue
656
+ dv = dst_v.get(kk)
657
+ if not _almost_equal(sv, dv):
658
+ non_matching[f"{k}-{kk}"] = {"src": sv, "dst": dv}
659
+ else:
660
+ if not _almost_equal(src_v, dst_v):
661
+ non_matching[k] = {"src": src_v, "dst": dst_v}
662
+
663
+ return non_matching
664
+
665
+ def _collect_failed_artifact_sequences(self) -> Iterable[ArtifactSequence]:
666
+ if (df := _read_ndjson(ARTIFACT_ERRORS_FNAME)) is None:
667
+ logger.debug(f"{ARTIFACT_ERRORS_FNAME=} is empty, returning nothing")
668
+ return
669
+
670
+ unique_failed_sequences = df[
671
+ ["src_entity", "src_project", "name", "type"]
672
+ ].unique()
673
+
674
+ for row in unique_failed_sequences.iter_rows(named=True):
675
+ entity = row["src_entity"]
676
+ project = row["src_project"]
677
+ name = row["name"]
678
+ _type = row["type"]
679
+
680
+ art_name = f"{entity}/{project}/{name}"
681
+ arts = self.src_api.artifacts(_type, art_name)
682
+ arts = sorted(arts, key=lambda a: int(a.version.lstrip("v")))
683
+ arts = sorted(arts, key=lambda a: a.type)
684
+
685
+ yield ArtifactSequence(arts, entity, project, _type, name)
686
+
687
+ def _cleanup_dummy_runs(
688
+ self,
689
+ *,
690
+ namespaces: Optional[Iterable[Namespace]] = None,
691
+ api: Optional[Api] = None,
692
+ remapping: Optional[Dict[Namespace, Namespace]] = None,
693
+ ) -> None:
694
+ api = coalesce(api, self.dst_api)
695
+ namespaces = coalesce(namespaces, self._all_namespaces())
696
+
697
+ for ns in namespaces:
698
+ if remapping and ns in remapping:
699
+ ns = remapping[ns]
700
+
701
+ logger.debug(f"Cleaning up, {ns=}")
702
+ try:
703
+ runs = list(
704
+ api.runs(ns.path, filters={"displayName": RUN_DUMMY_PLACEHOLDER})
705
+ )
706
+ except ValueError as e:
707
+ if "Could not find project" in str(e):
708
+ logger.exception("Could not find project, does it exist?")
709
+ continue
710
+
711
+ for run in runs:
712
+ logger.debug(f"Deleting dummy {run=}")
713
+ run.delete(delete_artifacts=False)
714
+
715
+ def _import_report(
716
+ self, report: Report, *, namespace: Optional[Namespace] = None
717
+ ) -> None:
718
+ """Import one wandb.Report.
719
+
720
+ Use `namespace` to specify alternate settings like where the report should be uploaded
721
+ """
722
+ if namespace is None:
723
+ namespace = Namespace(report.entity, report.project)
724
+
725
+ entity = coalesce(namespace.entity, report.entity)
726
+ project = coalesce(namespace.project, report.project)
727
+ name = report.name
728
+ title = report.title
729
+ description = report.description
730
+
731
+ api = self.dst_api
732
+
733
+ # We shouldn't need to upsert the project for every report
734
+ logger.debug(f"Upserting {entity=}/{project=}")
735
+ try:
736
+ api.create_project(project, entity)
737
+ except requests.exceptions.HTTPError as e:
738
+ if e.response.status_code != 409:
739
+ logger.warning(f"Issue upserting {entity=}/{project=}, {e=}")
740
+
741
+ logger.debug(f"Upserting report {entity=}, {project=}, {name=}, {title=}")
742
+ api.client.execute(
743
+ wr.report.UPSERT_VIEW,
744
+ variable_values={
745
+ "id": None, # Is there any benefit for this to be the same as default report?
746
+ "name": name,
747
+ "entityName": entity,
748
+ "projectName": project,
749
+ "description": description,
750
+ "displayName": title,
751
+ "type": "runs",
752
+ "spec": json.dumps(report.spec),
753
+ },
754
+ )
755
+
756
+ def _use_artifact_sequence(
757
+ self,
758
+ sequence: ArtifactSequence,
759
+ *,
760
+ namespace: Optional[Namespace] = None,
761
+ ):
762
+ if namespace is None:
763
+ namespace = Namespace(sequence.entity, sequence.project)
764
+
765
+ settings_override = {
766
+ "api_key": self.dst_api_key,
767
+ "base_url": self.dst_base_url,
768
+ "resume": "true",
769
+ "resumed": True,
770
+ }
771
+ logger.debug(f"Using artifact sequence with {settings_override=}, {namespace=}")
772
+
773
+ send_manager_config = internal.SendManagerConfig(use_artifacts=True)
774
+
775
+ for art in sequence:
776
+ if (used_by := art.used_by()) is None:
777
+ continue
778
+
779
+ for wandb_run in used_by:
780
+ run = WandbRun(wandb_run, **self.run_api_kwargs)
781
+
782
+ internal.send_run(
783
+ run,
784
+ overrides=namespace.send_manager_overrides,
785
+ settings_override=settings_override,
786
+ config=send_manager_config,
787
+ )
788
+
789
+ def import_runs(
790
+ self,
791
+ *,
792
+ namespaces: Optional[Iterable[Namespace]] = None,
793
+ remapping: Optional[Dict[Namespace, Namespace]] = None,
794
+ parallel: bool = True,
795
+ incremental: bool = True,
796
+ max_workers: Optional[int] = None,
797
+ limit: Optional[int] = None,
798
+ metadata: bool = True,
799
+ files: bool = True,
800
+ media: bool = True,
801
+ code: bool = True,
802
+ history: bool = True,
803
+ summary: bool = True,
804
+ terminal_output: bool = True,
805
+ ):
806
+ logger.info("START: Import runs")
807
+
808
+ logger.info("Setting up for import")
809
+ _create_files_if_not_exists()
810
+ _clear_fname(RUN_ERRORS_FNAME)
811
+
812
+ logger.info("Collecting runs")
813
+ runs = list(self._collect_runs(namespaces=namespaces, limit=limit))
814
+
815
+ logger.info(f"Validating runs, {len(runs)=}")
816
+ self._validate_runs(
817
+ runs,
818
+ skip_previously_validated=incremental,
819
+ remapping=remapping,
820
+ )
821
+
822
+ logger.info("Collecting failed runs")
823
+ runs = list(self._collect_failed_runs())
824
+
825
+ logger.info(f"Importing runs, {len(runs)=}")
826
+
827
+ def _import_run_wrapped(run):
828
+ namespace = Namespace(run.entity(), run.project())
829
+ if remapping is not None and namespace in remapping:
830
+ namespace = remapping[namespace]
831
+
832
+ config = internal.SendManagerConfig(
833
+ metadata=metadata,
834
+ files=files,
835
+ media=media,
836
+ code=code,
837
+ history=history,
838
+ summary=summary,
839
+ terminal_output=terminal_output,
840
+ )
841
+
842
+ logger.debug(f"Importing {run=}, {namespace=}, {config=}")
843
+ self._import_run(run, namespace=namespace, config=config)
844
+ logger.debug(f"Finished importing {run=}, {namespace=}, {config=}")
845
+
846
+ for_each(_import_run_wrapped, runs, max_workers=max_workers, parallel=parallel)
847
+ logger.info("END: Importing runs")
848
+
849
+ def import_reports(
850
+ self,
851
+ *,
852
+ namespaces: Optional[Iterable[Namespace]] = None,
853
+ limit: Optional[int] = None,
854
+ remapping: Optional[Dict[Namespace, Namespace]] = None,
855
+ ):
856
+ logger.info("START: Importing reports")
857
+
858
+ logger.info("Collecting reports")
859
+ reports = self._collect_reports(namespaces=namespaces, limit=limit)
860
+
861
+ logger.info("Importing reports")
862
+
863
+ def _import_report_wrapped(report):
864
+ namespace = Namespace(report.entity, report.project)
865
+ if remapping is not None and namespace in remapping:
866
+ namespace = remapping[namespace]
867
+
868
+ logger.debug(f"Importing {report=}, {namespace=}")
869
+ self._import_report(report, namespace=namespace)
870
+ logger.debug(f"Finished importing {report=}, {namespace=}")
871
+
872
+ for_each(_import_report_wrapped, reports)
873
+
874
+ logger.info("END: Importing reports")
875
+
876
+ def import_artifact_sequences(
877
+ self,
878
+ *,
879
+ namespaces: Optional[Iterable[Namespace]] = None,
880
+ incremental: bool = True,
881
+ max_workers: Optional[int] = None,
882
+ remapping: Optional[Dict[Namespace, Namespace]] = None,
883
+ ):
884
+ """Import all artifact sequences from `namespaces`.
885
+
886
+ Note: There is a known bug with the AWS backend where artifacts > 2048MB will fail to upload. This seems to be related to multipart uploads, but we don't have a fix yet.
887
+ """
888
+ logger.info("START: Importing artifact sequences")
889
+ _clear_fname(ARTIFACT_ERRORS_FNAME)
890
+
891
+ logger.info("Collecting artifact sequences")
892
+ seqs = list(self._collect_artifact_sequences(namespaces=namespaces))
893
+
894
+ logger.info("Validating artifact sequences")
895
+ self._validate_artifact_sequences(
896
+ seqs,
897
+ incremental=incremental,
898
+ remapping=remapping,
899
+ )
900
+
901
+ logger.info("Collecting failed artifact sequences")
902
+ seqs = list(self._collect_failed_artifact_sequences())
903
+
904
+ logger.info(f"Importing artifact sequences, {len(seqs)=}")
905
+
906
+ def _import_artifact_sequence_wrapped(seq):
907
+ namespace = Namespace(seq.entity, seq.project)
908
+ if remapping is not None and namespace in remapping:
909
+ namespace = remapping[namespace]
910
+
911
+ logger.debug(f"Importing artifact sequence {seq=}, {namespace=}")
912
+ self._import_artifact_sequence(seq, namespace=namespace)
913
+ logger.debug(f"Finished importing artifact sequence {seq=}, {namespace=}")
914
+
915
+ for_each(_import_artifact_sequence_wrapped, seqs, max_workers=max_workers)
916
+
917
+ # it's safer to just use artifact on all seqs to make sure we don't miss anything
918
+ # For seqs that have already been used, this is a no-op.
919
+ logger.debug(f"Using artifact sequences, {len(seqs)=}")
920
+
921
+ def _use_artifact_sequence_wrapped(seq):
922
+ namespace = Namespace(seq.entity, seq.project)
923
+ if remapping is not None and namespace in remapping:
924
+ namespace = remapping[namespace]
925
+
926
+ logger.debug(f"Using artifact sequence {seq=}, {namespace=}")
927
+ self._use_artifact_sequence(seq, namespace=namespace)
928
+ logger.debug(f"Finished using artifact sequence {seq=}, {namespace=}")
929
+
930
+ for_each(_use_artifact_sequence_wrapped, seqs, max_workers=max_workers)
931
+
932
+ # Artifacts whose parent runs have been deleted should have that run deleted in the
933
+ # destination as well
934
+
935
+ logger.info("Cleaning up dummy runs")
936
+ self._cleanup_dummy_runs(
937
+ namespaces=namespaces,
938
+ remapping=remapping,
939
+ )
940
+
941
+ logger.info("END: Importing artifact sequences")
942
+
943
+ def import_all(
944
+ self,
945
+ *,
946
+ runs: bool = True,
947
+ artifacts: bool = True,
948
+ reports: bool = True,
949
+ namespaces: Optional[Iterable[Namespace]] = None,
950
+ incremental: bool = True,
951
+ remapping: Optional[Dict[Namespace, Namespace]] = None,
952
+ ):
953
+ logger.info(f"START: Importing all, {runs=}, {artifacts=}, {reports=}")
954
+ if runs:
955
+ self.import_runs(
956
+ namespaces=namespaces,
957
+ incremental=incremental,
958
+ remapping=remapping,
959
+ )
960
+
961
+ if reports:
962
+ self.import_reports(
963
+ namespaces=namespaces,
964
+ remapping=remapping,
965
+ )
966
+
967
+ if artifacts:
968
+ self.import_artifact_sequences(
969
+ namespaces=namespaces,
970
+ incremental=incremental,
971
+ remapping=remapping,
972
+ )
973
+
974
+ logger.info("END: Importing all")
975
+
976
+ def _validate_run(
977
+ self,
978
+ src_run: Run,
979
+ *,
980
+ remapping: Optional[Dict[Namespace, Namespace]] = None,
981
+ ) -> None:
982
+ namespace = Namespace(src_run.entity, src_run.project)
983
+ if remapping is not None and namespace in remapping:
984
+ namespace = remapping[namespace]
985
+
986
+ dst_entity = namespace.entity
987
+ dst_project = namespace.project
988
+ run_id = src_run.id
989
+
990
+ try:
991
+ dst_run = self.dst_api.run(f"{dst_entity}/{dst_project}/{run_id}")
992
+ except wandb.CommError:
993
+ problems = [f"run does not exist in dst at {dst_entity=}/{dst_project=}"]
994
+ else:
995
+ problems = self._get_run_problems(src_run, dst_run)
996
+
997
+ d = {
998
+ "src_entity": src_run.entity,
999
+ "src_project": src_run.project,
1000
+ "dst_entity": dst_entity,
1001
+ "dst_project": dst_project,
1002
+ "run_id": run_id,
1003
+ }
1004
+ if problems:
1005
+ d["problems"] = problems
1006
+ fname = RUN_ERRORS_FNAME
1007
+ else:
1008
+ fname = RUN_SUCCESSES_FNAME
1009
+
1010
+ with filelock.FileLock("runs.lock"):
1011
+ with open(fname, "a") as f:
1012
+ f.write(json.dumps(d) + "\n")
1013
+
1014
+ def _filter_previously_checked_runs(
1015
+ self,
1016
+ runs: Iterable[Run],
1017
+ *,
1018
+ remapping: Optional[Dict[Namespace, Namespace]] = None,
1019
+ ) -> Iterable[Run]:
1020
+ if (df := _read_ndjson(RUN_SUCCESSES_FNAME)) is None:
1021
+ logger.debug(f"{RUN_SUCCESSES_FNAME=} is empty, yielding all runs")
1022
+ yield from runs
1023
+ return
1024
+
1025
+ data = []
1026
+ for r in runs:
1027
+ namespace = Namespace(r.entity, r.project)
1028
+ if remapping is not None and namespace in remapping:
1029
+ namespace = remapping[namespace]
1030
+
1031
+ data.append(
1032
+ {
1033
+ "src_entity": r.entity,
1034
+ "src_project": r.project,
1035
+ "dst_entity": namespace.entity,
1036
+ "dst_project": namespace.project,
1037
+ "run_id": r.id,
1038
+ "data": r,
1039
+ }
1040
+ )
1041
+ df2 = pl.DataFrame(data)
1042
+ logger.debug(f"Starting with {len(runs)=} in namespaces")
1043
+
1044
+ results = df2.join(
1045
+ df,
1046
+ how="anti",
1047
+ on=["src_entity", "src_project", "dst_entity", "dst_project", "run_id"],
1048
+ )
1049
+ logger.debug(f"After filtering out already successful runs, {len(results)=}")
1050
+
1051
+ if not results.is_empty():
1052
+ results = results.filter(~results["run_id"].is_null())
1053
+ results = results.unique(
1054
+ ["src_entity", "src_project", "dst_entity", "dst_project", "run_id"]
1055
+ )
1056
+
1057
+ for r in results.iter_rows(named=True):
1058
+ yield r["data"]
1059
+
1060
+ def _validate_artifact(
1061
+ self,
1062
+ src_art: Artifact,
1063
+ dst_entity: str,
1064
+ dst_project: str,
1065
+ download_files_and_compare: bool = False,
1066
+ check_entries_are_downloadable: bool = True,
1067
+ ):
1068
+ problems = []
1069
+
1070
+ # These patterns of artifacts are special and should not be validated
1071
+ ignore_patterns = [
1072
+ r"^job-(.*?)\.py(:v\d+)?$",
1073
+ # r"^run-.*-history(?:\:v\d+)?$$",
1074
+ ]
1075
+ for pattern in ignore_patterns:
1076
+ if re.search(pattern, src_art.name):
1077
+ return (src_art, dst_entity, dst_project, problems)
1078
+
1079
+ try:
1080
+ dst_art = self._get_dst_art(src_art, dst_entity, dst_project)
1081
+ except Exception:
1082
+ problems.append("destination artifact not found")
1083
+ return (src_art, dst_entity, dst_project, problems)
1084
+
1085
+ try:
1086
+ logger.debug("Comparing artifact manifests")
1087
+ except Exception as e:
1088
+ problems.append(
1089
+ f"Problem getting problems! problem with {src_art.entity=}, {src_art.project=}, {src_art.name=} {e=}"
1090
+ )
1091
+ else:
1092
+ problems += validation._compare_artifact_manifests(src_art, dst_art)
1093
+
1094
+ if check_entries_are_downloadable:
1095
+ # validation._check_entries_are_downloadable(src_art)
1096
+ validation._check_entries_are_downloadable(dst_art)
1097
+
1098
+ if download_files_and_compare:
1099
+ logger.debug(f"Downloading {src_art=}")
1100
+ try:
1101
+ src_dir = _download_art(src_art, root=f"{SRC_ART_PATH}/{src_art.name}")
1102
+ except requests.HTTPError as e:
1103
+ problems.append(
1104
+ f"Invalid download link for src {src_art.entity=}, {src_art.project=}, {src_art.name=}, {e}"
1105
+ )
1106
+
1107
+ logger.debug(f"Downloading {dst_art=}")
1108
+ try:
1109
+ dst_dir = _download_art(dst_art, root=f"{DST_ART_PATH}/{dst_art.name}")
1110
+ except requests.HTTPError as e:
1111
+ problems.append(
1112
+ f"Invalid download link for dst {dst_art.entity=}, {dst_art.project=}, {dst_art.name=}, {e}"
1113
+ )
1114
+ else:
1115
+ logger.debug(f"Comparing artifact dirs {src_dir=}, {dst_dir=}")
1116
+ if problem := validation._compare_artifact_dirs(src_dir, dst_dir):
1117
+ problems.append(problem)
1118
+
1119
+ return (src_art, dst_entity, dst_project, problems)
1120
+
1121
+ def _validate_runs(
1122
+ self,
1123
+ runs: Iterable[WandbRun],
1124
+ *,
1125
+ skip_previously_validated: bool = True,
1126
+ remapping: Optional[Dict[Namespace, Namespace]] = None,
1127
+ ):
1128
+ base_runs = [r.run for r in runs]
1129
+ if skip_previously_validated:
1130
+ base_runs = list(
1131
+ self._filter_previously_checked_runs(
1132
+ base_runs,
1133
+ remapping=remapping,
1134
+ )
1135
+ )
1136
+
1137
+ def _validate_run(run):
1138
+ logger.debug(f"Validating {run=}")
1139
+ self._validate_run(run, remapping=remapping)
1140
+ logger.debug(f"Finished validating {run=}")
1141
+
1142
+ for_each(_validate_run, base_runs)
1143
+
1144
+ def _collect_failed_runs(self):
1145
+ if (df := _read_ndjson(RUN_ERRORS_FNAME)) is None:
1146
+ logger.debug(f"{RUN_ERRORS_FNAME=} is empty, returning nothing")
1147
+ return
1148
+
1149
+ unique_failed_runs = df[
1150
+ ["src_entity", "src_project", "dst_entity", "dst_project", "run_id"]
1151
+ ].unique()
1152
+
1153
+ for row in unique_failed_runs.iter_rows(named=True):
1154
+ src_entity = row["src_entity"]
1155
+ src_project = row["src_project"]
1156
+ # dst_entity = row["dst_entity"]
1157
+ # dst_project = row["dst_project"]
1158
+ run_id = row["run_id"]
1159
+
1160
+ run = self.src_api.run(f"{src_entity}/{src_project}/{run_id}")
1161
+ yield WandbRun(run, **self.run_api_kwargs)
1162
+
1163
+ def _filter_previously_checked_artifacts(self, seqs: Iterable[ArtifactSequence]):
1164
+ if (df := _read_ndjson(ARTIFACT_SUCCESSES_FNAME)) is None:
1165
+ logger.info(
1166
+ f"{ARTIFACT_SUCCESSES_FNAME=} is empty, yielding all artifact sequences"
1167
+ )
1168
+ for seq in seqs:
1169
+ yield from seq.artifacts
1170
+ return
1171
+
1172
+ for seq in seqs:
1173
+ for art in seq:
1174
+ try:
1175
+ logged_by = _get_run_or_dummy_from_art(art, self.src_api)
1176
+ except requests.HTTPError:
1177
+ logger.exception(f"Failed to get run, skipping: {art=}")
1178
+ continue
1179
+
1180
+ if art.type == "wandb-history" and isinstance(logged_by, _DummyRun):
1181
+ logger.debug(f"Skipping history artifact {art=}")
1182
+ # We can never upload valid history for a deleted run, so skip it
1183
+ continue
1184
+
1185
+ entity = art.entity
1186
+ project = art.project
1187
+ _type = art.type
1188
+ name, ver = _get_art_name_ver(art)
1189
+
1190
+ filtered_df = df.filter(
1191
+ (df["src_entity"] == entity)
1192
+ & (df["src_project"] == project)
1193
+ & (df["name"] == name)
1194
+ & (df["version"] == ver)
1195
+ & (df["type"] == _type)
1196
+ )
1197
+
1198
+ # not in file, so not verified yet, don't filter out
1199
+ if len(filtered_df) == 0:
1200
+ yield art
1201
+
1202
+ def _validate_artifact_sequences(
1203
+ self,
1204
+ seqs: Iterable[ArtifactSequence],
1205
+ *,
1206
+ incremental: bool = True,
1207
+ download_files_and_compare: bool = False,
1208
+ check_entries_are_downloadable: bool = True,
1209
+ remapping: Optional[Dict[Namespace, Namespace]] = None,
1210
+ ):
1211
+ if incremental:
1212
+ logger.info("Validating in incremental mode")
1213
+
1214
+ def filtered_sequences():
1215
+ for seq in seqs:
1216
+ if not seq.artifacts:
1217
+ continue
1218
+
1219
+ art = seq.artifacts[0]
1220
+ try:
1221
+ logged_by = _get_run_or_dummy_from_art(art, self.src_api)
1222
+ except requests.HTTPError:
1223
+ logger.exception(
1224
+ f"Validate Artifact http error: {art.entity=},"
1225
+ f" {art.project=}, {art.name=}"
1226
+ )
1227
+ continue
1228
+
1229
+ if art.type == "wandb-history" and isinstance(logged_by, _DummyRun):
1230
+ # We can never upload valid history for a deleted run, so skip it
1231
+ continue
1232
+
1233
+ yield seq
1234
+
1235
+ artifacts = self._filter_previously_checked_artifacts(filtered_sequences())
1236
+ else:
1237
+ logger.info("Validating in non-incremental mode")
1238
+ artifacts = [art for seq in seqs for art in seq.artifacts]
1239
+
1240
+ def _validate_artifact_wrapped(args):
1241
+ art, entity, project = args
1242
+ if (
1243
+ remapping is not None
1244
+ and (namespace := Namespace(entity, project)) in remapping
1245
+ ):
1246
+ remapped_ns = remapping[namespace]
1247
+ entity = remapped_ns.entity
1248
+ project = remapped_ns.project
1249
+
1250
+ logger.debug(f"Validating {art=}, {entity=}, {project=}")
1251
+ result = self._validate_artifact(
1252
+ art,
1253
+ entity,
1254
+ project,
1255
+ download_files_and_compare=download_files_and_compare,
1256
+ check_entries_are_downloadable=check_entries_are_downloadable,
1257
+ )
1258
+ logger.debug(f"Finished validating {art=}, {entity=}, {project=}")
1259
+ return result
1260
+
1261
+ args = ((art, art.entity, art.project) for art in artifacts)
1262
+ art_problems = for_each(_validate_artifact_wrapped, args)
1263
+ for art, dst_entity, dst_project, problems in art_problems:
1264
+ name, ver = _get_art_name_ver(art)
1265
+ d = {
1266
+ "src_entity": art.entity,
1267
+ "src_project": art.project,
1268
+ "dst_entity": dst_entity,
1269
+ "dst_project": dst_project,
1270
+ "name": name,
1271
+ "version": ver,
1272
+ "type": art.type,
1273
+ }
1274
+
1275
+ if problems:
1276
+ d["problems"] = problems
1277
+ fname = ARTIFACT_ERRORS_FNAME
1278
+ else:
1279
+ fname = ARTIFACT_SUCCESSES_FNAME
1280
+
1281
+ with open(fname, "a") as f:
1282
+ f.write(json.dumps(d) + "\n")
1283
+
1284
+ def _collect_runs(
1285
+ self,
1286
+ *,
1287
+ namespaces: Optional[Iterable[Namespace]] = None,
1288
+ limit: Optional[int] = None,
1289
+ skip_ids: Optional[List[str]] = None,
1290
+ start_date: Optional[str] = None,
1291
+ api: Optional[Api] = None,
1292
+ ) -> Iterable[WandbRun]:
1293
+ api = coalesce(api, self.src_api)
1294
+ namespaces = coalesce(namespaces, self._all_namespaces())
1295
+
1296
+ filters: Dict[str, Any] = {}
1297
+ if skip_ids is not None:
1298
+ filters["name"] = {"$nin": skip_ids}
1299
+ if start_date is not None:
1300
+ filters["createdAt"] = {"$gte": start_date}
1301
+
1302
+ def _runs():
1303
+ for ns in namespaces:
1304
+ logger.debug(f"Collecting runs from {ns=}")
1305
+ for run in api.runs(ns.path, filters=filters):
1306
+ yield WandbRun(run, **self.run_api_kwargs)
1307
+
1308
+ runs = itertools.islice(_runs(), limit)
1309
+ yield from runs
1310
+
1311
+ def _all_namespaces(
1312
+ self, *, entity: Optional[str] = None, api: Optional[Api] = None
1313
+ ):
1314
+ api = coalesce(api, self.src_api)
1315
+ entity = coalesce(entity, api.default_entity)
1316
+ projects = api.projects(entity)
1317
+ for p in projects:
1318
+ yield Namespace(p.entity, p.name)
1319
+
1320
+ def _collect_reports(
1321
+ self,
1322
+ *,
1323
+ namespaces: Optional[Iterable[Namespace]] = None,
1324
+ limit: Optional[int] = None,
1325
+ api: Optional[Api] = None,
1326
+ ):
1327
+ api = coalesce(api, self.src_api)
1328
+ namespaces = coalesce(namespaces, self._all_namespaces())
1329
+
1330
+ wandb.login(key=self.src_api_key, host=self.src_base_url)
1331
+
1332
+ def reports():
1333
+ for ns in namespaces:
1334
+ for r in api.reports(ns.path):
1335
+ yield wr.Report.from_url(r.url, api=api)
1336
+
1337
+ yield from itertools.islice(reports(), limit)
1338
+
1339
+ def _collect_artifact_sequences(
1340
+ self,
1341
+ *,
1342
+ namespaces: Optional[Iterable[Namespace]] = None,
1343
+ limit: Optional[int] = None,
1344
+ api: Optional[Api] = None,
1345
+ ):
1346
+ api = coalesce(api, self.src_api)
1347
+ namespaces = coalesce(namespaces, self._all_namespaces())
1348
+
1349
+ def artifact_sequences():
1350
+ for ns in namespaces:
1351
+ logger.debug(f"Collecting artifact sequences from {ns=}")
1352
+ types = []
1353
+ try:
1354
+ types = [t for t in api.artifact_types(ns.path)]
1355
+ except Exception:
1356
+ logger.exception("Failed to get artifact types.")
1357
+
1358
+ for t in types:
1359
+ collections = []
1360
+
1361
+ # Skip history because it's really for run history
1362
+ if t.name == "wandb-history":
1363
+ continue
1364
+
1365
+ try:
1366
+ collections = t.collections()
1367
+ except Exception:
1368
+ logger.exception("Failed to get artifact collections.")
1369
+
1370
+ for c in collections:
1371
+ if c.is_sequence():
1372
+ yield ArtifactSequence.from_collection(c)
1373
+
1374
+ seqs = itertools.islice(artifact_sequences(), limit)
1375
+ unique_sequences = {seq.identifier: seq for seq in seqs}
1376
+ yield from unique_sequences.values()
1377
+
1378
+
1379
+ def _get_art_name_ver(art: Artifact) -> Tuple[str, int]:
1380
+ name, ver = art.name.split(":v")
1381
+ return name, int(ver)
1382
+
1383
+
1384
+ def _make_dummy_art(name: str, _type: str, ver: int):
1385
+ art = Artifact(name, ART_DUMMY_PLACEHOLDER_TYPE)
1386
+ art._type = _type
1387
+ art._description = ART_SEQUENCE_DUMMY_PLACEHOLDER
1388
+
1389
+ p = Path(ART_DUMMY_PLACEHOLDER_PATH)
1390
+ p.mkdir(parents=True, exist_ok=True)
1391
+
1392
+ # dummy file with different name to prevent dedupe
1393
+ fname = p / str(ver)
1394
+ with open(fname, "w"):
1395
+ pass
1396
+ art.add_file(fname)
1397
+
1398
+ return art
1399
+
1400
+
1401
+ def _make_groups_of_artifacts(seq: ArtifactSequence, start: int = 0):
1402
+ prev_ver = start - 1
1403
+ for art in seq:
1404
+ name, ver = _get_art_name_ver(art)
1405
+
1406
+ # If there's a gap between versions, fill with dummy artifacts
1407
+ if ver - prev_ver > 1:
1408
+ yield [_make_dummy_art(name, art.type, v) for v in range(prev_ver + 1, ver)]
1409
+
1410
+ # Then yield the actual artifact
1411
+ # Must always be a list of one artifact to guarantee ordering
1412
+ yield [art]
1413
+ prev_ver = ver
1414
+
1415
+
1416
+ def _recursive_cast_to_dict(obj):
1417
+ if isinstance(obj, list):
1418
+ return [_recursive_cast_to_dict(item) for item in obj]
1419
+ elif isinstance(obj, dict) or hasattr(obj, "items"):
1420
+ new_dict = {}
1421
+ for key, value in obj.items():
1422
+ new_dict[key] = _recursive_cast_to_dict(value)
1423
+ return new_dict
1424
+ else:
1425
+ return obj
1426
+
1427
+
1428
+ def _almost_equal(x, y, eps=1e-6):
1429
+ if isinstance(x, dict) and isinstance(y, dict):
1430
+ if x.keys() != y.keys():
1431
+ return False
1432
+ return all(_almost_equal(x[k], y[k], eps) for k in x)
1433
+
1434
+ if isinstance(x, numbers.Number) and isinstance(y, numbers.Number):
1435
+ return abs(x - y) < eps
1436
+
1437
+ if type(x) is not type(y):
1438
+ return False
1439
+
1440
+ return x == y
1441
+
1442
+
1443
+ @dataclass
1444
+ class _DummyUser:
1445
+ username: str = ""
1446
+
1447
+
1448
+ @dataclass
1449
+ class _DummyRun:
1450
+ entity: str = ""
1451
+ project: str = ""
1452
+ run_id: str = RUN_DUMMY_PLACEHOLDER
1453
+ id: str = RUN_DUMMY_PLACEHOLDER
1454
+ display_name: str = RUN_DUMMY_PLACEHOLDER
1455
+ notes: str = ""
1456
+ url: str = ""
1457
+ group: str = ""
1458
+ created_at: str = "2000-01-01"
1459
+ user: _DummyUser = field(default_factory=_DummyUser)
1460
+ tags: list = field(default_factory=list)
1461
+ summary: dict = field(default_factory=dict)
1462
+ config: dict = field(default_factory=dict)
1463
+
1464
+ def files(self):
1465
+ return []
1466
+
1467
+
1468
+ def _read_ndjson(fname: str) -> Optional[pl.DataFrame]:
1469
+ try:
1470
+ df = pl.read_ndjson(fname)
1471
+ except FileNotFoundError:
1472
+ return None
1473
+ except RuntimeError as e:
1474
+ # No runs previously checked
1475
+ if "empty string is not a valid JSON value" in str(e):
1476
+ return None
1477
+ if "error parsing ndjson" in str(e):
1478
+ return None
1479
+ raise
1480
+
1481
+ return df
1482
+
1483
+
1484
+ def _get_run_or_dummy_from_art(art: Artifact, api=None):
1485
+ run = None
1486
+
1487
+ try:
1488
+ run = art.logged_by()
1489
+ except ValueError as e:
1490
+ logger.warning(
1491
+ f"Can't log artifact because run doesn't exist, {art=}, {run=}, {e=}"
1492
+ )
1493
+
1494
+ if run is not None:
1495
+ return run
1496
+
1497
+ query = gql(
1498
+ """
1499
+ query ArtifactCreatedBy(
1500
+ $id: ID!
1501
+ ) {
1502
+ artifact(id: $id) {
1503
+ createdBy {
1504
+ ... on Run {
1505
+ name
1506
+ project {
1507
+ name
1508
+ entityName
1509
+ }
1510
+ }
1511
+ }
1512
+ }
1513
+ }
1514
+ """
1515
+ )
1516
+ response = api.client.execute(query, variable_values={"id": art.id})
1517
+ creator = response.get("artifact", {}).get("createdBy", {})
1518
+ run = _DummyRun(
1519
+ entity=art.entity,
1520
+ project=art.project,
1521
+ run_id=creator.get("name", RUN_DUMMY_PLACEHOLDER),
1522
+ id=creator.get("name", RUN_DUMMY_PLACEHOLDER),
1523
+ )
1524
+ return run
1525
+
1526
+
1527
+ def _clear_fname(fname: str) -> None:
1528
+ old_fname = f"{internal.ROOT_DIR}/{fname}"
1529
+ new_fname = f"{internal.ROOT_DIR}/prev_{fname}"
1530
+
1531
+ logger.debug(f"Moving {old_fname=} to {new_fname=}")
1532
+ try:
1533
+ shutil.copy2(old_fname, new_fname)
1534
+ except FileNotFoundError:
1535
+ # this is just to make a copy of the last iteration, so its ok if the src doesn't exist
1536
+ pass
1537
+
1538
+ with open(fname, "w"):
1539
+ pass
1540
+
1541
+
1542
+ def _download_art(art: Artifact, root: str) -> Optional[str]:
1543
+ try:
1544
+ with patch("click.echo"):
1545
+ return art.download(root=root, skip_cache=True)
1546
+ except Exception:
1547
+ logger.exception(f"Error downloading artifact {art=}")
1548
+
1549
+
1550
+ def _clone_art(art: Artifact, root: Optional[str] = None):
1551
+ if root is None:
1552
+ # Currently, we would only ever clone a src artifact to move it to dst.
1553
+ root = f"{SRC_ART_PATH}/{art.name}"
1554
+
1555
+ if (path := _download_art(art, root=root)) is None:
1556
+ raise ValueError(f"Problem downloading {art=}")
1557
+
1558
+ name, _ = art.name.split(":v")
1559
+
1560
+ # Hack: skip naming validation check for wandb-* types
1561
+ new_art = Artifact(name, ART_DUMMY_PLACEHOLDER_TYPE)
1562
+ new_art._type = art.type
1563
+ new_art._created_at = art.created_at
1564
+
1565
+ new_art._aliases = art.aliases
1566
+ new_art._description = art.description
1567
+
1568
+ with patch("click.echo"):
1569
+ new_art.add_dir(path)
1570
+
1571
+ return new_art
1572
+
1573
+
1574
+ def _create_files_if_not_exists() -> None:
1575
+ fnames = [
1576
+ ARTIFACT_ERRORS_FNAME,
1577
+ ARTIFACT_SUCCESSES_FNAME,
1578
+ RUN_ERRORS_FNAME,
1579
+ RUN_SUCCESSES_FNAME,
1580
+ ]
1581
+
1582
+ for fname in fnames:
1583
+ logger.debug(f"Creating {fname=} if not exists")
1584
+ with open(fname, "a"):
1585
+ pass
1586
+
1587
+
1588
+ def _merge_dfs(dfs: List[pl.DataFrame]) -> pl.DataFrame:
1589
+ # Ensure there are DataFrames in the list
1590
+ if len(dfs) == 0:
1591
+ return pl.DataFrame()
1592
+
1593
+ if len(dfs) == 1:
1594
+ return dfs[0]
1595
+
1596
+ merged_df = dfs[0]
1597
+ for df in dfs[1:]:
1598
+ merged_df = merged_df.join(df, how="outer", on=["_step"])
1599
+ col_pairs = [
1600
+ (c, f"{c}_right")
1601
+ for c in merged_df.columns
1602
+ if f"{c}_right" in merged_df.columns
1603
+ ]
1604
+ for col, right in col_pairs:
1605
+ new_col = merged_df[col].fill_null(merged_df[right])
1606
+ merged_df = merged_df.with_columns(new_col).drop(right)
1607
+
1608
+ return merged_df
lib/python3.12/site-packages/wandb/apis/internal.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+
3
+ from wandb.sdk.internal.internal_api import Api as InternalApi
4
+
5
+
6
+ class Api:
7
+ """Internal proxy to the official internal API."""
8
+
9
+ # TODO: Move these methods to PublicApi.
10
+
11
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
12
+ self._api_args = args
13
+ self._api_kwargs = kwargs
14
+ self._api = None
15
+
16
+ def __getstate__(self):
17
+ """Use for serializing.
18
+
19
+ self._api is not serializable, so it's dropped
20
+ """
21
+ state = self.__dict__.copy()
22
+ del state["_api"]
23
+ return state
24
+
25
+ def __setstate__(self, state):
26
+ """Used for deserializing.
27
+
28
+ Don't need to set self._api because it's constructed when needed.
29
+ """
30
+ self.__dict__.update(state)
31
+ self._api = None
32
+
33
+ @property
34
+ def api(self) -> InternalApi:
35
+ # This is a property in order to delay construction of Internal API
36
+ # for as long as possible. If constructed in constructor, then the
37
+ # whole InternalAPI is started when simply importing wandb.
38
+ if self._api is None:
39
+ self._api = InternalApi(*self._api_args, **self._api_kwargs)
40
+ return self._api
41
+
42
+ @property
43
+ def api_key(self):
44
+ return self.api.api_key
45
+
46
+ @property
47
+ def is_authenticated(self):
48
+ return self.api.access_token is not None or self.api.api_key is not None
49
+
50
+ @property
51
+ def api_url(self):
52
+ return self.api.api_url
53
+
54
+ @property
55
+ def app_url(self):
56
+ return self.api.app_url
57
+
58
+ @property
59
+ def default_entity(self):
60
+ return self.api.default_entity
61
+
62
+ @property
63
+ def git(self):
64
+ return self.api.git
65
+
66
+ def validate_api_key(self) -> bool:
67
+ """Returns whether the API key stored on initialization is valid."""
68
+ return self.api.validate_api_key()
69
+
70
+ def file_current(self, *args):
71
+ return self.api.file_current(*args)
72
+
73
+ def download_file(self, *args, **kwargs):
74
+ return self.api.download_file(*args, **kwargs)
75
+
76
+ def download_write_file(self, *args, **kwargs):
77
+ return self.api.download_write_file(*args, **kwargs)
78
+
79
+ def set_current_run_id(self, run_id):
80
+ return self.api.set_current_run_id(run_id)
81
+
82
+ def viewer(self):
83
+ return self.api.viewer()
84
+
85
+ def max_cli_version(self):
86
+ return self.api.max_cli_version()
87
+
88
+ def viewer_server_info(self):
89
+ return self.api.viewer_server_info()
90
+
91
+ def list_projects(self, entity=None):
92
+ return self.api.list_projects(entity=entity)
93
+
94
+ def format_project(self, project):
95
+ return self.api.format_project(project)
96
+
97
+ def upsert_project(self, project, id=None, description=None, entity=None):
98
+ return self.api.upsert_project(
99
+ project, id=id, description=description, entity=entity
100
+ )
101
+
102
+ def upsert_run(self, *args, **kwargs):
103
+ return self.api.upsert_run(*args, **kwargs)
104
+
105
+ def settings(self, *args, **kwargs):
106
+ return self.api.settings(*args, **kwargs)
107
+
108
+ def clear_setting(
109
+ self, key: str, globally: bool = False, persist: bool = False
110
+ ) -> None:
111
+ return self.api.clear_setting(key, globally, persist)
112
+
113
+ def set_setting(
114
+ self, key: str, value: Any, globally: bool = False, persist: bool = False
115
+ ) -> None:
116
+ return self.api.set_setting(key, value, globally, persist)
117
+
118
+ def parse_slug(self, *args, **kwargs):
119
+ return self.api.parse_slug(*args, **kwargs)
120
+
121
+ def download_url(self, *args, **kwargs):
122
+ return self.api.download_url(*args, **kwargs)
123
+
124
+ def download_urls(self, *args, **kwargs):
125
+ return self.api.download_urls(*args, **kwargs)
126
+
127
+ def create_anonymous_api_key(self) -> str:
128
+ return self.api.create_anonymous_api_key()
129
+
130
+ def push(self, *args, **kwargs):
131
+ return self.api.push(*args, **kwargs)
132
+
133
+ def sweep(self, *args, **kwargs):
134
+ return self.api.sweep(*args, **kwargs)
135
+
136
+ def upsert_sweep(self, *args, **kwargs):
137
+ return self.api.upsert_sweep(*args, **kwargs)
138
+
139
+ def set_sweep_state(self, *args, **kwargs):
140
+ return self.api.set_sweep_state(*args, **kwargs)
141
+
142
+ def get_sweep_state(self, *args, **kwargs):
143
+ return self.api.get_sweep_state(*args, **kwargs)
144
+
145
+ def stop_sweep(self, *args, **kwargs):
146
+ return self.api.stop_sweep(*args, **kwargs)
147
+
148
+ def cancel_sweep(self, *args, **kwargs):
149
+ return self.api.cancel_sweep(*args, **kwargs)
150
+
151
+ def pause_sweep(self, *args, **kwargs):
152
+ return self.api.pause_sweep(*args, **kwargs)
153
+
154
+ def resume_sweep(self, *args, **kwargs):
155
+ return self.api.resume_sweep(*args, **kwargs)
156
+
157
+ def register_agent(self, *args, **kwargs):
158
+ return self.api.register_agent(*args, **kwargs)
159
+
160
+ def agent_heartbeat(self, *args, **kwargs):
161
+ return self.api.agent_heartbeat(*args, **kwargs)
162
+
163
+ def use_artifact(self, *args, **kwargs):
164
+ return self.api.use_artifact(*args, **kwargs)
165
+
166
+ def create_artifact(self, *args, **kwargs):
167
+ return self.api.create_artifact(*args, **kwargs)
168
+
169
+ def complete_multipart_upload_artifact(self, *args, **kwargs):
170
+ return self.api.complete_multipart_upload_artifact(*args, **kwargs)
171
+
172
+ def run_config(self, *args, **kwargs):
173
+ return self.api.run_config(*args, **kwargs)
174
+
175
+ def upload_file_retry(self, *args, **kwargs):
176
+ return self.api.upload_file_retry(*args, **kwargs)
177
+
178
+ def upload_multipart_file_chunk_retry(self, *args, **kwargs):
179
+ return self.api.upload_multipart_file_chunk_retry(*args, **kwargs)
180
+
181
+ def get_run_info(self, *args, **kwargs):
182
+ return self.api.get_run_info(*args, **kwargs)
183
+
184
+ def get_run_state(self, *args, **kwargs):
185
+ return self.api.get_run_state(*args, **kwargs)
186
+
187
+ def entity_is_team(self, *args, **kwargs):
188
+ return self.api.entity_is_team(*args, **kwargs)
189
+
190
+ def get_project_run_queues(self, *args, **kwargs):
191
+ return self.api.get_project_run_queues(*args, **kwargs)
192
+
193
+ def push_to_run_queue(self, *args, **kwargs):
194
+ return self.api.push_to_run_queue(*args, **kwargs)
195
+
196
+ def pop_from_run_queue(self, *args, **kwargs):
197
+ return self.api.pop_from_run_queue(*args, **kwargs)
198
+
199
+ def ack_run_queue_item(self, *args, **kwargs):
200
+ return self.api.ack_run_queue_item(*args, **kwargs)
201
+
202
+ def create_launch_agent(self, *args, **kwargs):
203
+ return self.api.create_launch_agent(*args, **kwargs)
204
+
205
+ def create_default_resource_config(self, *args, **kwargs):
206
+ return self.api.create_default_resource_config(*args, **kwargs)
207
+
208
+ def create_run_queue(self, *args, **kwargs):
209
+ return self.api.create_run_queue(*args, **kwargs)
210
+
211
+ def upsert_run_queue(self, *args, **kwargs):
212
+ return self.api.upsert_run_queue(*args, **kwargs)
213
+
214
+ def create_custom_chart(self, *args, **kwargs):
215
+ return self.api.create_custom_chart(*args, **kwargs)
216
+
217
+ def update_launch_agent_status(self, *args, **kwargs):
218
+ return self.api.update_launch_agent_status(*args, **kwargs)
219
+
220
+ def launch_agent_introspection(self, *args, **kwargs):
221
+ return self.api.launch_agent_introspection(*args, **kwargs)
222
+
223
+ def fail_run_queue_item_introspection(self, *args, **kwargs):
224
+ return self.api.fail_run_queue_item_introspection(*args, **kwargs)
225
+
226
+ def fail_run_queue_item(self, *args, **kwargs):
227
+ return self.api.fail_run_queue_item(*args, **kwargs)
228
+
229
+ def update_run_queue_item_warning(self, *args, **kwargs):
230
+ return self.api.update_run_queue_item_warning(*args, **kwargs)
231
+
232
+ def get_launch_agent(self, *args, **kwargs):
233
+ return self.api.get_launch_agent(*args, **kwargs)
234
+
235
+ def stop_run(self, *args, **kwargs):
236
+ return self.api.stop_run(*args, **kwargs)
237
+
238
+
239
+ __all__ = ["Api"]
lib/python3.12/site-packages/wandb/apis/normalize.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """normalize."""
2
+
3
+ import ast
4
+ import sys
5
+ from functools import wraps
6
+ from typing import Callable, TypeVar
7
+
8
+ import requests
9
+ from wandb_gql.client import RetryError
10
+
11
+ from wandb import env
12
+ from wandb.errors import CommError, Error
13
+ from wandb.util import parse_backend_error_messages
14
+
15
+ _F = TypeVar("_F", bound=Callable)
16
+
17
+
18
+ def normalize_exceptions(func: _F) -> _F:
19
+ """Function decorator for catching common errors and re-raising as wandb.Error."""
20
+
21
+ @wraps(func)
22
+ def wrapper(*args, **kwargs):
23
+ message = "Whoa, you found a bug."
24
+ try:
25
+ return func(*args, **kwargs)
26
+
27
+ except requests.HTTPError as error:
28
+ errors = parse_backend_error_messages(error.response)
29
+ status = error.response.status_code
30
+
31
+ if errors:
32
+ message = f"HTTP {status}: {'; '.join(errors)}"
33
+ elif error.response.text:
34
+ message = f"HTTP {status}: {error.response.text}"
35
+ elif error.response.reason:
36
+ # Visually different to distinguish backend errors from
37
+ # standard HTTP status descriptions.
38
+ message = f"HTTP {status} ({error.response.reason})"
39
+ else:
40
+ message = f"HTTP {status}"
41
+
42
+ raise CommError(message, error)
43
+
44
+ except RetryError as err:
45
+ if (
46
+ "response" in dir(err.last_exception)
47
+ and err.last_exception.response is not None
48
+ ):
49
+ try:
50
+ message = err.last_exception.response.json().get(
51
+ "errors", [{"message": message}]
52
+ )[0]["message"]
53
+ except ValueError:
54
+ message = err.last_exception.response.text
55
+ else:
56
+ message = err.last_exception
57
+
58
+ if env.is_debug():
59
+ raise err.last_exception.with_traceback(sys.exc_info()[2])
60
+ else:
61
+ raise CommError(message, err.last_exception).with_traceback(
62
+ sys.exc_info()[2]
63
+ )
64
+ except Error:
65
+ raise
66
+ except Exception as err:
67
+ # gql raises server errors with dict's as strings...
68
+ if len(err.args) > 0:
69
+ payload = err.args[0]
70
+ else:
71
+ payload = err
72
+ if str(payload).startswith("{"):
73
+ message = ast.literal_eval(str(payload))["message"]
74
+ else:
75
+ message = str(err)
76
+ if env.is_debug():
77
+ raise
78
+ else:
79
+ raise CommError(message, err).with_traceback(sys.exc_info()[2])
80
+
81
+ return wrapper
lib/python3.12/site-packages/wandb/apis/paginator.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from abc import abstractmethod
4
+ from typing import (
5
+ TYPE_CHECKING,
6
+ Any,
7
+ ClassVar,
8
+ Iterator,
9
+ Mapping,
10
+ Protocol,
11
+ Sized,
12
+ TypeVar,
13
+ overload,
14
+ )
15
+
16
+ import wandb
17
+
18
+ if TYPE_CHECKING:
19
+ from wandb_graphql.language.ast import Document
20
+
21
+ T = TypeVar("T")
22
+
23
+
24
+ # Structural type hint for the client instance
25
+ class _Client(Protocol):
26
+ def execute(self, *args: Any, **kwargs: Any) -> dict[str, Any]: ...
27
+
28
+
29
+ class Paginator(Iterator[T]):
30
+ """An iterator for paginated objects from GraphQL requests."""
31
+
32
+ QUERY: ClassVar[Document | None] = None
33
+
34
+ def __init__(
35
+ self,
36
+ client: _Client,
37
+ variables: Mapping[str, Any],
38
+ per_page: int = 50, # We don't allow unbounded paging
39
+ ):
40
+ self.client: _Client = client
41
+
42
+ # shallow copy partly guards against mutating the original input
43
+ self.variables: dict[str, Any] = dict(variables)
44
+
45
+ self.per_page: int = per_page
46
+ self.objects: list[T] = []
47
+ self.index: int = -1
48
+ self.last_response: object | None = None
49
+
50
+ def __iter__(self) -> Iterator[T]:
51
+ self.index = -1
52
+ return self
53
+
54
+ @property
55
+ @abstractmethod
56
+ def more(self) -> bool:
57
+ """Whether there are more pages to be fetched."""
58
+ raise NotImplementedError
59
+
60
+ @property
61
+ @abstractmethod
62
+ def cursor(self) -> str | None:
63
+ """The start cursor to use for the next fetched page."""
64
+ raise NotImplementedError
65
+
66
+ @abstractmethod
67
+ def convert_objects(self) -> list[T]:
68
+ """Convert the last fetched response data into the iterated objects."""
69
+ raise NotImplementedError
70
+
71
+ def update_variables(self) -> None:
72
+ """Update the query variables for the next page fetch."""
73
+ self.variables.update({"perPage": self.per_page, "cursor": self.cursor})
74
+
75
+ def _update_response(self) -> None:
76
+ """Fetch and store the response data for the next page."""
77
+ self.last_response = self.client.execute(
78
+ self.QUERY, variable_values=self.variables
79
+ )
80
+
81
+ def _load_page(self) -> bool:
82
+ """Fetch the next page, if any, returning True and storing the response if there was one."""
83
+ if not self.more:
84
+ return False
85
+ self.update_variables()
86
+ self._update_response()
87
+ self.objects.extend(self.convert_objects())
88
+ return True
89
+
90
+ @overload
91
+ def __getitem__(self, index: int) -> T: ...
92
+ @overload
93
+ def __getitem__(self, index: slice) -> list[T]: ...
94
+
95
+ def __getitem__(self, index: int | slice) -> T | list[T]:
96
+ loaded = True
97
+ stop = index.stop if isinstance(index, slice) else index
98
+ while loaded and stop > len(self.objects) - 1:
99
+ loaded = self._load_page()
100
+ return self.objects[index]
101
+
102
+ def __next__(self) -> T:
103
+ self.index += 1
104
+ if len(self.objects) <= self.index:
105
+ if not self._load_page():
106
+ raise StopIteration
107
+ if len(self.objects) <= self.index:
108
+ raise StopIteration
109
+ return self.objects[self.index]
110
+
111
+ next = __next__
112
+
113
+
114
+ class SizedPaginator(Paginator[T], Sized):
115
+ """A Paginator for objects with a known total count."""
116
+
117
+ @property
118
+ def length(self) -> int | None:
119
+ wandb.termwarn(
120
+ (
121
+ "`.length` is deprecated and will be removed in a future version. "
122
+ "Use `len(...)` instead."
123
+ ),
124
+ repeat=False,
125
+ )
126
+ return len(self)
127
+
128
+ def __len__(self) -> int:
129
+ if self._length is None:
130
+ self._load_page()
131
+ if self._length is None:
132
+ raise ValueError("Object doesn't provide length")
133
+ return self._length
134
+
135
+ @property
136
+ @abstractmethod
137
+ def _length(self) -> int | None:
138
+ raise NotImplementedError
lib/python3.12/site-packages/wandb/apis/public/__init__.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from wandb.apis.public.api import Api, RetryingClient, requests
2
+ from wandb.apis.public.artifacts import (
3
+ ArtifactCollection,
4
+ ArtifactCollections,
5
+ ArtifactFiles,
6
+ Artifacts,
7
+ ArtifactType,
8
+ ArtifactTypes,
9
+ RunArtifacts,
10
+ )
11
+ from wandb.apis.public.automations import Automations
12
+ from wandb.apis.public.files import FILE_FRAGMENT, File, Files
13
+ from wandb.apis.public.history import HistoryScan, SampledHistoryScan
14
+ from wandb.apis.public.integrations import SlackIntegrations, WebhookIntegrations
15
+ from wandb.apis.public.jobs import (
16
+ Job,
17
+ QueuedRun,
18
+ RunQueue,
19
+ RunQueueAccessType,
20
+ RunQueuePrioritizationMode,
21
+ RunQueueResourceType,
22
+ )
23
+ from wandb.apis.public.projects import PROJECT_FRAGMENT, Project, Projects
24
+ from wandb.apis.public.query_generator import QueryGenerator
25
+ from wandb.apis.public.registries.registry import Registry
26
+ from wandb.apis.public.reports import (
27
+ BetaReport,
28
+ PanelMetricsHelper,
29
+ PythonMongoishQueryGenerator,
30
+ Reports,
31
+ )
32
+ from wandb.apis.public.runs import RUN_FRAGMENT, Run, Runs
33
+ from wandb.apis.public.sweeps import SWEEP_FRAGMENT, Sweep
34
+ from wandb.apis.public.teams import Member, Team
35
+ from wandb.apis.public.users import User
lib/python3.12/site-packages/wandb/apis/public/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (1.78 kB). View file
 
lib/python3.12/site-packages/wandb/apis/public/__pycache__/api.cpython-312.pyc ADDED
Binary file (90.4 kB). View file
 
lib/python3.12/site-packages/wandb/apis/public/__pycache__/artifacts.cpython-312.pyc ADDED
Binary file (38.6 kB). View file
 
lib/python3.12/site-packages/wandb/apis/public/__pycache__/automations.cpython-312.pyc ADDED
Binary file (4.02 kB). View file
 
lib/python3.12/site-packages/wandb/apis/public/__pycache__/const.cpython-312.pyc ADDED
Binary file (318 Bytes). View file
 
lib/python3.12/site-packages/wandb/apis/public/__pycache__/files.cpython-312.pyc ADDED
Binary file (11.3 kB). View file
 
lib/python3.12/site-packages/wandb/apis/public/__pycache__/history.cpython-312.pyc ADDED
Binary file (6.48 kB). View file
 
lib/python3.12/site-packages/wandb/apis/public/__pycache__/integrations.cpython-312.pyc ADDED
Binary file (8.6 kB). View file
 
lib/python3.12/site-packages/wandb/apis/public/__pycache__/jobs.cpython-312.pyc ADDED
Binary file (29.5 kB). View file
 
lib/python3.12/site-packages/wandb/apis/public/__pycache__/projects.cpython-312.pyc ADDED
Binary file (9.61 kB). View file
 
lib/python3.12/site-packages/wandb/apis/public/__pycache__/query_generator.cpython-312.pyc ADDED
Binary file (7.34 kB). View file
 
lib/python3.12/site-packages/wandb/apis/public/__pycache__/reports.cpython-312.pyc ADDED
Binary file (23.5 kB). View file
 
lib/python3.12/site-packages/wandb/apis/public/__pycache__/runs.cpython-312.pyc ADDED
Binary file (42.5 kB). View file
 
lib/python3.12/site-packages/wandb/apis/public/__pycache__/sweeps.cpython-312.pyc ADDED
Binary file (9.52 kB). View file
 
lib/python3.12/site-packages/wandb/apis/public/__pycache__/teams.cpython-312.pyc ADDED
Binary file (7.59 kB). View file
 
lib/python3.12/site-packages/wandb/apis/public/__pycache__/users.cpython-312.pyc ADDED
Binary file (5.75 kB). View file
 
lib/python3.12/site-packages/wandb/apis/public/__pycache__/utils.cpython-312.pyc ADDED
Binary file (10.1 kB). View file