pyqwest 0.2.0__cp313-cp313-win_amd64.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,383 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import math
5
+ import threading
6
+ import time
7
+ from collections.abc import Callable, Iterator
8
+ from concurrent.futures import Future, ThreadPoolExecutor
9
+ from queue import Empty, Queue
10
+ from typing import TYPE_CHECKING
11
+ from urllib.parse import unquote, urlparse
12
+
13
+ from pyqwest import (
14
+ Headers,
15
+ HTTPVersion,
16
+ ReadError,
17
+ SyncRequest,
18
+ SyncResponse,
19
+ SyncTransport,
20
+ WriteError,
21
+ )
22
+
23
+ if TYPE_CHECKING:
24
+ import sys
25
+ from types import TracebackType
26
+
27
+ if sys.version_info >= (3, 11):
28
+ from wsgiref.types import WSGIApplication, WSGIEnvironment
29
+ else:
30
+ from _typeshed.wsgi import WSGIApplication, WSGIEnvironment
31
+
32
+ _UNSET_STATUS = "unset"
33
+
34
+
35
+ class WSGITransport(SyncTransport):
36
+ _app: WSGIApplication
37
+ _http_version: HTTPVersion
38
+ _executor: ThreadPoolExecutor
39
+ _close_executor: bool
40
+ _closed: bool
41
+
42
+ def __init__(
43
+ self,
44
+ app: WSGIApplication,
45
+ http_version: HTTPVersion = HTTPVersion.HTTP2,
46
+ executor: ThreadPoolExecutor | None = None,
47
+ ) -> None:
48
+ self._app = app
49
+ self._http_version = http_version
50
+ if executor is None:
51
+ self._executor = ThreadPoolExecutor()
52
+ self._close_executor = True
53
+ else:
54
+ self._executor = executor
55
+ self._close_executor = False
56
+ self._closed = False
57
+
58
+ def execute_sync(self, request: SyncRequest) -> SyncResponse:
59
+ timeout: float | None = request._timeout # pyright: ignore[reportAttributeAccessIssue] # noqa: SLF001
60
+ if timeout is not None and (timeout < 0 or not math.isfinite(timeout)):
61
+ msg = "Timeout must be non-negative"
62
+ raise ValueError(msg)
63
+
64
+ deadline = time.monotonic() + timeout if timeout is not None else None
65
+
66
+ parsed_url = urlparse(request.url)
67
+ raw_path = parsed_url.path or "/"
68
+ path = unquote(raw_path).encode().decode("latin-1")
69
+ query = parsed_url.query.encode().decode("latin-1")
70
+
71
+ match self._http_version:
72
+ case HTTPVersion.HTTP1:
73
+ server_protocol = "HTTP/1.1"
74
+ case HTTPVersion.HTTP2:
75
+ server_protocol = "HTTP/2"
76
+ case HTTPVersion.HTTP3:
77
+ server_protocol = "HTTP/3"
78
+ case _:
79
+ server_protocol = "HTTP/1.1"
80
+
81
+ trailers = Headers()
82
+ trailers_supported = (
83
+ self._http_version == HTTPVersion.HTTP2
84
+ and request.headers.get("te", "") == "trailers"
85
+ )
86
+
87
+ def send_trailers(headers: list[tuple[str, str]]) -> None:
88
+ if not trailers_supported:
89
+ return
90
+ for k, v in headers:
91
+ trailers.add(k, v)
92
+
93
+ request_input = RequestInput(request.content, self._http_version)
94
+ environ: WSGIEnvironment = {
95
+ "REQUEST_METHOD": request.method,
96
+ "SCRIPT_NAME": "",
97
+ "PATH_INFO": path,
98
+ "QUERY_STRING": query,
99
+ "SERVER_NAME": parsed_url.hostname or "",
100
+ "SERVER_PORT": str(
101
+ parsed_url.port or (443 if parsed_url.scheme == "https" else 80)
102
+ ),
103
+ "SERVER_PROTOCOL": server_protocol,
104
+ "wsgi.url_scheme": parsed_url.scheme,
105
+ "wsgi.version": (1, 0),
106
+ "wsgi.multithread": True,
107
+ "wsgi.multiprocess": False,
108
+ "wsgi.run_once": False,
109
+ "wsgi.input": request_input,
110
+ "wsgi.ext.http.send_trailers": send_trailers,
111
+ }
112
+
113
+ for k, v in request.headers.items():
114
+ match k:
115
+ case "content-type":
116
+ environ["CONTENT_TYPE"] = v
117
+ case "content-length":
118
+ environ["CONTENT_LENGTH"] = v
119
+ case _:
120
+ name = f"HTTP_{k.upper().replace('-', '_')}"
121
+ value = f"{existing},{v}" if (existing := environ.get(name)) else v
122
+ environ[name] = value
123
+
124
+ response_queue: Queue[bytes | None | Exception] = Queue()
125
+
126
+ status_str: str = _UNSET_STATUS
127
+ headers: list[tuple[str, str]] = []
128
+ exc: (
129
+ tuple[type[BaseException], BaseException, object]
130
+ | tuple[None, None, None]
131
+ | None
132
+ ) = None
133
+ response_started = threading.Event()
134
+
135
+ def start_response(
136
+ status: str,
137
+ response_headers: list[tuple[str, str]],
138
+ exc_info: tuple[type[BaseException], BaseException, object]
139
+ | tuple[None, None, None]
140
+ | None = None,
141
+ ) -> Callable[[bytes], object]:
142
+ nonlocal status_str, headers, exc
143
+ status_str = status
144
+ headers = response_headers
145
+ exc = exc_info
146
+
147
+ def write(body: bytes) -> None:
148
+ if not response_started.is_set():
149
+ response_started.set()
150
+ if body:
151
+ response_queue.put(body)
152
+
153
+ return write
154
+
155
+ def run_app() -> None:
156
+ response_iter = self._app(environ, start_response)
157
+ try:
158
+ for chunk in response_iter:
159
+ if chunk:
160
+ if not response_started.is_set():
161
+ response_started.set()
162
+ response_queue.put(chunk)
163
+ except Exception as e:
164
+ response_queue.put(e)
165
+ else:
166
+ response_queue.put(None)
167
+ finally:
168
+ if not response_started.is_set():
169
+ request_input.close()
170
+ response_started.set()
171
+ with contextlib.suppress(Exception):
172
+ response_iter.close() # pyright: ignore[reportAttributeAccessIssue]
173
+
174
+ app_future = self._executor.submit(run_app)
175
+
176
+ response_started.wait()
177
+
178
+ if status_str is _UNSET_STATUS:
179
+ return SyncResponse(
180
+ status=500,
181
+ http_version=self._http_version,
182
+ headers=Headers((("content-type", "text/plain"),)),
183
+ content=b"WSGI application did not call start_response",
184
+ )
185
+
186
+ if exc and exc[0]:
187
+ return SyncResponse(
188
+ status=500,
189
+ http_version=self._http_version,
190
+ headers=Headers((("content-type", "text/plain"),)),
191
+ content=str(exc[0]).encode(),
192
+ )
193
+
194
+ response_content = ResponseContent(
195
+ response_queue, request_input, app_future, deadline
196
+ )
197
+
198
+ status = int(status_str.split(" ", 1)[0])
199
+
200
+ return SyncResponse(
201
+ status=status,
202
+ http_version=self._http_version,
203
+ headers=Headers(headers),
204
+ content=response_content,
205
+ trailers=trailers,
206
+ )
207
+
208
+ def __enter__(self) -> WSGITransport:
209
+ return self
210
+
211
+ def __exit__(
212
+ self,
213
+ _exc_type: type[BaseException] | None,
214
+ _exc_value: BaseException | None,
215
+ _traceback: TracebackType | None,
216
+ ) -> None:
217
+ self.close()
218
+
219
+ def close(self) -> None:
220
+ if self._closed:
221
+ return
222
+ self._closed = True
223
+ if self._close_executor:
224
+ self._executor.shutdown()
225
+
226
+
227
+ class RequestInput:
228
+ def __init__(self, content: Iterator[bytes], http_version: HTTPVersion) -> None:
229
+ self._content = content
230
+ self._http_version = http_version
231
+ self._closed = False
232
+ self._buffer = bytearray()
233
+
234
+ def read(self, size: int = -1) -> bytes:
235
+ return self._do_read(size)
236
+
237
+ def readline(self, size: int = -1) -> bytes:
238
+ if self._closed or size == 0:
239
+ return b""
240
+
241
+ line = bytearray()
242
+ while True:
243
+ sz = size - len(line) if size >= 0 else -1
244
+ read_bytes = self._do_read(sz)
245
+ if not read_bytes:
246
+ return bytes(line)
247
+ if len(line) + len(read_bytes) == size:
248
+ return bytes(line + read_bytes)
249
+ newline_index = read_bytes.find(b"\n")
250
+ if newline_index == -1:
251
+ line.extend(read_bytes)
252
+ continue
253
+ res = line + read_bytes[: newline_index + 1]
254
+ self._buffer.extend(read_bytes[newline_index + 1 :])
255
+ return bytes(res)
256
+
257
+ def __iter__(self) -> Iterator[bytes]:
258
+ return self
259
+
260
+ def __next__(self) -> bytes:
261
+ line = self.readline()
262
+ if not line:
263
+ raise StopIteration
264
+ return line
265
+
266
+ def readlines(self, hint: int = -1) -> list[bytes]:
267
+ return list(self)
268
+
269
+ def _do_read(self, size: int) -> bytes:
270
+ if self._closed or size == 0:
271
+ return b""
272
+
273
+ try:
274
+ while True:
275
+ chunk = next(self._content)
276
+ if size < 0:
277
+ self._buffer.extend(chunk)
278
+ continue
279
+ if len(self._buffer) + len(chunk) >= size:
280
+ to_read = size - len(self._buffer)
281
+ res = self._buffer + chunk[:to_read]
282
+ self._buffer.clear()
283
+ self._buffer.extend(chunk[to_read:])
284
+ return bytes(res)
285
+ if len(self._buffer) == 0:
286
+ return chunk
287
+ res = self._buffer + chunk
288
+ self._buffer.clear()
289
+ return bytes(res)
290
+ except StopIteration:
291
+ self.close()
292
+ res = bytes(self._buffer)
293
+ self._buffer = bytearray()
294
+ return res
295
+ except Exception as e:
296
+ self.close()
297
+ if self._http_version != HTTPVersion.HTTP2:
298
+ msg = f"Request failed: {e}"
299
+ else:
300
+ # With HTTP/2, reqwest seems to squash the original error message.
301
+ msg = "Request failed: stream error sent by user"
302
+ raise WriteError(msg) from e
303
+
304
+ def close(self) -> None:
305
+ if self._closed:
306
+ return
307
+ self._closed = True
308
+ with contextlib.suppress(Exception):
309
+ self._content.close() # pyright: ignore[reportAttributeAccessIssue]
310
+
311
+
312
+ class ResponseContent(Iterator[bytes]):
313
+ def __init__(
314
+ self,
315
+ response_queue: Queue[bytes | None | Exception],
316
+ request_input: RequestInput,
317
+ app_future: Future,
318
+ deadline: float | None,
319
+ ) -> None:
320
+ self._response_queue = response_queue
321
+ self._request_input = request_input
322
+ self._app_future = app_future
323
+ self._closed = False
324
+ self._read_pending = False
325
+ self._deadline = deadline
326
+
327
+ def __iter__(self) -> Iterator[bytes]:
328
+ return self
329
+
330
+ def __next__(self) -> bytes:
331
+ if self._closed:
332
+ raise StopIteration
333
+ err: Exception | None = None
334
+ self._read_pending = True
335
+ chunk = b""
336
+ try:
337
+ if self._deadline:
338
+ while True:
339
+ time_left = self._deadline - time.monotonic()
340
+ if time_left <= 0:
341
+ msg = "Response read timed out"
342
+ message = TimeoutError(msg)
343
+ break
344
+ try:
345
+ message = self._response_queue.get(timeout=time_left)
346
+ break
347
+ except Empty:
348
+ continue
349
+ else:
350
+ message = self._response_queue.get()
351
+ finally:
352
+ self._read_pending = False
353
+ if isinstance(message, Exception):
354
+ match message:
355
+ case WriteError() | TimeoutError():
356
+ err = message
357
+ case _:
358
+ msg = "Request Failed: Error reading response body"
359
+ err = ReadError(msg)
360
+ elif message is None:
361
+ err = StopIteration()
362
+ else:
363
+ chunk = message
364
+
365
+ if err:
366
+ self._closed = True
367
+ self._request_input.close()
368
+ with contextlib.suppress(Exception):
369
+ self._app_future.result()
370
+ raise err
371
+ return chunk
372
+
373
+ def __del__(self) -> None:
374
+ self.close()
375
+
376
+ def close(self) -> None:
377
+ if self._closed:
378
+ return
379
+ self._closed = True
380
+ self._request_input.close()
381
+ self._response_queue.put(ReadError("Response body read cancelled"))
382
+ with contextlib.suppress(Exception):
383
+ self._app_future.result()
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyqwest
3
+ Version: 0.2.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=-u46hIqP0IGssz_qZUt2MlH7XZmK4FyRlKic8Tghjvo,860
2
+ pyqwest\_coro.py,sha256=9Kkeyk9gA8fzFpGHhhjAz63msb98J4tuIQMPpDCOm3Y,7636
3
+ pyqwest\_glue.py,sha256=gk--twq1LRc-tsaDVwcIxcwPmaMz5kt94QQGJ0fTNFw,2530
4
+ pyqwest\_pyqwest.cp313-win_amd64.pyd,sha256=DLHi18Xl0Eq1pxCD2Y_-xEEHlrIxcnWzqdNWtUqVjE4,10728960
5
+ pyqwest\_pyqwest.pyi,sha256=RkpFULa9imDZwej6D4v_yprlALlp0ngTcxu5nKWpc6I,38756
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=Jxd2cNG_DbYbUenW2LiV3Uc5x9d5ynUD7YISWiu7owU,12526
11
+ pyqwest\testing\_asgi_compatibility.py,sha256=30rCjvyLg5L9x4vyI0LS8oqPtfY4oLctINJgqMzgLDU,3206
12
+ pyqwest\testing\_wsgi.py,sha256=IgNpv6cGV-z1Mrrn8p2xh8XWsEOPEl5JVtuYyi57LsM,12464
13
+ pyqwest-0.2.0.dist-info\METADATA,sha256=mHLRxgcqJckHC26TYH0gMeTi4_hvhjO5LAhh___pbMk,886
14
+ pyqwest-0.2.0.dist-info\WHEEL,sha256=n_BmF69IyGtioVWE9c3M_zsEfe6-xMZy1v5HCL_6qE0,97
15
+ pyqwest-0.2.0.dist-info\licenses\LICENSE,sha256=e7sDs6pM-apXvhJO1bLtzngKeI1aQ6bDIyJxO7Sgv1E,1085
16
+ pyqwest-0.2.0.dist-info\RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.11.5)
3
+ Root-Is-Purelib: false
4
+ Tag: cp313-cp313-win_amd64
@@ -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.