mockserver-client 7.6.0__tar.gz → 9.0.0__tar.gz

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 (46) hide show
  1. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/PKG-INFO +86 -2
  2. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/README.md +85 -1
  3. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/__init__.py +32 -0
  4. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/a2a.py +1 -1
  5. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/async_client.py +194 -11
  6. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/client.py +80 -6
  7. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/launcher.py +67 -13
  8. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/models.py +800 -43
  9. mockserver_client-9.0.0/mockserver/py.typed +0 -0
  10. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver_client.egg-info/PKG-INFO +86 -2
  11. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver_client.egg-info/SOURCES.txt +7 -0
  12. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/pyproject.toml +7 -1
  13. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_a2a.py +3 -3
  14. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_async_client.py +42 -0
  15. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_client.py +73 -2
  16. mockserver_client-9.0.0/tests/test_conditional_request_and_load_check.py +281 -0
  17. mockserver_client-9.0.0/tests/test_explain.py +321 -0
  18. mockserver_client-9.0.0/tests/test_from_dict_fidelity.py +28 -0
  19. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_init.py +2 -0
  20. mockserver_client-9.0.0/tests/test_key_multi_value_forms.py +186 -0
  21. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_launcher.py +85 -0
  22. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_models.py +115 -3
  23. mockserver_client-9.0.0/tests/test_override_forwarded_request.py +193 -0
  24. mockserver_client-9.0.0/tests/test_sync_chain_parity.py +83 -0
  25. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/exceptions.py +0 -0
  26. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/fluent.py +0 -0
  27. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/llm.py +0 -0
  28. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/mcp.py +0 -0
  29. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/pytest_plugin.py +0 -0
  30. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/websocket_client.py +0 -0
  31. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver_client.egg-info/dependency_links.txt +0 -0
  32. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver_client.egg-info/entry_points.txt +0 -0
  33. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver_client.egg-info/requires.txt +0 -0
  34. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver_client.egg-info/top_level.txt +0 -0
  35. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/setup.cfg +0 -0
  36. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_auth_tls.py +0 -0
  37. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_breakpoint.py +0 -0
  38. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_control_plane.py +0 -0
  39. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_exceptions.py +0 -0
  40. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_fluent.py +0 -0
  41. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_integration.py +0 -0
  42. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_llm.py +0 -0
  43. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_mcp.py +0 -0
  44. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_pytest_plugin.py +0 -0
  45. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_roundtrip_fidelity.py +0 -0
  46. {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_websocket_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mockserver-client
3
- Version: 7.6.0
3
+ Version: 9.0.0
4
4
  Summary: Official Python client for MockServer — create expectations, verify requests, and register dynamic callbacks via WebSocket
5
5
  Author-email: James Bloom <jamesdbloom@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -162,6 +162,27 @@ client.mock_with_forward_callback(
162
162
  )
163
163
  ```
164
164
 
165
+ ## Forward with Overrides
166
+
167
+ Forward the matched request with fields replaced (`request_override`), modified
168
+ (`request_modifier`), or with its response overridden (`response_override`,
169
+ `response_modifier`, `response_template`), without a callback:
170
+
171
+ ```python
172
+ from mockserver import HttpOverrideForwardedRequest, HttpRequest, HttpResponse
173
+
174
+ client.when(HttpRequest.request("/some/path")).forward(
175
+ HttpOverrideForwardedRequest(
176
+ request_override=HttpRequest(path="/other/path", headers={"Host": ["target.host.com"]}),
177
+ response_override=HttpResponse(headers={"X-Proxied": ["true"]}),
178
+ response_modifier={"headers": {"remove": ["Server"]}},
179
+ )
180
+ )
181
+ ```
182
+
183
+ `http_request` and `http_response` are deprecated aliases of `request_override`
184
+ and `response_override`, kept for existing code; set one name of each pair, not both.
185
+
165
186
  ## Verification
166
187
 
167
188
  ```python
@@ -192,6 +213,31 @@ client.verify_zero_interactions()
192
213
 
193
214
  ## Request Matching
194
215
 
216
+ ### Headers, query parameters and cookies
217
+
218
+ Pass headers, query and path parameters, trailers and cookies as a dict, as in
219
+ MockServer's JSON, or as a list of `KeyToMultiValue`. A `"keyMatchStyle"` key
220
+ (`SUB_SET`, the default, or `MATCHING_KEY`) sets how the values of a key are
221
+ matched.
222
+
223
+ ```python
224
+ from mockserver import HttpRequest, HttpResponse
225
+
226
+ client.when(
227
+ HttpRequest(
228
+ path="/search",
229
+ headers={"Accept": "application/json"}, # one value
230
+ query_string_parameters={"tag": ["a", "b"]}, # several values
231
+ cookies={"session": "abc"},
232
+ )
233
+ ).respond(
234
+ HttpResponse(status_code=302, headers={"Location": ["https://example.com"]})
235
+ )
236
+
237
+ # every value the request has for X-Tag must match, not just one
238
+ HttpRequest(headers={"keyMatchStyle": "MATCHING_KEY", "X-Tag": ["a", "b"]})
239
+ ```
240
+
195
241
  ### JWT matcher
196
242
 
197
243
  Match requests by the claims (and metadata) of a JWT carried in a header. Each
@@ -247,6 +293,27 @@ HttpRequest.request("/api/orders").with_body(
247
293
  )
248
294
  ```
249
295
 
296
+ ### Conditional (if/then/else) matcher
297
+
298
+ Pass a `ConditionalRequestDefinition` where a request matcher goes: when
299
+ `if_request` matches, `then_request` must match too; otherwise `else_request`
300
+ must (with no `else_request` it matches whenever `if_request` does not). Each
301
+ branch is an `HttpRequest`, an `OpenAPIDefinition` or another
302
+ `ConditionalRequestDefinition`. Retrieved expectations whose `httpRequest` holds
303
+ `if` read back as this type.
304
+
305
+ ```python
306
+ from mockserver import Body, ConditionalRequestDefinition, HttpRequest, HttpResponse
307
+
308
+ client.when(
309
+ ConditionalRequestDefinition(
310
+ if_request=HttpRequest(method="POST").with_header("content-type", "application/json"),
311
+ then_request=HttpRequest(body=Body.json_schema('{"type": "object", "required": ["orderId"]}')),
312
+ else_request=HttpRequest(method="GET"),
313
+ )
314
+ ).respond(HttpResponse(status_code=200))
315
+ ```
316
+
250
317
  ## Retrieval
251
318
 
252
319
  ```python
@@ -362,6 +429,21 @@ client.start_chaos_experiment({
362
429
  Both methods are available on the async client too (`await client.verify_slo(...)`,
363
430
  `await client.start_chaos_experiment(...)`).
364
431
 
432
+ ## Diagnose Matching
433
+
434
+ `explain(correlation_id)` tells you which expectation would serve a recorded request now and why
435
+ each expectation ahead of it was passed over; `debug_mismatch(request)` compares a request with
436
+ every active expectation; `explain_unmatched(limit=..., suggest_fixes=...)` explains recent
437
+ requests that matched nothing. Each returns a typed dataclass (`RequestExplanation`,
438
+ `DebugMismatchResult`, `ExplainUnmatchedResult`) and raises `MockServerError` with the server's
439
+ reason on failure. The async client has the same methods.
440
+
441
+ ```python
442
+ explanation = client.explain("abc-123-def-456")
443
+ for skipped in explanation.skipped_expectations:
444
+ print(skipped.expectation_id, skipped.reason, skipped.explanation)
445
+ ```
446
+
365
447
  ## TLS Support
366
448
 
367
449
  ```python
@@ -486,7 +568,7 @@ launcher_path = ensure_binary() # returns Path to the launcher executable
486
568
  ```python
487
569
  from mockserver.launcher import start
488
570
 
489
- server = start(port=1080, version="7.6.0")
571
+ server = start(port=1080, version="9.0.0")
490
572
  # ...
491
573
  server.stop()
492
574
  ```
@@ -499,6 +581,8 @@ server.stop()
499
581
  | `start(port, version=None, *, extra_args=None, log=True)` | Ensure the binary and start MockServer. Returns a `MockServerProcess`. |
500
582
  | `MockServerProcess` | Handle to the running process. Properties: `port`, `pid`, `launcher`, `returncode`. Methods: `stop(timeout=10.0)`. Supports `with` statement. |
501
583
 
584
+ On Windows the launcher runs `mockserver.bat` through `cmd.exe` with every argument quoted, so characters such as `&`, `|` and `^` reach MockServer unchanged. `start()` raises `ValueError` if the launcher path or an `extra_args` entry contains a double quote, a line break or NUL, or if together they contain more than one `%` (which `cmd.exe` would expand as a `%NAME%` variable). No shell is used on Linux or macOS.
585
+
502
586
  ### Supported platforms
503
587
 
504
588
  | OS | Architecture |
@@ -132,6 +132,27 @@ client.mock_with_forward_callback(
132
132
  )
133
133
  ```
134
134
 
135
+ ## Forward with Overrides
136
+
137
+ Forward the matched request with fields replaced (`request_override`), modified
138
+ (`request_modifier`), or with its response overridden (`response_override`,
139
+ `response_modifier`, `response_template`), without a callback:
140
+
141
+ ```python
142
+ from mockserver import HttpOverrideForwardedRequest, HttpRequest, HttpResponse
143
+
144
+ client.when(HttpRequest.request("/some/path")).forward(
145
+ HttpOverrideForwardedRequest(
146
+ request_override=HttpRequest(path="/other/path", headers={"Host": ["target.host.com"]}),
147
+ response_override=HttpResponse(headers={"X-Proxied": ["true"]}),
148
+ response_modifier={"headers": {"remove": ["Server"]}},
149
+ )
150
+ )
151
+ ```
152
+
153
+ `http_request` and `http_response` are deprecated aliases of `request_override`
154
+ and `response_override`, kept for existing code; set one name of each pair, not both.
155
+
135
156
  ## Verification
136
157
 
137
158
  ```python
@@ -162,6 +183,31 @@ client.verify_zero_interactions()
162
183
 
163
184
  ## Request Matching
164
185
 
186
+ ### Headers, query parameters and cookies
187
+
188
+ Pass headers, query and path parameters, trailers and cookies as a dict, as in
189
+ MockServer's JSON, or as a list of `KeyToMultiValue`. A `"keyMatchStyle"` key
190
+ (`SUB_SET`, the default, or `MATCHING_KEY`) sets how the values of a key are
191
+ matched.
192
+
193
+ ```python
194
+ from mockserver import HttpRequest, HttpResponse
195
+
196
+ client.when(
197
+ HttpRequest(
198
+ path="/search",
199
+ headers={"Accept": "application/json"}, # one value
200
+ query_string_parameters={"tag": ["a", "b"]}, # several values
201
+ cookies={"session": "abc"},
202
+ )
203
+ ).respond(
204
+ HttpResponse(status_code=302, headers={"Location": ["https://example.com"]})
205
+ )
206
+
207
+ # every value the request has for X-Tag must match, not just one
208
+ HttpRequest(headers={"keyMatchStyle": "MATCHING_KEY", "X-Tag": ["a", "b"]})
209
+ ```
210
+
165
211
  ### JWT matcher
166
212
 
167
213
  Match requests by the claims (and metadata) of a JWT carried in a header. Each
@@ -217,6 +263,27 @@ HttpRequest.request("/api/orders").with_body(
217
263
  )
218
264
  ```
219
265
 
266
+ ### Conditional (if/then/else) matcher
267
+
268
+ Pass a `ConditionalRequestDefinition` where a request matcher goes: when
269
+ `if_request` matches, `then_request` must match too; otherwise `else_request`
270
+ must (with no `else_request` it matches whenever `if_request` does not). Each
271
+ branch is an `HttpRequest`, an `OpenAPIDefinition` or another
272
+ `ConditionalRequestDefinition`. Retrieved expectations whose `httpRequest` holds
273
+ `if` read back as this type.
274
+
275
+ ```python
276
+ from mockserver import Body, ConditionalRequestDefinition, HttpRequest, HttpResponse
277
+
278
+ client.when(
279
+ ConditionalRequestDefinition(
280
+ if_request=HttpRequest(method="POST").with_header("content-type", "application/json"),
281
+ then_request=HttpRequest(body=Body.json_schema('{"type": "object", "required": ["orderId"]}')),
282
+ else_request=HttpRequest(method="GET"),
283
+ )
284
+ ).respond(HttpResponse(status_code=200))
285
+ ```
286
+
220
287
  ## Retrieval
221
288
 
222
289
  ```python
@@ -332,6 +399,21 @@ client.start_chaos_experiment({
332
399
  Both methods are available on the async client too (`await client.verify_slo(...)`,
333
400
  `await client.start_chaos_experiment(...)`).
334
401
 
402
+ ## Diagnose Matching
403
+
404
+ `explain(correlation_id)` tells you which expectation would serve a recorded request now and why
405
+ each expectation ahead of it was passed over; `debug_mismatch(request)` compares a request with
406
+ every active expectation; `explain_unmatched(limit=..., suggest_fixes=...)` explains recent
407
+ requests that matched nothing. Each returns a typed dataclass (`RequestExplanation`,
408
+ `DebugMismatchResult`, `ExplainUnmatchedResult`) and raises `MockServerError` with the server's
409
+ reason on failure. The async client has the same methods.
410
+
411
+ ```python
412
+ explanation = client.explain("abc-123-def-456")
413
+ for skipped in explanation.skipped_expectations:
414
+ print(skipped.expectation_id, skipped.reason, skipped.explanation)
415
+ ```
416
+
335
417
  ## TLS Support
336
418
 
337
419
  ```python
@@ -456,7 +538,7 @@ launcher_path = ensure_binary() # returns Path to the launcher executable
456
538
  ```python
457
539
  from mockserver.launcher import start
458
540
 
459
- server = start(port=1080, version="7.6.0")
541
+ server = start(port=1080, version="9.0.0")
460
542
  # ...
461
543
  server.stop()
462
544
  ```
@@ -469,6 +551,8 @@ server.stop()
469
551
  | `start(port, version=None, *, extra_args=None, log=True)` | Ensure the binary and start MockServer. Returns a `MockServerProcess`. |
470
552
  | `MockServerProcess` | Handle to the running process. Properties: `port`, `pid`, `launcher`, `returncode`. Methods: `stop(timeout=10.0)`. Supports `with` statement. |
471
553
 
554
+ On Windows the launcher runs `mockserver.bat` through `cmd.exe` with every argument quoted, so characters such as `&`, `|` and `^` reach MockServer unchanged. `start()` raises `ValueError` if the launcher path or an `extra_args` entry contains a double quote, a line break or NUL, or if together they contain more than one `%` (which `cmd.exe` would expand as a `%NAME%` variable). No shell is used on Linux or macOS.
555
+
472
556
  ### Supported platforms
473
557
 
474
558
  | OS | Architecture |
@@ -2,9 +2,11 @@ from mockserver.models import (
2
2
  AfterAction,
3
3
  AllOfBody,
4
4
  BinaryResponse,
5
+ BinaryUpstream,
5
6
  Body,
6
7
  CaptureRule,
7
8
  CaptureSource,
9
+ ConditionalRequestDefinition,
8
10
  ConnectionOptions,
9
11
  CrossProtocolScenario,
10
12
  CrossProtocolTrigger,
@@ -19,6 +21,7 @@ from mockserver.models import (
19
21
  ExpectationId,
20
22
  ExpectationStep,
21
23
  GraphQLBody,
24
+ GraphQLSubscriptionFilter,
22
25
  GrpcBidiResponse,
23
26
  GrpcBidiRule,
24
27
  GrpcStreamMessage,
@@ -47,12 +50,14 @@ from mockserver.models import (
47
50
  Protocol,
48
51
  RateLimit,
49
52
  RateLimitAlgorithm,
53
+ RecoverAfter,
50
54
  ValidationMode,
51
55
  WasmBody,
52
56
  XmlBody,
53
57
  XmlSchemaBody,
54
58
  XPathBody,
55
59
  LoadCapture,
60
+ LoadCheck,
56
61
  LoadFeeder,
57
62
  LoadPacing,
58
63
  LoadProfile,
@@ -77,6 +82,17 @@ from mockserver.models import (
77
82
  VerificationTimes,
78
83
  WebSocketFrameMatcher,
79
84
  WebSocketMessage,
85
+ Arrival,
86
+ ArrivalForwardFailure,
87
+ ClosestMatch,
88
+ DebugMismatchResult,
89
+ ExplainedExpectation,
90
+ ExplainedRequest,
91
+ ExplainUnmatchedResult,
92
+ ExplanationNarration,
93
+ RequestExplanation,
94
+ SuggestedFix,
95
+ UnmatchedRequest,
80
96
  )
81
97
  from mockserver.llm import (
82
98
  Completion,
@@ -151,9 +167,11 @@ __all__ = [
151
167
  "AllOfBody",
152
168
  "AsyncMockServerClient",
153
169
  "BinaryResponse",
170
+ "BinaryUpstream",
154
171
  "Body",
155
172
  "CaptureRule",
156
173
  "CaptureSource",
174
+ "ConditionalRequestDefinition",
157
175
  "ConnectionOptions",
158
176
  "CrossProtocolScenario",
159
177
  "CrossProtocolTrigger",
@@ -168,6 +186,7 @@ __all__ = [
168
186
  "ExpectationId",
169
187
  "ExpectationStep",
170
188
  "GraphQLBody",
189
+ "GraphQLSubscriptionFilter",
171
190
  "GrpcBidiResponse",
172
191
  "GrpcBidiRule",
173
192
  "GrpcStreamMessage",
@@ -196,12 +215,14 @@ __all__ = [
196
215
  "Protocol",
197
216
  "RateLimit",
198
217
  "RateLimitAlgorithm",
218
+ "RecoverAfter",
199
219
  "ValidationMode",
200
220
  "WasmBody",
201
221
  "XmlBody",
202
222
  "XmlSchemaBody",
203
223
  "XPathBody",
204
224
  "LoadCapture",
225
+ "LoadCheck",
205
226
  "LoadFeeder",
206
227
  "LoadPacing",
207
228
  "LoadProfile",
@@ -276,6 +297,17 @@ __all__ = [
276
297
  "VerificationTimes",
277
298
  "WebSocketFrameMatcher",
278
299
  "WebSocketMessage",
300
+ "Arrival",
301
+ "ArrivalForwardFailure",
302
+ "ClosestMatch",
303
+ "DebugMismatchResult",
304
+ "ExplainedExpectation",
305
+ "ExplainedRequest",
306
+ "ExplainUnmatchedResult",
307
+ "ExplanationNarration",
308
+ "RequestExplanation",
309
+ "SuggestedFix",
310
+ "UnmatchedRequest",
279
311
  "MockServerProcess",
280
312
  "bundle_base_name",
281
313
  "cache_dir",
@@ -428,7 +428,7 @@ class A2aMockBuilder:
428
428
  return Expectation(
429
429
  http_request=self._json_rpc_request("tasks/send"),
430
430
  http_override_forwarded_request=HttpOverrideForwardedRequest(
431
- http_request=webhook_request,
431
+ request_override=webhook_request,
432
432
  response_template=client_response_template,
433
433
  ),
434
434
  )
@@ -18,7 +18,11 @@ from mockserver.exceptions import (
18
18
  )
19
19
  from mockserver.fluent import ForwardChainExpectation
20
20
  from mockserver.models import (
21
+ ConditionalRequestDefinition,
22
+ DebugMismatchResult,
23
+ DnsRequestDefinition,
21
24
  Expectation,
25
+ ExplainUnmatchedResult,
22
26
  HttpChaosProfile,
23
27
  HttpObjectCallback,
24
28
  HttpRequest,
@@ -27,11 +31,13 @@ from mockserver.models import (
27
31
  LoadScenario,
28
32
  OpenAPIExpectation,
29
33
  Ports,
34
+ RequestExplanation,
30
35
  TimeToLive,
31
36
  Times,
32
37
  Verification,
33
38
  VerificationSequence,
34
39
  VerificationTimes,
40
+ _deserialize_recorded_request,
35
41
  )
36
42
  from mockserver.websocket_client import MockServerWebSocketClient
37
43
 
@@ -757,18 +763,21 @@ class AsyncMockServerClient:
757
763
  Returns the verdict dict with a ``result`` of ``PASS`` or ``INCONCLUSIVE``
758
764
  (HTTP 200). A ``FAIL`` verdict (HTTP 406) raises
759
765
  :class:`MockServerVerificationError` so a CI or chaos gate can assert on it
760
- directly. SLO tracking is off by default — the server returns 400 (raised as
761
- :class:`MockServerError`) until started with ``sloTrackingEnabled=true``.
766
+ directly. SLO tracking is off by default — the server returns 403 (raised as
767
+ :class:`MockServerError`) until started with ``sloTrackingEnabled=true``;
768
+ malformed criteria are a separate 400.
762
769
  """
763
770
  body = json.dumps(criteria)
764
771
  status, response_body = await self._request("PUT", "/mockserver/verifySLO", body)
765
772
  if status == 406:
766
773
  raise MockServerVerificationError(response_body or "SLO verdict: FAIL")
767
- if status == 400:
774
+ if status == 403:
768
775
  raise MockServerError(
769
- "Invalid SLO criteria (or SLO tracking disabled — set "
770
- f"sloTrackingEnabled=true): {response_body}"
776
+ "SLO tracking is disabled — start MockServer with "
777
+ f"sloTrackingEnabled=true: {response_body}"
771
778
  )
779
+ if status == 400:
780
+ raise MockServerError(f"Invalid SLO criteria: {response_body}")
772
781
  if status >= 400:
773
782
  raise MockServerError(
774
783
  f"Failed to verify SLO (status={status}): {response_body}"
@@ -1114,7 +1123,7 @@ class AsyncMockServerClient:
1114
1123
 
1115
1124
  async def verify(
1116
1125
  self,
1117
- request: HttpRequest | None = None,
1126
+ request: HttpRequest | ConditionalRequestDefinition | None = None,
1118
1127
  times: VerificationTimes | None = None,
1119
1128
  *,
1120
1129
  response: HttpResponse | None = None,
@@ -1131,7 +1140,7 @@ class AsyncMockServerClient:
1131
1140
 
1132
1141
  async def verify_sequence(
1133
1142
  self,
1134
- *requests: HttpRequest,
1143
+ *requests: HttpRequest | ConditionalRequestDefinition,
1135
1144
  responses: list[HttpResponse] | None = None,
1136
1145
  ) -> None:
1137
1146
  verification = VerificationSequence(
@@ -1156,8 +1165,12 @@ class AsyncMockServerClient:
1156
1165
  )
1157
1166
 
1158
1167
  async def retrieve_recorded_requests(
1159
- self, request: HttpRequest | None = None
1160
- ) -> list[HttpRequest]:
1168
+ self, request: HttpRequest | DnsRequestDefinition | None = None
1169
+ ) -> list[HttpRequest | DnsRequestDefinition | dict]:
1170
+ """Retrieve the recorded requests matching ``request`` (all HTTP requests when
1171
+ omitted). With a DNS filter the DNS queries come back as
1172
+ :class:`DnsRequestDefinition`; a binary message comes back as its raw dict.
1173
+ """
1161
1174
  body = json.dumps(request.to_dict()) if request else ""
1162
1175
  status, response_body = await self._request(
1163
1176
  "PUT",
@@ -1172,7 +1185,7 @@ class AsyncMockServerClient:
1172
1185
  if response_body:
1173
1186
  parsed = json.loads(response_body)
1174
1187
  if isinstance(parsed, list):
1175
- return [HttpRequest.from_dict(r) for r in parsed]
1188
+ return [_deserialize_recorded_request(r) for r in parsed]
1176
1189
  return []
1177
1190
 
1178
1191
  async def retrieve_active_expectations(
@@ -1215,6 +1228,176 @@ class AsyncMockServerClient:
1215
1228
  return [Expectation.from_dict(e) for e in parsed]
1216
1229
  return []
1217
1230
 
1231
+ async def _retrieve_by_id(
1232
+ self, expectation_id: str, query_params: dict[str, str]
1233
+ ) -> tuple[int, str]:
1234
+ """Send a retrieve request filtered by expectation id.
1235
+
1236
+ An expectation id may be sent in place of a request matcher. For
1237
+ ``ACTIVE_EXPECTATIONS`` the server returns only the expectation with that
1238
+ id; for every other type it returns the entries matching that
1239
+ expectation's request, exactly as verify by expectation id matches them.
1240
+ An unknown id is rejected with a 400.
1241
+ """
1242
+ return await self._request(
1243
+ "PUT",
1244
+ "/mockserver/retrieve",
1245
+ json.dumps({"id": expectation_id}),
1246
+ query_params,
1247
+ )
1248
+
1249
+ async def retrieve_recorded_requests_by_id(
1250
+ self, expectation_id: str
1251
+ ) -> list[HttpRequest | DnsRequestDefinition | dict]:
1252
+ status, response_body = await self._retrieve_by_id(
1253
+ expectation_id, {"type": "REQUESTS", "format": "JSON"}
1254
+ )
1255
+ if status >= 400:
1256
+ raise MockServerError(
1257
+ f"Failed to retrieve recorded requests by id (status={status}): {response_body}"
1258
+ )
1259
+ if response_body:
1260
+ parsed = json.loads(response_body)
1261
+ if isinstance(parsed, list):
1262
+ return [_deserialize_recorded_request(r) for r in parsed]
1263
+ return []
1264
+
1265
+ async def retrieve_active_expectations_by_id(
1266
+ self, expectation_id: str
1267
+ ) -> list[Expectation]:
1268
+ status, response_body = await self._retrieve_by_id(
1269
+ expectation_id, {"type": "ACTIVE_EXPECTATIONS", "format": "JSON"}
1270
+ )
1271
+ if status >= 400:
1272
+ raise MockServerError(
1273
+ f"Failed to retrieve active expectations by id (status={status}): {response_body}"
1274
+ )
1275
+ if response_body:
1276
+ parsed = json.loads(response_body)
1277
+ if isinstance(parsed, list):
1278
+ return [Expectation.from_dict(e) for e in parsed]
1279
+ return []
1280
+
1281
+ async def retrieve_recorded_expectations_by_id(
1282
+ self, expectation_id: str
1283
+ ) -> list[Expectation]:
1284
+ status, response_body = await self._retrieve_by_id(
1285
+ expectation_id, {"type": "RECORDED_EXPECTATIONS", "format": "JSON"}
1286
+ )
1287
+ if status >= 400:
1288
+ raise MockServerError(
1289
+ f"Failed to retrieve recorded expectations by id (status={status}): {response_body}"
1290
+ )
1291
+ if response_body:
1292
+ parsed = json.loads(response_body)
1293
+ if isinstance(parsed, list):
1294
+ return [Expectation.from_dict(e) for e in parsed]
1295
+ return []
1296
+
1297
+ async def retrieve_recorded_requests_and_responses_by_id(
1298
+ self, expectation_id: str
1299
+ ) -> list[HttpRequestAndHttpResponse]:
1300
+ status, response_body = await self._retrieve_by_id(
1301
+ expectation_id, {"type": "REQUEST_RESPONSES", "format": "JSON"}
1302
+ )
1303
+ if status >= 400:
1304
+ raise MockServerError(
1305
+ f"Failed to retrieve request/responses by id (status={status}): {response_body}"
1306
+ )
1307
+ if response_body:
1308
+ parsed = json.loads(response_body)
1309
+ if isinstance(parsed, list):
1310
+ return [HttpRequestAndHttpResponse.from_dict(rr) for rr in parsed]
1311
+ return []
1312
+
1313
+ async def retrieve_log_messages_by_id(self, expectation_id: str) -> list[str]:
1314
+ status, response_body = await self._retrieve_by_id(
1315
+ expectation_id, {"type": "LOGS"}
1316
+ )
1317
+ if status >= 400:
1318
+ raise MockServerError(
1319
+ f"Failed to retrieve log messages by id (status={status}): {response_body}"
1320
+ )
1321
+ if response_body:
1322
+ try:
1323
+ parsed = json.loads(response_body)
1324
+ if isinstance(parsed, list):
1325
+ return parsed
1326
+ except json.JSONDecodeError:
1327
+ return response_body.split("------------------------------------\n")
1328
+ return []
1329
+
1330
+ async def _diagnostic(
1331
+ self, path: str, body: str | None, query_params: dict[str, str] | None, failure: str
1332
+ ) -> dict:
1333
+ status, response_body = await self._request("PUT", path, body, query_params)
1334
+ try:
1335
+ parsed = json.loads(response_body) if response_body else {}
1336
+ except json.JSONDecodeError:
1337
+ parsed = None
1338
+ if status != 200 or not isinstance(parsed, dict):
1339
+ reason = parsed.get("error") if isinstance(parsed, dict) else None
1340
+ raise MockServerError(f"{failure} (status={status}): {reason or response_body}")
1341
+ return parsed
1342
+
1343
+ async def explain(self, correlation_id: str) -> RequestExplanation:
1344
+ """Explain how a recorded request would be served now.
1345
+
1346
+ Issues ``PUT /mockserver/explain?correlationId=<id>``, which re-checks the
1347
+ request with that correlation id against the current expectations, in the
1348
+ order MockServer evaluates them, and returns the expectation that would
1349
+ serve it, why each expectation ahead of it was passed over, and which
1350
+ lower-priority expectations it shadows. Read-only: it consumes no Times
1351
+ and moves no scenario.
1352
+
1353
+ Raises :class:`ValueError` for a blank id, and :class:`MockServerError`
1354
+ with the server's reason when no request is recorded with this id (never
1355
+ received, cleared or evicted) or it is not an HTTP request.
1356
+ """
1357
+ if not correlation_id or not correlation_id.strip():
1358
+ raise ValueError("explain requires a non blank correlation_id")
1359
+ data = await self._diagnostic(
1360
+ "/mockserver/explain", None, {"correlationId": correlation_id},
1361
+ f'explain failed for correlationId "{correlation_id}"',
1362
+ )
1363
+ return RequestExplanation.from_dict(data)
1364
+
1365
+ async def debug_mismatch(self, request: HttpRequest | dict) -> DebugMismatchResult:
1366
+ """Compare a request with every active expectation.
1367
+
1368
+ Issues ``PUT /mockserver/debugMismatch`` and returns, for each expectation,
1369
+ whether it matches and the per-field differences, plus the closest match.
1370
+ Only an HTTP request is supported; any other definition raises
1371
+ :class:`MockServerError`.
1372
+ """
1373
+ payload = request.to_dict() if hasattr(request, "to_dict") else request
1374
+ data = await self._diagnostic(
1375
+ "/mockserver/debugMismatch", json.dumps(payload), None, "debugMismatch failed"
1376
+ )
1377
+ return DebugMismatchResult.from_dict(data)
1378
+
1379
+ async def explain_unmatched(
1380
+ self, limit: int | None = None, suggest_fixes: bool | None = None
1381
+ ) -> ExplainUnmatchedResult:
1382
+ """Explain why recent requests matched no expectation.
1383
+
1384
+ Issues ``PUT /mockserver/explainUnmatched`` and returns each recent
1385
+ unmatched request with its closest expectations, closest first. ``limit``
1386
+ bounds how many requests are analysed (server default 10);
1387
+ ``suggest_fixes`` adds a suggested JSON patch to each of the five closest
1388
+ non-matching expectations (nothing is applied).
1389
+ """
1390
+ options: dict[str, Any] = {}
1391
+ if limit is not None:
1392
+ options["limit"] = limit
1393
+ if suggest_fixes is not None:
1394
+ options["suggestFixes"] = suggest_fixes
1395
+ data = await self._diagnostic(
1396
+ "/mockserver/explainUnmatched", json.dumps(options) if options else None, None,
1397
+ "explainUnmatched failed",
1398
+ )
1399
+ return ExplainUnmatchedResult.from_dict(data)
1400
+
1218
1401
  async def retrieve_expectations_as_code(
1219
1402
  self, fmt: str = "java", request: HttpRequest | None = None
1220
1403
  ) -> str:
@@ -1333,7 +1516,7 @@ class AsyncMockServerClient:
1333
1516
 
1334
1517
  def when(
1335
1518
  self,
1336
- request: HttpRequest,
1519
+ request: HttpRequest | ConditionalRequestDefinition,
1337
1520
  times: Times | None = None,
1338
1521
  time_to_live: TimeToLive | None = None,
1339
1522
  priority: int | None = None,