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.
Files changed (52) hide show
  1. pyreqwest/__init__.py +3 -0
  2. pyreqwest/__init__.pyi +1 -0
  3. pyreqwest/_pyreqwest.cpython-313-x86_64-linux-gnu.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 +349 -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/logging/__init__.py +7 -0
  19. pyreqwest/logging/__init__.pyi +4 -0
  20. pyreqwest/middleware/__init__.py +5 -0
  21. pyreqwest/middleware/__init__.pyi +12 -0
  22. pyreqwest/middleware/asgi/__init__.py +5 -0
  23. pyreqwest/middleware/asgi/asgi.py +168 -0
  24. pyreqwest/middleware/types.py +26 -0
  25. pyreqwest/multipart/__init__.py +5 -0
  26. pyreqwest/multipart/__init__.pyi +75 -0
  27. pyreqwest/proxy/__init__.py +5 -0
  28. pyreqwest/proxy/__init__.pyi +47 -0
  29. pyreqwest/py.typed +0 -0
  30. pyreqwest/pytest_plugin/__init__.py +8 -0
  31. pyreqwest/pytest_plugin/internal/__init__.py +0 -0
  32. pyreqwest/pytest_plugin/internal/assert_eq.py +6 -0
  33. pyreqwest/pytest_plugin/internal/assert_message.py +123 -0
  34. pyreqwest/pytest_plugin/internal/matcher.py +34 -0
  35. pyreqwest/pytest_plugin/internal/plugin.py +15 -0
  36. pyreqwest/pytest_plugin/mock.py +512 -0
  37. pyreqwest/pytest_plugin/types.py +26 -0
  38. pyreqwest/request/__init__.py +29 -0
  39. pyreqwest/request/__init__.pyi +218 -0
  40. pyreqwest/response/__init__.py +19 -0
  41. pyreqwest/response/__init__.pyi +157 -0
  42. pyreqwest/simple/__init__.py +1 -0
  43. pyreqwest/simple/request/__init__.py +21 -0
  44. pyreqwest/simple/request/__init__.pyi +29 -0
  45. pyreqwest/simple/sync_request/__init__.py +21 -0
  46. pyreqwest/simple/sync_request/__init__.pyi +29 -0
  47. pyreqwest/types.py +12 -0
  48. pyreqwest-0.8.0.dist-info/METADATA +148 -0
  49. pyreqwest-0.8.0.dist-info/RECORD +52 -0
  50. pyreqwest-0.8.0.dist-info/WHEEL +5 -0
  51. pyreqwest-0.8.0.dist-info/entry_points.txt +2 -0
  52. 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
@@ -0,0 +1,8 @@
1
+ """PyReqwest pytest plugin for HTTP client mocking."""
2
+
3
+ from .mock import ClientMocker, Mock
4
+
5
+ __all__ = [
6
+ "ClientMocker",
7
+ "Mock",
8
+ ]
File without changes
@@ -0,0 +1,6 @@
1
+ from typing import Any
2
+
3
+
4
+ # pytest register_assert_rewrite for pretty diffs
5
+ def assert_eq(actual: Any, expected: Any, msg: str) -> None:
6
+ assert actual == expected, msg
@@ -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
+ )