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,47 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Self
|
|
3
|
+
|
|
4
|
+
from pyreqwest.http import Url
|
|
5
|
+
from pyreqwest.types import HeadersType
|
|
6
|
+
|
|
7
|
+
class ProxyBuilder:
|
|
8
|
+
"""Configuration of a proxy that a Client should pass requests to.
|
|
9
|
+
|
|
10
|
+
The resulting instance is passed to `ClientBuilder.proxy(...)`.
|
|
11
|
+
Based on reqwest's `Proxy` type.
|
|
12
|
+
See also Rust [docs](https://docs.rs/reqwest/latest/reqwest/struct.Proxy.html) for more details.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def http(url: Url | str) -> "ProxyBuilder":
|
|
17
|
+
"""Proxy all HTTP traffic to the passed URL."""
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def https(url: Url | str) -> "ProxyBuilder":
|
|
21
|
+
"""Proxy all HTTPS traffic to the passed URL."""
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def all(url: Url | str) -> "ProxyBuilder":
|
|
25
|
+
"""Proxy all traffic to the passed URL.
|
|
26
|
+
|
|
27
|
+
"All" refers to https and http URLs. Other schemes are not recognized.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def custom(fun: Callable[[Url], Url | str | None]) -> "ProxyBuilder":
|
|
32
|
+
"""Provide a custom function to determine what traffic to proxy to where.
|
|
33
|
+
|
|
34
|
+
Any exception raised or an invalid/relative return value surfaces as a `RequestPanicError`.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def basic_auth(self, username: str, password: str) -> Self:
|
|
38
|
+
"""Set the Proxy-Authorization header using Basic auth."""
|
|
39
|
+
|
|
40
|
+
def custom_http_auth(self, header_value: str) -> Self:
|
|
41
|
+
"""Set the Proxy-Authorization header to a specified value."""
|
|
42
|
+
|
|
43
|
+
def headers(self, headers: HeadersType) -> Self:
|
|
44
|
+
"""Add custom headers."""
|
|
45
|
+
|
|
46
|
+
def no_proxy(self, no_proxy_list: str | None) -> Self:
|
|
47
|
+
"""Adds a No Proxy exclusion list to this proxy."""
|
pyreqwest/py.typed
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Literal, assert_never
|
|
3
|
+
|
|
4
|
+
from pyreqwest.pytest_plugin import Mock
|
|
5
|
+
from pyreqwest.pytest_plugin.internal.assert_eq import assert_eq
|
|
6
|
+
from pyreqwest.pytest_plugin.internal.matcher import InternalMatcher
|
|
7
|
+
from pyreqwest.request import Request
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def assert_fail(
|
|
11
|
+
mock: Mock,
|
|
12
|
+
*,
|
|
13
|
+
count: int | None = None,
|
|
14
|
+
min_count: int | None = None,
|
|
15
|
+
max_count: int | None = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
msg = _format_counts_assert_message(mock, count, min_count, max_count)
|
|
18
|
+
|
|
19
|
+
if mock._unmatched_requests_repr_parts:
|
|
20
|
+
not_matched = {*mock._unmatched_requests_repr_parts[-1].keys()}
|
|
21
|
+
assert not_matched
|
|
22
|
+
|
|
23
|
+
msg = f"{msg}. Diff with last unmatched request:"
|
|
24
|
+
assert_eq(mock._unmatched_requests_repr_parts[-1], _format_mock_matchers_parts(mock, not_matched), msg)
|
|
25
|
+
else:
|
|
26
|
+
raise AssertionError(msg)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _format_counts_assert_message(
|
|
30
|
+
mock: Mock,
|
|
31
|
+
count: int | None = None,
|
|
32
|
+
min_count: int | None = None,
|
|
33
|
+
max_count: int | None = None,
|
|
34
|
+
) -> str:
|
|
35
|
+
if count is not None:
|
|
36
|
+
expected_desc = f"exactly {count}"
|
|
37
|
+
else:
|
|
38
|
+
expectations = []
|
|
39
|
+
if min_count is not None:
|
|
40
|
+
expectations.append(f"at least {min_count}")
|
|
41
|
+
if max_count is not None:
|
|
42
|
+
expectations.append(f"at most {max_count}")
|
|
43
|
+
expected_desc = " and ".join(expectations)
|
|
44
|
+
|
|
45
|
+
method_path = " ".join(
|
|
46
|
+
[
|
|
47
|
+
mock._method_matcher.matcher_repr if mock._method_matcher is not None else "*",
|
|
48
|
+
mock._path_matcher.matcher_repr if mock._path_matcher is not None else "*",
|
|
49
|
+
],
|
|
50
|
+
)
|
|
51
|
+
return f'Expected {expected_desc} request(s) but received {len(mock._matched_requests)} to: "{method_path}"'
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def format_unmatched_request_parts(request: Request, unmatched: set[str]) -> dict[str, str | None]:
|
|
55
|
+
req_parts: dict[str, str | None] = {
|
|
56
|
+
"method": request.method,
|
|
57
|
+
"url": str(request.url),
|
|
58
|
+
"path": request.url.path,
|
|
59
|
+
"query": None,
|
|
60
|
+
"headers": None,
|
|
61
|
+
"body": None,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if request.url.query_pairs:
|
|
65
|
+
query_parts = [f"{k}={v}" for k, v in request.url.query_pairs]
|
|
66
|
+
req_parts["query"] = ", ".join(query_parts)
|
|
67
|
+
|
|
68
|
+
if request.headers:
|
|
69
|
+
header_parts = [f"{name.title()}: {value}" for name, value in request.headers.items()]
|
|
70
|
+
req_parts["headers"] = ", ".join(header_parts)
|
|
71
|
+
|
|
72
|
+
if request.body:
|
|
73
|
+
if (bytes_body := request.body.copy_bytes()) is not None:
|
|
74
|
+
req_parts["body"] = bytes_body.to_bytes().decode("utf8", errors="replace")
|
|
75
|
+
elif (stream_body := request.body.get_stream()) is not None:
|
|
76
|
+
req_parts["body"] = repr(stream_body)
|
|
77
|
+
else:
|
|
78
|
+
req_parts["body"] = repr(request.body)
|
|
79
|
+
|
|
80
|
+
fmt_parts: dict[str, str | None] = {
|
|
81
|
+
"custom": f"No match with request {req_parts}",
|
|
82
|
+
"handler": f"No match with request {req_parts}",
|
|
83
|
+
}
|
|
84
|
+
fmt_parts = {**req_parts, **fmt_parts}
|
|
85
|
+
|
|
86
|
+
return {k: v for k, v in fmt_parts.items() if k in unmatched}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _format_mock_matchers_parts(mock: Mock, unmatched: set[str] | None) -> dict[str, str | None]:
|
|
90
|
+
parts: dict[str, str | None] = {
|
|
91
|
+
"method": mock._method_matcher.matcher_repr if mock._method_matcher is not None else None,
|
|
92
|
+
"path": mock._path_matcher.matcher_repr if mock._path_matcher is not None else None,
|
|
93
|
+
"query": _format_query_matcher(mock._query_matcher) if mock._query_matcher is not None else None,
|
|
94
|
+
"headers": _format_header_matchers(mock._header_matchers) if mock._header_matchers is not None else None,
|
|
95
|
+
"body": _format_body_matcher(*mock._body_matcher) if mock._body_matcher is not None else None,
|
|
96
|
+
"custom": f"Custom matcher: {mock._custom_matcher.__name__}" if mock._custom_matcher is not None else None,
|
|
97
|
+
"handler": f"Custom handler: {mock._custom_handler.__name__}" if mock._custom_handler is not None else None,
|
|
98
|
+
}
|
|
99
|
+
return {k: v for k, v in parts.items() if unmatched is None or k in unmatched}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _format_query_matcher(query_matcher: dict[str, InternalMatcher] | InternalMatcher) -> str:
|
|
103
|
+
if isinstance(query_matcher, dict):
|
|
104
|
+
query_parts = [f"{k}={v.matcher_repr}" for k, v in query_matcher.items()]
|
|
105
|
+
return ", ".join(query_parts)
|
|
106
|
+
return query_matcher.matcher_repr
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _format_header_matchers(header_matchers: dict[str, InternalMatcher]) -> str:
|
|
110
|
+
header_parts = [f"{name.title()}: {value.matcher_repr}" for name, value in header_matchers.items()]
|
|
111
|
+
return ", ".join(header_parts)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _format_body_matcher(matcher: InternalMatcher, kind: Literal["content", "json"]) -> str:
|
|
115
|
+
if kind == "json":
|
|
116
|
+
try:
|
|
117
|
+
return json.dumps(matcher.matcher, separators=(",", ":"))
|
|
118
|
+
except (TypeError, ValueError):
|
|
119
|
+
return matcher.matcher_repr
|
|
120
|
+
elif kind == "content":
|
|
121
|
+
return matcher.matcher_repr
|
|
122
|
+
else:
|
|
123
|
+
assert_never(kind)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from re import Pattern
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from dirty_equals import DirtyEquals as _DirtyEqualsBase
|
|
7
|
+
except ImportError:
|
|
8
|
+
_DirtyEqualsBase = None # type: ignore[assignment,misc]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class InternalMatcher:
|
|
13
|
+
matcher: Any
|
|
14
|
+
matcher_repr: str = ""
|
|
15
|
+
|
|
16
|
+
def matches(self, value: Any) -> bool:
|
|
17
|
+
if isinstance(self.matcher, Pattern):
|
|
18
|
+
return self.matcher.search(str(value)) is not None
|
|
19
|
+
return bool(value == self.matcher)
|
|
20
|
+
|
|
21
|
+
def __post_init__(self) -> None:
|
|
22
|
+
if _DirtyEqualsBase is not None and isinstance(self.matcher, _DirtyEqualsBase):
|
|
23
|
+
# Need to memoize DirtyEquals repr so it is not messing its repr when doing __eq__:
|
|
24
|
+
# https://dirty-equals.helpmanual.io/latest/usage/#__repr__-and-pytest-compatibility
|
|
25
|
+
self.matcher_repr = repr(self.matcher)
|
|
26
|
+
elif isinstance(self.matcher, str):
|
|
27
|
+
self.matcher_repr = self.matcher
|
|
28
|
+
elif isinstance(self.matcher, Pattern):
|
|
29
|
+
self.matcher_repr = f"{self.matcher.pattern} (regex)"
|
|
30
|
+
else:
|
|
31
|
+
self.matcher_repr = repr(self.matcher)
|
|
32
|
+
|
|
33
|
+
def __repr__(self) -> str:
|
|
34
|
+
return f"Matcher({self.matcher_repr})"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""pyreqwest pytest plugin."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from pyreqwest.pytest_plugin.mock import client_mocker as client_mocker # load the client_mocker fixture
|
|
6
|
+
|
|
7
|
+
pytest.register_assert_rewrite("pyreqwest.pytest_plugin.internal.assert_eq")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def pytest_configure(config: pytest.Config) -> None:
|
|
11
|
+
"""Configure the pytest plugin."""
|
|
12
|
+
config.addinivalue_line(
|
|
13
|
+
"markers",
|
|
14
|
+
"pyreqwest: mark test to use PyReqwest HTTP client mocking",
|
|
15
|
+
)
|