pyreqwest 0.8.0__cp313-cp313-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.
- pyreqwest/__init__.py +3 -0
- pyreqwest/__init__.pyi +1 -0
- pyreqwest/_pyreqwest.cpython-313-x86_64-linux-gnu.so +0 -0
- pyreqwest/bytes/__init__.py +16 -0
- pyreqwest/bytes/__init__.pyi +106 -0
- pyreqwest/client/__init__.py +21 -0
- pyreqwest/client/__init__.pyi +349 -0
- pyreqwest/client/types.py +54 -0
- pyreqwest/compatibility/__init__.py +4 -0
- pyreqwest/compatibility/httpx/__init__.py +11 -0
- pyreqwest/compatibility/httpx/_internal.py +60 -0
- pyreqwest/compatibility/httpx/transport.py +154 -0
- pyreqwest/cookie/__init__.py +5 -0
- pyreqwest/cookie/__init__.pyi +174 -0
- pyreqwest/exceptions/__init__.py +193 -0
- pyreqwest/http/__init__.py +19 -0
- pyreqwest/http/__init__.pyi +344 -0
- pyreqwest/logging/__init__.py +7 -0
- pyreqwest/logging/__init__.pyi +4 -0
- pyreqwest/middleware/__init__.py +5 -0
- pyreqwest/middleware/__init__.pyi +12 -0
- pyreqwest/middleware/asgi/__init__.py +5 -0
- pyreqwest/middleware/asgi/asgi.py +168 -0
- pyreqwest/middleware/types.py +26 -0
- pyreqwest/multipart/__init__.py +5 -0
- pyreqwest/multipart/__init__.pyi +75 -0
- pyreqwest/proxy/__init__.py +5 -0
- pyreqwest/proxy/__init__.pyi +47 -0
- pyreqwest/py.typed +0 -0
- pyreqwest/pytest_plugin/__init__.py +8 -0
- pyreqwest/pytest_plugin/internal/__init__.py +0 -0
- pyreqwest/pytest_plugin/internal/assert_eq.py +6 -0
- pyreqwest/pytest_plugin/internal/assert_message.py +123 -0
- pyreqwest/pytest_plugin/internal/matcher.py +34 -0
- pyreqwest/pytest_plugin/internal/plugin.py +15 -0
- pyreqwest/pytest_plugin/mock.py +512 -0
- pyreqwest/pytest_plugin/types.py +26 -0
- pyreqwest/request/__init__.py +29 -0
- pyreqwest/request/__init__.pyi +218 -0
- pyreqwest/response/__init__.py +19 -0
- pyreqwest/response/__init__.pyi +157 -0
- pyreqwest/simple/__init__.py +1 -0
- pyreqwest/simple/request/__init__.py +21 -0
- pyreqwest/simple/request/__init__.pyi +29 -0
- pyreqwest/simple/sync_request/__init__.py +21 -0
- pyreqwest/simple/sync_request/__init__.pyi +29 -0
- pyreqwest/types.py +12 -0
- pyreqwest-0.8.0.dist-info/METADATA +148 -0
- pyreqwest-0.8.0.dist-info/RECORD +52 -0
- pyreqwest-0.8.0.dist-info/WHEEL +5 -0
- pyreqwest-0.8.0.dist-info/entry_points.txt +2 -0
- pyreqwest-0.8.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
from typing import Any, Self
|
|
3
|
+
|
|
4
|
+
from pyreqwest.bytes import Bytes
|
|
5
|
+
from pyreqwest.http import HeaderMap, Url
|
|
6
|
+
from pyreqwest.middleware.types import Middleware, SyncMiddleware
|
|
7
|
+
from pyreqwest.multipart import FormBuilder
|
|
8
|
+
from pyreqwest.response import Response, SyncResponse
|
|
9
|
+
from pyreqwest.types import ExtensionsType, FormParams, HeadersType, QueryParams, Stream, SyncStream
|
|
10
|
+
|
|
11
|
+
class Request:
|
|
12
|
+
@property
|
|
13
|
+
def method(self) -> str:
|
|
14
|
+
"""Get the HTTP method. (e.g. GET, POST)."""
|
|
15
|
+
|
|
16
|
+
@method.setter
|
|
17
|
+
def method(self, value: str) -> None:
|
|
18
|
+
"""Set the HTTP method."""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def url(self) -> Url:
|
|
22
|
+
"""Get the url."""
|
|
23
|
+
|
|
24
|
+
@url.setter
|
|
25
|
+
def url(self, value: Url | str) -> None:
|
|
26
|
+
"""Set the url."""
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def headers(self) -> HeaderMap:
|
|
30
|
+
"""Get the headers. This is not a copy. Modifying it modifies the request."""
|
|
31
|
+
|
|
32
|
+
@headers.setter
|
|
33
|
+
def headers(self, headers: HeadersType) -> None:
|
|
34
|
+
"""Replace headers. Given value is copied."""
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def body(self) -> "RequestBody | None":
|
|
38
|
+
"""Get the body."""
|
|
39
|
+
|
|
40
|
+
@body.setter
|
|
41
|
+
def body(self, value: "RequestBody | None") -> None:
|
|
42
|
+
"""Set the body or remove body."""
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def extensions(self) -> dict[str, Any]:
|
|
46
|
+
"""Arbitrary per-request data storage. Useful for passing through data to middleware and response."""
|
|
47
|
+
|
|
48
|
+
@extensions.setter
|
|
49
|
+
def extensions(self, value: ExtensionsType) -> None:
|
|
50
|
+
"""Replace extensions. Given value is shallow copied."""
|
|
51
|
+
|
|
52
|
+
def copy(self) -> Self:
|
|
53
|
+
"""Copy the request. Byte-bodies are zero-copied. Stream bodies are re-created via their own copy logic."""
|
|
54
|
+
|
|
55
|
+
def __copy__(self) -> Self: ...
|
|
56
|
+
def repr_full(self) -> str:
|
|
57
|
+
"""Verbose repr including non-sensitive headers and body summary."""
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_request_and_body(cls, request: Self, body: "RequestBody | None") -> Self:
|
|
61
|
+
"""Clone request with a new body instance."""
|
|
62
|
+
|
|
63
|
+
class ConsumedRequest(Request):
|
|
64
|
+
"""Request that will fully read the response body when sent."""
|
|
65
|
+
|
|
66
|
+
async def send(self) -> Response:
|
|
67
|
+
"""Execute the request returning a Response with fully read response body."""
|
|
68
|
+
|
|
69
|
+
class StreamRequest(Request):
|
|
70
|
+
"""Request whose response body is streamed."""
|
|
71
|
+
|
|
72
|
+
async def __aenter__(self) -> Response:
|
|
73
|
+
"""Execute the request returning a Response with streaming response body."""
|
|
74
|
+
|
|
75
|
+
async def __aexit__(self, *args: object, **kwargs: Any) -> None:
|
|
76
|
+
"""Close streaming response."""
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def read_buffer_limit(self) -> int:
|
|
80
|
+
"""Max bytes buffered when reading streamed body."""
|
|
81
|
+
|
|
82
|
+
class SyncConsumedRequest(Request):
|
|
83
|
+
"""Synchronous request that will fully read the response body when sent."""
|
|
84
|
+
|
|
85
|
+
def send(self) -> SyncResponse:
|
|
86
|
+
"""Execute the request returning a Response with fully read response body."""
|
|
87
|
+
|
|
88
|
+
class SyncStreamRequest(Request):
|
|
89
|
+
"""Synchronous request whose response body is streamed."""
|
|
90
|
+
|
|
91
|
+
def __enter__(self) -> SyncResponse:
|
|
92
|
+
"""Execute the request returning a Response with streaming response body."""
|
|
93
|
+
|
|
94
|
+
def __exit__(self, *args: object, **kwargs: Any) -> None:
|
|
95
|
+
"""Close streaming response."""
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def read_buffer_limit(self) -> int:
|
|
99
|
+
"""Max bytes buffered when reading streamed body."""
|
|
100
|
+
|
|
101
|
+
class RequestBody:
|
|
102
|
+
"""Represents request body content (bytes, text, or async stream). Bodies are single-use."""
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def from_text(body: str) -> "RequestBody":
|
|
106
|
+
"""Create body from text."""
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def from_bytes(body: bytes | bytearray | memoryview) -> "RequestBody":
|
|
110
|
+
"""Create body from raw bytes."""
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def from_stream(stream: Stream) -> "RequestBody":
|
|
114
|
+
"""Create body from async byte stream."""
|
|
115
|
+
|
|
116
|
+
def copy_bytes(self) -> Bytes | None:
|
|
117
|
+
"""Return bytes zero-copy. None for stream."""
|
|
118
|
+
|
|
119
|
+
def get_stream(self) -> Stream | None:
|
|
120
|
+
"""Return underlying stream if streaming body else None."""
|
|
121
|
+
|
|
122
|
+
def __copy__(self) -> Self:
|
|
123
|
+
"""Copy body (Zero-copied bytes. Stream supplies its own copy)."""
|
|
124
|
+
|
|
125
|
+
class BaseRequestBuilder:
|
|
126
|
+
def error_for_status(self, enable: bool) -> Self:
|
|
127
|
+
"""Enable automatic HTTP error raising (4xx/5xx)."""
|
|
128
|
+
|
|
129
|
+
def header(self, name: str, value: str) -> Self:
|
|
130
|
+
"""Append single header value."""
|
|
131
|
+
|
|
132
|
+
def headers(self, headers: HeadersType) -> Self:
|
|
133
|
+
"""Merge multiple headers (mapping or sequence)."""
|
|
134
|
+
|
|
135
|
+
def basic_auth(self, username: str, password: str | None) -> Self:
|
|
136
|
+
"""Add Basic Authorization header."""
|
|
137
|
+
|
|
138
|
+
def bearer_auth(self, token: str) -> Self:
|
|
139
|
+
"""Add Bearer token Authorization header."""
|
|
140
|
+
|
|
141
|
+
def body_bytes(self, body: bytes | bytearray | memoryview) -> Self:
|
|
142
|
+
"""Set body from raw bytes."""
|
|
143
|
+
|
|
144
|
+
def body_text(self, body: str) -> Self:
|
|
145
|
+
"""Set body from text."""
|
|
146
|
+
|
|
147
|
+
def body_json(self, body: Any) -> Self:
|
|
148
|
+
"""Serialize body as JSON. Sets Content-Type header."""
|
|
149
|
+
|
|
150
|
+
def query(self, query: QueryParams) -> Self:
|
|
151
|
+
"""Add/merge query parameters."""
|
|
152
|
+
|
|
153
|
+
def timeout(self, timeout: timedelta) -> Self:
|
|
154
|
+
"""Set per-request total timeout."""
|
|
155
|
+
|
|
156
|
+
def multipart(self, multipart: FormBuilder) -> Self:
|
|
157
|
+
"""Attach multipart form body builder."""
|
|
158
|
+
|
|
159
|
+
def form(self, form: FormParams) -> Self:
|
|
160
|
+
"""Set application/x-www-form-urlencoded body."""
|
|
161
|
+
|
|
162
|
+
def extensions(self, extensions: ExtensionsType) -> Self:
|
|
163
|
+
"""Arbitrary per-request data storage. Useful for passing through data to middleware and response."""
|
|
164
|
+
|
|
165
|
+
def streamed_read_buffer_limit(self, value: int) -> Self:
|
|
166
|
+
"""Max bytes buffered when reading streamed body."""
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def default_streamed_read_buffer_limit() -> int:
|
|
170
|
+
"""Default max bytes buffered when reading streamed body."""
|
|
171
|
+
|
|
172
|
+
class RequestBuilder(BaseRequestBuilder):
|
|
173
|
+
"""Request builder. Use `build()` or `build_streamed()` to create the request to send."""
|
|
174
|
+
|
|
175
|
+
def build(self) -> ConsumedRequest:
|
|
176
|
+
"""Build request that full reads the response body on send()."""
|
|
177
|
+
|
|
178
|
+
def build_streamed(self) -> StreamRequest:
|
|
179
|
+
"""Build request whose response body is streamed."""
|
|
180
|
+
|
|
181
|
+
def body_stream(self, stream: Stream) -> Self:
|
|
182
|
+
"""Set streaming request body."""
|
|
183
|
+
|
|
184
|
+
def with_middleware(self, middleware: Middleware) -> Self:
|
|
185
|
+
"""Use a middleware component (added after client level middlewares, executed in chain order)."""
|
|
186
|
+
|
|
187
|
+
class SyncRequestBuilder(BaseRequestBuilder):
|
|
188
|
+
"""Synchronous request builder. Use `build()` or `build_streamed()` to create the request to send."""
|
|
189
|
+
|
|
190
|
+
def build(self) -> SyncConsumedRequest:
|
|
191
|
+
"""Build request that full reads the response body on send()."""
|
|
192
|
+
|
|
193
|
+
def build_streamed(self) -> SyncStreamRequest:
|
|
194
|
+
"""Build request whose response body is streamed."""
|
|
195
|
+
|
|
196
|
+
def body_stream(self, stream: SyncStream) -> Self:
|
|
197
|
+
"""Set streaming request body."""
|
|
198
|
+
|
|
199
|
+
def with_middleware(self, middleware: SyncMiddleware) -> Self:
|
|
200
|
+
"""Use a middleware component (added after client level middlewares, executed in chain order)."""
|
|
201
|
+
|
|
202
|
+
class OneOffRequestBuilder(BaseRequestBuilder):
|
|
203
|
+
"""One-off request builder. Use `send()` to execute the request."""
|
|
204
|
+
|
|
205
|
+
async def send(self) -> Response:
|
|
206
|
+
"""Execute the request returning a Response with fully read response body."""
|
|
207
|
+
|
|
208
|
+
def with_middleware(self, middleware: Middleware) -> Self:
|
|
209
|
+
"""Use a middleware component."""
|
|
210
|
+
|
|
211
|
+
class SyncOneOffRequestBuilder(BaseRequestBuilder):
|
|
212
|
+
"""Synchronous one-off request builder. Use `send()` to execute the request."""
|
|
213
|
+
|
|
214
|
+
def send(self) -> SyncResponse:
|
|
215
|
+
"""Execute the request returning a Response with fully read response body."""
|
|
216
|
+
|
|
217
|
+
def with_middleware(self, middleware: SyncMiddleware) -> Self:
|
|
218
|
+
"""Use a middleware component."""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Response classes and builders."""
|
|
2
|
+
|
|
3
|
+
from pyreqwest._pyreqwest.response import (
|
|
4
|
+
BaseResponse,
|
|
5
|
+
Response,
|
|
6
|
+
ResponseBodyReader,
|
|
7
|
+
ResponseBuilder,
|
|
8
|
+
SyncResponse,
|
|
9
|
+
SyncResponseBodyReader,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"BaseResponse",
|
|
14
|
+
"Response",
|
|
15
|
+
"SyncResponse",
|
|
16
|
+
"ResponseBuilder",
|
|
17
|
+
"ResponseBodyReader",
|
|
18
|
+
"SyncResponseBodyReader",
|
|
19
|
+
]
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from typing import Any, Self
|
|
2
|
+
|
|
3
|
+
from pyreqwest.bytes import Bytes
|
|
4
|
+
from pyreqwest.http import HeaderMap, Mime
|
|
5
|
+
from pyreqwest.types import ExtensionsType, HeadersType, Stream
|
|
6
|
+
|
|
7
|
+
class BaseResponse:
|
|
8
|
+
@property
|
|
9
|
+
def status(self) -> int:
|
|
10
|
+
"""HTTP status code (e.g. 200, 404)."""
|
|
11
|
+
|
|
12
|
+
@status.setter
|
|
13
|
+
def status(self, value: int) -> None:
|
|
14
|
+
"""Set HTTP status code."""
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def headers(self) -> HeaderMap:
|
|
18
|
+
"""Get the headers. This is not a copy. Modifying it modifies the response.
|
|
19
|
+
You can also use `get_header` or `get_header_all` to access headers.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@headers.setter
|
|
23
|
+
def headers(self, headers: HeadersType) -> None:
|
|
24
|
+
"""Replace headers. Given value is copied."""
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def extensions(self) -> dict[str, Any]:
|
|
28
|
+
"""Arbitrary per-request data storage. This is the data that was passed via request and middlewares.
|
|
29
|
+
This is not a copy. Modifying it modifies the response.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
@extensions.setter
|
|
33
|
+
def extensions(self, value: ExtensionsType) -> None:
|
|
34
|
+
"""Replace extensions. Given value is shallow copied."""
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def version(self) -> str:
|
|
38
|
+
"""Used HTTP version (e.g. 'HTTP/1.1', 'HTTP/2.0')."""
|
|
39
|
+
|
|
40
|
+
@version.setter
|
|
41
|
+
def version(self, value: str) -> None:
|
|
42
|
+
"""Set HTTP version."""
|
|
43
|
+
|
|
44
|
+
def error_for_status(self) -> None:
|
|
45
|
+
"""Raise StatusError for 4xx/5xx."""
|
|
46
|
+
|
|
47
|
+
def get_header(self, key: str) -> str | None:
|
|
48
|
+
"""Return first matching header value else None (case-insensitive)."""
|
|
49
|
+
|
|
50
|
+
def get_header_all(self, key: str) -> list[str]:
|
|
51
|
+
"""Return all values for header name (case-insensitive). Empty if absent."""
|
|
52
|
+
|
|
53
|
+
def content_type_mime(self) -> Mime | None:
|
|
54
|
+
"""Parsed Content-Type header as Mime or None if absent."""
|
|
55
|
+
|
|
56
|
+
class Response(BaseResponse):
|
|
57
|
+
"""Asynchronous response with optionally streamed body."""
|
|
58
|
+
|
|
59
|
+
async def bytes(self) -> Bytes:
|
|
60
|
+
"""Return entire body as bytes (cached after first read)."""
|
|
61
|
+
|
|
62
|
+
async def json(self) -> Any:
|
|
63
|
+
"""Decode body as JSON (underlying bytes cached after first read). Uses serde for decoding.
|
|
64
|
+
User can provide custom deserializer via `ClientBuilder.json_handler`.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
async def text(self) -> str:
|
|
68
|
+
"""Decode body to text (underlying bytes cached after first read). Uses charset from Content-Type."""
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def body_reader(self) -> "ResponseBodyReader":
|
|
72
|
+
"""Access streaming reader. Using bytes(), json() or text() is not allowed after reading body partially."""
|
|
73
|
+
|
|
74
|
+
class SyncResponse(BaseResponse):
|
|
75
|
+
"""Synchronous response variant."""
|
|
76
|
+
|
|
77
|
+
def bytes(self) -> Bytes:
|
|
78
|
+
"""Return entire body as bytes (cached after first read)."""
|
|
79
|
+
|
|
80
|
+
def json(self) -> Any:
|
|
81
|
+
"""Decode body as JSON (underlying bytes cached after first read). Uses serde for decoding.
|
|
82
|
+
User can provide custom deserializer via `SyncClientBuilder.json_handler`.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def text(self) -> str:
|
|
86
|
+
"""Decode body to text (underlying bytes cached after first read). Uses charset from Content-Type."""
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def body_reader(self) -> "SyncResponseBodyReader":
|
|
90
|
+
"""Access streaming reader. Using bytes(), json() or text() is not allowed after reading body partially."""
|
|
91
|
+
|
|
92
|
+
class ResponseBuilder:
|
|
93
|
+
"""Programmatic response construction (for testing, middlewares, manual responses)."""
|
|
94
|
+
|
|
95
|
+
def __init__(self) -> None:
|
|
96
|
+
"""Create empty response builder (defaults: 200, HTTP/1.1, empty headers/body)."""
|
|
97
|
+
|
|
98
|
+
async def build(self) -> Response:
|
|
99
|
+
"""Build asynchronous response."""
|
|
100
|
+
|
|
101
|
+
def build_sync(self) -> SyncResponse:
|
|
102
|
+
"""Build synchronous response (disallows async streams)."""
|
|
103
|
+
|
|
104
|
+
def status(self, value: int) -> Self:
|
|
105
|
+
"""Set status code."""
|
|
106
|
+
|
|
107
|
+
def version(self, value: str) -> Self:
|
|
108
|
+
"""Set HTTP version string."""
|
|
109
|
+
|
|
110
|
+
def header(self, name: str, value: str) -> Self:
|
|
111
|
+
"""Append single header value (multiple allowed)."""
|
|
112
|
+
|
|
113
|
+
def headers(self, headers: HeadersType) -> Self:
|
|
114
|
+
"""Merge multiple headers (mapping or sequence)."""
|
|
115
|
+
|
|
116
|
+
def extensions(self, extensions: ExtensionsType) -> Self:
|
|
117
|
+
"""Set extensions."""
|
|
118
|
+
|
|
119
|
+
def body_bytes(self, body: bytes | bytearray | memoryview) -> Self:
|
|
120
|
+
"""Set fixed byte body (zero-copied where possible)."""
|
|
121
|
+
|
|
122
|
+
def body_text(self, body: str) -> Self:
|
|
123
|
+
"""Set text body (UTF-8 encoded)."""
|
|
124
|
+
|
|
125
|
+
def body_json(self, body: Any) -> Self:
|
|
126
|
+
"""Serialize body to JSON (sets Content-Type). Uses serde for serialization."""
|
|
127
|
+
|
|
128
|
+
def body_stream(self, stream: Stream) -> Self:
|
|
129
|
+
"""Set streaming body. `build_sync` can not be mixed with async streams."""
|
|
130
|
+
|
|
131
|
+
def copy(self) -> Self:
|
|
132
|
+
"""Copy builder."""
|
|
133
|
+
def __copy__(self) -> Self: ...
|
|
134
|
+
|
|
135
|
+
class ResponseBodyReader:
|
|
136
|
+
"""Streaming body reader."""
|
|
137
|
+
|
|
138
|
+
async def bytes(self) -> Bytes:
|
|
139
|
+
"""Read remaining stream fully and return bytes (caches)."""
|
|
140
|
+
|
|
141
|
+
async def read(self, amount: int = ...) -> Bytes | None:
|
|
142
|
+
"""Read up to amount bytes (or default chunk size) from stream. None when stream is exhausted."""
|
|
143
|
+
|
|
144
|
+
async def read_chunk(self) -> Bytes | None:
|
|
145
|
+
"""Return next raw chunk. Sizes are arbitrary and depend on OS. None when stream is exhausted."""
|
|
146
|
+
|
|
147
|
+
class SyncResponseBodyReader:
|
|
148
|
+
"""Streaming body reader."""
|
|
149
|
+
|
|
150
|
+
def bytes(self) -> Bytes:
|
|
151
|
+
"""Read remaining stream fully and return bytes (caches)."""
|
|
152
|
+
|
|
153
|
+
def read(self, amount: int = ...) -> Bytes | None:
|
|
154
|
+
"""Read up to amount bytes (or default chunk size) from stream. None when stream is exhausted."""
|
|
155
|
+
|
|
156
|
+
def read_chunk(self) -> Bytes | None:
|
|
157
|
+
"""Return next raw chunk. Sizes are arbitrary and depend on OS. None when stream is exhausted."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Simple interfaces for doing one-off requests."""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Simple async interfaces for doing one-off requests."""
|
|
2
|
+
|
|
3
|
+
from pyreqwest._pyreqwest.simple.request import (
|
|
4
|
+
pyreqwest_delete,
|
|
5
|
+
pyreqwest_get,
|
|
6
|
+
pyreqwest_head,
|
|
7
|
+
pyreqwest_patch,
|
|
8
|
+
pyreqwest_post,
|
|
9
|
+
pyreqwest_put,
|
|
10
|
+
pyreqwest_request,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"pyreqwest_request",
|
|
15
|
+
"pyreqwest_get",
|
|
16
|
+
"pyreqwest_post",
|
|
17
|
+
"pyreqwest_put",
|
|
18
|
+
"pyreqwest_patch",
|
|
19
|
+
"pyreqwest_delete",
|
|
20
|
+
"pyreqwest_head",
|
|
21
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from pyreqwest.http import Url
|
|
2
|
+
from pyreqwest.request import OneOffRequestBuilder
|
|
3
|
+
|
|
4
|
+
def pyreqwest_request(method: str, url: Url | str) -> OneOffRequestBuilder:
|
|
5
|
+
"""Create a simple request with the given HTTP method and URL.
|
|
6
|
+
|
|
7
|
+
Returns a request builder, which will allow setting headers and the request body before sending.
|
|
8
|
+
|
|
9
|
+
NOTE: This is only recommended for simple scripting use-cases. Usually, the client should be reused for multiple
|
|
10
|
+
requests to benefit from connection pooling and other optimizations (via ClientBuilder).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def pyreqwest_get(url: Url | str) -> OneOffRequestBuilder:
|
|
14
|
+
"""Same as `pyreqwest_request("GET", url)`."""
|
|
15
|
+
|
|
16
|
+
def pyreqwest_post(url: Url | str) -> OneOffRequestBuilder:
|
|
17
|
+
"""Same as `pyreqwest_request("POST", url)`."""
|
|
18
|
+
|
|
19
|
+
def pyreqwest_put(url: Url | str) -> OneOffRequestBuilder:
|
|
20
|
+
"""Same as `pyreqwest_request("PUT", url)`."""
|
|
21
|
+
|
|
22
|
+
def pyreqwest_patch(url: Url | str) -> OneOffRequestBuilder:
|
|
23
|
+
"""Same as `pyreqwest_request("PATCH", url)`."""
|
|
24
|
+
|
|
25
|
+
def pyreqwest_delete(url: Url | str) -> OneOffRequestBuilder:
|
|
26
|
+
"""Same as `pyreqwest_request("DELETE", url)`."""
|
|
27
|
+
|
|
28
|
+
def pyreqwest_head(url: Url | str) -> OneOffRequestBuilder:
|
|
29
|
+
"""Same as `pyreqwest_request("HEAD", url)`."""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Simple sync interfaces for doing one-off requests."""
|
|
2
|
+
|
|
3
|
+
from pyreqwest._pyreqwest.simple.sync_request import (
|
|
4
|
+
pyreqwest_delete,
|
|
5
|
+
pyreqwest_get,
|
|
6
|
+
pyreqwest_head,
|
|
7
|
+
pyreqwest_patch,
|
|
8
|
+
pyreqwest_post,
|
|
9
|
+
pyreqwest_put,
|
|
10
|
+
pyreqwest_request,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"pyreqwest_request",
|
|
15
|
+
"pyreqwest_get",
|
|
16
|
+
"pyreqwest_post",
|
|
17
|
+
"pyreqwest_put",
|
|
18
|
+
"pyreqwest_patch",
|
|
19
|
+
"pyreqwest_delete",
|
|
20
|
+
"pyreqwest_head",
|
|
21
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from pyreqwest.http import Url
|
|
2
|
+
from pyreqwest.request import SyncOneOffRequestBuilder
|
|
3
|
+
|
|
4
|
+
def pyreqwest_request(method: str, url: Url | str) -> SyncOneOffRequestBuilder:
|
|
5
|
+
"""Create a simple request with the given HTTP method and URL.
|
|
6
|
+
|
|
7
|
+
Returns a request builder, which will allow setting headers and the request body before sending.
|
|
8
|
+
|
|
9
|
+
NOTE: This is only recommended for simple scripting use-cases. Usually, the client should be reused for multiple
|
|
10
|
+
requests to benefit from connection pooling and other optimizations (via ClientBuilder).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def pyreqwest_get(url: Url | str) -> SyncOneOffRequestBuilder:
|
|
14
|
+
"""Same as `pyreqwest_request("GET", url)`."""
|
|
15
|
+
|
|
16
|
+
def pyreqwest_post(url: Url | str) -> SyncOneOffRequestBuilder:
|
|
17
|
+
"""Same as `pyreqwest_request("POST", url)`."""
|
|
18
|
+
|
|
19
|
+
def pyreqwest_put(url: Url | str) -> SyncOneOffRequestBuilder:
|
|
20
|
+
"""Same as `pyreqwest_request("PUT", url)`."""
|
|
21
|
+
|
|
22
|
+
def pyreqwest_patch(url: Url | str) -> SyncOneOffRequestBuilder:
|
|
23
|
+
"""Same as `pyreqwest_request("PATCH", url)`."""
|
|
24
|
+
|
|
25
|
+
def pyreqwest_delete(url: Url | str) -> SyncOneOffRequestBuilder:
|
|
26
|
+
"""Same as `pyreqwest_request("DELETE", url)`."""
|
|
27
|
+
|
|
28
|
+
def pyreqwest_head(url: Url | str) -> SyncOneOffRequestBuilder:
|
|
29
|
+
"""Same as `pyreqwest_request("HEAD", url)`."""
|
pyreqwest/types.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Common types and interfaces used in the library."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterable, Iterable, Mapping, Sequence
|
|
4
|
+
from typing import Any, TypeAlias
|
|
5
|
+
|
|
6
|
+
HeadersType: TypeAlias = Mapping[str, str] | Sequence[tuple[str, str]]
|
|
7
|
+
QueryParams: TypeAlias = Mapping[str, Any] | Sequence[tuple[str, Any]]
|
|
8
|
+
FormParams: TypeAlias = Mapping[str, Any] | Sequence[tuple[str, Any]]
|
|
9
|
+
ExtensionsType: TypeAlias = Mapping[str, Any] | Sequence[tuple[str, Any]]
|
|
10
|
+
|
|
11
|
+
SyncStream: TypeAlias = Iterable[bytes] | Iterable[bytearray] | Iterable[memoryview]
|
|
12
|
+
Stream: TypeAlias = AsyncIterable[bytes] | AsyncIterable[bytearray] | AsyncIterable[memoryview] | SyncStream
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyreqwest
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Classifier: Development Status :: 4 - Beta
|
|
5
|
+
Classifier: Programming Language :: Python
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
13
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
14
|
+
Classifier: Programming Language :: Python :: Implementation :: GraalPy
|
|
15
|
+
Classifier: Programming Language :: Rust
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Information Technology
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
20
|
+
Classifier: Operating System :: MacOS
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Summary: Powerful and fast Rust based HTTP client
|
|
24
|
+
Author-email: Markus Sintonen <pyreqwest@gmail.com>
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
27
|
+
Project-URL: Homepage, https://github.com/MarkusSintonen/pyreqwest
|
|
28
|
+
Project-URL: Source, https://github.com/MarkusSintonen/pyreqwest
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<img width="250" alt="logo" src="https://raw.githubusercontent.com/MarkusSintonen/pyreqwest/refs/heads/main/docs/logo.png" />
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
[](https://codecov.io/github/markussintonen/pyreqwest)
|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
pyreqwest - Powerful and fast Rust based HTTP client. Built on top of and inspired by [reqwest](https://github.com/seanmonstar/reqwest).
|
|
39
|
+
|
|
40
|
+
## Why
|
|
41
|
+
|
|
42
|
+
- No reinvention of the wheel - built on top of widely used reqwest and other Rust HTTP crates
|
|
43
|
+
- Secure and fast - no C-extension code, no Python code/dependencies, no `unsafe` code
|
|
44
|
+
- Ergonomic and easy to use - similar API as in reqwest, fully type-annotated
|
|
45
|
+
- Testing ergonomics - mocking included, can also connect into ASGI apps
|
|
46
|
+
|
|
47
|
+
Using this is a good choice when:
|
|
48
|
+
|
|
49
|
+
- You care about throughput and latency, especially in high concurrency scenarios
|
|
50
|
+
- You want a single solution to serve all your HTTP client needs
|
|
51
|
+
|
|
52
|
+
This is not a good choice when:
|
|
53
|
+
|
|
54
|
+
- You want a pure Python solution allowing debugging of the HTTP client internals
|
|
55
|
+
- You use alternative Python implementations or Python version older than 3.11
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- High performance, see [notes](https://github.com/MarkusSintonen/pyreqwest/blob/main/docs/performance.md) and [benchmarks](https://github.com/MarkusSintonen/pyreqwest/blob/main/docs/benchmarks.md)
|
|
60
|
+
- Asynchronous and synchronous HTTP clients
|
|
61
|
+
- Customizable via middlewares and custom JSON serializers
|
|
62
|
+
- Ergonomic as `reqwest`
|
|
63
|
+
- HTTP/1.1 and HTTP/2 support (also HTTP/3 when it [stabilizes](https://docs.rs/reqwest/latest/reqwest/#unstable-features))
|
|
64
|
+
- Mocking and testing utilities (can also connect to ASGI apps)
|
|
65
|
+
- Fully type-safe with Python type hints
|
|
66
|
+
- Full test coverage
|
|
67
|
+
- Free threading, see [notes](https://github.com/MarkusSintonen/pyreqwest/blob/main/docs/performance.md#python-313-free-threading)
|
|
68
|
+
|
|
69
|
+
### Standard HTTP features you would expect
|
|
70
|
+
|
|
71
|
+
- HTTPS support (using [rustls](https://github.com/rustls/rustls))
|
|
72
|
+
- Request and response body streaming
|
|
73
|
+
- Connection pooling
|
|
74
|
+
- JSON, URLs, Headers, Cookies etc. (all serializers in Rust)
|
|
75
|
+
- Automatic decompression (zstd, gzip, brotli, deflate)
|
|
76
|
+
- Automatic response decoding (charset detection)
|
|
77
|
+
- Multipart form support
|
|
78
|
+
- Proxy support
|
|
79
|
+
- Redirects
|
|
80
|
+
- Timeouts
|
|
81
|
+
- Authentication (Basic, Bearer)
|
|
82
|
+
- Cookie management
|
|
83
|
+
|
|
84
|
+
## Quickstart
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# uv add pyreqwest
|
|
88
|
+
|
|
89
|
+
from pyreqwest.client import ClientBuilder, SyncClientBuilder
|
|
90
|
+
|
|
91
|
+
async def example_async():
|
|
92
|
+
async with ClientBuilder().error_for_status(True).build() as client:
|
|
93
|
+
response = await client.get("https://httpbun.com/get").query({"q": "val"}).build().send()
|
|
94
|
+
print(await response.json())
|
|
95
|
+
|
|
96
|
+
def example_sync():
|
|
97
|
+
with SyncClientBuilder().error_for_status(True).build() as client:
|
|
98
|
+
print(client.get("https://httpbun.com/get").query({"q": "val"}).build().send().json())
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Context manager usage is optional, but recommended. Also `close()` methods are available.
|
|
102
|
+
|
|
103
|
+
#### Mocking in pytest
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from pyreqwest.client import ClientBuilder
|
|
107
|
+
from pyreqwest.pytest_plugin import ClientMocker
|
|
108
|
+
|
|
109
|
+
async def test_client(client_mocker: ClientMocker) -> None:
|
|
110
|
+
client_mocker.get(path="/api").with_body_text("Hello Mock")
|
|
111
|
+
|
|
112
|
+
async with ClientBuilder().build() as client:
|
|
113
|
+
response = await client.get("http://example.invalid/api").build().send()
|
|
114
|
+
assert response.status == 200 and await response.text() == "Hello Mock"
|
|
115
|
+
assert client_mocker.get_call_count() == 1
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Manual mocking is available via `ClientMocker.create_mocker(MonkeyPatch)`.
|
|
119
|
+
|
|
120
|
+
#### Simple request interface
|
|
121
|
+
|
|
122
|
+
This is only recommended for simple use-cases such as scripts. Usually, the full client API should be used which reuses
|
|
123
|
+
connections and has other optimizations.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
# Sync example
|
|
127
|
+
from pyreqwest.simple.sync_request import pyreqwest_get
|
|
128
|
+
response = pyreqwest_get("https://httpbun.com/get").query({"q": "val"}).send()
|
|
129
|
+
print(response.json())
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
# Async example
|
|
134
|
+
from pyreqwest.simple.request import pyreqwest_get
|
|
135
|
+
response = await pyreqwest_get("https://httpbun.com/get").query({"q": "val"}).send()
|
|
136
|
+
print(await response.json())
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Documentation
|
|
140
|
+
|
|
141
|
+
See [docs](https://markussintonen.github.io/pyreqwest/pyreqwest.html)
|
|
142
|
+
|
|
143
|
+
See [examples](https://github.com/MarkusSintonen/pyreqwest/tree/main/examples)
|
|
144
|
+
|
|
145
|
+
## Compatibility with other libraries
|
|
146
|
+
|
|
147
|
+
See [compatibility docs](https://markussintonen.github.io/pyreqwest/pyreqwest/compatibility.html)
|
|
148
|
+
|