wandb 0.19.7__py3-none-win_amd64.whl → 0.19.9__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. wandb/__init__.py +5 -1
  2. wandb/__init__.pyi +43 -9
  3. wandb/_pydantic/__init__.py +23 -0
  4. wandb/_pydantic/base.py +113 -0
  5. wandb/_pydantic/v1_compat.py +262 -0
  6. wandb/apis/paginator.py +82 -38
  7. wandb/apis/public/api.py +10 -64
  8. wandb/apis/public/artifacts.py +73 -17
  9. wandb/apis/public/files.py +2 -2
  10. wandb/apis/public/projects.py +3 -2
  11. wandb/apis/public/reports.py +2 -2
  12. wandb/apis/public/runs.py +19 -11
  13. wandb/bin/gpu_stats.exe +0 -0
  14. wandb/bin/wandb-core +0 -0
  15. wandb/data_types.py +1 -1
  16. wandb/filesync/dir_watcher.py +2 -1
  17. wandb/integration/metaflow/metaflow.py +19 -17
  18. wandb/integration/sacred/__init__.py +1 -1
  19. wandb/jupyter.py +18 -15
  20. wandb/proto/v3/wandb_internal_pb2.py +7 -3
  21. wandb/proto/v3/wandb_settings_pb2.py +2 -2
  22. wandb/proto/v3/wandb_telemetry_pb2.py +10 -10
  23. wandb/proto/v4/wandb_internal_pb2.py +3 -3
  24. wandb/proto/v4/wandb_settings_pb2.py +2 -2
  25. wandb/proto/v4/wandb_telemetry_pb2.py +10 -10
  26. wandb/proto/v5/wandb_internal_pb2.py +3 -3
  27. wandb/proto/v5/wandb_settings_pb2.py +2 -2
  28. wandb/proto/v5/wandb_telemetry_pb2.py +10 -10
  29. wandb/proto/wandb_deprecated.py +2 -0
  30. wandb/sdk/artifacts/_graphql_fragments.py +18 -20
  31. wandb/sdk/artifacts/_validators.py +1 -0
  32. wandb/sdk/artifacts/artifact.py +81 -46
  33. wandb/sdk/artifacts/artifact_saver.py +16 -2
  34. wandb/sdk/artifacts/storage_policies/wandb_storage_policy.py +23 -2
  35. wandb/sdk/backend/backend.py +16 -5
  36. wandb/sdk/data_types/audio.py +1 -3
  37. wandb/sdk/data_types/base_types/media.py +11 -4
  38. wandb/sdk/data_types/image.py +44 -25
  39. wandb/sdk/data_types/molecule.py +1 -5
  40. wandb/sdk/data_types/object_3d.py +2 -1
  41. wandb/sdk/data_types/saved_model.py +7 -9
  42. wandb/sdk/data_types/video.py +1 -4
  43. wandb/sdk/interface/interface.py +65 -43
  44. wandb/sdk/interface/interface_queue.py +0 -7
  45. wandb/sdk/interface/interface_relay.py +6 -16
  46. wandb/sdk/interface/interface_shared.py +47 -40
  47. wandb/sdk/interface/interface_sock.py +1 -8
  48. wandb/sdk/interface/router.py +22 -54
  49. wandb/sdk/interface/router_queue.py +11 -10
  50. wandb/sdk/interface/router_relay.py +24 -12
  51. wandb/sdk/interface/router_sock.py +6 -11
  52. wandb/{apis/public → sdk/internal}/_generated/__init__.py +0 -6
  53. wandb/sdk/internal/_generated/base.py +226 -0
  54. wandb/{apis/public → sdk/internal}/_generated/server_features_query.py +3 -3
  55. wandb/{apis/public → sdk/internal}/_generated/typing_compat.py +1 -1
  56. wandb/sdk/internal/internal_api.py +138 -47
  57. wandb/sdk/internal/sender.py +5 -1
  58. wandb/sdk/internal/sender_config.py +8 -11
  59. wandb/sdk/internal/settings_static.py +24 -2
  60. wandb/sdk/lib/apikey.py +15 -16
  61. wandb/sdk/lib/console_capture.py +172 -0
  62. wandb/sdk/lib/redirect.py +102 -76
  63. wandb/sdk/lib/run_moment.py +4 -6
  64. wandb/sdk/lib/service_connection.py +37 -17
  65. wandb/sdk/lib/sock_client.py +2 -52
  66. wandb/sdk/lib/wb_logging.py +161 -0
  67. wandb/sdk/mailbox/__init__.py +3 -3
  68. wandb/sdk/mailbox/mailbox.py +31 -17
  69. wandb/sdk/mailbox/mailbox_handle.py +127 -0
  70. wandb/sdk/mailbox/{handles.py → response_handle.py} +34 -66
  71. wandb/sdk/mailbox/wait_with_progress.py +16 -15
  72. wandb/sdk/service/server_sock.py +4 -2
  73. wandb/sdk/service/streams.py +10 -5
  74. wandb/sdk/wandb_config.py +44 -43
  75. wandb/sdk/wandb_init.py +151 -92
  76. wandb/sdk/wandb_metadata.py +107 -91
  77. wandb/sdk/wandb_run.py +160 -54
  78. wandb/sdk/wandb_settings.py +410 -202
  79. wandb/sdk/wandb_setup.py +3 -1
  80. wandb/sdk/wandb_sync.py +1 -7
  81. {wandb-0.19.7.dist-info → wandb-0.19.9.dist-info}/METADATA +3 -3
  82. {wandb-0.19.7.dist-info → wandb-0.19.9.dist-info}/RECORD +88 -84
  83. wandb/apis/public/_generated/base.py +0 -128
  84. wandb/sdk/interface/message_future.py +0 -27
  85. wandb/sdk/interface/message_future_poll.py +0 -50
  86. /wandb/{apis/public → sdk/internal}/_generated/enums.py +0 -0
  87. /wandb/{apis/public → sdk/internal}/_generated/input_types.py +0 -0
  88. /wandb/{apis/public → sdk/internal}/_generated/operations.py +0 -0
  89. {wandb-0.19.7.dist-info → wandb-0.19.9.dist-info}/WHEEL +0 -0
  90. {wandb-0.19.7.dist-info → wandb-0.19.9.dist-info}/entry_points.txt +0 -0
  91. {wandb-0.19.7.dist-info → wandb-0.19.9.dist-info}/licenses/LICENSE +0 -0
@@ -150,10 +150,10 @@ class SockClient:
150
150
  with self._lock:
151
151
  self._sendall_with_error_handle(header + data)
152
152
 
153
- def send_server_request(self, msg: Any) -> None:
153
+ def send_server_request(self, msg: spb.ServerRequest) -> None:
154
154
  self._send_message(msg)
155
155
 
156
- def send_server_response(self, msg: Any) -> None:
156
+ def send_server_response(self, msg: spb.ServerResponse) -> None:
157
157
  try:
158
158
  self._send_message(msg)
159
159
  except BrokenPipeError:
@@ -161,56 +161,6 @@ class SockClient:
161
161
  # things like network status poll loop, there might be a better way to quiesce
162
162
  pass
163
163
 
164
- def send_and_recv(
165
- self,
166
- *,
167
- inform_init: Optional[spb.ServerInformInitRequest] = None,
168
- inform_start: Optional[spb.ServerInformStartRequest] = None,
169
- inform_attach: Optional[spb.ServerInformAttachRequest] = None,
170
- inform_finish: Optional[spb.ServerInformFinishRequest] = None,
171
- inform_teardown: Optional[spb.ServerInformTeardownRequest] = None,
172
- ) -> spb.ServerResponse:
173
- self.send(
174
- inform_init=inform_init,
175
- inform_start=inform_start,
176
- inform_attach=inform_attach,
177
- inform_finish=inform_finish,
178
- inform_teardown=inform_teardown,
179
- )
180
-
181
- # HACK: This assumes nothing else is reading on the socket, and that
182
- # the next response is for this request.
183
- response = self.read_server_response(timeout=1)
184
-
185
- if response is None:
186
- raise SockClientTimeoutError("No response after 1 second.")
187
-
188
- return response
189
-
190
- def send(
191
- self,
192
- *,
193
- inform_init: Optional[spb.ServerInformInitRequest] = None,
194
- inform_start: Optional[spb.ServerInformStartRequest] = None,
195
- inform_attach: Optional[spb.ServerInformAttachRequest] = None,
196
- inform_finish: Optional[spb.ServerInformFinishRequest] = None,
197
- inform_teardown: Optional[spb.ServerInformTeardownRequest] = None,
198
- ) -> None:
199
- server_req = spb.ServerRequest()
200
- if inform_init:
201
- server_req.inform_init.CopyFrom(inform_init)
202
- elif inform_start:
203
- server_req.inform_start.CopyFrom(inform_start)
204
- elif inform_attach:
205
- server_req.inform_attach.CopyFrom(inform_attach)
206
- elif inform_finish:
207
- server_req.inform_finish.CopyFrom(inform_finish)
208
- elif inform_teardown:
209
- server_req.inform_teardown.CopyFrom(inform_teardown)
210
- else:
211
- raise Exception("unmatched")
212
- self.send_server_request(server_req)
213
-
214
164
  def send_record_communicate(self, record: "pb.Record") -> None:
215
165
  server_req = spb.ServerRequest()
216
166
  server_req.request_id = record.control.mailbox_slot
@@ -0,0 +1,161 @@
1
+ """Logging configuration for the "wandb" logger.
2
+
3
+ Most log statements in wandb are made in the context of a run and should be
4
+ redirected to that run's log file (usually named 'debug.log'). This module
5
+ provides a context manager to temporarily set the current run ID and registers
6
+ a global handler for the 'wandb' logger that sends log statements to the right
7
+ place.
8
+
9
+ All functions in this module are threadsafe.
10
+
11
+ NOTE: The pytest caplog fixture will fail to capture logs from the wandb logger
12
+ because they are not propagated to the root logger.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import contextlib
18
+ import contextvars
19
+ import logging
20
+ import pathlib
21
+ from typing import Iterator
22
+
23
+
24
+ class _NotRunSpecific:
25
+ """Sentinel for `not_run_specific()`."""
26
+
27
+
28
+ _NOT_RUN_SPECIFIC = _NotRunSpecific()
29
+
30
+
31
+ _run_id: contextvars.ContextVar[str | _NotRunSpecific | None] = contextvars.ContextVar(
32
+ "_run_id",
33
+ default=None,
34
+ )
35
+
36
+ _logger = logging.getLogger("wandb")
37
+
38
+
39
+ def configure_wandb_logger() -> None:
40
+ """Configures the global 'wandb' logger.
41
+
42
+ The wandb logger is not intended to be customized by users. Instead, it is
43
+ used as a mechanism to redirect log messages into wandb run-specific log
44
+ files.
45
+
46
+ This function is idempotent: calling it multiple times has the same effect.
47
+ """
48
+ # Send all DEBUG and above messages to registered handlers.
49
+ #
50
+ # Per-run handlers can set different levels.
51
+ _logger.setLevel(logging.DEBUG)
52
+
53
+ # Do not propagate wandb logs to the root logger, which the user may have
54
+ # configured to point elsewhere. All wandb log messages should go to a run's
55
+ # log file.
56
+ _logger.propagate = False
57
+
58
+ # If no handlers are configured for the 'wandb' logger, don't activate the
59
+ # "lastResort" handler which sends messages to stderr with a level of
60
+ # WARNING by default.
61
+ #
62
+ # This occurs in wandb code that runs outside the context of a Run and
63
+ # not as part of the CLI.
64
+ #
65
+ # Most such code uses the `termlog` / `termwarn` / `termerror` methods
66
+ # to communicate with the user. When that code executes while a run is
67
+ # active, its logger messages go to that run's log file.
68
+ if not _logger.handlers:
69
+ _logger.addHandler(logging.NullHandler())
70
+
71
+
72
+ @contextlib.contextmanager
73
+ def log_to_run(run_id: str | None) -> Iterator[None]:
74
+ """Direct all wandb log messages to the given run.
75
+
76
+ Args:
77
+ id: The current run ID, or None if actions in the context manager are
78
+ not associated to a specific run. In the latter case, log messages
79
+ will go to all runs.
80
+
81
+ Usage:
82
+
83
+ with wb_logging.run_id(...):
84
+ ... # Log messages here go to the specified run's logger.
85
+ """
86
+ token = _run_id.set(run_id)
87
+ try:
88
+ yield
89
+ finally:
90
+ _run_id.reset(token)
91
+
92
+
93
+ @contextlib.contextmanager
94
+ def log_to_all_runs() -> Iterator[None]:
95
+ """Direct wandb log messages to all runs.
96
+
97
+ Unlike `log_to_run(None)`, this indicates an intentional choice.
98
+ This is often convenient to use as a decorator:
99
+
100
+ @wb_logging.log_to_all_runs()
101
+ def my_func():
102
+ ... # Log messages here go to the specified run's logger.
103
+ """
104
+ token = _run_id.set(_NOT_RUN_SPECIFIC)
105
+ try:
106
+ yield
107
+ finally:
108
+ _run_id.reset(token)
109
+
110
+
111
+ def add_file_handler(run_id: str, filepath: pathlib.Path) -> logging.Handler:
112
+ """Direct log messages for a run to a file.
113
+
114
+ Args:
115
+ run_id: The run for which to create a log file.
116
+ filepath: The file to write log messages to.
117
+
118
+ Returns:
119
+ The added handler which can then be configured further or removed
120
+ from the 'wandb' logger directly.
121
+
122
+ The default logging level is INFO.
123
+ """
124
+ handler = logging.FileHandler(filepath)
125
+ handler.setLevel(logging.INFO)
126
+ handler.addFilter(_RunIDFilter(run_id))
127
+ handler.setFormatter(
128
+ logging.Formatter(
129
+ "%(asctime)s %(levelname)-7s %(threadName)-10s:%(process)d"
130
+ " [%(filename)s:%(funcName)s():%(lineno)s]%(run_id_tag)s"
131
+ " %(message)s"
132
+ )
133
+ )
134
+
135
+ _logger.addHandler(handler)
136
+ return handler
137
+
138
+
139
+ class _RunIDFilter(logging.Filter):
140
+ """Filters out messages logged for a different run."""
141
+
142
+ def __init__(self, run_id: str) -> None:
143
+ """Create a _RunIDFilter.
144
+
145
+ Args:
146
+ run_id: Allows messages when the run ID is this or None.
147
+ """
148
+ self._run_id = run_id
149
+
150
+ def filter(self, record: logging.LogRecord) -> bool:
151
+ run_id = _run_id.get()
152
+
153
+ if run_id is None:
154
+ record.run_id_tag = " [no run ID]"
155
+ return True
156
+ elif isinstance(run_id, _NotRunSpecific):
157
+ record.run_id_tag = " [all runs]"
158
+ return True
159
+ else:
160
+ record.run_id_tag = ""
161
+ return run_id == self._run_id
@@ -9,15 +9,15 @@ The Mailbox handles matching responses to requests. An internal thread
9
9
  continuously reads data from the service and passes it to the mailbox.
10
10
  """
11
11
 
12
- from .handles import HandleAbandonedError, MailboxHandle
13
12
  from .mailbox import Mailbox, MailboxClosedError
13
+ from .mailbox_handle import HandleAbandonedError, MailboxHandle
14
14
  from .wait_with_progress import wait_all_with_progress, wait_with_progress
15
15
 
16
16
  __all__ = [
17
- "HandleAbandonedError",
18
- "MailboxHandle",
19
17
  "Mailbox",
20
18
  "MailboxClosedError",
19
+ "HandleAbandonedError",
20
+ "MailboxHandle",
21
21
  "wait_all_with_progress",
22
22
  "wait_with_progress",
23
23
  ]
@@ -6,8 +6,10 @@ import string
6
6
  import threading
7
7
 
8
8
  from wandb.proto import wandb_internal_pb2 as pb
9
+ from wandb.proto import wandb_server_pb2 as spb
9
10
 
10
- from . import handles
11
+ from .mailbox_handle import MailboxHandle
12
+ from .response_handle import MailboxResponseHandle
11
13
 
12
14
  _logger = logging.getLogger(__name__)
13
15
 
@@ -19,22 +21,25 @@ class MailboxClosedError(Exception):
19
21
  class Mailbox:
20
22
  """Matches service responses to requests.
21
23
 
22
- The mailbox can set an address on a Record and create a handle for
24
+ The mailbox can set an address on a server request and create a handle for
23
25
  waiting for a response to that record. Responses are delivered by calling
24
26
  `deliver()`. The `close()` method abandons all handles in case the
25
27
  service process becomes unreachable.
26
28
  """
27
29
 
28
30
  def __init__(self) -> None:
29
- self._handles: dict[str, handles.MailboxHandle] = {}
31
+ self._handles: dict[str, MailboxResponseHandle] = {}
30
32
  self._handles_lock = threading.Lock()
31
33
  self._closed = False
32
34
 
33
- def require_response(self, request: pb.Record) -> handles.MailboxHandle:
35
+ def require_response(
36
+ self,
37
+ request: spb.ServerRequest | pb.Record,
38
+ ) -> MailboxHandle[spb.ServerResponse]:
34
39
  """Set a response address on a request.
35
40
 
36
41
  Args:
37
- request: The request on which to set a mailbox slot.
42
+ request: The request on which to set a request ID or mailbox slot.
38
43
  This is mutated. An address must not already be set.
39
44
 
40
45
  Returns:
@@ -45,17 +50,24 @@ class Mailbox:
45
50
  no new responses are expected to be delivered and new handles
46
51
  cannot be created.
47
52
  """
48
- if address := request.control.mailbox_slot:
49
- raise ValueError(f"Request already has an address ({address})")
53
+ if isinstance(request, spb.ServerRequest):
54
+ if address := request.request_id:
55
+ raise ValueError(f"Request already has an address ({address})")
50
56
 
51
- address = self._new_address()
52
- request.control.mailbox_slot = address
57
+ address = self._new_address()
58
+ request.request_id = address
59
+ else:
60
+ if address := request.control.mailbox_slot:
61
+ raise ValueError(f"Request already has an address ({address})")
62
+
63
+ address = self._new_address()
64
+ request.control.mailbox_slot = address
53
65
 
54
66
  with self._handles_lock:
55
67
  if self._closed:
56
68
  raise MailboxClosedError()
57
69
 
58
- handle = handles.MailboxHandle(address)
70
+ handle = MailboxResponseHandle(address)
59
71
  self._handles[address] = handle
60
72
 
61
73
  return handle
@@ -80,18 +92,20 @@ class Mailbox:
80
92
 
81
93
  return address
82
94
 
83
- def deliver(self, result: pb.Result) -> None:
95
+ def deliver(self, response: spb.ServerResponse) -> None:
84
96
  """Deliver a response from the service.
85
97
 
86
98
  If the response address is invalid, this does nothing.
87
99
  It is a no-op if the mailbox has been closed.
88
100
  """
89
- address = result.control.mailbox_slot
101
+ address = response.request_id
90
102
  if not address:
91
- _logger.error(
92
- "Received response with no mailbox slot."
93
- f" Kind: {result.WhichOneof('result_type')}"
94
- )
103
+ kind: str | None = response.WhichOneof("server_response_type")
104
+ if kind == "result_communicate":
105
+ result_type = response.result_communicate.WhichOneof("result_type")
106
+ kind = f"result_communicate.{result_type}"
107
+
108
+ _logger.error(f"Received response with no mailbox slot: {kind}")
95
109
  return
96
110
 
97
111
  with self._handles_lock:
@@ -102,7 +116,7 @@ class Mailbox:
102
116
  # It is not an error if there is no handle for the address:
103
117
  # handles can be abandoned if the result is no longer needed.
104
118
  if handle:
105
- handle.deliver(result)
119
+ handle.deliver(response)
106
120
 
107
121
  def close(self) -> None:
108
122
  """Indicate no further responses will be delivered.
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import sys
5
+ from typing import TYPE_CHECKING, Callable, Generic, TypeVar
6
+
7
+ # Necessary to break an import loop.
8
+ if TYPE_CHECKING:
9
+ from wandb.sdk.interface import interface
10
+
11
+ if sys.version_info >= (3, 12):
12
+ from typing import override
13
+ else:
14
+ from typing_extensions import override
15
+
16
+
17
+ _T = TypeVar("_T")
18
+ _S = TypeVar("_S")
19
+
20
+
21
+ class HandleAbandonedError(Exception):
22
+ """The handle has no response and has been abandoned."""
23
+
24
+
25
+ class MailboxHandle(abc.ABC, Generic[_T]):
26
+ """A thread-safe handle that allows waiting for a response to a request."""
27
+
28
+ def map(self, fn: Callable[[_T], _S]) -> MailboxHandle[_S]:
29
+ """Returns a transformed handle.
30
+
31
+ Methods on the returned handle call methods on this handle, but the
32
+ response type is derived using the given function.
33
+
34
+ Args:
35
+ fn: A function to apply to this handle's result to get the new
36
+ handle's result. The function should be pure and fast.
37
+ """
38
+ return _MailboxMappedHandle(self, fn)
39
+
40
+ @abc.abstractmethod
41
+ def abandon(self) -> None:
42
+ """Abandon the handle, indicating it will not receive a response."""
43
+
44
+ @abc.abstractmethod
45
+ def cancel(self, iface: interface.InterfaceBase) -> None:
46
+ """Cancel the handle, requesting any associated work to not complete.
47
+
48
+ This automatically abandons the handle, as a response is no longer
49
+ guaranteed.
50
+
51
+ Args:
52
+ iface: The interface on which to publish the cancel request.
53
+ """
54
+
55
+ @abc.abstractmethod
56
+ def check(self) -> _T | None:
57
+ """Returns the response if it's ready."""
58
+
59
+ @abc.abstractmethod
60
+ def wait_or(self, *, timeout: float | None) -> _T:
61
+ """Wait for a response or a timeout.
62
+
63
+ Args:
64
+ timeout: A finite number of seconds or None to never time out.
65
+ If less than or equal to zero, times out immediately unless
66
+ the response is available.
67
+
68
+ Returns:
69
+ The response if it arrives before the timeout or has already arrived.
70
+
71
+ Raises:
72
+ TimeoutError: If the timeout is reached.
73
+ HandleAbandonedError: If the handle becomes abandoned.
74
+ """
75
+
76
+ @abc.abstractmethod
77
+ async def wait_async(self, *, timeout: float | None) -> _T:
78
+ """Wait for a response or timeout.
79
+
80
+ This must run in an `asyncio` event loop.
81
+
82
+ Args:
83
+ timeout: A finite number of seconds or None to never time out.
84
+
85
+ Returns:
86
+ The response if it arrives before the timeout or has already arrived.
87
+
88
+ Raises:
89
+ TimeoutError: If the timeout is reached.
90
+ HandleAbandonedError: If the handle becomes abandoned.
91
+ """
92
+
93
+
94
+ class _MailboxMappedHandle(Generic[_S], MailboxHandle[_S]):
95
+ """A mailbox handle whose result is derived from another handle."""
96
+
97
+ def __init__(
98
+ self,
99
+ handle: MailboxHandle[_T],
100
+ fn: Callable[[_T], _S],
101
+ ) -> None:
102
+ self._handle = handle
103
+ self._fn = fn
104
+
105
+ @override
106
+ def abandon(self) -> None:
107
+ self._handle.abandon()
108
+
109
+ @override
110
+ def cancel(self, iface: interface.InterfaceBase) -> None:
111
+ self._handle.cancel(iface)
112
+
113
+ @override
114
+ def check(self) -> _S | None:
115
+ if response := self._handle.check():
116
+ return self._fn(response)
117
+ else:
118
+ return None
119
+
120
+ @override
121
+ def wait_or(self, *, timeout: float | None) -> _S:
122
+ return self._fn(self._handle.wait_or(timeout=timeout))
123
+
124
+ @override
125
+ async def wait_async(self, *, timeout: float | None) -> _S:
126
+ response = await self._handle.wait_async(timeout=timeout)
127
+ return self._fn(response)
@@ -2,22 +2,26 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import math
5
+ import sys
5
6
  import threading
6
7
  from typing import TYPE_CHECKING
7
8
 
8
- from wandb.proto import wandb_internal_pb2 as pb
9
+ from wandb.proto import wandb_server_pb2 as spb
10
+
11
+ from .mailbox_handle import HandleAbandonedError, MailboxHandle
9
12
 
10
13
  # Necessary to break an import loop.
11
14
  if TYPE_CHECKING:
12
15
  from wandb.sdk.interface import interface
13
16
 
14
-
15
- class HandleAbandonedError(Exception):
16
- """The handle has no result and has been abandoned."""
17
+ if sys.version_info >= (3, 12):
18
+ from typing import override
19
+ else:
20
+ from typing_extensions import override
17
21
 
18
22
 
19
- class MailboxHandle:
20
- """A thread-safe handle that allows waiting for a response to a request."""
23
+ class MailboxResponseHandle(MailboxHandle[spb.ServerResponse]):
24
+ """A general handle for any ServerResponse."""
21
25
 
22
26
  def __init__(self, address: str) -> None:
23
27
  self._address = address
@@ -25,11 +29,11 @@ class MailboxHandle:
25
29
  self._event = threading.Event()
26
30
 
27
31
  self._abandoned = False
28
- self._result: pb.Result | None = None
32
+ self._response: spb.ServerResponse | None = None
29
33
 
30
34
  self._asyncio_events: dict[asyncio.Event, _AsyncioEvent] = dict()
31
35
 
32
- def deliver(self, result: pb.Result) -> None:
36
+ def deliver(self, response: spb.ServerResponse) -> None:
33
37
  """Deliver the response.
34
38
 
35
39
  This may only be called once. It is an error to respond to the same
@@ -39,34 +43,27 @@ class MailboxHandle:
39
43
  if self._abandoned:
40
44
  return
41
45
 
42
- if self._result:
46
+ if self._response:
43
47
  raise ValueError(
44
48
  f"A response has already been delivered to {self._address}."
45
49
  )
46
50
 
47
- self._result = result
51
+ self._response = response
48
52
  self._signal_done()
49
53
 
54
+ @override
50
55
  def cancel(self, iface: interface.InterfaceBase) -> None:
51
- """Cancel the handle, requesting any associated work to not complete.
52
-
53
- This automatically abandons the handle, as a response is no longer
54
- guaranteed.
55
-
56
- Args:
57
- interface: The interface on which to publish the cancel request.
58
- """
59
56
  iface.publish_cancel(self._address)
60
57
  self.abandon()
61
58
 
59
+ @override
62
60
  def abandon(self) -> None:
63
- """Abandon the handle, indicating it will not receive a response."""
64
61
  with self._lock:
65
62
  self._abandoned = True
66
63
  self._signal_done()
67
64
 
68
65
  def _signal_done(self) -> None:
69
- """Indicate that the handle either got a result or became abandoned.
66
+ """Indicate that the handle either got a response or became abandoned.
70
67
 
71
68
  The lock must be held.
72
69
  """
@@ -78,29 +75,13 @@ class MailboxHandle:
78
75
  asyncio_event.set_threadsafe()
79
76
  self._asyncio_events.clear()
80
77
 
81
- def check(self) -> pb.Result | None:
82
- """Returns the result if it's ready."""
78
+ @override
79
+ def check(self) -> spb.ServerResponse | None:
83
80
  with self._lock:
84
- return self._result
85
-
86
- def wait_or(self, *, timeout: float | None) -> pb.Result:
87
- """Wait for a response or a timeout.
88
-
89
- This is called `wait_or` because it replaces a method called `wait`
90
- with different semantics.
91
-
92
- Args:
93
- timeout: A finite number of seconds or None to never time out.
94
- If less than or equal to zero, times out immediately unless
95
- the result is available.
81
+ return self._response
96
82
 
97
- Returns:
98
- The result if it arrives before the timeout or has already arrived.
99
-
100
- Raises:
101
- TimeoutError: If the timeout is reached.
102
- HandleAbandonedError: If the handle becomes abandoned.
103
- """
83
+ @override
84
+ def wait_or(self, *, timeout: float | None) -> spb.ServerResponse:
104
85
  if timeout is not None and not math.isfinite(timeout):
105
86
  raise ValueError("Timeout must be finite or None.")
106
87
 
@@ -110,27 +91,14 @@ class MailboxHandle:
110
91
  )
111
92
 
112
93
  with self._lock:
113
- if self._result:
114
- return self._result
94
+ if self._response:
95
+ return self._response
115
96
 
116
97
  assert self._abandoned
117
98
  raise HandleAbandonedError()
118
99
 
119
- async def wait_async(self, *, timeout: float | None) -> pb.Result:
120
- """Wait for a response or timeout.
121
-
122
- This must run in an `asyncio` event loop.
123
-
124
- Args:
125
- timeout: A finite number of seconds or None to never time out.
126
-
127
- Returns:
128
- The result if it arrives before the timeout or has already arrived.
129
-
130
- Raises:
131
- TimeoutError: If the timeout is reached.
132
- HandleAbandonedError: If the handle becomes abandoned.
133
- """
100
+ @override
101
+ async def wait_async(self, *, timeout: float | None) -> spb.ServerResponse:
134
102
  if timeout is not None and not math.isfinite(timeout):
135
103
  raise ValueError("Timeout must be finite or None.")
136
104
 
@@ -142,8 +110,8 @@ class MailboxHandle:
142
110
 
143
111
  except (asyncio.TimeoutError, TimeoutError) as e:
144
112
  with self._lock:
145
- if self._result:
146
- return self._result
113
+ if self._response:
114
+ return self._response
147
115
  elif self._abandoned:
148
116
  raise HandleAbandonedError()
149
117
  else:
@@ -153,8 +121,8 @@ class MailboxHandle:
153
121
 
154
122
  else:
155
123
  with self._lock:
156
- if self._result:
157
- return self._result
124
+ if self._response:
125
+ return self._response
158
126
 
159
127
  assert self._abandoned
160
128
  raise HandleAbandonedError()
@@ -167,20 +135,20 @@ class MailboxHandle:
167
135
  loop: asyncio.AbstractEventLoop,
168
136
  event: asyncio.Event,
169
137
  ) -> None:
170
- """Add an event to signal when a result is received.
138
+ """Add an event to signal when a response is received.
171
139
 
172
- If a result already exists, this notifies the event loop immediately.
140
+ If a response already exists, this notifies the event loop immediately.
173
141
  """
174
142
  asyncio_event = _AsyncioEvent(loop, event)
175
143
 
176
144
  with self._lock:
177
- if self._result or self._abandoned:
145
+ if self._response or self._abandoned:
178
146
  asyncio_event.set_threadsafe()
179
147
  else:
180
148
  self._asyncio_events[event] = asyncio_event
181
149
 
182
150
  def _forget_asyncio_event(self, event: asyncio.Event) -> None:
183
- """Cancel signalling an event when a result is received."""
151
+ """Cancel signalling an event when a response is received."""
184
152
  with self._lock:
185
153
  self._asyncio_events.pop(event, None)
186
154