runloop_api_client 0.61.0__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 (187) hide show
  1. runloop_api_client/__init__.py +92 -0
  2. runloop_api_client/_base_client.py +1995 -0
  3. runloop_api_client/_client.py +668 -0
  4. runloop_api_client/_compat.py +219 -0
  5. runloop_api_client/_constants.py +14 -0
  6. runloop_api_client/_exceptions.py +108 -0
  7. runloop_api_client/_files.py +123 -0
  8. runloop_api_client/_models.py +835 -0
  9. runloop_api_client/_qs.py +150 -0
  10. runloop_api_client/_resource.py +43 -0
  11. runloop_api_client/_response.py +832 -0
  12. runloop_api_client/_streaming.py +520 -0
  13. runloop_api_client/_types.py +260 -0
  14. runloop_api_client/_utils/__init__.py +64 -0
  15. runloop_api_client/_utils/_compat.py +45 -0
  16. runloop_api_client/_utils/_datetime_parse.py +136 -0
  17. runloop_api_client/_utils/_logs.py +25 -0
  18. runloop_api_client/_utils/_proxy.py +65 -0
  19. runloop_api_client/_utils/_reflection.py +42 -0
  20. runloop_api_client/_utils/_resources_proxy.py +24 -0
  21. runloop_api_client/_utils/_streams.py +12 -0
  22. runloop_api_client/_utils/_sync.py +86 -0
  23. runloop_api_client/_utils/_transform.py +457 -0
  24. runloop_api_client/_utils/_typing.py +156 -0
  25. runloop_api_client/_utils/_utils.py +421 -0
  26. runloop_api_client/_version.py +4 -0
  27. runloop_api_client/lib/.keep +4 -0
  28. runloop_api_client/lib/polling.py +75 -0
  29. runloop_api_client/lib/polling_async.py +60 -0
  30. runloop_api_client/pagination.py +761 -0
  31. runloop_api_client/py.typed +0 -0
  32. runloop_api_client/resources/__init__.py +103 -0
  33. runloop_api_client/resources/benchmarks/__init__.py +33 -0
  34. runloop_api_client/resources/benchmarks/benchmarks.py +982 -0
  35. runloop_api_client/resources/benchmarks/runs.py +587 -0
  36. runloop_api_client/resources/blueprints.py +1206 -0
  37. runloop_api_client/resources/devboxes/__init__.py +89 -0
  38. runloop_api_client/resources/devboxes/browsers.py +267 -0
  39. runloop_api_client/resources/devboxes/computers.py +648 -0
  40. runloop_api_client/resources/devboxes/devboxes.py +3414 -0
  41. runloop_api_client/resources/devboxes/disk_snapshots.py +519 -0
  42. runloop_api_client/resources/devboxes/executions.py +1059 -0
  43. runloop_api_client/resources/devboxes/logs.py +197 -0
  44. runloop_api_client/resources/objects.py +860 -0
  45. runloop_api_client/resources/repositories.py +717 -0
  46. runloop_api_client/resources/scenarios/__init__.py +47 -0
  47. runloop_api_client/resources/scenarios/runs.py +949 -0
  48. runloop_api_client/resources/scenarios/scenarios.py +1079 -0
  49. runloop_api_client/resources/scenarios/scorers.py +629 -0
  50. runloop_api_client/resources/secrets.py +500 -0
  51. runloop_api_client/types/__init__.py +95 -0
  52. runloop_api_client/types/benchmark_create_params.py +40 -0
  53. runloop_api_client/types/benchmark_definitions_params.py +15 -0
  54. runloop_api_client/types/benchmark_list_params.py +15 -0
  55. runloop_api_client/types/benchmark_list_public_params.py +15 -0
  56. runloop_api_client/types/benchmark_run_list_view.py +19 -0
  57. runloop_api_client/types/benchmark_run_view.py +51 -0
  58. runloop_api_client/types/benchmark_start_run_params.py +25 -0
  59. runloop_api_client/types/benchmark_update_params.py +40 -0
  60. runloop_api_client/types/benchmark_view.py +45 -0
  61. runloop_api_client/types/benchmarks/__init__.py +6 -0
  62. runloop_api_client/types/benchmarks/run_list_params.py +18 -0
  63. runloop_api_client/types/benchmarks/run_list_scenario_runs_params.py +18 -0
  64. runloop_api_client/types/blueprint_build_log.py +16 -0
  65. runloop_api_client/types/blueprint_build_logs_list_view.py +16 -0
  66. runloop_api_client/types/blueprint_build_parameters.py +87 -0
  67. runloop_api_client/types/blueprint_create_params.py +90 -0
  68. runloop_api_client/types/blueprint_list_params.py +18 -0
  69. runloop_api_client/types/blueprint_list_public_params.py +18 -0
  70. runloop_api_client/types/blueprint_list_view.py +19 -0
  71. runloop_api_client/types/blueprint_preview_params.py +90 -0
  72. runloop_api_client/types/blueprint_preview_view.py +10 -0
  73. runloop_api_client/types/blueprint_view.py +86 -0
  74. runloop_api_client/types/devbox_async_execution_detail_view.py +40 -0
  75. runloop_api_client/types/devbox_create_params.py +70 -0
  76. runloop_api_client/types/devbox_create_ssh_key_response.py +16 -0
  77. runloop_api_client/types/devbox_create_tunnel_params.py +12 -0
  78. runloop_api_client/types/devbox_download_file_params.py +15 -0
  79. runloop_api_client/types/devbox_execute_async_params.py +25 -0
  80. runloop_api_client/types/devbox_execute_params.py +34 -0
  81. runloop_api_client/types/devbox_execute_sync_params.py +25 -0
  82. runloop_api_client/types/devbox_execution_detail_view.py +24 -0
  83. runloop_api_client/types/devbox_list_disk_snapshots_params.py +29 -0
  84. runloop_api_client/types/devbox_list_params.py +20 -0
  85. runloop_api_client/types/devbox_list_view.py +19 -0
  86. runloop_api_client/types/devbox_read_file_contents_params.py +15 -0
  87. runloop_api_client/types/devbox_read_file_contents_response.py +7 -0
  88. runloop_api_client/types/devbox_remove_tunnel_params.py +12 -0
  89. runloop_api_client/types/devbox_snapshot_disk_async_params.py +16 -0
  90. runloop_api_client/types/devbox_snapshot_disk_params.py +16 -0
  91. runloop_api_client/types/devbox_snapshot_list_view.py +19 -0
  92. runloop_api_client/types/devbox_snapshot_view.py +24 -0
  93. runloop_api_client/types/devbox_tunnel_view.py +16 -0
  94. runloop_api_client/types/devbox_update_params.py +16 -0
  95. runloop_api_client/types/devbox_upload_file_params.py +19 -0
  96. runloop_api_client/types/devbox_view.py +94 -0
  97. runloop_api_client/types/devbox_wait_for_command_params.py +25 -0
  98. runloop_api_client/types/devbox_write_file_contents_params.py +18 -0
  99. runloop_api_client/types/devboxes/__init__.py +32 -0
  100. runloop_api_client/types/devboxes/browser_create_params.py +13 -0
  101. runloop_api_client/types/devboxes/browser_view.py +25 -0
  102. runloop_api_client/types/devboxes/computer_create_params.py +24 -0
  103. runloop_api_client/types/devboxes/computer_keyboard_interaction_params.py +16 -0
  104. runloop_api_client/types/devboxes/computer_keyboard_interaction_response.py +15 -0
  105. runloop_api_client/types/devboxes/computer_mouse_interaction_params.py +30 -0
  106. runloop_api_client/types/devboxes/computer_mouse_interaction_response.py +15 -0
  107. runloop_api_client/types/devboxes/computer_screen_interaction_params.py +12 -0
  108. runloop_api_client/types/devboxes/computer_screen_interaction_response.py +15 -0
  109. runloop_api_client/types/devboxes/computer_view.py +19 -0
  110. runloop_api_client/types/devboxes/devbox_logs_list_view.py +39 -0
  111. runloop_api_client/types/devboxes/devbox_snapshot_async_status_view.py +20 -0
  112. runloop_api_client/types/devboxes/disk_snapshot_list_params.py +29 -0
  113. runloop_api_client/types/devboxes/disk_snapshot_update_params.py +16 -0
  114. runloop_api_client/types/devboxes/execution_execute_async_params.py +25 -0
  115. runloop_api_client/types/devboxes/execution_execute_sync_params.py +25 -0
  116. runloop_api_client/types/devboxes/execution_kill_params.py +18 -0
  117. runloop_api_client/types/devboxes/execution_retrieve_params.py +14 -0
  118. runloop_api_client/types/devboxes/execution_stream_stderr_updates_params.py +14 -0
  119. runloop_api_client/types/devboxes/execution_stream_stdout_updates_params.py +14 -0
  120. runloop_api_client/types/devboxes/execution_update_chunk.py +15 -0
  121. runloop_api_client/types/devboxes/log_list_params.py +15 -0
  122. runloop_api_client/types/input_context.py +15 -0
  123. runloop_api_client/types/input_context_param.py +16 -0
  124. runloop_api_client/types/input_context_update_param.py +16 -0
  125. runloop_api_client/types/object_create_params.py +19 -0
  126. runloop_api_client/types/object_download_params.py +12 -0
  127. runloop_api_client/types/object_download_url_view.py +10 -0
  128. runloop_api_client/types/object_list_params.py +27 -0
  129. runloop_api_client/types/object_list_public_params.py +27 -0
  130. runloop_api_client/types/object_list_view.py +22 -0
  131. runloop_api_client/types/object_view.py +28 -0
  132. runloop_api_client/types/repository_connection_list_view.py +19 -0
  133. runloop_api_client/types/repository_connection_view.py +16 -0
  134. runloop_api_client/types/repository_create_params.py +22 -0
  135. runloop_api_client/types/repository_inspection_details.py +77 -0
  136. runloop_api_client/types/repository_inspection_list_view.py +13 -0
  137. runloop_api_client/types/repository_list_params.py +21 -0
  138. runloop_api_client/types/repository_manifest_view.py +158 -0
  139. runloop_api_client/types/repository_refresh_params.py +16 -0
  140. runloop_api_client/types/scenario_create_params.py +53 -0
  141. runloop_api_client/types/scenario_definition_list_view.py +19 -0
  142. runloop_api_client/types/scenario_environment.py +25 -0
  143. runloop_api_client/types/scenario_environment_param.py +27 -0
  144. runloop_api_client/types/scenario_list_params.py +21 -0
  145. runloop_api_client/types/scenario_list_public_params.py +18 -0
  146. runloop_api_client/types/scenario_run_list_view.py +19 -0
  147. runloop_api_client/types/scenario_run_view.py +50 -0
  148. runloop_api_client/types/scenario_start_run_params.py +28 -0
  149. runloop_api_client/types/scenario_update_params.py +46 -0
  150. runloop_api_client/types/scenario_view.py +57 -0
  151. runloop_api_client/types/scenarios/__init__.py +14 -0
  152. runloop_api_client/types/scenarios/run_list_params.py +18 -0
  153. runloop_api_client/types/scenarios/scorer_create_params.py +18 -0
  154. runloop_api_client/types/scenarios/scorer_create_response.py +16 -0
  155. runloop_api_client/types/scenarios/scorer_list_params.py +15 -0
  156. runloop_api_client/types/scenarios/scorer_list_response.py +16 -0
  157. runloop_api_client/types/scenarios/scorer_retrieve_response.py +16 -0
  158. runloop_api_client/types/scenarios/scorer_update_params.py +18 -0
  159. runloop_api_client/types/scenarios/scorer_update_response.py +16 -0
  160. runloop_api_client/types/scenarios/scorer_validate_params.py +17 -0
  161. runloop_api_client/types/scenarios/scorer_validate_response.py +23 -0
  162. runloop_api_client/types/scoring_contract.py +13 -0
  163. runloop_api_client/types/scoring_contract_param.py +15 -0
  164. runloop_api_client/types/scoring_contract_result_view.py +16 -0
  165. runloop_api_client/types/scoring_contract_update_param.py +15 -0
  166. runloop_api_client/types/scoring_function.py +135 -0
  167. runloop_api_client/types/scoring_function_param.py +131 -0
  168. runloop_api_client/types/scoring_function_result_view.py +21 -0
  169. runloop_api_client/types/secret_create_params.py +23 -0
  170. runloop_api_client/types/secret_list_params.py +12 -0
  171. runloop_api_client/types/secret_list_view.py +22 -0
  172. runloop_api_client/types/secret_update_params.py +16 -0
  173. runloop_api_client/types/secret_view.py +22 -0
  174. runloop_api_client/types/shared/__init__.py +6 -0
  175. runloop_api_client/types/shared/after_idle.py +15 -0
  176. runloop_api_client/types/shared/code_mount_parameters.py +24 -0
  177. runloop_api_client/types/shared/launch_parameters.py +79 -0
  178. runloop_api_client/types/shared/run_profile.py +33 -0
  179. runloop_api_client/types/shared_params/__init__.py +6 -0
  180. runloop_api_client/types/shared_params/after_idle.py +15 -0
  181. runloop_api_client/types/shared_params/code_mount_parameters.py +25 -0
  182. runloop_api_client/types/shared_params/launch_parameters.py +81 -0
  183. runloop_api_client/types/shared_params/run_profile.py +34 -0
  184. runloop_api_client-0.61.0.dist-info/METADATA +496 -0
  185. runloop_api_client-0.61.0.dist-info/RECORD +187 -0
  186. runloop_api_client-0.61.0.dist-info/WHEEL +4 -0
  187. runloop_api_client-0.61.0.dist-info/licenses/LICENSE +7 -0
@@ -0,0 +1,520 @@
1
+ # Note: initially copied from https://github.com/florimondmanca/httpx-sse/blob/master/src/httpx_sse/_decoders.py
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import inspect
6
+ from types import TracebackType
7
+ from typing import (
8
+ TYPE_CHECKING,
9
+ Any,
10
+ Generic,
11
+ TypeVar,
12
+ Callable,
13
+ Iterator,
14
+ Optional,
15
+ Awaitable,
16
+ AsyncIterator,
17
+ cast,
18
+ )
19
+ from typing_extensions import (
20
+ Self,
21
+ Protocol,
22
+ TypeGuard,
23
+ override,
24
+ get_origin,
25
+ runtime_checkable,
26
+ )
27
+
28
+ import httpx
29
+
30
+ from ._utils import extract_type_var_from_base
31
+ from ._exceptions import APIStatusError, APITimeoutError
32
+
33
+ if TYPE_CHECKING:
34
+ from ._client import Runloop, AsyncRunloop
35
+
36
+
37
+ _T = TypeVar("_T")
38
+
39
+
40
+ class Stream(Generic[_T]):
41
+ """Provides the core interface to iterate over a synchronous stream response."""
42
+
43
+ response: httpx.Response
44
+
45
+ _decoder: SSEBytesDecoder
46
+
47
+ def __init__(
48
+ self,
49
+ *,
50
+ cast_to: type[_T],
51
+ response: httpx.Response,
52
+ client: Runloop,
53
+ ) -> None:
54
+ self.response = response
55
+ self._cast_to = cast_to
56
+ self._client = client
57
+ self._decoder = client._make_sse_decoder()
58
+ self._iterator = self.__stream__()
59
+
60
+ def __next__(self) -> _T:
61
+ return self._iterator.__next__()
62
+
63
+ def __iter__(self) -> Iterator[_T]:
64
+ for item in self._iterator:
65
+ yield item
66
+
67
+ def _iter_events(self) -> Iterator[ServerSentEvent]:
68
+ yield from self._decoder.iter_bytes(self.response.iter_bytes())
69
+
70
+ def __stream__(self) -> Iterator[_T]:
71
+ cast_to = cast(Any, self._cast_to)
72
+ response = self.response
73
+ process_data = self._client._process_response_data
74
+ iterator = self._iter_events()
75
+
76
+ for sse in iterator:
77
+ # Surface server-sent error events as API errors to allow callers to handle/retry
78
+ if sse.event == "error":
79
+ try:
80
+ error_obj = json.loads(sse.data)
81
+ status_code = int(error_obj.get("code", 500))
82
+ # Build a synthetic response to mirror normal error handling
83
+ fake_resp = httpx.Response(status_code, request=response.request, content=sse.data)
84
+ except Exception:
85
+ fake_resp = httpx.Response(500, request=response.request, content=sse.data)
86
+ raise self._client._make_status_error_from_response(fake_resp)
87
+
88
+ yield process_data(data=sse.json(), cast_to=cast_to, response=response)
89
+
90
+ # Ensure the entire stream is consumed
91
+ for _sse in iterator:
92
+ ...
93
+
94
+ def __enter__(self) -> Self:
95
+ return self
96
+
97
+ def __exit__(
98
+ self,
99
+ exc_type: type[BaseException] | None,
100
+ exc: BaseException | None,
101
+ exc_tb: TracebackType | None,
102
+ ) -> None:
103
+ self.close()
104
+
105
+ def close(self) -> None:
106
+ """
107
+ Close the response and release the connection.
108
+
109
+ Automatically called if the response body is read to completion.
110
+ """
111
+ self.response.close()
112
+
113
+
114
+ class AsyncStream(Generic[_T]):
115
+ """Provides the core interface to iterate over an asynchronous stream response."""
116
+
117
+ response: httpx.Response
118
+
119
+ _decoder: SSEDecoder | SSEBytesDecoder
120
+
121
+ def __init__(
122
+ self,
123
+ *,
124
+ cast_to: type[_T],
125
+ response: httpx.Response,
126
+ client: AsyncRunloop,
127
+ ) -> None:
128
+ self.response = response
129
+ self._cast_to = cast_to
130
+ self._client = client
131
+ self._decoder = client._make_sse_decoder()
132
+ self._iterator = self.__stream__()
133
+
134
+ async def __anext__(self) -> _T:
135
+ return await self._iterator.__anext__()
136
+
137
+ async def __aiter__(self) -> AsyncIterator[_T]:
138
+ async for item in self._iterator:
139
+ yield item
140
+
141
+ async def _iter_events(self) -> AsyncIterator[ServerSentEvent]:
142
+ async for sse in self._decoder.aiter_bytes(self.response.aiter_bytes()):
143
+ yield sse
144
+
145
+ async def __stream__(self) -> AsyncIterator[_T]:
146
+ cast_to = cast(Any, self._cast_to)
147
+ response = self.response
148
+ process_data = self._client._process_response_data
149
+ iterator = self._iter_events()
150
+
151
+ async for sse in iterator:
152
+ # Surface server-sent error events as API errors to allow callers to handle/retry
153
+ if sse.event == "error":
154
+ try:
155
+ error_obj = json.loads(sse.data)
156
+ status_code = int(error_obj.get("code", 500))
157
+ # Build a synthetic response to mirror normal error handling
158
+ fake_resp = httpx.Response(status_code, request=response.request, content=sse.data)
159
+ except Exception:
160
+ fake_resp = httpx.Response(500, request=response.request, content=sse.data)
161
+ raise self._client._make_status_error_from_response(fake_resp)
162
+
163
+ yield process_data(data=sse.json(), cast_to=cast_to, response=response)
164
+
165
+ # Ensure the entire stream is consumed
166
+ async for _sse in iterator:
167
+ ...
168
+
169
+ async def __aenter__(self) -> Self:
170
+ return self
171
+
172
+ async def __aexit__(
173
+ self,
174
+ exc_type: type[BaseException] | None,
175
+ exc: BaseException | None,
176
+ exc_tb: TracebackType | None,
177
+ ) -> None:
178
+ await self.close()
179
+
180
+ async def close(self) -> None:
181
+ """
182
+ Close the response and release the connection.
183
+
184
+ Automatically called if the response body is read to completion.
185
+ """
186
+ await self.response.aclose()
187
+
188
+
189
+ class ServerSentEvent:
190
+ def __init__(
191
+ self,
192
+ *,
193
+ event: str | None = None,
194
+ data: str | None = None,
195
+ id: str | None = None,
196
+ retry: int | None = None,
197
+ ) -> None:
198
+ if data is None:
199
+ data = ""
200
+
201
+ self._id = id
202
+ self._data = data
203
+ self._event = event or None
204
+ self._retry = retry
205
+
206
+ @property
207
+ def event(self) -> str | None:
208
+ return self._event
209
+
210
+ @property
211
+ def id(self) -> str | None:
212
+ return self._id
213
+
214
+ @property
215
+ def retry(self) -> int | None:
216
+ return self._retry
217
+
218
+ @property
219
+ def data(self) -> str:
220
+ return self._data
221
+
222
+ def json(self) -> Any:
223
+ return json.loads(self.data)
224
+
225
+ @override
226
+ def __repr__(self) -> str:
227
+ return f"ServerSentEvent(event={self.event}, data={self.data}, id={self.id}, retry={self.retry})"
228
+
229
+
230
+ class SSEDecoder:
231
+ _data: list[str]
232
+ _event: str | None
233
+ _retry: int | None
234
+ _last_event_id: str | None
235
+
236
+ def __init__(self) -> None:
237
+ self._event = None
238
+ self._data = []
239
+ self._last_event_id = None
240
+ self._retry = None
241
+
242
+ def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]:
243
+ """Given an iterator that yields raw binary data, iterate over it & yield every event encountered"""
244
+ for chunk in self._iter_chunks(iterator):
245
+ # Split before decoding so splitlines() only uses \r and \n
246
+ for raw_line in chunk.splitlines():
247
+ line = raw_line.decode("utf-8")
248
+ sse = self.decode(line)
249
+ if sse:
250
+ yield sse
251
+
252
+ def _iter_chunks(self, iterator: Iterator[bytes]) -> Iterator[bytes]:
253
+ """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks"""
254
+ data = b""
255
+ for chunk in iterator:
256
+ for line in chunk.splitlines(keepends=True):
257
+ data += line
258
+ if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")):
259
+ yield data
260
+ data = b""
261
+ if data:
262
+ yield data
263
+
264
+ async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]:
265
+ """Given an iterator that yields raw binary data, iterate over it & yield every event encountered"""
266
+ async for chunk in self._aiter_chunks(iterator):
267
+ # Split before decoding so splitlines() only uses \r and \n
268
+ for raw_line in chunk.splitlines():
269
+ line = raw_line.decode("utf-8")
270
+ sse = self.decode(line)
271
+ if sse:
272
+ yield sse
273
+
274
+ async def _aiter_chunks(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[bytes]:
275
+ """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks"""
276
+ data = b""
277
+ async for chunk in iterator:
278
+ for line in chunk.splitlines(keepends=True):
279
+ data += line
280
+ if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")):
281
+ yield data
282
+ data = b""
283
+ if data:
284
+ yield data
285
+
286
+ def decode(self, line: str) -> ServerSentEvent | None:
287
+ # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501
288
+
289
+ if not line:
290
+ if not self._event and not self._data and not self._last_event_id and self._retry is None:
291
+ return None
292
+
293
+ sse = ServerSentEvent(
294
+ event=self._event,
295
+ data="\n".join(self._data),
296
+ id=self._last_event_id,
297
+ retry=self._retry,
298
+ )
299
+
300
+ # NOTE: as per the SSE spec, do not reset last_event_id.
301
+ self._event = None
302
+ self._data = []
303
+ self._retry = None
304
+
305
+ return sse
306
+
307
+ if line.startswith(":"):
308
+ return None
309
+
310
+ fieldname, _, value = line.partition(":")
311
+
312
+ if value.startswith(" "):
313
+ value = value[1:]
314
+
315
+ if fieldname == "event":
316
+ self._event = value
317
+ elif fieldname == "data":
318
+ self._data.append(value)
319
+ elif fieldname == "id":
320
+ if "\0" in value:
321
+ pass
322
+ else:
323
+ self._last_event_id = value
324
+ elif fieldname == "retry":
325
+ try:
326
+ self._retry = int(value)
327
+ except (TypeError, ValueError):
328
+ pass
329
+ else:
330
+ pass # Field is ignored.
331
+
332
+ return None
333
+
334
+
335
+ @runtime_checkable
336
+ class SSEBytesDecoder(Protocol):
337
+ def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]:
338
+ """Given an iterator that yields raw binary data, iterate over it & yield every event encountered"""
339
+ ...
340
+
341
+ def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]:
342
+ """Given an async iterator that yields raw binary data, iterate over it & yield every event encountered"""
343
+ ...
344
+
345
+
346
+ def is_stream_class_type(typ: type) -> TypeGuard[type[Stream[object]] | type[AsyncStream[object]]]:
347
+ """TypeGuard for determining whether or not the given type is a subclass of `Stream` / `AsyncStream`"""
348
+ origin = get_origin(typ) or typ
349
+ return inspect.isclass(origin) and issubclass(origin, (Stream, AsyncStream))
350
+
351
+
352
+ def extract_stream_chunk_type(
353
+ stream_cls: type,
354
+ *,
355
+ failure_message: str | None = None,
356
+ ) -> type:
357
+ """Given a type like `Stream[T]`, returns the generic type variable `T`.
358
+
359
+ This also handles the case where a concrete subclass is given, e.g.
360
+ ```py
361
+ class MyStream(Stream[bytes]):
362
+ ...
363
+
364
+ extract_stream_chunk_type(MyStream) -> bytes
365
+ ```
366
+ """
367
+ from ._base_client import Stream, AsyncStream
368
+
369
+ return extract_type_var_from_base(
370
+ stream_cls,
371
+ index=0,
372
+ generic_bases=cast("tuple[type, ...]", (Stream, AsyncStream)),
373
+ failure_message=failure_message,
374
+ )
375
+
376
+
377
+ class ReconnectingStream(Generic[_T]):
378
+ """Wraps a Stream with automatic reconnection on timeout (HTTP 408) or read timeouts.
379
+
380
+ The reconnection uses the last observed offset from each item, as provided by
381
+ the given `get_offset` callback. The `stream_creator` will be called with the
382
+ last known offset to resume the stream.
383
+ """
384
+
385
+ def __init__(
386
+ self,
387
+ *,
388
+ current_stream: Stream[_T],
389
+ stream_creator: Callable[[Optional[str]], Stream[_T]],
390
+ get_offset: Callable[[_T], Optional[str]],
391
+ ) -> None:
392
+ self._current_stream = current_stream
393
+ self._stream_creator = stream_creator
394
+ self._get_offset = get_offset
395
+ self._last_offset: Optional[str] = None
396
+ self._iterator = self.__stream__()
397
+
398
+ @property
399
+ def response(self) -> httpx.Response:
400
+ return self._current_stream.response
401
+
402
+ def __next__(self) -> _T:
403
+ return self._iterator.__next__()
404
+
405
+ def __iter__(self) -> Iterator[_T]:
406
+ for item in self._iterator:
407
+ yield item
408
+
409
+ def __enter__(self) -> "ReconnectingStream[_T]":
410
+ return self
411
+
412
+ def __exit__(
413
+ self,
414
+ exc_type: type[BaseException] | None,
415
+ exc: BaseException | None,
416
+ exc_tb: TracebackType | None,
417
+ ) -> None:
418
+ self.close()
419
+
420
+ def close(self) -> None:
421
+ self._current_stream.close()
422
+
423
+ def __stream__(self) -> Iterator[_T]:
424
+ while True:
425
+ try:
426
+ for item in self._current_stream:
427
+ offset = self._get_offset(item)
428
+ if offset is not None:
429
+ self._last_offset = offset
430
+ yield item
431
+ return
432
+ except Exception as e:
433
+ # Reconnect on timeouts
434
+ should_reconnect = False
435
+ if isinstance(e, APITimeoutError):
436
+ should_reconnect = True
437
+ elif isinstance(e, APIStatusError) and getattr(e, "status_code", None) == 408:
438
+ should_reconnect = True
439
+ elif isinstance(e, httpx.TimeoutException):
440
+ should_reconnect = True
441
+
442
+ if should_reconnect:
443
+ # Close existing response before reconnecting
444
+ try:
445
+ self._current_stream.close()
446
+ except Exception:
447
+ pass
448
+ self._current_stream = self._stream_creator(self._last_offset)
449
+ continue
450
+ raise
451
+
452
+
453
+ class AsyncReconnectingStream(Generic[_T]):
454
+ """Async variant of ReconnectingStream supporting auto-reconnect on timeouts."""
455
+
456
+ def __init__(
457
+ self,
458
+ *,
459
+ current_stream: AsyncStream[_T],
460
+ stream_creator: Callable[[Optional[str]], Awaitable[AsyncStream[_T]]],
461
+ get_offset: Callable[[_T], Optional[str]],
462
+ ) -> None:
463
+ self._current_stream = current_stream
464
+ self._stream_creator = stream_creator
465
+ self._get_offset = get_offset
466
+ self._last_offset: Optional[str] = None
467
+ self._iterator = self.__stream__()
468
+
469
+ @property
470
+ def response(self) -> httpx.Response:
471
+ return self._current_stream.response
472
+
473
+ async def __anext__(self) -> _T:
474
+ return await self._iterator.__anext__()
475
+
476
+ async def __aiter__(self) -> AsyncIterator[_T]:
477
+ async for item in self._iterator:
478
+ yield item
479
+
480
+ async def __aenter__(self) -> "AsyncReconnectingStream[_T]":
481
+ return self
482
+
483
+ async def __aexit__(
484
+ self,
485
+ exc_type: type[BaseException] | None,
486
+ exc: BaseException | None,
487
+ exc_tb: TracebackType | None,
488
+ ) -> None:
489
+ await self.close()
490
+
491
+ async def close(self) -> None:
492
+ await self._current_stream.close()
493
+
494
+ async def __stream__(self) -> AsyncIterator[_T]:
495
+ while True:
496
+ try:
497
+ async for item in self._current_stream:
498
+ offset = self._get_offset(item)
499
+ if offset is not None:
500
+ self._last_offset = offset
501
+ yield item
502
+ return
503
+ except Exception as e:
504
+ # Reconnect on timeouts
505
+ should_reconnect = False
506
+ if isinstance(e, APITimeoutError):
507
+ should_reconnect = True
508
+ elif isinstance(e, APIStatusError) and getattr(e, "status_code", None) == 408:
509
+ should_reconnect = True
510
+ elif isinstance(e, httpx.TimeoutException):
511
+ should_reconnect = True
512
+
513
+ if should_reconnect:
514
+ try:
515
+ await self._current_stream.close()
516
+ except Exception:
517
+ pass
518
+ self._current_stream = await self._stream_creator(self._last_offset)
519
+ continue
520
+ raise