pyreqwest 0.6.0__cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.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.
Files changed (45) hide show
  1. pyreqwest/__init__.py +3 -0
  2. pyreqwest/__init__.pyi +1 -0
  3. pyreqwest/_pyreqwest.cpython-313-arm-linux-gnueabihf.so +0 -0
  4. pyreqwest/bytes/__init__.py +16 -0
  5. pyreqwest/bytes/__init__.pyi +106 -0
  6. pyreqwest/client/__init__.py +21 -0
  7. pyreqwest/client/__init__.pyi +341 -0
  8. pyreqwest/client/types.py +54 -0
  9. pyreqwest/compatibility/__init__.py +4 -0
  10. pyreqwest/compatibility/httpx/__init__.py +11 -0
  11. pyreqwest/compatibility/httpx/_internal.py +60 -0
  12. pyreqwest/compatibility/httpx/transport.py +154 -0
  13. pyreqwest/cookie/__init__.py +5 -0
  14. pyreqwest/cookie/__init__.pyi +174 -0
  15. pyreqwest/exceptions/__init__.py +193 -0
  16. pyreqwest/http/__init__.py +19 -0
  17. pyreqwest/http/__init__.pyi +344 -0
  18. pyreqwest/middleware/__init__.py +5 -0
  19. pyreqwest/middleware/__init__.pyi +12 -0
  20. pyreqwest/middleware/asgi/__init__.py +5 -0
  21. pyreqwest/middleware/asgi/asgi.py +168 -0
  22. pyreqwest/middleware/types.py +26 -0
  23. pyreqwest/multipart/__init__.py +5 -0
  24. pyreqwest/multipart/__init__.pyi +75 -0
  25. pyreqwest/proxy/__init__.py +5 -0
  26. pyreqwest/proxy/__init__.pyi +47 -0
  27. pyreqwest/py.typed +0 -0
  28. pyreqwest/pytest_plugin/__init__.py +8 -0
  29. pyreqwest/pytest_plugin/internal/__init__.py +0 -0
  30. pyreqwest/pytest_plugin/internal/assert_eq.py +6 -0
  31. pyreqwest/pytest_plugin/internal/assert_message.py +123 -0
  32. pyreqwest/pytest_plugin/internal/matcher.py +34 -0
  33. pyreqwest/pytest_plugin/internal/plugin.py +15 -0
  34. pyreqwest/pytest_plugin/mock.py +493 -0
  35. pyreqwest/pytest_plugin/types.py +26 -0
  36. pyreqwest/request/__init__.py +25 -0
  37. pyreqwest/request/__init__.pyi +200 -0
  38. pyreqwest/response/__init__.py +19 -0
  39. pyreqwest/response/__init__.pyi +157 -0
  40. pyreqwest/types.py +12 -0
  41. pyreqwest-0.6.0.dist-info/METADATA +112 -0
  42. pyreqwest-0.6.0.dist-info/RECORD +45 -0
  43. pyreqwest-0.6.0.dist-info/WHEEL +5 -0
  44. pyreqwest-0.6.0.dist-info/entry_points.txt +2 -0
  45. pyreqwest-0.6.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,154 @@
1
+ """Compatibility layer for httpx, which allows pyreqwest to replace httpcore transport for httpx."""
2
+
3
+ from collections.abc import AsyncIterator, Iterator
4
+ from contextlib import AsyncExitStack, ExitStack
5
+
6
+ import httpx
7
+
8
+ from pyreqwest.client import Client, ClientBuilder, SyncClient, SyncClientBuilder
9
+ from pyreqwest.exceptions import PyreqwestError
10
+ from pyreqwest.request import RequestBuilder, SyncRequestBuilder
11
+ from pyreqwest.response import Response, SyncResponse
12
+
13
+ from ._internal import build_httpx_response, map_exception, map_extensions
14
+
15
+
16
+ class HttpxTransport(httpx.AsyncBaseTransport):
17
+ """httpx transport that uses pyreqwest for HTTP requests.
18
+
19
+ Example usage:
20
+ ```python
21
+ import httpx
22
+ from pyreqwest.compatibility.httpx import HttpxTransport
23
+
24
+ async with httpx.AsyncClient(transport=HttpxTransport()) as httpx_client:
25
+ print(await httpx_client.get("https://example.com"))
26
+ ```
27
+ """
28
+
29
+ def __init__(self, client: Client | None = None, *, close_client: bool = True) -> None:
30
+ """Initialize the HttpxTransport.
31
+ :param client: An optional pyreqwest Client instance. If not provided, a default Client will be created.
32
+ :param close_client: Whether to close the provided Client when the transport is closed.
33
+ """
34
+ self._client: Client = client or ClientBuilder().build()
35
+ self._close_client = (client is None) or close_client
36
+
37
+ async def aclose(self) -> None:
38
+ """Close the underlying pyreqwest Client if needed."""
39
+ if self._close_client:
40
+ await self._client.close()
41
+
42
+ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
43
+ """AsyncBaseTransport implementation to handle httpx request using pyreqwest."""
44
+ req_builder = self._client.request(request.method, str(request.url)).headers(request.headers.multi_items())
45
+ req_builder = self._map_body(req_builder, request)
46
+ req_builder = map_extensions(req_builder, request)
47
+ return await self._map_response(req_builder, request)
48
+
49
+ def _map_body(self, builder: RequestBuilder, request: httpx.Request) -> RequestBuilder:
50
+ try:
51
+ return builder.body_bytes(request.content)
52
+ except httpx.RequestNotRead:
53
+ return builder.body_stream(request.stream)
54
+
55
+ async def _map_response(self, builder: RequestBuilder, request: httpx.Request) -> httpx.Response:
56
+ req_exit_stack = AsyncExitStack()
57
+ try:
58
+ response = await req_exit_stack.enter_async_context(builder.build_streamed())
59
+ return build_httpx_response(response, response_stream=ResponseStream(response, req_exit_stack))
60
+ except Exception as exc:
61
+ await req_exit_stack.aclose()
62
+ if isinstance(exc, PyreqwestError) and (mapped := map_exception(exc, request)):
63
+ raise mapped from exc
64
+ raise
65
+
66
+
67
+ class SyncHttpxTransport(httpx.BaseTransport):
68
+ """httpx transport that uses pyreqwest for HTTP requests.
69
+
70
+ Example usage:
71
+ ```python
72
+ import httpx
73
+ from pyreqwest.compatibility.httpx import SyncHttpxTransport
74
+
75
+ with httpx.Client(transport=SyncHttpxTransport()) as httpx_client:
76
+ print(httpx_client.get("https://example.com"))
77
+ ```
78
+ """
79
+
80
+ def __init__(self, client: SyncClient | None = None, *, close_client: bool = True) -> None:
81
+ """Initialize the SyncHttpxTransport.
82
+ :param client: An optional pyreqwest SyncClient instance. If not provided, a default SyncClient will be created.
83
+ :param close_client: Whether to close the provided SyncClient when the transport is closed.
84
+ """
85
+ self._client: SyncClient = client or SyncClientBuilder().build()
86
+ self._close_client = (client is None) or close_client
87
+
88
+ def close(self) -> None:
89
+ """Close the underlying pyreqwest SyncClient if needed."""
90
+ if self._close_client:
91
+ self._client.close()
92
+
93
+ def handle_request(self, request: httpx.Request) -> httpx.Response:
94
+ """BaseTransport implementation to handle httpx request using pyreqwest."""
95
+ req_builder = self._client.request(request.method, str(request.url)).headers(request.headers.multi_items())
96
+ req_builder = self._map_body(req_builder, request)
97
+ req_builder = map_extensions(req_builder, request)
98
+ return self._map_response(req_builder, request)
99
+
100
+ def _map_body(self, builder: SyncRequestBuilder, request: httpx.Request) -> SyncRequestBuilder:
101
+ try:
102
+ return builder.body_bytes(request.content)
103
+ except httpx.RequestNotRead:
104
+ if isinstance(request.stream, httpx.AsyncByteStream):
105
+ err = "Cannot use async stream in sync transport"
106
+ raise TypeError(err) from None
107
+ return builder.body_stream(request.stream)
108
+
109
+ def _map_response(self, builder: SyncRequestBuilder, request: httpx.Request) -> httpx.Response:
110
+ req_exit_stack = ExitStack()
111
+ try:
112
+ response = req_exit_stack.enter_context(builder.build_streamed())
113
+ return build_httpx_response(response, response_stream=SyncResponseStream(response, req_exit_stack))
114
+ except Exception as exc:
115
+ req_exit_stack.close()
116
+ if isinstance(exc, PyreqwestError) and (mapped := map_exception(exc, request)):
117
+ raise mapped from exc
118
+ raise
119
+
120
+
121
+ class ResponseStream(httpx.AsyncByteStream):
122
+ """httpx AsyncByteStream that wraps a pyreqwest Response body reader."""
123
+
124
+ def __init__(self, response: Response, exit_stack: AsyncExitStack) -> None:
125
+ """Internally initialized."""
126
+ self._body_reader = response.body_reader
127
+ self._exit_stack = exit_stack
128
+
129
+ async def __aiter__(self) -> AsyncIterator[bytes]:
130
+ """Asynchronously iterate over the response body in chunks."""
131
+ while (chunk := await self._body_reader.read()) is not None:
132
+ yield bytes(chunk)
133
+
134
+ async def aclose(self) -> None:
135
+ """Close the response stream."""
136
+ await self._exit_stack.aclose()
137
+
138
+
139
+ class SyncResponseStream(httpx.SyncByteStream):
140
+ """httpx SyncByteStream that wraps a pyreqwest SyncResponse body reader."""
141
+
142
+ def __init__(self, response: SyncResponse, exit_stack: ExitStack) -> None:
143
+ """Internally initialized."""
144
+ self._body_reader = response.body_reader
145
+ self._exit_stack = exit_stack
146
+
147
+ def __iter__(self) -> Iterator[bytes]:
148
+ """Iterate over the response body in chunks."""
149
+ while (chunk := self._body_reader.read()) is not None:
150
+ yield bytes(chunk)
151
+
152
+ def close(self) -> None:
153
+ """Close the response stream."""
154
+ self._exit_stack.close()
@@ -0,0 +1,5 @@
1
+ """Cookie related classes."""
2
+
3
+ from pyreqwest._pyreqwest.cookie import Cookie, CookieStore
4
+
5
+ __all__ = ["Cookie", "CookieStore"]
@@ -0,0 +1,174 @@
1
+ """HTTP cookie types backed by Rust's cookie and cookie_store crates."""
2
+
3
+ from collections.abc import Sequence
4
+ from datetime import datetime, timedelta
5
+ from typing import Any, Literal, Self, TypeAlias, overload
6
+
7
+ from pyreqwest.http import Url
8
+
9
+ SameSite: TypeAlias = Literal["Strict", "Lax", "None"]
10
+
11
+ class Cookie:
12
+ """An immutable HTTP cookie. Lightweight Python wrapper around the internal Rust cookie::Cookie type.
13
+ Use `with_*` methods to create modified copies of a Cookie.
14
+
15
+ See also Rust [docs](https://docs.rs/cookie/latest/cookie/struct.Cookie.html) for more details.
16
+ """
17
+
18
+ def __init__(self, name: str, value: str) -> None:
19
+ """Create a cookie with the given name and value (no attributes)."""
20
+
21
+ @staticmethod
22
+ def parse(cookie: str) -> "Cookie":
23
+ """Parses a Cookie from the given HTTP cookie header value string."""
24
+
25
+ @staticmethod
26
+ def parse_encoded(cookie: str) -> "Cookie":
27
+ """Like parse, but does percent-decoding of keys and values."""
28
+
29
+ @staticmethod
30
+ def split_parse(cookie: str) -> list["Cookie"]:
31
+ """Parses the HTTP Cookie header, a series of cookie names and value separated by `;`."""
32
+
33
+ @staticmethod
34
+ def split_parse_encoded(cookie: str) -> list["Cookie"]:
35
+ """Like split_parse, but does percent-decoding of keys and values."""
36
+
37
+ @property
38
+ def name(self) -> str:
39
+ """Cookie name."""
40
+
41
+ @property
42
+ def value(self) -> str:
43
+ """Raw cookie value as set (may contain surrounding whitespace)."""
44
+
45
+ @property
46
+ def value_trimmed(self) -> str:
47
+ """Value with surrounding whitespace trimmed."""
48
+
49
+ @property
50
+ def http_only(self) -> bool:
51
+ """Whether the HttpOnly attribute is set."""
52
+
53
+ @property
54
+ def secure(self) -> bool:
55
+ """Whether the Secure attribute is set."""
56
+
57
+ @property
58
+ def same_site(self) -> SameSite | None:
59
+ """SameSite attribute, or None if unspecified."""
60
+
61
+ @property
62
+ def partitioned(self) -> bool:
63
+ """Whether the Partitioned attribute is set."""
64
+
65
+ @property
66
+ def max_age(self) -> timedelta | None:
67
+ """Max-Age attribute duration, or None if not present."""
68
+
69
+ @property
70
+ def path(self) -> str | None:
71
+ """Path attribute that scopes the cookie, or None if not present."""
72
+
73
+ @property
74
+ def domain(self) -> str | None:
75
+ """Domain attribute that scopes the cookie, or None if not present."""
76
+
77
+ @property
78
+ def expires_datetime(self) -> datetime | None:
79
+ """Absolute expiration time (Expires), or None if not present."""
80
+
81
+ def encode(self) -> str:
82
+ """Returns cookie string with percent-encoding applied."""
83
+
84
+ def stripped(self) -> str:
85
+ """Return just the 'name=value' pair."""
86
+
87
+ def with_name(self, name: str) -> Self:
88
+ """Set name, returning a new Cookie."""
89
+
90
+ def with_value(self, value: str) -> Self:
91
+ """Set value, returning a new Cookie."""
92
+
93
+ def with_http_only(self, http_only: bool) -> Self:
94
+ """Set HttpOnly attribute, returning a new Cookie."""
95
+
96
+ def with_secure(self, secure: bool) -> Self:
97
+ """Set Secure attribute, returning a new Cookie."""
98
+
99
+ def with_same_site(self, same_site: SameSite | None) -> Self:
100
+ """Set SameSite attribute, returning a new Cookie."""
101
+
102
+ def with_partitioned(self, partitioned: bool) -> Self:
103
+ """Set Partitioned attribute, returning a new Cookie."""
104
+
105
+ def with_max_age(self, max_age: timedelta | None) -> Self:
106
+ """Set Max-Age attribute, returning a new Cookie."""
107
+
108
+ def with_path(self, path: str | None) -> Self:
109
+ """Set Path attribute, returning a new Cookie."""
110
+
111
+ def with_domain(self, domain: str | None) -> Self:
112
+ """Set Domain attribute, returning a new Cookie."""
113
+
114
+ def with_expires_datetime(self, expires: datetime | None) -> Self:
115
+ """Set Expires attribute, returning a new Cookie."""
116
+
117
+ def __contains__(self, item: Any) -> bool: ...
118
+ def __copy__(self) -> Self: ...
119
+ def __hash__(self) -> int: ...
120
+ @overload
121
+ def __getitem__(self, index: int) -> str: ...
122
+ @overload
123
+ def __getitem__(self, index: slice) -> Sequence[str]: ...
124
+ def __len__(self) -> int: ...
125
+ def __eq__(self, other: object) -> bool: ...
126
+ def __lt__(self, other: object) -> bool: ...
127
+ def __le__(self, other: object) -> bool: ...
128
+
129
+ class CookieStore:
130
+ """Thread-safe in-memory cookie store (domain/path aware). Mirrors the behavior of Rust's cookie_store.
131
+
132
+ See also Rust [docs](https://docs.rs/cookie_store/latest/cookie_store/struct.CookieStore.html) for more details.
133
+ """
134
+
135
+ def __init__(self) -> None:
136
+ """Create an empty cookie store."""
137
+
138
+ def contains(self, domain: str, path: str, name: str) -> bool:
139
+ """Returns true if the CookieStore contains an unexpired Cookie corresponding to the specified domain, path,
140
+ and name.
141
+ """
142
+
143
+ def contains_any(self, domain: str, path: str, name: str) -> bool:
144
+ """Returns true if the CookieStore contains any (even an expired) Cookie corresponding to the specified
145
+ domain, path, and name.
146
+ """
147
+
148
+ def get(self, domain: str, path: str, name: str) -> Cookie | None:
149
+ """Returns a reference to the unexpired Cookie corresponding to the specified domain, path, and name."""
150
+
151
+ def get_any(self, domain: str, path: str, name: str) -> Cookie | None:
152
+ """Returns a reference to the (possibly expired) Cookie corresponding to the specified domain, path, and
153
+ name.
154
+ """
155
+
156
+ def remove(self, domain: str, path: str, name: str) -> Cookie | None:
157
+ """Removes a Cookie from the store, returning the Cookie if it was in the store."""
158
+
159
+ def matches(self, url: Url | str) -> list[Cookie]:
160
+ """Returns a collection of references to unexpired cookies that path- and domain-match request_url, as well as
161
+ having HttpOnly and Secure attributes compatible with the request_url.
162
+ """
163
+
164
+ def insert(self, cookie: Cookie | str, request_url: Url | str) -> None:
165
+ """Insert a cookie as if set by a response for request_url."""
166
+
167
+ def clear(self) -> None:
168
+ """Remove all cookies from the store."""
169
+
170
+ def get_all_unexpired(self) -> list[Cookie]:
171
+ """Return all unexpired cookies currently stored."""
172
+
173
+ def get_all_any(self) -> list[Cookie]:
174
+ """Return all cookies in the store, including expired ones."""
@@ -0,0 +1,193 @@
1
+ """Exception classes."""
2
+
3
+ from json import JSONDecodeError as JSONDecodeError_
4
+ from typing import Any, Generic, TypedDict, TypeVar
5
+
6
+
7
+ class Cause(TypedDict):
8
+ """A cause of an error."""
9
+
10
+ message: str
11
+
12
+
13
+ class CauseErrorDetails(TypedDict):
14
+ """Details for errors that may have causes."""
15
+
16
+ causes: list[Cause] | None
17
+
18
+
19
+ class StatusErrorDetails(TypedDict):
20
+ """Details for errors that have an associated HTTP status code."""
21
+
22
+ status: int
23
+
24
+
25
+ T = TypeVar("T", bound=CauseErrorDetails | StatusErrorDetails)
26
+
27
+
28
+ class PyreqwestError(Exception):
29
+ """Base class for all pyreqwest errors."""
30
+
31
+ def __init__(self, message: str, *args: Any) -> None:
32
+ """Internally initialized."""
33
+ assert isinstance(message, str)
34
+ Exception.__init__(self, message, *args)
35
+ self.message = message
36
+
37
+
38
+ class DetailedPyreqwestError(PyreqwestError, Generic[T]):
39
+ """Base class for all pyreqwest errors with details.
40
+
41
+ Details may be available in `details`.
42
+ """
43
+
44
+ def __init__(self, message: str, details: T) -> None:
45
+ """Internally initialized."""
46
+ assert isinstance(details, dict)
47
+ PyreqwestError.__init__(self, message, details)
48
+ self.details = details
49
+
50
+
51
+ class RequestError(DetailedPyreqwestError[T], Generic[T]):
52
+ """Error while processing a request.
53
+
54
+ Details may be available in `details`.
55
+ """
56
+
57
+
58
+ class StatusError(RequestError[StatusErrorDetails]):
59
+ """Error due to HTTP 4xx or 5xx status code. Raised when `error_for_status` is enabled.
60
+
61
+ The status code is available in `details["status"]`.
62
+ """
63
+
64
+
65
+ class RedirectError(RequestError[CauseErrorDetails]):
66
+ """Error due to too many redirects. Raised when `max_redirects` is exceeded.
67
+
68
+ Cause details may be available in `details["causes"]`.
69
+ """
70
+
71
+
72
+ class DecodeError(RequestError[CauseErrorDetails]):
73
+ """Error while decoding the response.
74
+
75
+ Cause details may be available in `details["causes"]`.
76
+ """
77
+
78
+
79
+ class BodyDecodeError(DecodeError):
80
+ """Error while decoding the request or response body.
81
+
82
+ Cause details may be available in `details["causes"]`.
83
+ """
84
+
85
+
86
+ class JSONDecodeError(BodyDecodeError, JSONDecodeError_):
87
+ """Error while decoding the response body as JSON.
88
+
89
+ This corresponds to Python's built-in `json.JSONDecodeError`. With the difference that `pos` and `colno` are byte
90
+ offsets instead of UTF8 char offsets. This difference is for efficient error handling (avoiding UTF8 conversions).
91
+ """
92
+
93
+ def __init__(self, message: str, details: dict[str, Any]) -> None:
94
+ """Internally initialized."""
95
+ assert isinstance(details, dict)
96
+ assert isinstance(details["doc"], str) and isinstance(details["pos"], int)
97
+ JSONDecodeError_.__init__(self, message, details["doc"], details["pos"])
98
+ BodyDecodeError.__init__(self, message, {"causes": details["causes"]})
99
+
100
+
101
+ class TransportError(RequestError[CauseErrorDetails]):
102
+ """Error while processing the transport layer.
103
+
104
+ Cause details may be available in `details["causes"]`.
105
+ """
106
+
107
+
108
+ class RequestTimeoutError(TransportError, TimeoutError):
109
+ """Error due to a timeout.
110
+
111
+ This indicates that the timeout configured for the request was reached.
112
+ Cause details may be available in `details["causes"]`.
113
+ """
114
+
115
+
116
+ class NetworkError(TransportError):
117
+ """Error due to a network failure.
118
+
119
+ This indicates that the request could not be completed due to a network failure.
120
+ Cause details may be available in `details["causes"]`.
121
+ """
122
+
123
+
124
+ class ConnectTimeoutError(RequestTimeoutError):
125
+ """Timeout while connecting.
126
+
127
+ Cause details may be available in `details["causes"]`.
128
+ """
129
+
130
+
131
+ class ReadTimeoutError(RequestTimeoutError):
132
+ """Timeout while reading body.
133
+
134
+ Cause details may be available in `details["causes"]`.
135
+ """
136
+
137
+
138
+ class WriteTimeoutError(RequestTimeoutError):
139
+ """Timeout while sending body.
140
+
141
+ Cause details may be available in `details["causes"]`.
142
+ """
143
+
144
+
145
+ class PoolTimeoutError(RequestTimeoutError):
146
+ """Timeout while acquiring a connection from the pool.
147
+
148
+ Cause details may be available in `details["causes"]`.
149
+ """
150
+
151
+
152
+ class ConnectError(NetworkError):
153
+ """Network error while connecting.
154
+
155
+ Cause details may be available in `details["causes"]`.
156
+ """
157
+
158
+
159
+ class ReadError(NetworkError):
160
+ """Network error while reading body.
161
+
162
+ Cause details may be available in `details["causes"]`.
163
+ """
164
+
165
+
166
+ class WriteError(NetworkError):
167
+ """Network error while sending body.
168
+
169
+ Cause details may be available in `details["causes"]`.
170
+ """
171
+
172
+
173
+ class ClientClosedError(RequestError[CauseErrorDetails]):
174
+ """Error due to user closing the client while request was being processed.
175
+
176
+ Cause details may be available in `details["causes"]`.
177
+ """
178
+
179
+
180
+ class BuilderError(DetailedPyreqwestError[CauseErrorDetails], ValueError):
181
+ """Error while building a request.
182
+
183
+ Cause details may be available in `details["causes"]`.
184
+ """
185
+
186
+
187
+ class RequestPanicError(RequestError[CauseErrorDetails]):
188
+ """Error due to a panic in the request processing.
189
+
190
+ This indicates a bug in pyreqwest or one of its dependencies.
191
+ Also, might be raised due to incorrect ProxyBuilder.custom implementation (limitation in reqwest error handling).
192
+ Cause details may be available in `details["causes"]`.
193
+ """
@@ -0,0 +1,19 @@
1
+ """HTTP utils classes and types."""
2
+
3
+ from pyreqwest._pyreqwest.http import (
4
+ HeaderMap,
5
+ HeaderMapItemsView,
6
+ HeaderMapKeysView,
7
+ HeaderMapValuesView,
8
+ Mime,
9
+ Url,
10
+ )
11
+
12
+ __all__ = [ # noqa: RUF022
13
+ "Url",
14
+ "HeaderMap",
15
+ "Mime",
16
+ "HeaderMapItemsView",
17
+ "HeaderMapKeysView",
18
+ "HeaderMapValuesView",
19
+ ]