pyqwest 0.1.0__cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.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.
pyqwest/__init__.py ADDED
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = [
4
+ "Client",
5
+ "FullResponse",
6
+ "HTTPTransport",
7
+ "HTTPVersion",
8
+ "Headers",
9
+ "ReadError",
10
+ "Request",
11
+ "Response",
12
+ "StreamError",
13
+ "StreamErrorCode",
14
+ "SyncClient",
15
+ "SyncHTTPTransport",
16
+ "SyncRequest",
17
+ "SyncResponse",
18
+ "SyncTransport",
19
+ "Transport",
20
+ "WriteError",
21
+ "get_default_sync_transport",
22
+ "get_default_transport",
23
+ ]
24
+
25
+ from .pyqwest import (
26
+ Client,
27
+ FullResponse,
28
+ Headers,
29
+ HTTPTransport,
30
+ HTTPVersion,
31
+ ReadError,
32
+ Request,
33
+ Response,
34
+ StreamError,
35
+ StreamErrorCode,
36
+ SyncClient,
37
+ SyncHTTPTransport,
38
+ SyncRequest,
39
+ SyncResponse,
40
+ SyncTransport,
41
+ Transport,
42
+ WriteError,
43
+ get_default_sync_transport,
44
+ get_default_transport,
45
+ )
46
+
47
+ __doc__ = pyqwest.__doc__ # noqa: F821
pyqwest/_glue.py ADDED
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from typing import TYPE_CHECKING, Protocol, TypeVar
5
+
6
+ from .pyqwest import FullResponse, Headers, Request, Transport
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import AsyncIterator, Awaitable, Callable
10
+
11
+ T_contra = TypeVar("T_contra", contravariant=True)
12
+ U = TypeVar("U")
13
+
14
+
15
+ async def wrap_body_gen(
16
+ gen: AsyncIterator[T_contra], wrap_fn: Callable[[T_contra], U]
17
+ ) -> AsyncIterator[U]:
18
+ try:
19
+ async for item in gen:
20
+ yield wrap_fn(item)
21
+ finally:
22
+ try:
23
+ aclose = gen.aclose # type: ignore[attr-defined]
24
+ except AttributeError:
25
+ pass
26
+ else:
27
+ await aclose()
28
+
29
+
30
+ async def new_full_response(
31
+ status: int, headers: Headers, content: AsyncIterator[bytes], trailers: Headers
32
+ ) -> FullResponse:
33
+ buf = bytearray()
34
+ try:
35
+ async for chunk in content:
36
+ buf.extend(chunk)
37
+ finally:
38
+ try:
39
+ aclose = content.aclose # type: ignore[attr-defined]
40
+ except AttributeError:
41
+ pass
42
+ else:
43
+ await aclose()
44
+ return FullResponse(status, headers, bytes(buf), trailers)
45
+
46
+
47
+ async def execute_and_read_full(transport: Transport, request: Request) -> FullResponse:
48
+ resp = await transport.execute(request)
49
+ return await new_full_response(
50
+ resp.status, resp.headers, resp.content, resp.trailers
51
+ )
52
+
53
+
54
+ # Vendored from pyo3-async-runtimes to apply some fixes
55
+
56
+
57
+ class Sender(Protocol[T_contra]):
58
+ def send(self, item: T_contra | BaseException) -> bool | Awaitable[bool]: ...
59
+
60
+ def close(self) -> None: ...
61
+
62
+
63
+ async def forward(gen: AsyncIterator[T_contra], sender: Sender[T_contra]) -> None:
64
+ try:
65
+ async for item in gen:
66
+ should_continue = sender.send(item)
67
+
68
+ if inspect.isawaitable(should_continue):
69
+ should_continue = await should_continue
70
+
71
+ if should_continue:
72
+ continue
73
+ break
74
+ except Exception as e:
75
+ res = sender.send(e)
76
+ if inspect.isawaitable(res):
77
+ await res
78
+ finally:
79
+ sender.close()
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = ["AsyncPyqwestTransport", "PyqwestTransport"]
4
+
5
+ from ._transport import AsyncPyqwestTransport, PyqwestTransport
@@ -0,0 +1,280 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ from typing import TYPE_CHECKING, cast
6
+
7
+ import httpx
8
+ from h2.errors import ErrorCodes
9
+ from h2.events import StreamReset
10
+
11
+ from pyqwest import (
12
+ Headers,
13
+ HTTPTransport,
14
+ Request,
15
+ Response,
16
+ StreamError,
17
+ StreamErrorCode,
18
+ SyncHTTPTransport,
19
+ SyncRequest,
20
+ SyncResponse,
21
+ )
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import AsyncIterator, Iterator
25
+
26
+
27
+ class AsyncPyqwestTransport(httpx.AsyncBaseTransport):
28
+ """An HTTPX transport implementation that delegates to pyqwest.
29
+
30
+ This can be used with any existing code using httpx.AsyncClient, and will enable
31
+ use of bidirectional streaming and response trailers.
32
+ """
33
+
34
+ _transport: HTTPTransport
35
+
36
+ def __init__(self, transport: HTTPTransport) -> None:
37
+ """Creates a new AsyncPyQwestTransport.
38
+
39
+ Args:
40
+ transport: The pyqwest HTTPTransport to delegate requests to.
41
+ """
42
+ self._transport = transport
43
+
44
+ async def handle_async_request(
45
+ self, httpx_request: httpx.Request
46
+ ) -> httpx.Response:
47
+ request_headers = convert_headers(httpx_request.headers)
48
+ request_content = async_request_content(httpx_request.stream)
49
+ timeout = convert_timeout(httpx_request.extensions)
50
+
51
+ try:
52
+ response = await self._transport.execute(
53
+ Request(
54
+ httpx_request.method,
55
+ str(httpx_request.url),
56
+ headers=request_headers,
57
+ content=request_content,
58
+ timeout=timeout, # pyright: ignore[reportCallIssue]
59
+ )
60
+ )
61
+ except StreamError as e:
62
+ raise map_stream_error(e) from e
63
+
64
+ def get_trailers() -> httpx.Headers:
65
+ return httpx.Headers(tuple(response.trailers.items()))
66
+
67
+ return httpx.Response(
68
+ status_code=response.status,
69
+ headers=httpx.Headers(tuple(response.headers.items())),
70
+ stream=AsyncIteratorByteStream(response),
71
+ extensions={"get_trailers": get_trailers},
72
+ )
73
+
74
+
75
+ def async_request_content(
76
+ stream: httpx.AsyncByteStream | httpx.SyncByteStream | httpx.ByteStream,
77
+ ) -> bytes | AsyncIterator[bytes]:
78
+ match stream:
79
+ case httpx.ByteStream():
80
+ # Buffered bytes
81
+ return next(iter(stream))
82
+ case _:
83
+ return async_request_content_iter(stream)
84
+
85
+
86
+ async def async_request_content_iter(
87
+ stream: httpx.AsyncByteStream | httpx.SyncByteStream,
88
+ ) -> AsyncIterator[bytes]:
89
+ match stream:
90
+ case httpx.AsyncByteStream():
91
+ async with contextlib.aclosing(stream):
92
+ async for chunk in stream:
93
+ yield chunk
94
+ case httpx.SyncByteStream():
95
+ with contextlib.closing(stream):
96
+ stream_iter = iter(stream)
97
+ while True:
98
+ chunk = await asyncio.to_thread(next, stream_iter, None)
99
+ if chunk is None:
100
+ break
101
+ yield chunk
102
+
103
+
104
+ class AsyncIteratorByteStream(httpx.AsyncByteStream):
105
+ def __init__(self, response: Response) -> None:
106
+ self._response = response
107
+ self._is_stream_consumed = False
108
+
109
+ async def __aiter__(self) -> AsyncIterator[bytes]:
110
+ if self._is_stream_consumed:
111
+ raise httpx.StreamConsumed
112
+ self._is_stream_consumed = True
113
+ try:
114
+ async for chunk in self._response.content:
115
+ yield chunk
116
+ except StreamError as e:
117
+ raise map_stream_error(e) from e
118
+
119
+ async def aclose(self) -> None:
120
+ await self._response.close()
121
+
122
+
123
+ class PyqwestTransport(httpx.BaseTransport):
124
+ """An HTTPX transport implementation that delegates to pyqwest.
125
+
126
+ This can be used with any existing code using httpx.Client, and will enable
127
+ use of bidirectional streaming and response trailers.
128
+ """
129
+
130
+ _transport: SyncHTTPTransport
131
+
132
+ def __init__(self, transport: SyncHTTPTransport) -> None:
133
+ """Creates a new PyQwestTransport.
134
+
135
+ Args:
136
+ transport: The pyqwest HTTPTransport to delegate requests to.
137
+ """
138
+ self._transport = transport
139
+
140
+ def handle_request(self, httpx_request: httpx.Request) -> httpx.Response:
141
+ request_headers = convert_headers(httpx_request.headers)
142
+ request_content = sync_request_content(httpx_request.stream)
143
+ timeout = convert_timeout(httpx_request.extensions)
144
+
145
+ try:
146
+ response = self._transport.execute(
147
+ SyncRequest(
148
+ httpx_request.method,
149
+ str(httpx_request.url),
150
+ headers=request_headers,
151
+ content=request_content,
152
+ timeout=timeout, # pyright: ignore[reportCallIssue]
153
+ )
154
+ )
155
+ except StreamError as e:
156
+ raise map_stream_error(e) from e
157
+
158
+ def get_trailers() -> httpx.Headers:
159
+ return httpx.Headers(tuple(response.trailers.items()))
160
+
161
+ return httpx.Response(
162
+ status_code=response.status,
163
+ headers=httpx.Headers(tuple(response.headers.items())),
164
+ stream=IteratorByteStream(response),
165
+ extensions={"get_trailers": get_trailers},
166
+ )
167
+
168
+
169
+ def sync_request_content(
170
+ stream: httpx.AsyncByteStream | httpx.SyncByteStream | httpx.ByteStream,
171
+ ) -> bytes | Iterator[bytes]:
172
+ match stream:
173
+ case httpx.ByteStream():
174
+ # Buffered bytes
175
+ return next(iter(stream))
176
+ case _:
177
+ return sync_request_content_iter(stream)
178
+
179
+
180
+ def sync_request_content_iter(
181
+ stream: httpx.AsyncByteStream | httpx.SyncByteStream,
182
+ ) -> Iterator[bytes]:
183
+ match stream:
184
+ case httpx.AsyncByteStream():
185
+ msg = "unreachable"
186
+ raise TypeError(msg)
187
+ case httpx.SyncByteStream():
188
+ with contextlib.closing(stream):
189
+ yield from stream
190
+
191
+
192
+ class IteratorByteStream(httpx.SyncByteStream):
193
+ def __init__(self, response: SyncResponse) -> None:
194
+ self._response = response
195
+ self._is_stream_consumed = False
196
+
197
+ def __iter__(self) -> Iterator[bytes]:
198
+ if self._is_stream_consumed:
199
+ raise httpx.StreamConsumed
200
+ self._is_stream_consumed = True
201
+ try:
202
+ yield from self._response.content
203
+ except StreamError as e:
204
+ raise map_stream_error(e) from e
205
+
206
+ def close(self) -> None:
207
+ self._response.close()
208
+
209
+
210
+ # Headers that are managed by the transport and should not be forwarded.
211
+ TRANSPORT_HEADERS = {
212
+ "connection",
213
+ "keep-alive",
214
+ "proxy-connection",
215
+ "transfer-encoding",
216
+ "upgrade",
217
+ }
218
+
219
+
220
+ def convert_headers(headers: httpx.Headers) -> Headers:
221
+ return Headers(
222
+ (k, v) for k, v in headers.multi_items() if k.lower() not in TRANSPORT_HEADERS
223
+ )
224
+
225
+
226
+ def convert_timeout(extensions: dict) -> float | None:
227
+ httpx_timeout = cast("dict | None", extensions.get("timeout"))
228
+ if httpx_timeout is None:
229
+ return None
230
+ # reqwest does not support setting individual timeout settings
231
+ # per call, only an operation timeout, so we need to approximate
232
+ # that from the httpx timeout dict. Connect usually happens once
233
+ # and can be given a longer timeout - we assume the operation timeout
234
+ # is the max of read/write if present, or connect if not. We ignore
235
+ # pool for now
236
+ read_timeout = httpx_timeout.get("read", -1)
237
+ if read_timeout is None:
238
+ read_timeout = -1
239
+ write_timeout = httpx_timeout.get("write", -1)
240
+ if write_timeout is None:
241
+ write_timeout = -1
242
+ operation_timeout = max(read_timeout, write_timeout)
243
+ if operation_timeout != -1:
244
+ return operation_timeout
245
+ return httpx_timeout.get("connect")
246
+
247
+
248
+ def map_stream_error(e: StreamError) -> httpx.RemoteProtocolError:
249
+ match e.code:
250
+ case StreamErrorCode.NO_ERROR:
251
+ code = ErrorCodes.NO_ERROR
252
+ case StreamErrorCode.PROTOCOL_ERROR:
253
+ code = ErrorCodes.PROTOCOL_ERROR
254
+ case StreamErrorCode.INTERNAL_ERROR:
255
+ code = ErrorCodes.INTERNAL_ERROR
256
+ case StreamErrorCode.FLOW_CONTROL_ERROR:
257
+ code = ErrorCodes.FLOW_CONTROL_ERROR
258
+ case StreamErrorCode.SETTINGS_TIMEOUT:
259
+ code = ErrorCodes.SETTINGS_TIMEOUT
260
+ case StreamErrorCode.STREAM_CLOSED:
261
+ code = ErrorCodes.STREAM_CLOSED
262
+ case StreamErrorCode.FRAME_SIZE_ERROR:
263
+ code = ErrorCodes.FRAME_SIZE_ERROR
264
+ case StreamErrorCode.REFUSED_STREAM:
265
+ code = ErrorCodes.REFUSED_STREAM
266
+ case StreamErrorCode.CANCEL:
267
+ code = ErrorCodes.CANCEL
268
+ case StreamErrorCode.COMPRESSION_ERROR:
269
+ code = ErrorCodes.COMPRESSION_ERROR
270
+ case StreamErrorCode.CONNECT_ERROR:
271
+ code = ErrorCodes.CONNECT_ERROR
272
+ case StreamErrorCode.ENHANCE_YOUR_CALM:
273
+ code = ErrorCodes.ENHANCE_YOUR_CALM
274
+ case StreamErrorCode.INADEQUATE_SECURITY:
275
+ code = ErrorCodes.INADEQUATE_SECURITY
276
+ case StreamErrorCode.HTTP_1_1_REQUIRED:
277
+ code = ErrorCodes.HTTP_1_1_REQUIRED
278
+ case _:
279
+ code = ErrorCodes.INTERNAL_ERROR
280
+ return httpx.RemoteProtocolError(str(StreamReset(stream_id=-1, error_code=code)))