pyreqwest 0.10.1__cp313-cp313-win_arm64.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 (54) hide show
  1. pyreqwest/__init__.py +3 -0
  2. pyreqwest/__init__.pyi +1 -0
  3. pyreqwest/_pyreqwest.cp313-win_arm64.pyd +0 -0
  4. pyreqwest/bytes/__init__.py +16 -0
  5. pyreqwest/bytes/__init__.pyi +106 -0
  6. pyreqwest/client/__init__.py +19 -0
  7. pyreqwest/client/__init__.pyi +351 -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 +196 -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/runtime/__init__.py +15 -0
  43. pyreqwest/runtime/__init__.pyi +25 -0
  44. pyreqwest/simple/__init__.py +1 -0
  45. pyreqwest/simple/request/__init__.py +21 -0
  46. pyreqwest/simple/request/__init__.pyi +29 -0
  47. pyreqwest/simple/sync_request/__init__.py +21 -0
  48. pyreqwest/simple/sync_request/__init__.pyi +29 -0
  49. pyreqwest/types.py +12 -0
  50. pyreqwest-0.10.1.dist-info/METADATA +148 -0
  51. pyreqwest-0.10.1.dist-info/RECORD +54 -0
  52. pyreqwest-0.10.1.dist-info/WHEEL +4 -0
  53. pyreqwest-0.10.1.dist-info/entry_points.txt +2 -0
  54. pyreqwest-0.10.1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,512 @@
1
+ """Module providing HTTP request mocking capabilities for pyreqwest clients in tests."""
2
+
3
+ import json
4
+ from collections.abc import AsyncIterable, Awaitable, Callable, Iterable
5
+ from functools import cached_property
6
+ from re import Pattern
7
+ from typing import Any, Literal, Self, TypeVar, assert_never
8
+
9
+ from pyreqwest.middleware import Next, SyncNext
10
+ from pyreqwest.middleware.types import Middleware, SyncMiddleware
11
+ from pyreqwest.pytest_plugin.internal.matcher import InternalMatcher
12
+ from pyreqwest.pytest_plugin.types import (
13
+ BodyContentMatcher,
14
+ CustomHandler,
15
+ CustomMatcher,
16
+ JsonMatcher,
17
+ Matcher,
18
+ MethodMatcher,
19
+ PathMatcher,
20
+ QueryMatcher,
21
+ UrlMatcher,
22
+ )
23
+ from pyreqwest.request import (
24
+ BaseRequestBuilder,
25
+ OneOffRequestBuilder,
26
+ Request,
27
+ RequestBody,
28
+ RequestBuilder,
29
+ SyncOneOffRequestBuilder,
30
+ SyncRequestBuilder,
31
+ )
32
+ from pyreqwest.response import BaseResponse, Response, ResponseBuilder, SyncResponse
33
+ from pyreqwest.types import HeadersType
34
+
35
+ try:
36
+ import pytest
37
+
38
+ pytest_fixture = pytest.fixture
39
+ MonkeyPatch = pytest.MonkeyPatch
40
+ except ImportError:
41
+ pytest_fixture = None # type: ignore[assignment]
42
+ MonkeyPatch = Any # type: ignore[assignment,misc]
43
+
44
+ _R = TypeVar("_R", bound=BaseResponse)
45
+
46
+
47
+ class Mock:
48
+ """Class representing a single mock rule."""
49
+
50
+ def __init__(
51
+ self, method: MethodMatcher | None = None, *, path: PathMatcher | None = None, url: UrlMatcher | None = None
52
+ ) -> None:
53
+ """Do not use directly. Instead, use ClientMocker.mock()."""
54
+ self._method_matcher = InternalMatcher(method) if method is not None else None
55
+ self._path_matcher = InternalMatcher(path) if path is not None else None
56
+ self._url_matcher = InternalMatcher(url) if url is not None else None
57
+ self._query_matcher: dict[str, InternalMatcher] | InternalMatcher | None = None
58
+ self._header_matchers: dict[str, InternalMatcher] = {}
59
+ self._body_matcher: tuple[InternalMatcher, Literal["content", "json"]] | None = None
60
+ self._custom_matcher: CustomMatcher | None = None
61
+ self._custom_handler: CustomHandler | None = None
62
+
63
+ self._matched_requests: list[Request] = []
64
+ self._unmatched_requests_repr_parts: list[dict[str, str | None]] = []
65
+ self._using_response_builder = False
66
+
67
+ def assert_called(
68
+ self,
69
+ *,
70
+ count: int | None = None,
71
+ min_count: int | None = None,
72
+ max_count: int | None = None,
73
+ ) -> None:
74
+ """Assert that this mock was called the expected number of times. By default, exactly once."""
75
+ if count is None and min_count is None and max_count is None:
76
+ count = 1
77
+
78
+ if self._assertion_passes(count, min_count, max_count):
79
+ return
80
+
81
+ from pyreqwest.pytest_plugin.internal.assert_message import assert_fail
82
+
83
+ assert_fail(self, count=count, min_count=min_count, max_count=max_count)
84
+
85
+ def _assertion_passes(
86
+ self,
87
+ count: int | None,
88
+ min_count: int | None,
89
+ max_count: int | None,
90
+ ) -> bool:
91
+ actual_count = len(self._matched_requests)
92
+ if count is not None:
93
+ return actual_count == count
94
+
95
+ min_satisfied = min_count is None or actual_count >= min_count
96
+ max_satisfied = max_count is None or actual_count <= max_count
97
+
98
+ return min_satisfied and max_satisfied
99
+
100
+ def get_requests(self) -> list[Request]:
101
+ """Get all captured requests by this mock."""
102
+ return [*self._matched_requests]
103
+
104
+ def get_call_count(self) -> int:
105
+ """Get the total number of calls to this mock."""
106
+ return len(self._matched_requests)
107
+
108
+ def reset_requests(self) -> None:
109
+ """Reset all captured requests for this mock."""
110
+ self._matched_requests.clear()
111
+
112
+ def match_query(self, query: QueryMatcher) -> Self:
113
+ """Set a matcher to match the entire query string or query parameters."""
114
+ if isinstance(query, dict):
115
+ self._query_matcher = {k: InternalMatcher(v) for k, v in query.items()}
116
+ else:
117
+ self._query_matcher = InternalMatcher(query)
118
+ return self
119
+
120
+ def match_query_param(self, name: str, value: Matcher) -> Self:
121
+ """Set a matcher to match a specific query parameter."""
122
+ if not isinstance(self._query_matcher, dict):
123
+ self._query_matcher = {}
124
+ self._query_matcher[name] = InternalMatcher(value)
125
+ return self
126
+
127
+ def match_header(self, name: str, value: Matcher) -> Self:
128
+ """Set a matcher to match a specific request header."""
129
+ self._header_matchers[name] = InternalMatcher(value)
130
+ return self
131
+
132
+ def match_body(self, matcher: BodyContentMatcher) -> Self:
133
+ """Set a matcher to match request bodies as raw content (text or bytes)."""
134
+ self._body_matcher = (InternalMatcher(matcher), "content")
135
+ return self
136
+
137
+ def match_body_json(self, matcher: JsonMatcher) -> Self:
138
+ """Set a matcher to match JSON request bodies."""
139
+ self._body_matcher = (InternalMatcher(matcher), "json")
140
+ return self
141
+
142
+ def match_request(self, matcher: CustomMatcher) -> Self:
143
+ """Set a custom matcher to match requests."""
144
+ self._custom_matcher = matcher
145
+ return self
146
+
147
+ def match_request_with_response(self, handler: CustomHandler) -> Self:
148
+ """Set a custom handler to generate the response for matched requests."""
149
+ assert not self._using_response_builder, "Cannot use response builder and custom handler together"
150
+ self._custom_handler = handler
151
+ return self
152
+
153
+ def with_status(self, status: int) -> Self:
154
+ """Set the mocked response status code."""
155
+ self._response_builder.status(status)
156
+ return self
157
+
158
+ def with_header(self, name: str, value: str) -> Self:
159
+ """Add a header to the mocked response."""
160
+ self._response_builder.header(name, value)
161
+ return self
162
+
163
+ def with_headers(self, headers: HeadersType) -> Self:
164
+ """Add headers to the mocked response."""
165
+ self._response_builder.headers(headers)
166
+ return self
167
+
168
+ def with_body_bytes(self, body: bytes | bytearray | memoryview) -> Self:
169
+ """Set the mocked response body to the given bytes."""
170
+ self._response_builder.body_bytes(body)
171
+ return self
172
+
173
+ def with_body_text(self, body: str) -> Self:
174
+ """Set the mocked response body to the given text."""
175
+ self._response_builder.body_text(body)
176
+ return self
177
+
178
+ def with_body_json(self, json_body: Any) -> Self:
179
+ """Set the mocked response body to the given JSON-serializable object."""
180
+ self._response_builder.body_json(json_body)
181
+ return self
182
+
183
+ def with_version(self, version: str) -> Self:
184
+ """Set the mocked response HTTP version."""
185
+ self._response_builder.version(version)
186
+ return self
187
+
188
+ def _handle_common_matchers(self, request: Request) -> dict[str, bool]:
189
+ return {
190
+ "method": self._matches_method(request),
191
+ "url": self._matches_url(request),
192
+ "path": self._matches_path(request),
193
+ "query": self._match_query(request),
194
+ "headers": self._match_headers(request),
195
+ "body": self._match_body(request),
196
+ }
197
+
198
+ async def _handle(self, request: Request) -> Response | None:
199
+ matches = self._handle_common_matchers(request)
200
+ response = await self._handle_callbacks(request, matches)
201
+ return self._check_matched(request, matches, response)
202
+
203
+ def _handle_sync(self, request: Request) -> SyncResponse | None:
204
+ matches = self._handle_common_matchers(request)
205
+ response = self._handle_callbacks_sync(request, matches)
206
+ return self._check_matched(request, matches, response)
207
+
208
+ async def _handle_callbacks(self, request: Request, matches: dict[str, bool]) -> Response | None:
209
+ matches["custom"] = await self._matches_custom(request)
210
+
211
+ if self._custom_handler:
212
+ response = await self._handle_custom_handler(request)
213
+ matches["handler"] = response is not None
214
+ else:
215
+ response = await self._response()
216
+ matches["handler"] = True
217
+
218
+ return response if all(matches.values()) else None
219
+
220
+ def _handle_callbacks_sync(self, request: Request, matches: dict[str, bool]) -> SyncResponse | None:
221
+ matches["custom"] = self._matches_custom_sync(request)
222
+
223
+ if self._custom_handler:
224
+ response = self._handle_custom_handler_sync(request)
225
+ matches["handler"] = response is not None
226
+ else:
227
+ response = self._response_sync()
228
+ matches["handler"] = True
229
+
230
+ return response if all(matches.values()) else None
231
+
232
+ def _check_matched(self, request: Request, matches: dict[str, bool], response: _R | None) -> _R | None:
233
+ if response is not None:
234
+ self._matched_requests.append(request)
235
+ return response
236
+
237
+ from pyreqwest.pytest_plugin.internal.assert_message import format_unmatched_request_parts
238
+
239
+ # Memo the reprs as we may consume the request
240
+ self._unmatched_requests_repr_parts.append(
241
+ format_unmatched_request_parts(request, unmatched={k for k, matched in matches.items() if not matched}),
242
+ )
243
+ return None
244
+
245
+ @cached_property
246
+ def _response_builder(self) -> ResponseBuilder:
247
+ assert self._custom_handler is None, "Cannot use response builder and custom handler together"
248
+ self._using_response_builder = True
249
+ return ResponseBuilder()
250
+
251
+ async def _response(self) -> Response:
252
+ built_response = await self._response_builder.copy().build()
253
+ assert isinstance(built_response, Response)
254
+ return built_response
255
+
256
+ def _response_sync(self) -> SyncResponse:
257
+ built_response = self._response_builder.copy().build_sync()
258
+ assert isinstance(built_response, SyncResponse)
259
+ return built_response
260
+
261
+ def _matches_method(self, request: Request) -> bool:
262
+ return self._method_matcher is None or self._method_matcher.matches(request.method)
263
+
264
+ def _matches_url(self, request: Request) -> bool:
265
+ return self._url_matcher is None or self._url_matcher.matches(request.url)
266
+
267
+ def _matches_path(self, request: Request) -> bool:
268
+ return self._path_matcher is None or self._path_matcher.matches(request.url.path)
269
+
270
+ def _match_headers(self, request: Request) -> bool:
271
+ for header_name, expected_value in self._header_matchers.items():
272
+ actual_value = request.headers.get(header_name)
273
+ if actual_value is None or not expected_value.matches(actual_value):
274
+ return False
275
+ return True
276
+
277
+ def _match_body(self, request: Request) -> bool:
278
+ if self._body_matcher is None:
279
+ return True
280
+
281
+ if request.body is None:
282
+ return False
283
+
284
+ assert request.body.get_stream() is None, "Stream should have been consumed into body bytes by mock middleware"
285
+ body_buf = request.body.copy_bytes()
286
+ assert body_buf is not None, "Unknown body type"
287
+ body_bytes = body_buf.to_bytes()
288
+
289
+ matcher, kind = self._body_matcher
290
+ if kind == "json":
291
+ try:
292
+ return matcher.matches(json.loads(body_bytes))
293
+ except json.JSONDecodeError:
294
+ return False
295
+ elif kind == "content":
296
+ if isinstance(matcher.matcher, bytes):
297
+ return matcher.matches(body_bytes)
298
+ return matcher.matches(body_bytes.decode())
299
+ else:
300
+ assert_never(kind)
301
+
302
+ def _match_query(self, request: Request) -> bool:
303
+ if self._query_matcher is None:
304
+ return True
305
+
306
+ query_str = request.url.query_string or ""
307
+ query_dict = request.url.query_dict_multi_value
308
+
309
+ if isinstance(self._query_matcher, dict):
310
+ for key, expected_value in self._query_matcher.items():
311
+ actual_value = query_dict.get(key)
312
+ if actual_value is None or not expected_value.matches(actual_value):
313
+ return False
314
+ return True
315
+ if isinstance(self._query_matcher.matcher, str | Pattern):
316
+ return self._query_matcher.matches(query_str)
317
+ return self._query_matcher.matches(query_dict)
318
+
319
+ async def _matches_custom(self, request: Request) -> bool:
320
+ if self._custom_matcher is None:
321
+ return True
322
+ res = self._custom_matcher(request)
323
+ assert isinstance(res, Awaitable)
324
+ return await res
325
+
326
+ def _matches_custom_sync(self, request: Request) -> bool:
327
+ if self._custom_matcher is None:
328
+ return True
329
+ res = self._custom_matcher(request)
330
+ assert isinstance(res, bool)
331
+ return res
332
+
333
+ async def _handle_custom_handler(self, request: Request) -> Response | None:
334
+ assert self._custom_handler
335
+ res = self._custom_handler(request)
336
+ assert isinstance(res, Awaitable)
337
+ return await res
338
+
339
+ def _handle_custom_handler_sync(self, request: Request) -> SyncResponse | None:
340
+ assert self._custom_handler
341
+ res = self._custom_handler(request)
342
+ assert res is None or isinstance(res, SyncResponse)
343
+ return res
344
+
345
+ def __repr__(self) -> str:
346
+ """Return a string representation of the mock for debugging purposes."""
347
+ parts = []
348
+ if self._method_matcher is not None:
349
+ parts.append(f"method={self._method_matcher!r}")
350
+ if self._url_matcher is not None:
351
+ parts.append(f"url={self._url_matcher!r}")
352
+ if self._path_matcher is not None:
353
+ parts.append(f"path={self._path_matcher!r}")
354
+ if self._query_matcher is not None:
355
+ parts.append(f"query={self._query_matcher!r}")
356
+ if self._header_matchers:
357
+ parts.append(f"headers={self._header_matchers!r}")
358
+ if self._body_matcher is not None:
359
+ matcher, kind = self._body_matcher
360
+ parts.append(f"body.{kind}={matcher!r}")
361
+ if self._custom_matcher is not None:
362
+ parts.append(f"custom_matcher={self._custom_matcher!r}")
363
+ if self._custom_handler is not None:
364
+ parts.append(f"custom_handler={self._custom_handler!r}")
365
+ return "<Mock " + ", ".join(parts) + ">"
366
+
367
+
368
+ class ClientMocker:
369
+ """Main class for mocking HTTP requests.
370
+ Use the `client_mocker` fixture or `ClientMocker.create_mocker` to create an instance.
371
+ """
372
+
373
+ def __init__(self) -> None:
374
+ """@private"""
375
+ self._mocks: list[Mock] = []
376
+ self._strict = False
377
+
378
+ @staticmethod
379
+ def create_mocker(monkeypatch: MonkeyPatch) -> "ClientMocker":
380
+ """Create a ClientMocker for mocking HTTP requests in tests."""
381
+ mocker = ClientMocker()
382
+
383
+ def setup(klass: type[BaseRequestBuilder], *, is_async: bool) -> None:
384
+ orig_build_consumed = klass.build # type: ignore[attr-defined]
385
+ orig_build_streamed = klass.build_streamed # type: ignore[attr-defined]
386
+
387
+ def build_patch(self: BaseRequestBuilder, orig: Callable[[BaseRequestBuilder], Request]) -> Request:
388
+ middleware = mocker._create_middleware() if is_async else mocker._create_sync_middleware()
389
+ return orig(self.with_middleware(middleware)) # type: ignore[attr-defined]
390
+
391
+ monkeypatch.setattr(klass, "build", lambda slf: build_patch(slf, orig_build_consumed))
392
+ monkeypatch.setattr(klass, "build_streamed", lambda slf: build_patch(slf, orig_build_streamed))
393
+
394
+ def setup_oneoff(klass: type[BaseRequestBuilder], *, is_async: bool) -> None:
395
+ orig_send = klass.send # type: ignore[attr-defined]
396
+
397
+ def send_patch(self: BaseRequestBuilder) -> Any:
398
+ middleware = mocker._create_middleware() if is_async else mocker._create_sync_middleware()
399
+ return orig_send(self.with_middleware(middleware)) # type: ignore[attr-defined]
400
+
401
+ monkeypatch.setattr(klass, "send", send_patch)
402
+
403
+ setup(RequestBuilder, is_async=True)
404
+ setup(SyncRequestBuilder, is_async=False)
405
+ setup_oneoff(OneOffRequestBuilder, is_async=True)
406
+ setup_oneoff(SyncOneOffRequestBuilder, is_async=False)
407
+
408
+ return mocker
409
+
410
+ def mock(
411
+ self, method: MethodMatcher | None = None, *, path: PathMatcher | None = None, url: UrlMatcher | None = None
412
+ ) -> Mock:
413
+ """Add a mock rule for method and path or URL."""
414
+ mock = Mock(method, path=path, url=url)
415
+ self._mocks.append(mock)
416
+ return mock
417
+
418
+ def get(self, *, path: PathMatcher | None = None, url: UrlMatcher | None = None) -> Mock:
419
+ """Mock GET requests to the given path or URL."""
420
+ return self.mock("GET", path=path, url=url)
421
+
422
+ def post(self, *, path: PathMatcher | None = None, url: UrlMatcher | None = None) -> Mock:
423
+ """Mock POST requests to the given path or URL."""
424
+ return self.mock("POST", path=path, url=url)
425
+
426
+ def put(self, *, path: PathMatcher | None = None, url: UrlMatcher | None = None) -> Mock:
427
+ """Mock PUT requests to the given path or URL."""
428
+ return self.mock("PUT", path=path, url=url)
429
+
430
+ def patch(self, *, path: PathMatcher | None = None, url: UrlMatcher | None = None) -> Mock:
431
+ """Mock PATCH requests to the given path or URL."""
432
+ return self.mock("PATCH", path=path, url=url)
433
+
434
+ def delete(self, *, path: PathMatcher | None = None, url: UrlMatcher | None = None) -> Mock:
435
+ """Mock DELETE requests to the given path or URL."""
436
+ return self.mock("DELETE", path=path, url=url)
437
+
438
+ def head(self, *, path: PathMatcher | None = None, url: UrlMatcher | None = None) -> Mock:
439
+ """Mock HEAD requests to the given path or URL."""
440
+ return self.mock("HEAD", path=path, url=url)
441
+
442
+ def options(self, *, path: PathMatcher | None = None, url: UrlMatcher | None = None) -> Mock:
443
+ """Mock OPTIONS requests to the given path or URL."""
444
+ return self.mock("OPTIONS", path=path, url=url)
445
+
446
+ def strict(self, enabled: bool = True) -> Self:
447
+ """Enable strict mode - unmatched requests will raise an error."""
448
+ self._strict = enabled
449
+ return self
450
+
451
+ def get_requests(self) -> list[Request]:
452
+ """Get all captured requests in all mocks."""
453
+ return [request for mock in self._mocks for request in mock.get_requests()]
454
+
455
+ def get_call_count(self) -> int:
456
+ """Get the total number of calls in all mocks."""
457
+ return sum(mock.get_call_count() for mock in self._mocks)
458
+
459
+ def clear(self) -> None:
460
+ """Remove all mocks."""
461
+ self._mocks.clear()
462
+
463
+ def reset_requests(self) -> None:
464
+ """Reset all captured requests in all mocks."""
465
+ for mock in self._mocks:
466
+ mock.reset_requests()
467
+
468
+ def _create_middleware(self) -> Middleware:
469
+ async def mock_middleware(request: Request, next_handler: Next) -> Response:
470
+ if request.body is not None and (stream := request.body.get_stream()) is not None:
471
+ assert isinstance(stream, AsyncIterable)
472
+ body = [bytes(chunk) async for chunk in stream] # Read the body stream into bytes
473
+ request = request.from_request_and_body(request, RequestBody.from_bytes(b"".join(body)))
474
+
475
+ for mock in self._mocks:
476
+ if (response := await mock._handle(request)) is not None:
477
+ return response
478
+
479
+ # No rule matched
480
+ if self._strict:
481
+ msg = f"No mock rule matched request: {request.method} {request.url}"
482
+ raise AssertionError(msg)
483
+ return await next_handler.run(request) # Proceed normally
484
+
485
+ return mock_middleware
486
+
487
+ def _create_sync_middleware(self) -> SyncMiddleware:
488
+ def mock_middleware(request: Request, next_handler: SyncNext) -> SyncResponse:
489
+ if request.body is not None and (stream := request.body.get_stream()) is not None:
490
+ assert isinstance(stream, Iterable)
491
+ body = [bytes(chunk) for chunk in stream] # Read the body stream into bytes
492
+ request = request.from_request_and_body(request, RequestBody.from_bytes(b"".join(body)))
493
+
494
+ for mock in self._mocks:
495
+ if (response := mock._handle_sync(request)) is not None:
496
+ return response
497
+
498
+ # No rule matched
499
+ if self._strict:
500
+ msg = f"No mock rule matched request: {request.method} {request.url}"
501
+ raise AssertionError(msg)
502
+ return next_handler.run(request) # Proceed normally
503
+
504
+ return mock_middleware
505
+
506
+
507
+ if pytest_fixture is not None:
508
+
509
+ @pytest_fixture
510
+ def client_mocker(monkeypatch: MonkeyPatch) -> ClientMocker:
511
+ """Fixture that provides a ClientMocker for mocking HTTP requests in tests."""
512
+ return ClientMocker.create_mocker(monkeypatch)
@@ -0,0 +1,26 @@
1
+ """Types used in the pytest plugin."""
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from re import Pattern
5
+ from typing import Any
6
+
7
+ from pyreqwest.http import Url
8
+ from pyreqwest.request import Request
9
+ from pyreqwest.response import Response, SyncResponse
10
+
11
+ try:
12
+ from dirty_equals import DirtyEquals
13
+
14
+ Matcher = str | Pattern[str] | DirtyEquals[Any]
15
+ JsonMatcher = DirtyEquals[Any] | Any
16
+ except ImportError:
17
+ Matcher = str | Pattern[str] # type: ignore[assignment,misc]
18
+ JsonMatcher = Any # type: ignore[assignment,misc]
19
+
20
+ MethodMatcher = Matcher
21
+ PathMatcher = Matcher
22
+ UrlMatcher = Matcher | Url
23
+ QueryMatcher = dict[str, Matcher | list[str]] | Matcher
24
+ BodyContentMatcher = bytes | Matcher
25
+ CustomMatcher = Callable[[Request], Awaitable[bool]] | Callable[[Request], bool]
26
+ CustomHandler = Callable[[Request], Awaitable[Response | None]] | Callable[[Request], SyncResponse | None]
@@ -0,0 +1,29 @@
1
+ """Requests classes and builders."""
2
+
3
+ from pyreqwest._pyreqwest.request import (
4
+ BaseRequestBuilder,
5
+ ConsumedRequest,
6
+ OneOffRequestBuilder,
7
+ Request,
8
+ RequestBody,
9
+ RequestBuilder,
10
+ StreamRequest,
11
+ SyncConsumedRequest,
12
+ SyncOneOffRequestBuilder,
13
+ SyncRequestBuilder,
14
+ SyncStreamRequest,
15
+ )
16
+
17
+ __all__ = [
18
+ "BaseRequestBuilder",
19
+ "RequestBuilder",
20
+ "SyncRequestBuilder",
21
+ "Request",
22
+ "ConsumedRequest",
23
+ "StreamRequest",
24
+ "SyncConsumedRequest",
25
+ "SyncStreamRequest",
26
+ "RequestBody",
27
+ "OneOffRequestBuilder",
28
+ "SyncOneOffRequestBuilder",
29
+ ]