wandb 0.19.6rc4__py3-none-any.whl → 0.19.8__py3-none-any.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 (81) hide show
  1. wandb/__init__.py +1 -1
  2. wandb/__init__.pyi +56 -6
  3. wandb/apis/public/_generated/__init__.py +21 -0
  4. wandb/apis/public/_generated/base.py +128 -0
  5. wandb/apis/public/_generated/enums.py +4 -0
  6. wandb/apis/public/_generated/input_types.py +4 -0
  7. wandb/apis/public/_generated/operations.py +15 -0
  8. wandb/apis/public/_generated/server_features_query.py +27 -0
  9. wandb/apis/public/_generated/typing_compat.py +14 -0
  10. wandb/apis/public/api.py +192 -6
  11. wandb/apis/public/artifacts.py +13 -45
  12. wandb/apis/public/registries.py +573 -0
  13. wandb/apis/public/utils.py +36 -0
  14. wandb/bin/gpu_stats +0 -0
  15. wandb/cli/cli.py +11 -20
  16. wandb/data_types.py +1 -1
  17. wandb/env.py +10 -0
  18. wandb/filesync/dir_watcher.py +2 -1
  19. wandb/proto/v3/wandb_internal_pb2.py +243 -222
  20. wandb/proto/v3/wandb_server_pb2.py +4 -4
  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 +226 -222
  24. wandb/proto/v4/wandb_server_pb2.py +4 -4
  25. wandb/proto/v4/wandb_settings_pb2.py +2 -2
  26. wandb/proto/v4/wandb_telemetry_pb2.py +10 -10
  27. wandb/proto/v5/wandb_internal_pb2.py +226 -222
  28. wandb/proto/v5/wandb_server_pb2.py +4 -4
  29. wandb/proto/v5/wandb_settings_pb2.py +2 -2
  30. wandb/proto/v5/wandb_telemetry_pb2.py +10 -10
  31. wandb/sdk/artifacts/_graphql_fragments.py +126 -0
  32. wandb/sdk/artifacts/artifact.py +51 -95
  33. wandb/sdk/backend/backend.py +17 -6
  34. wandb/sdk/data_types/helper_types/bounding_boxes_2d.py +14 -6
  35. wandb/sdk/data_types/helper_types/image_mask.py +12 -6
  36. wandb/sdk/data_types/saved_model.py +35 -46
  37. wandb/sdk/data_types/video.py +7 -16
  38. wandb/sdk/interface/interface.py +87 -49
  39. wandb/sdk/interface/interface_queue.py +5 -15
  40. wandb/sdk/interface/interface_relay.py +7 -22
  41. wandb/sdk/interface/interface_shared.py +65 -136
  42. wandb/sdk/interface/interface_sock.py +3 -21
  43. wandb/sdk/interface/router.py +42 -68
  44. wandb/sdk/interface/router_queue.py +13 -11
  45. wandb/sdk/interface/router_relay.py +26 -13
  46. wandb/sdk/interface/router_sock.py +12 -16
  47. wandb/sdk/internal/handler.py +4 -3
  48. wandb/sdk/internal/internal_api.py +12 -1
  49. wandb/sdk/internal/sender.py +3 -19
  50. wandb/sdk/lib/apikey.py +87 -26
  51. wandb/sdk/lib/asyncio_compat.py +210 -0
  52. wandb/sdk/lib/console_capture.py +172 -0
  53. wandb/sdk/lib/progress.py +78 -16
  54. wandb/sdk/lib/redirect.py +102 -76
  55. wandb/sdk/lib/service_connection.py +37 -17
  56. wandb/sdk/lib/sock_client.py +6 -56
  57. wandb/sdk/mailbox/__init__.py +23 -0
  58. wandb/sdk/mailbox/mailbox.py +135 -0
  59. wandb/sdk/mailbox/mailbox_handle.py +127 -0
  60. wandb/sdk/mailbox/response_handle.py +167 -0
  61. wandb/sdk/mailbox/wait_with_progress.py +135 -0
  62. wandb/sdk/service/server_sock.py +9 -3
  63. wandb/sdk/service/streams.py +75 -78
  64. wandb/sdk/verify/verify.py +54 -2
  65. wandb/sdk/wandb_init.py +72 -75
  66. wandb/sdk/wandb_login.py +7 -4
  67. wandb/sdk/wandb_metadata.py +65 -34
  68. wandb/sdk/wandb_require.py +14 -8
  69. wandb/sdk/wandb_run.py +90 -97
  70. wandb/sdk/wandb_settings.py +10 -4
  71. wandb/sdk/wandb_setup.py +19 -8
  72. wandb/sdk/wandb_sync.py +2 -10
  73. wandb/util.py +3 -1
  74. {wandb-0.19.6rc4.dist-info → wandb-0.19.8.dist-info}/METADATA +2 -2
  75. {wandb-0.19.6rc4.dist-info → wandb-0.19.8.dist-info}/RECORD +78 -65
  76. wandb/sdk/interface/message_future.py +0 -27
  77. wandb/sdk/interface/message_future_poll.py +0 -50
  78. wandb/sdk/lib/mailbox.py +0 -442
  79. {wandb-0.19.6rc4.dist-info → wandb-0.19.8.dist-info}/WHEEL +0 -0
  80. {wandb-0.19.6rc4.dist-info → wandb-0.19.8.dist-info}/entry_points.txt +0 -0
  81. {wandb-0.19.6rc4.dist-info → wandb-0.19.8.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,23 @@
1
+ """A message protocol for the internal service process.
2
+
3
+ The core of W&B is implemented by a side process that asynchronously uploads
4
+ data. The client process (such as this Python code) sends requests to the
5
+ service, and for some requests, the service eventually sends a response.
6
+
7
+ The client can send multiple requests before the service provides a response.
8
+ The Mailbox handles matching responses to requests. An internal thread
9
+ continuously reads data from the service and passes it to the mailbox.
10
+ """
11
+
12
+ from .mailbox import Mailbox, MailboxClosedError
13
+ from .mailbox_handle import HandleAbandonedError, MailboxHandle
14
+ from .wait_with_progress import wait_all_with_progress, wait_with_progress
15
+
16
+ __all__ = [
17
+ "Mailbox",
18
+ "MailboxClosedError",
19
+ "HandleAbandonedError",
20
+ "MailboxHandle",
21
+ "wait_all_with_progress",
22
+ "wait_with_progress",
23
+ ]
@@ -0,0 +1,135 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import secrets
5
+ import string
6
+ import threading
7
+
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 MailboxHandle
12
+ from .response_handle import MailboxResponseHandle
13
+
14
+ _logger = logging.getLogger(__name__)
15
+
16
+
17
+ class MailboxClosedError(Exception):
18
+ """The mailbox has been closed and cannot be used."""
19
+
20
+
21
+ class Mailbox:
22
+ """Matches service responses to requests.
23
+
24
+ The mailbox can set an address on a server request and create a handle for
25
+ waiting for a response to that record. Responses are delivered by calling
26
+ `deliver()`. The `close()` method abandons all handles in case the
27
+ service process becomes unreachable.
28
+ """
29
+
30
+ def __init__(self) -> None:
31
+ self._handles: dict[str, MailboxResponseHandle] = {}
32
+ self._handles_lock = threading.Lock()
33
+ self._closed = False
34
+
35
+ def require_response(
36
+ self,
37
+ request: spb.ServerRequest | pb.Record,
38
+ ) -> MailboxHandle[spb.ServerResponse]:
39
+ """Set a response address on a request.
40
+
41
+ Args:
42
+ request: The request on which to set a request ID or mailbox slot.
43
+ This is mutated. An address must not already be set.
44
+
45
+ Returns:
46
+ A handle for waiting for the response to the request.
47
+
48
+ Raises:
49
+ MailboxClosedError: If the mailbox has been closed, in which case
50
+ no new responses are expected to be delivered and new handles
51
+ cannot be created.
52
+ """
53
+ if isinstance(request, spb.ServerRequest):
54
+ if address := request.request_id:
55
+ raise ValueError(f"Request already has an address ({address})")
56
+
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
65
+
66
+ with self._handles_lock:
67
+ if self._closed:
68
+ raise MailboxClosedError()
69
+
70
+ handle = MailboxResponseHandle(address)
71
+ self._handles[address] = handle
72
+
73
+ return handle
74
+
75
+ def _new_address(self) -> str:
76
+ """Returns an unused address for a request.
77
+
78
+ Assumes `_handles_lock` is held.
79
+ """
80
+
81
+ def generate():
82
+ return "".join(
83
+ secrets.choice(string.ascii_lowercase + string.digits)
84
+ for i in range(12)
85
+ )
86
+
87
+ address = generate()
88
+
89
+ # Being extra cautious. This loop will almost never be entered.
90
+ while address in self._handles:
91
+ address = generate()
92
+
93
+ return address
94
+
95
+ def deliver(self, response: spb.ServerResponse) -> None:
96
+ """Deliver a response from the service.
97
+
98
+ If the response address is invalid, this does nothing.
99
+ It is a no-op if the mailbox has been closed.
100
+ """
101
+ address = response.request_id
102
+ if not address:
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}")
109
+ return
110
+
111
+ with self._handles_lock:
112
+ # NOTE: If the mailbox is closed, this returns None because
113
+ # we clear the dict.
114
+ handle = self._handles.pop(address, None)
115
+
116
+ # It is not an error if there is no handle for the address:
117
+ # handles can be abandoned if the result is no longer needed.
118
+ if handle:
119
+ handle.deliver(response)
120
+
121
+ def close(self) -> None:
122
+ """Indicate no further responses will be delivered.
123
+
124
+ Abandons all handles.
125
+ """
126
+ with self._handles_lock:
127
+ self._closed = True
128
+
129
+ _logger.info(
130
+ f"Closing mailbox, abandoning {len(self._handles)} handles.",
131
+ )
132
+
133
+ for handle in self._handles.values():
134
+ handle.abandon()
135
+ self._handles.clear()
@@ -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)
@@ -0,0 +1,167 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import math
5
+ import sys
6
+ import threading
7
+ from typing import TYPE_CHECKING
8
+
9
+ from wandb.proto import wandb_server_pb2 as spb
10
+
11
+ from .mailbox_handle import HandleAbandonedError, MailboxHandle
12
+
13
+ # Necessary to break an import loop.
14
+ if TYPE_CHECKING:
15
+ from wandb.sdk.interface import interface
16
+
17
+ if sys.version_info >= (3, 12):
18
+ from typing import override
19
+ else:
20
+ from typing_extensions import override
21
+
22
+
23
+ class MailboxResponseHandle(MailboxHandle[spb.ServerResponse]):
24
+ """A general handle for any ServerResponse."""
25
+
26
+ def __init__(self, address: str) -> None:
27
+ self._address = address
28
+ self._lock = threading.Lock()
29
+ self._event = threading.Event()
30
+
31
+ self._abandoned = False
32
+ self._response: spb.ServerResponse | None = None
33
+
34
+ self._asyncio_events: dict[asyncio.Event, _AsyncioEvent] = dict()
35
+
36
+ def deliver(self, response: spb.ServerResponse) -> None:
37
+ """Deliver the response.
38
+
39
+ This may only be called once. It is an error to respond to the same
40
+ request more than once. It is a no-op if the handle has been abandoned.
41
+ """
42
+ with self._lock:
43
+ if self._abandoned:
44
+ return
45
+
46
+ if self._response:
47
+ raise ValueError(
48
+ f"A response has already been delivered to {self._address}."
49
+ )
50
+
51
+ self._response = response
52
+ self._signal_done()
53
+
54
+ @override
55
+ def cancel(self, iface: interface.InterfaceBase) -> None:
56
+ iface.publish_cancel(self._address)
57
+ self.abandon()
58
+
59
+ @override
60
+ def abandon(self) -> None:
61
+ with self._lock:
62
+ self._abandoned = True
63
+ self._signal_done()
64
+
65
+ def _signal_done(self) -> None:
66
+ """Indicate that the handle either got a response or became abandoned.
67
+
68
+ The lock must be held.
69
+ """
70
+ # Unblock threads blocked on `wait_or`.
71
+ self._event.set()
72
+
73
+ # Unblock asyncio loops blocked on `wait_async`.
74
+ for asyncio_event in self._asyncio_events.values():
75
+ asyncio_event.set_threadsafe()
76
+ self._asyncio_events.clear()
77
+
78
+ @override
79
+ def check(self) -> spb.ServerResponse | None:
80
+ with self._lock:
81
+ return self._response
82
+
83
+ @override
84
+ def wait_or(self, *, timeout: float | None) -> spb.ServerResponse:
85
+ if timeout is not None and not math.isfinite(timeout):
86
+ raise ValueError("Timeout must be finite or None.")
87
+
88
+ if not self._event.wait(timeout=timeout):
89
+ raise TimeoutError(
90
+ f"Timed out waiting for response on {self._address}",
91
+ )
92
+
93
+ with self._lock:
94
+ if self._response:
95
+ return self._response
96
+
97
+ assert self._abandoned
98
+ raise HandleAbandonedError()
99
+
100
+ @override
101
+ async def wait_async(self, *, timeout: float | None) -> spb.ServerResponse:
102
+ if timeout is not None and not math.isfinite(timeout):
103
+ raise ValueError("Timeout must be finite or None.")
104
+
105
+ evt = asyncio.Event()
106
+ self._add_asyncio_event(asyncio.get_event_loop(), evt)
107
+
108
+ try:
109
+ await asyncio.wait_for(evt.wait(), timeout=timeout)
110
+
111
+ except (asyncio.TimeoutError, TimeoutError) as e:
112
+ with self._lock:
113
+ if self._response:
114
+ return self._response
115
+ elif self._abandoned:
116
+ raise HandleAbandonedError()
117
+ else:
118
+ raise TimeoutError(
119
+ f"Timed out waiting for response on {self._address}"
120
+ ) from e
121
+
122
+ else:
123
+ with self._lock:
124
+ if self._response:
125
+ return self._response
126
+
127
+ assert self._abandoned
128
+ raise HandleAbandonedError()
129
+
130
+ finally:
131
+ self._forget_asyncio_event(evt)
132
+
133
+ def _add_asyncio_event(
134
+ self,
135
+ loop: asyncio.AbstractEventLoop,
136
+ event: asyncio.Event,
137
+ ) -> None:
138
+ """Add an event to signal when a response is received.
139
+
140
+ If a response already exists, this notifies the event loop immediately.
141
+ """
142
+ asyncio_event = _AsyncioEvent(loop, event)
143
+
144
+ with self._lock:
145
+ if self._response or self._abandoned:
146
+ asyncio_event.set_threadsafe()
147
+ else:
148
+ self._asyncio_events[event] = asyncio_event
149
+
150
+ def _forget_asyncio_event(self, event: asyncio.Event) -> None:
151
+ """Cancel signalling an event when a response is received."""
152
+ with self._lock:
153
+ self._asyncio_events.pop(event, None)
154
+
155
+
156
+ class _AsyncioEvent:
157
+ def __init__(
158
+ self,
159
+ loop: asyncio.AbstractEventLoop,
160
+ event: asyncio.Event,
161
+ ):
162
+ self._loop = loop
163
+ self._event = event
164
+
165
+ def set_threadsafe(self) -> None:
166
+ """Set the asyncio event in its own loop."""
167
+ self._loop.call_soon_threadsafe(self._event.set)
@@ -0,0 +1,135 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Any, Callable, Coroutine, List, TypeVar, cast
5
+
6
+ from wandb.sdk.lib import asyncio_compat
7
+
8
+ from .mailbox_handle import MailboxHandle
9
+
10
+ _T = TypeVar("_T")
11
+
12
+
13
+ def wait_with_progress(
14
+ handle: MailboxHandle[_T],
15
+ *,
16
+ timeout: float | None,
17
+ progress_after: float,
18
+ display_progress: Callable[[], Coroutine[Any, Any, None]],
19
+ ) -> _T:
20
+ """Wait for a handle, possibly displaying progress to the user.
21
+
22
+ Equivalent to passing a single handle to `wait_all_with_progress`.
23
+ """
24
+ return wait_all_with_progress(
25
+ [handle],
26
+ timeout=timeout,
27
+ progress_after=progress_after,
28
+ display_progress=display_progress,
29
+ )[0]
30
+
31
+
32
+ def wait_all_with_progress(
33
+ handle_list: list[MailboxHandle[_T]],
34
+ *,
35
+ timeout: float | None,
36
+ progress_after: float,
37
+ display_progress: Callable[[], Coroutine[Any, Any, None]],
38
+ ) -> list[_T]:
39
+ """Wait for multiple handles, possibly displaying progress to the user.
40
+
41
+ Args:
42
+ handle_list: The handles to wait for.
43
+ timeout: A number of seconds after which to raise a TimeoutError,
44
+ or None if this should never timeout.
45
+ progress_after: A number of seconds after which to start the
46
+ display_progress callback. Starting the callback creates a thread
47
+ and starts an asyncio loop, so we want to avoid doing it if
48
+ the handle is resolved quickly.
49
+ display_progress: An asyncio function that displays progress to
50
+ the user. This function is executed on a new thread and cancelled
51
+ if the timeout is exceeded.
52
+
53
+ Returns:
54
+ A list where the Nth item is the Nth handle's result.
55
+
56
+ Raises:
57
+ TimeoutError: If the overall timeout expires.
58
+ HandleAbandonedError: If any handle becomes abandoned.
59
+ Exception: Any exception from the display function is propagated.
60
+ """
61
+ if not handle_list:
62
+ return []
63
+
64
+ if timeout is not None and timeout <= progress_after:
65
+ return _wait_handles(handle_list, timeout=timeout)
66
+
67
+ start_time = time.monotonic()
68
+
69
+ try:
70
+ return _wait_handles(handle_list, timeout=progress_after)
71
+ except TimeoutError:
72
+ pass
73
+
74
+ async def progress_loop_with_timeout() -> list[_T]:
75
+ with asyncio_compat.cancel_on_exit(display_progress()):
76
+ if timeout is not None:
77
+ elapsed_time = time.monotonic() - start_time
78
+ remaining_timeout = timeout - elapsed_time
79
+ else:
80
+ remaining_timeout = None
81
+
82
+ return await _wait_handles_async(
83
+ handle_list,
84
+ timeout=remaining_timeout,
85
+ )
86
+
87
+ return asyncio_compat.run(progress_loop_with_timeout)
88
+
89
+
90
+ def _wait_handles(
91
+ handle_list: list[MailboxHandle[_T]],
92
+ *,
93
+ timeout: float,
94
+ ) -> list[_T]:
95
+ """Wait for multiple mailbox handles.
96
+
97
+ Returns:
98
+ Each handle's result, in the same order as the given handles.
99
+
100
+ Raises:
101
+ TimeoutError: If the overall timeout expires.
102
+ HandleAbandonedError: If any handle becomes abandoned.
103
+ """
104
+ results: list[_T] = []
105
+
106
+ start_time = time.monotonic()
107
+ for handle in handle_list:
108
+ elapsed_time = time.monotonic() - start_time
109
+ remaining_timeout = timeout - elapsed_time
110
+ results.append(handle.wait_or(timeout=remaining_timeout))
111
+
112
+ return results
113
+
114
+
115
+ async def _wait_handles_async(
116
+ handle_list: list[MailboxHandle[_T]],
117
+ *,
118
+ timeout: float | None,
119
+ ) -> list[_T]:
120
+ """Asynchronously wait for multiple mailbox handles.
121
+
122
+ Just like _wait_handles.
123
+ """
124
+ results: list[_T | None] = [None for _ in handle_list]
125
+
126
+ async def wait_single(index: int) -> None:
127
+ handle = handle_list[index]
128
+ results[index] = await handle.wait_async(timeout=timeout)
129
+
130
+ async with asyncio_compat.open_task_group() as task_group:
131
+ for index in range(len(handle_list)):
132
+ task_group.start_soon(wait_single(index))
133
+
134
+ # NOTE: `list` is not subscriptable until Python 3.10, so we use List.
135
+ return cast(List[_T], results)
@@ -44,7 +44,10 @@ class SockServerInterfaceReaderThread(threading.Thread):
44
44
  _stopped: "Event"
45
45
 
46
46
  def __init__(
47
- self, clients: ClientDict, iface: "InterfaceRelay", stopped: "Event"
47
+ self,
48
+ clients: ClientDict,
49
+ iface: "InterfaceRelay",
50
+ stopped: "Event",
48
51
  ) -> None:
49
52
  self._iface = iface
50
53
  self._clients = clients
@@ -53,7 +56,6 @@ class SockServerInterfaceReaderThread(threading.Thread):
53
56
  self._stopped = stopped
54
57
 
55
58
  def run(self) -> None:
56
- assert self._iface.relay_q
57
59
  while not self._stopped.is_set():
58
60
  try:
59
61
  result = self._iface.relay_q.get(timeout=1)
@@ -70,6 +72,7 @@ class SockServerInterfaceReaderThread(threading.Thread):
70
72
  sock_client = self._clients.get_client(sockid)
71
73
  assert sock_client
72
74
  sresp = spb.ServerResponse()
75
+ sresp.request_id = result.control.mailbox_slot
73
76
  sresp.result_communicate.CopyFrom(result)
74
77
  sock_client.send_server_response(sresp)
75
78
 
@@ -148,7 +151,10 @@ class SockServerReadThread(threading.Thread):
148
151
  inform_attach_response.settings.CopyFrom(
149
152
  self._mux._streams[stream_id]._settings._proto,
150
153
  )
151
- response = spb.ServerResponse(inform_attach_response=inform_attach_response)
154
+ response = spb.ServerResponse(
155
+ request_id=sreq.request_id,
156
+ inform_attach_response=inform_attach_response,
157
+ )
152
158
  self._sock_client.send_server_response(response)
153
159
  iface = self._mux.get_stream(stream_id).interface
154
160