pyqwest 0.1.0__cp310-cp310-musllinux_1_2_aarch64.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 +47 -0
- pyqwest/_glue.py +79 -0
- pyqwest/httpx/__init__.py +5 -0
- pyqwest/httpx/_transport.py +280 -0
- pyqwest/pyqwest.cpython-310-aarch64-linux-gnu.so +0 -0
- pyqwest/pyqwest.pyi +1104 -0
- pyqwest-0.1.0.dist-info/METADATA +21 -0
- pyqwest-0.1.0.dist-info/RECORD +11 -0
- pyqwest-0.1.0.dist-info/WHEEL +4 -0
- pyqwest-0.1.0.dist-info/licenses/LICENSE +21 -0
- pyqwest.libs/libgcc_s-39080030.so.1 +0 -0
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,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)))
|
|
Binary file
|