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.
- wandb/__init__.py +5 -1
- wandb/__init__.pyi +43 -9
- wandb/_pydantic/__init__.py +23 -0
- wandb/_pydantic/base.py +113 -0
- wandb/_pydantic/v1_compat.py +262 -0
- wandb/apis/paginator.py +82 -38
- wandb/apis/public/api.py +10 -64
- wandb/apis/public/artifacts.py +73 -17
- wandb/apis/public/files.py +2 -2
- wandb/apis/public/projects.py +3 -2
- wandb/apis/public/reports.py +2 -2
- wandb/apis/public/runs.py +19 -11
- wandb/bin/gpu_stats.exe +0 -0
- wandb/bin/wandb-core +0 -0
- wandb/data_types.py +1 -1
- wandb/filesync/dir_watcher.py +2 -1
- wandb/integration/metaflow/metaflow.py +19 -17
- wandb/integration/sacred/__init__.py +1 -1
- wandb/jupyter.py +18 -15
- wandb/proto/v3/wandb_internal_pb2.py +7 -3
- wandb/proto/v3/wandb_settings_pb2.py +2 -2
- wandb/proto/v3/wandb_telemetry_pb2.py +10 -10
- wandb/proto/v4/wandb_internal_pb2.py +3 -3
- wandb/proto/v4/wandb_settings_pb2.py +2 -2
- wandb/proto/v4/wandb_telemetry_pb2.py +10 -10
- wandb/proto/v5/wandb_internal_pb2.py +3 -3
- wandb/proto/v5/wandb_settings_pb2.py +2 -2
- wandb/proto/v5/wandb_telemetry_pb2.py +10 -10
- wandb/proto/wandb_deprecated.py +2 -0
- wandb/sdk/artifacts/_graphql_fragments.py +18 -20
- wandb/sdk/artifacts/_validators.py +1 -0
- wandb/sdk/artifacts/artifact.py +81 -46
- wandb/sdk/artifacts/artifact_saver.py +16 -2
- wandb/sdk/artifacts/storage_policies/wandb_storage_policy.py +23 -2
- wandb/sdk/backend/backend.py +16 -5
- wandb/sdk/data_types/audio.py +1 -3
- wandb/sdk/data_types/base_types/media.py +11 -4
- wandb/sdk/data_types/image.py +44 -25
- wandb/sdk/data_types/molecule.py +1 -5
- wandb/sdk/data_types/object_3d.py +2 -1
- wandb/sdk/data_types/saved_model.py +7 -9
- wandb/sdk/data_types/video.py +1 -4
- wandb/sdk/interface/interface.py +65 -43
- wandb/sdk/interface/interface_queue.py +0 -7
- wandb/sdk/interface/interface_relay.py +6 -16
- wandb/sdk/interface/interface_shared.py +47 -40
- wandb/sdk/interface/interface_sock.py +1 -8
- wandb/sdk/interface/router.py +22 -54
- wandb/sdk/interface/router_queue.py +11 -10
- wandb/sdk/interface/router_relay.py +24 -12
- wandb/sdk/interface/router_sock.py +6 -11
- wandb/{apis/public → sdk/internal}/_generated/__init__.py +0 -6
- wandb/sdk/internal/_generated/base.py +226 -0
- wandb/{apis/public → sdk/internal}/_generated/server_features_query.py +3 -3
- wandb/{apis/public → sdk/internal}/_generated/typing_compat.py +1 -1
- wandb/sdk/internal/internal_api.py +138 -47
- wandb/sdk/internal/sender.py +5 -1
- wandb/sdk/internal/sender_config.py +8 -11
- wandb/sdk/internal/settings_static.py +24 -2
- wandb/sdk/lib/apikey.py +15 -16
- wandb/sdk/lib/console_capture.py +172 -0
- wandb/sdk/lib/redirect.py +102 -76
- wandb/sdk/lib/run_moment.py +4 -6
- wandb/sdk/lib/service_connection.py +37 -17
- wandb/sdk/lib/sock_client.py +2 -52
- wandb/sdk/lib/wb_logging.py +161 -0
- wandb/sdk/mailbox/__init__.py +3 -3
- wandb/sdk/mailbox/mailbox.py +31 -17
- wandb/sdk/mailbox/mailbox_handle.py +127 -0
- wandb/sdk/mailbox/{handles.py → response_handle.py} +34 -66
- wandb/sdk/mailbox/wait_with_progress.py +16 -15
- wandb/sdk/service/server_sock.py +4 -2
- wandb/sdk/service/streams.py +10 -5
- wandb/sdk/wandb_config.py +44 -43
- wandb/sdk/wandb_init.py +151 -92
- wandb/sdk/wandb_metadata.py +107 -91
- wandb/sdk/wandb_run.py +160 -54
- wandb/sdk/wandb_settings.py +410 -202
- wandb/sdk/wandb_setup.py +3 -1
- wandb/sdk/wandb_sync.py +1 -7
- {wandb-0.19.7.dist-info → wandb-0.19.9.dist-info}/METADATA +3 -3
- {wandb-0.19.7.dist-info → wandb-0.19.9.dist-info}/RECORD +88 -84
- wandb/apis/public/_generated/base.py +0 -128
- wandb/sdk/interface/message_future.py +0 -27
- wandb/sdk/interface/message_future_poll.py +0 -50
- /wandb/{apis/public → sdk/internal}/_generated/enums.py +0 -0
- /wandb/{apis/public → sdk/internal}/_generated/input_types.py +0 -0
- /wandb/{apis/public → sdk/internal}/_generated/operations.py +0 -0
- {wandb-0.19.7.dist-info → wandb-0.19.9.dist-info}/WHEEL +0 -0
- {wandb-0.19.7.dist-info → wandb-0.19.9.dist-info}/entry_points.txt +0 -0
- {wandb-0.19.7.dist-info → wandb-0.19.9.dist-info}/licenses/LICENSE +0 -0
wandb/sdk/lib/sock_client.py
CHANGED
@@ -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:
|
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:
|
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
|
wandb/sdk/mailbox/__init__.py
CHANGED
@@ -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
|
]
|
wandb/sdk/mailbox/mailbox.py
CHANGED
@@ -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
|
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
|
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,
|
31
|
+
self._handles: dict[str, MailboxResponseHandle] = {}
|
30
32
|
self._handles_lock = threading.Lock()
|
31
33
|
self._closed = False
|
32
34
|
|
33
|
-
def require_response(
|
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
|
49
|
-
|
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
|
-
|
52
|
-
|
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 =
|
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,
|
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 =
|
101
|
+
address = response.request_id
|
90
102
|
if not address:
|
91
|
-
|
92
|
-
|
93
|
-
|
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(
|
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
|
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
|
-
|
16
|
-
|
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
|
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.
|
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,
|
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.
|
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.
|
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
|
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
|
-
|
82
|
-
|
78
|
+
@override
|
79
|
+
def check(self) -> spb.ServerResponse | None:
|
83
80
|
with self._lock:
|
84
|
-
return self.
|
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
|
-
|
98
|
-
|
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.
|
114
|
-
return self.
|
94
|
+
if self._response:
|
95
|
+
return self._response
|
115
96
|
|
116
97
|
assert self._abandoned
|
117
98
|
raise HandleAbandonedError()
|
118
99
|
|
119
|
-
|
120
|
-
|
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.
|
146
|
-
return self.
|
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.
|
157
|
-
return self.
|
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
|
138
|
+
"""Add an event to signal when a response is received.
|
171
139
|
|
172
|
-
If a
|
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.
|
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
|
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
|
|