pyqwest 0.3.0__cp314-cp314-manylinux_2_17_aarch64.manylinux2014_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.
@@ -0,0 +1,80 @@
1
+ # Includes work from:
2
+
3
+ # Copyright (c) Django Software Foundation and individual contributors.
4
+ # All rights reserved.
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without modification,
7
+ # are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice,
10
+ # this list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright
13
+ # notice, this list of conditions and the following disclaimer in the
14
+ # documentation and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of Django nor the names of its contributors may be used
17
+ # to endorse or promote products derived from this software without
18
+ # specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
21
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
24
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
27
+ # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ from __future__ import annotations
32
+
33
+ import asyncio
34
+ import inspect
35
+ from typing import TYPE_CHECKING, cast
36
+
37
+ if TYPE_CHECKING:
38
+ from asgiref.typing import (
39
+ ASGI2Application,
40
+ ASGI3Application,
41
+ ASGIApplication,
42
+ ASGIReceiveCallable,
43
+ ASGISendCallable,
44
+ Scope,
45
+ )
46
+
47
+ # Vendored from https://github.com/django/asgiref/blob/main/asgiref/compatibility.py
48
+
49
+ if hasattr(inspect, "markcoroutinefunction"):
50
+ iscoroutinefunction = inspect.iscoroutinefunction
51
+ else:
52
+ iscoroutinefunction = asyncio.iscoroutinefunction # type: ignore[assignment]
53
+
54
+
55
+ def is_double_callable(application: ASGIApplication) -> bool:
56
+ if getattr(application, "_asgi_single_callable", False):
57
+ return False
58
+ if getattr(application, "_asgi_double_callable", False):
59
+ return True
60
+ if inspect.isclass(application):
61
+ return True
62
+ if callable(application) and iscoroutinefunction(application.__call__):
63
+ return False
64
+ return not iscoroutinefunction(application)
65
+
66
+
67
+ def double_to_single_callable(application: ASGI2Application) -> ASGI3Application:
68
+ async def new_application(
69
+ scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable
70
+ ) -> None:
71
+ instance = application(scope)
72
+ return await instance(receive, send)
73
+
74
+ return new_application
75
+
76
+
77
+ def guarantee_single_callable(application: ASGIApplication) -> ASGI3Application:
78
+ if is_double_callable(application):
79
+ application = double_to_single_callable(cast("ASGI2Application", application))
80
+ return cast("ASGI3Application", application)
@@ -0,0 +1,373 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import threading
5
+ import time
6
+ from collections.abc import Callable, Iterator
7
+ from concurrent.futures import Future, ThreadPoolExecutor
8
+ from queue import Empty, Queue
9
+ from typing import TYPE_CHECKING
10
+ from urllib.parse import unquote, urlparse
11
+
12
+ from pyqwest import (
13
+ Headers,
14
+ HTTPVersion,
15
+ ReadError,
16
+ SyncRequest,
17
+ SyncResponse,
18
+ SyncTransport,
19
+ WriteError,
20
+ )
21
+ from pyqwest._pyqwest import get_sync_timeout
22
+
23
+ if TYPE_CHECKING:
24
+ import sys
25
+
26
+ if sys.version_info >= (3, 11):
27
+ from wsgiref.types import WSGIApplication, WSGIEnvironment
28
+ else:
29
+ from _typeshed.wsgi import WSGIApplication, WSGIEnvironment
30
+
31
+ _UNSET_STATUS = "unset"
32
+
33
+ _DEFAULT_EXECUTOR: ThreadPoolExecutor | None = None
34
+
35
+
36
+ def get_default_executor() -> ThreadPoolExecutor:
37
+ global _DEFAULT_EXECUTOR # noqa: PLW0603
38
+ if _DEFAULT_EXECUTOR is None:
39
+ _DEFAULT_EXECUTOR = ThreadPoolExecutor()
40
+ return _DEFAULT_EXECUTOR
41
+
42
+
43
+ class WSGITransport(SyncTransport):
44
+ """Transport implementation that directly invokes a WSGI application. Useful for testing."""
45
+
46
+ _app: WSGIApplication
47
+ _http_version: HTTPVersion
48
+ _closed: bool
49
+
50
+ def __init__(
51
+ self,
52
+ app: WSGIApplication,
53
+ http_version: HTTPVersion = HTTPVersion.HTTP2,
54
+ executor: ThreadPoolExecutor | None = None,
55
+ ) -> None:
56
+ """Creates a new WSGI transport.
57
+
58
+ Args:
59
+ app: The WSGI application to invoke for requests.
60
+ http_version: The HTTP version to simulate for requests.
61
+ executor: An optional ThreadPoolExecutor to use for running the WSGI app.
62
+ If not provided, a default executor will be used.
63
+ """
64
+ self._app = app
65
+ self._http_version = http_version
66
+ self._executor = executor or get_default_executor()
67
+ self._closed = False
68
+
69
+ def execute_sync(self, request: SyncRequest) -> SyncResponse:
70
+ deadline = None
71
+ if (to := get_sync_timeout()) is not None:
72
+ deadline = time.monotonic() + to.total_seconds()
73
+
74
+ parsed_url = urlparse(request.url)
75
+ raw_path = parsed_url.path or "/"
76
+ path = unquote(raw_path).encode().decode("latin-1")
77
+ query = parsed_url.query.encode().decode("latin-1")
78
+
79
+ match self._http_version:
80
+ case HTTPVersion.HTTP1:
81
+ server_protocol = "HTTP/1.1"
82
+ case HTTPVersion.HTTP2:
83
+ server_protocol = "HTTP/2"
84
+ case HTTPVersion.HTTP3:
85
+ server_protocol = "HTTP/3"
86
+ case _:
87
+ server_protocol = "HTTP/1.1"
88
+
89
+ trailers = Headers()
90
+ trailers_supported = (
91
+ self._http_version == HTTPVersion.HTTP2
92
+ and request.headers.get("te", "") == "trailers"
93
+ )
94
+
95
+ def send_trailers(headers: list[tuple[str, str]]) -> None:
96
+ if not trailers_supported:
97
+ return
98
+ for k, v in headers:
99
+ trailers.add(k, v)
100
+
101
+ request_input = RequestInput(request.content, self._http_version)
102
+ environ: WSGIEnvironment = {
103
+ "REQUEST_METHOD": request.method,
104
+ "SCRIPT_NAME": "",
105
+ "PATH_INFO": path,
106
+ "QUERY_STRING": query,
107
+ "SERVER_NAME": parsed_url.hostname or "",
108
+ "SERVER_PORT": str(
109
+ parsed_url.port or (443 if parsed_url.scheme == "https" else 80)
110
+ ),
111
+ "SERVER_PROTOCOL": server_protocol,
112
+ "wsgi.url_scheme": parsed_url.scheme,
113
+ "wsgi.version": (1, 0),
114
+ "wsgi.multithread": True,
115
+ "wsgi.multiprocess": False,
116
+ "wsgi.run_once": False,
117
+ "wsgi.input": request_input,
118
+ "wsgi.ext.http.send_trailers": send_trailers,
119
+ }
120
+
121
+ for k, v in request.headers.items():
122
+ match k:
123
+ case "content-type":
124
+ environ["CONTENT_TYPE"] = v
125
+ case "content-length":
126
+ environ["CONTENT_LENGTH"] = v
127
+ case _:
128
+ name = f"HTTP_{k.upper().replace('-', '_')}"
129
+ value = f"{existing},{v}" if (existing := environ.get(name)) else v
130
+ environ[name] = value
131
+
132
+ response_queue: Queue[bytes | None | Exception] = Queue()
133
+
134
+ status_str: str = _UNSET_STATUS
135
+ headers: list[tuple[str, str]] = []
136
+ exc: (
137
+ tuple[type[BaseException], BaseException, object]
138
+ | tuple[None, None, None]
139
+ | None
140
+ ) = None
141
+ response_started = threading.Event()
142
+
143
+ def start_response(
144
+ status: str,
145
+ response_headers: list[tuple[str, str]],
146
+ exc_info: tuple[type[BaseException], BaseException, object]
147
+ | tuple[None, None, None]
148
+ | None = None,
149
+ ) -> Callable[[bytes], object]:
150
+ nonlocal status_str, headers, exc
151
+ status_str = status
152
+ headers = response_headers
153
+ exc = exc_info
154
+
155
+ def write(body: bytes) -> None:
156
+ if not response_started.is_set():
157
+ response_started.set()
158
+ if body:
159
+ response_queue.put(body)
160
+
161
+ return write
162
+
163
+ def run_app() -> None:
164
+ response_iter = self._app(environ, start_response)
165
+ try:
166
+ for chunk in response_iter:
167
+ if chunk:
168
+ if not response_started.is_set():
169
+ response_started.set()
170
+ response_queue.put(chunk)
171
+ except Exception as e:
172
+ response_queue.put(e)
173
+ else:
174
+ response_queue.put(None)
175
+ finally:
176
+ if not response_started.is_set():
177
+ request_input.close()
178
+ response_started.set()
179
+ with contextlib.suppress(Exception):
180
+ response_iter.close() # pyright: ignore[reportAttributeAccessIssue]
181
+
182
+ app_future = self._executor.submit(run_app)
183
+
184
+ response_started.wait()
185
+
186
+ if status_str is _UNSET_STATUS:
187
+ return SyncResponse(
188
+ status=500,
189
+ http_version=self._http_version,
190
+ headers=Headers((("content-type", "text/plain"),)),
191
+ content=b"WSGI application did not call start_response",
192
+ )
193
+
194
+ if exc and exc[0]:
195
+ return SyncResponse(
196
+ status=500,
197
+ http_version=self._http_version,
198
+ headers=Headers((("content-type", "text/plain"),)),
199
+ content=str(exc[0]).encode(),
200
+ )
201
+
202
+ response_content = ResponseContent(
203
+ response_queue, request_input, app_future, deadline
204
+ )
205
+
206
+ status = int(status_str.split(" ", 1)[0])
207
+
208
+ return SyncResponse(
209
+ status=status,
210
+ http_version=self._http_version,
211
+ headers=Headers(headers),
212
+ content=response_content,
213
+ trailers=trailers,
214
+ )
215
+
216
+
217
+ class RequestInput:
218
+ def __init__(self, content: Iterator[bytes], http_version: HTTPVersion) -> None:
219
+ self._content = content
220
+ self._http_version = http_version
221
+ self._closed = False
222
+ self._buffer = bytearray()
223
+
224
+ def read(self, size: int = -1) -> bytes:
225
+ return self._do_read(size)
226
+
227
+ def readline(self, size: int = -1) -> bytes:
228
+ if self._closed or size == 0:
229
+ return b""
230
+
231
+ line = bytearray()
232
+ while True:
233
+ sz = size - len(line) if size >= 0 else -1
234
+ read_bytes = self._do_read(sz)
235
+ if not read_bytes:
236
+ return bytes(line)
237
+ if len(line) + len(read_bytes) == size:
238
+ return bytes(line + read_bytes)
239
+ newline_index = read_bytes.find(b"\n")
240
+ if newline_index == -1:
241
+ line.extend(read_bytes)
242
+ continue
243
+ res = line + read_bytes[: newline_index + 1]
244
+ self._buffer.extend(read_bytes[newline_index + 1 :])
245
+ return bytes(res)
246
+
247
+ def __iter__(self) -> Iterator[bytes]:
248
+ return self
249
+
250
+ def __next__(self) -> bytes:
251
+ line = self.readline()
252
+ if not line:
253
+ raise StopIteration
254
+ return line
255
+
256
+ def readlines(self, hint: int = -1) -> list[bytes]:
257
+ return list(self)
258
+
259
+ def _do_read(self, size: int) -> bytes:
260
+ if self._closed or size == 0:
261
+ return b""
262
+
263
+ try:
264
+ while True:
265
+ chunk = next(self._content)
266
+ if size < 0:
267
+ self._buffer.extend(chunk)
268
+ continue
269
+ if len(self._buffer) + len(chunk) >= size:
270
+ to_read = size - len(self._buffer)
271
+ res = self._buffer + chunk[:to_read]
272
+ self._buffer.clear()
273
+ self._buffer.extend(chunk[to_read:])
274
+ return bytes(res)
275
+ if len(self._buffer) == 0:
276
+ return chunk
277
+ res = self._buffer + chunk
278
+ self._buffer.clear()
279
+ return bytes(res)
280
+ except StopIteration:
281
+ self.close()
282
+ res = bytes(self._buffer)
283
+ self._buffer = bytearray()
284
+ return res
285
+ except Exception as e:
286
+ self.close()
287
+ if self._http_version != HTTPVersion.HTTP2:
288
+ msg = f"Request failed: {e}"
289
+ else:
290
+ # With HTTP/2, reqwest seems to squash the original error message.
291
+ msg = "Request failed: stream error sent by user"
292
+ raise WriteError(msg) from e
293
+
294
+ def close(self) -> None:
295
+ if self._closed:
296
+ return
297
+ self._closed = True
298
+ with contextlib.suppress(Exception):
299
+ self._content.close() # pyright: ignore[reportAttributeAccessIssue]
300
+
301
+
302
+ class ResponseContent(Iterator[bytes]):
303
+ def __init__(
304
+ self,
305
+ response_queue: Queue[bytes | None | Exception],
306
+ request_input: RequestInput,
307
+ app_future: Future,
308
+ deadline: float | None,
309
+ ) -> None:
310
+ self._response_queue = response_queue
311
+ self._request_input = request_input
312
+ self._app_future = app_future
313
+ self._closed = False
314
+ self._read_pending = False
315
+ self._deadline = deadline
316
+
317
+ def __iter__(self) -> Iterator[bytes]:
318
+ return self
319
+
320
+ def __next__(self) -> bytes:
321
+ if self._closed:
322
+ raise StopIteration
323
+ err: Exception | None = None
324
+ self._read_pending = True
325
+ chunk = b""
326
+ try:
327
+ if self._deadline:
328
+ while True:
329
+ time_left = self._deadline - time.monotonic()
330
+ if time_left <= 0:
331
+ msg = "Response read timed out"
332
+ message = TimeoutError(msg)
333
+ break
334
+ try:
335
+ message = self._response_queue.get(timeout=time_left)
336
+ break
337
+ except Empty:
338
+ continue
339
+ else:
340
+ message = self._response_queue.get()
341
+ finally:
342
+ self._read_pending = False
343
+ if isinstance(message, Exception):
344
+ match message:
345
+ case WriteError() | TimeoutError():
346
+ err = message
347
+ case _:
348
+ msg = "Request Failed: Error reading response body"
349
+ err = ReadError(msg)
350
+ elif message is None:
351
+ err = StopIteration()
352
+ else:
353
+ chunk = message
354
+
355
+ if err:
356
+ self._closed = True
357
+ self._request_input.close()
358
+ with contextlib.suppress(Exception):
359
+ self._app_future.result()
360
+ raise err
361
+ return chunk
362
+
363
+ def __del__(self) -> None:
364
+ self.close()
365
+
366
+ def close(self) -> None:
367
+ if self._closed:
368
+ return
369
+ self._closed = True
370
+ self._request_input.close()
371
+ self._response_queue.put(ReadError("Response body read cancelled"))
372
+ with contextlib.suppress(Exception):
373
+ self._app_future.result()
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyqwest
3
+ Version: 0.3.0
4
+ Classifier: Development Status :: 3 - Alpha
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Operating System :: MacOS :: MacOS X
8
+ Classifier: Operating System :: Microsoft :: Windows
9
+ Classifier: Operating System :: POSIX :: Linux
10
+ Classifier: Programming Language :: Python
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: Implementation :: CPython
18
+ Classifier: Programming Language :: Rust
19
+ Classifier: Topic :: Internet :: WWW/HTTP
20
+ License-File: LICENSE
21
+ Requires-Python: >=3.10
@@ -0,0 +1,16 @@
1
+ pyqwest/__init__.py,sha256=TqZ4ioJhL_RsgALEnb3iChY7gb9ysWc4cUA0k9rsjXI,902
2
+ pyqwest/_coro.py,sha256=k5vVz1zeg9HQYvbbhAg9WnYiBHskkJ3Qdqfmx16HcVs,7867
3
+ pyqwest/_glue.py,sha256=gk--twq1LRc-tsaDVwcIxcwPmaMz5kt94QQGJ0fTNFw,2530
4
+ pyqwest/_pyqwest.cpython-314-aarch64-linux-gnu.so,sha256=9hmyTuskWBMy7p9Utuofc4xft6-vpFJD2RNzLWEn_SY,13161488
5
+ pyqwest/_pyqwest.pyi,sha256=QFGjqouSggefDP6lZH-bJ2aQbEJbJI7dTuREdLOwaIc,45175
6
+ pyqwest/httpx/__init__.py,sha256=4DJZ0AMFfuScGLkd9ktwGMypll7RW6VhAMenwcEWkDw,157
7
+ pyqwest/httpx/_transport.py,sha256=3Os8YB3Wq9dFvvYugMsXKmpM6JmuUSuSbTzBbOSf-Uw,9378
8
+ pyqwest/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ pyqwest/testing/__init__.py,sha256=oNW9UbowpENKwUt0QvBzHFWLWq4_7b1LMfMz3QmftQE,148
10
+ pyqwest/testing/_asgi.py,sha256=SXYcCTmND2b-SDn18Kf8qDcBwjhh2SejD_fEs6Zz2Qs,12203
11
+ pyqwest/testing/_asgi_compatibility.py,sha256=30rCjvyLg5L9x4vyI0LS8oqPtfY4oLctINJgqMzgLDU,3206
12
+ pyqwest/testing/_wsgi.py,sha256=I8IZ8Dekwj_-OxiHqGZqPJP0sZs4-EywU2GELcWo_Bg,12313
13
+ pyqwest-0.3.0.dist-info/METADATA,sha256=9g9EE8GB_Qbyl-BRDYHmNsylvR0u1jrZookvPz9CLQ4,886
14
+ pyqwest-0.3.0.dist-info/WHEEL,sha256=Fx-7UOgjfhtK3XXmysArRGq3lEQiAORUMl6jtPUmCMs,149
15
+ pyqwest-0.3.0.dist-info/licenses/LICENSE,sha256=e7sDs6pM-apXvhJO1bLtzngKeI1aQ6bDIyJxO7Sgv1E,1085
16
+ pyqwest-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.11.5)
3
+ Root-Is-Purelib: false
4
+ Tag: cp314-cp314-manylinux_2_17_aarch64
5
+ Tag: cp314-cp314-manylinux2014_aarch64
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) CurioSwitch (oss@curioswitch.org)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.