mockserver-client 7.0.0__tar.gz → 7.1.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 (27) hide show
  1. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/PKG-INFO +113 -1
  2. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/README.md +112 -0
  3. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver/__init__.py +14 -0
  4. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver/async_client.py +185 -4
  5. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver/client.py +62 -4
  6. mockserver_client-7.1.0/mockserver/launcher.py +573 -0
  7. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver/models.py +28 -3
  8. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver/websocket_client.py +150 -2
  9. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver_client.egg-info/PKG-INFO +113 -1
  10. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver_client.egg-info/SOURCES.txt +3 -0
  11. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/pyproject.toml +2 -2
  12. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/tests/test_async_client.py +38 -0
  13. mockserver_client-7.1.0/tests/test_breakpoint.py +481 -0
  14. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/tests/test_client.py +35 -0
  15. mockserver_client-7.1.0/tests/test_launcher.py +817 -0
  16. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/tests/test_models.py +299 -0
  17. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver/exceptions.py +0 -0
  18. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver/fluent.py +0 -0
  19. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver_client.egg-info/dependency_links.txt +0 -0
  20. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver_client.egg-info/requires.txt +0 -0
  21. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver_client.egg-info/top_level.txt +0 -0
  22. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/setup.cfg +0 -0
  23. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/tests/test_exceptions.py +0 -0
  24. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/tests/test_fluent.py +0 -0
  25. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/tests/test_init.py +0 -0
  26. {mockserver_client-7.0.0 → mockserver_client-7.1.0}/tests/test_integration.py +0 -0
  27. {mockserver_client-7.0.0 → mockserver_client-7.1.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.0.0
3
+ Version: 7.1.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
@@ -273,6 +273,118 @@ response = (
273
273
  )
274
274
  ```
275
275
 
276
+ ## Interactive Breakpoints
277
+
278
+ The client supports matcher-driven interactive breakpoints over the callback WebSocket. Register a breakpoint matcher to pause forwarded/proxied exchanges at specific phases and inspect/modify/continue them via callback handlers.
279
+
280
+ ### Register a breakpoint (sync client)
281
+
282
+ ```python
283
+ from mockserver import MockServerClient, HttpRequest, HttpResponse
284
+
285
+ client = MockServerClient("localhost", 1080)
286
+
287
+ # REQUEST phase only
288
+ bp_id = client.add_request_breakpoint(
289
+ HttpRequest(path="/api/.*"),
290
+ lambda request: request, # continue unchanged (or return HttpResponse to abort)
291
+ )
292
+
293
+ # REQUEST + RESPONSE
294
+ bp_id = client.add_request_and_response_breakpoint(
295
+ HttpRequest(path="/api/.*"),
296
+ lambda request: request, # REQUEST handler
297
+ lambda request, response: response, # RESPONSE handler
298
+ )
299
+
300
+ # All phases with stream frame handler
301
+ bp_id = client.add_breakpoint(
302
+ HttpRequest(path="/stream/.*"),
303
+ ["REQUEST", "RESPONSE", "RESPONSE_STREAM", "INBOUND_STREAM"],
304
+ request_handler=lambda request: request,
305
+ response_handler=lambda request, response: response,
306
+ stream_frame_handler=lambda frame: {"action": "CONTINUE"},
307
+ # Other actions: MODIFY (with body), DROP, INJECT (with body), CLOSE
308
+ )
309
+ ```
310
+
311
+ ### Manage breakpoints
312
+
313
+ ```python
314
+ # List all matchers
315
+ matchers = client.list_breakpoint_matchers() # {"matchers": [...]}
316
+
317
+ # Remove a specific matcher
318
+ client.remove_breakpoint_matcher(bp_id)
319
+
320
+ # Clear all matchers
321
+ client.clear_breakpoint_matchers()
322
+ ```
323
+
324
+ The async client (`AsyncMockServerClient`) exposes the same methods as coroutines.
325
+
326
+ ## Start / Launch MockServer
327
+
328
+ The Python client can download and launch a local MockServer instance directly -- no Java installation and no Docker required. The launcher downloads a self-contained platform bundle (`mockserver-<version>-<os>-<arch>`) from the GitHub Release, verifies its SHA-256, caches it per-user, and starts it.
329
+
330
+ ### Quick start
331
+
332
+ ```python
333
+ from mockserver.launcher import start, MockServerProcess
334
+
335
+ # Download (first run) and start MockServer on port 1080
336
+ with start(port=1080) as server:
337
+ print(f"MockServer running on port {server.port}, PID {server.pid}")
338
+ # ... use MockServer ...
339
+ # Server is stopped automatically when the context manager exits
340
+ ```
341
+
342
+ ### Just ensure the binary is present
343
+
344
+ ```python
345
+ from mockserver.launcher import ensure_binary
346
+
347
+ launcher_path = ensure_binary() # returns Path to the launcher executable
348
+ ```
349
+
350
+ ### Specify a version
351
+
352
+ ```python
353
+ from mockserver.launcher import start
354
+
355
+ server = start(port=1080, version="7.1.0")
356
+ # ...
357
+ server.stop()
358
+ ```
359
+
360
+ ### API reference
361
+
362
+ | Function / Class | Description |
363
+ |---|---|
364
+ | `ensure_binary(version=None, *, log=True)` | Download, verify, cache, and return the launcher `Path`. Defaults to the client's own version. |
365
+ | `start(port, version=None, *, extra_args=None, log=True)` | Ensure the binary and start MockServer. Returns a `MockServerProcess`. |
366
+ | `MockServerProcess` | Handle to the running process. Properties: `port`, `pid`, `launcher`, `returncode`. Methods: `stop(timeout=10.0)`. Supports `with` statement. |
367
+
368
+ ### Supported platforms
369
+
370
+ | OS | Architecture |
371
+ |---|---|
372
+ | Linux | x86_64, aarch64 |
373
+ | macOS (darwin) | x86_64, aarch64 |
374
+ | Windows | x86_64, aarch64 |
375
+
376
+ ### Environment variables
377
+
378
+ | Variable | Purpose |
379
+ |---|---|
380
+ | `MOCKSERVER_BINARY_BASE_URL` | Mirror host for the release assets (corporate / air-gapped networks) |
381
+ | `MOCKSERVER_BINARY_CACHE` | Override the cache directory (default: `~/.cache/mockserver/binaries` on Unix) |
382
+ | `MOCKSERVER_SKIP_BINARY_DOWNLOAD` | Fail instead of downloading (use with a pre-seeded cache in CI) |
383
+
384
+ ### Version
385
+
386
+ By default the launcher downloads the MockServer version matching this client package (currently the version set in `pyproject.toml`). Pass an explicit `version` argument to override.
387
+
276
388
  ## Requirements
277
389
 
278
390
  - Python 3.9+
@@ -243,6 +243,118 @@ response = (
243
243
  )
244
244
  ```
245
245
 
246
+ ## Interactive Breakpoints
247
+
248
+ The client supports matcher-driven interactive breakpoints over the callback WebSocket. Register a breakpoint matcher to pause forwarded/proxied exchanges at specific phases and inspect/modify/continue them via callback handlers.
249
+
250
+ ### Register a breakpoint (sync client)
251
+
252
+ ```python
253
+ from mockserver import MockServerClient, HttpRequest, HttpResponse
254
+
255
+ client = MockServerClient("localhost", 1080)
256
+
257
+ # REQUEST phase only
258
+ bp_id = client.add_request_breakpoint(
259
+ HttpRequest(path="/api/.*"),
260
+ lambda request: request, # continue unchanged (or return HttpResponse to abort)
261
+ )
262
+
263
+ # REQUEST + RESPONSE
264
+ bp_id = client.add_request_and_response_breakpoint(
265
+ HttpRequest(path="/api/.*"),
266
+ lambda request: request, # REQUEST handler
267
+ lambda request, response: response, # RESPONSE handler
268
+ )
269
+
270
+ # All phases with stream frame handler
271
+ bp_id = client.add_breakpoint(
272
+ HttpRequest(path="/stream/.*"),
273
+ ["REQUEST", "RESPONSE", "RESPONSE_STREAM", "INBOUND_STREAM"],
274
+ request_handler=lambda request: request,
275
+ response_handler=lambda request, response: response,
276
+ stream_frame_handler=lambda frame: {"action": "CONTINUE"},
277
+ # Other actions: MODIFY (with body), DROP, INJECT (with body), CLOSE
278
+ )
279
+ ```
280
+
281
+ ### Manage breakpoints
282
+
283
+ ```python
284
+ # List all matchers
285
+ matchers = client.list_breakpoint_matchers() # {"matchers": [...]}
286
+
287
+ # Remove a specific matcher
288
+ client.remove_breakpoint_matcher(bp_id)
289
+
290
+ # Clear all matchers
291
+ client.clear_breakpoint_matchers()
292
+ ```
293
+
294
+ The async client (`AsyncMockServerClient`) exposes the same methods as coroutines.
295
+
296
+ ## Start / Launch MockServer
297
+
298
+ The Python client can download and launch a local MockServer instance directly -- no Java installation and no Docker required. The launcher downloads a self-contained platform bundle (`mockserver-<version>-<os>-<arch>`) from the GitHub Release, verifies its SHA-256, caches it per-user, and starts it.
299
+
300
+ ### Quick start
301
+
302
+ ```python
303
+ from mockserver.launcher import start, MockServerProcess
304
+
305
+ # Download (first run) and start MockServer on port 1080
306
+ with start(port=1080) as server:
307
+ print(f"MockServer running on port {server.port}, PID {server.pid}")
308
+ # ... use MockServer ...
309
+ # Server is stopped automatically when the context manager exits
310
+ ```
311
+
312
+ ### Just ensure the binary is present
313
+
314
+ ```python
315
+ from mockserver.launcher import ensure_binary
316
+
317
+ launcher_path = ensure_binary() # returns Path to the launcher executable
318
+ ```
319
+
320
+ ### Specify a version
321
+
322
+ ```python
323
+ from mockserver.launcher import start
324
+
325
+ server = start(port=1080, version="7.1.0")
326
+ # ...
327
+ server.stop()
328
+ ```
329
+
330
+ ### API reference
331
+
332
+ | Function / Class | Description |
333
+ |---|---|
334
+ | `ensure_binary(version=None, *, log=True)` | Download, verify, cache, and return the launcher `Path`. Defaults to the client's own version. |
335
+ | `start(port, version=None, *, extra_args=None, log=True)` | Ensure the binary and start MockServer. Returns a `MockServerProcess`. |
336
+ | `MockServerProcess` | Handle to the running process. Properties: `port`, `pid`, `launcher`, `returncode`. Methods: `stop(timeout=10.0)`. Supports `with` statement. |
337
+
338
+ ### Supported platforms
339
+
340
+ | OS | Architecture |
341
+ |---|---|
342
+ | Linux | x86_64, aarch64 |
343
+ | macOS (darwin) | x86_64, aarch64 |
344
+ | Windows | x86_64, aarch64 |
345
+
346
+ ### Environment variables
347
+
348
+ | Variable | Purpose |
349
+ |---|---|
350
+ | `MOCKSERVER_BINARY_BASE_URL` | Mirror host for the release assets (corporate / air-gapped networks) |
351
+ | `MOCKSERVER_BINARY_CACHE` | Override the cache directory (default: `~/.cache/mockserver/binaries` on Unix) |
352
+ | `MOCKSERVER_SKIP_BINARY_DOWNLOAD` | Fail instead of downloading (use with a pre-seeded cache in CI) |
353
+
354
+ ### Version
355
+
356
+ By default the launcher downloads the MockServer version matching this client package (currently the version set in `pyproject.toml`). Pass an explicit `version` argument to override.
357
+
246
358
  ## Requirements
247
359
 
248
360
  - Python 3.9+
@@ -50,6 +50,14 @@ from mockserver.exceptions import (
50
50
  )
51
51
  from mockserver.client import MockServerClient
52
52
  from mockserver.async_client import AsyncMockServerClient
53
+ from mockserver.launcher import (
54
+ MockServerProcess,
55
+ bundle_base_name,
56
+ cache_dir,
57
+ ensure_binary,
58
+ resolve_platform,
59
+ start as start_server,
60
+ )
53
61
 
54
62
  __all__ = [
55
63
  "AfterAction",
@@ -100,4 +108,10 @@ __all__ = [
100
108
  "VerificationSequence",
101
109
  "VerificationTimes",
102
110
  "WebSocketMessage",
111
+ "MockServerProcess",
112
+ "bundle_base_name",
113
+ "cache_dir",
114
+ "ensure_binary",
115
+ "resolve_platform",
116
+ "start_server",
103
117
  ]
@@ -284,10 +284,12 @@ class AsyncMockServerClient:
284
284
 
285
285
  async def verify(
286
286
  self,
287
- request: HttpRequest,
287
+ request: HttpRequest | None = None,
288
288
  times: VerificationTimes | None = None,
289
+ *,
290
+ response: HttpResponse | None = None,
289
291
  ) -> None:
290
- verification = Verification(http_request=request, times=times)
292
+ verification = Verification(http_request=request, http_response=response, times=times)
291
293
  body = json.dumps(verification.to_dict())
292
294
  status, response_body = await self._request("PUT", "/mockserver/verify", body)
293
295
  if status == 406:
@@ -297,9 +299,14 @@ class AsyncMockServerClient:
297
299
  f"Failed to verify (status={status}): {response_body}"
298
300
  )
299
301
 
300
- async def verify_sequence(self, *requests: HttpRequest) -> None:
302
+ async def verify_sequence(
303
+ self,
304
+ *requests: HttpRequest,
305
+ responses: list[HttpResponse] | None = None,
306
+ ) -> None:
301
307
  verification = VerificationSequence(
302
- http_requests=list(requests)
308
+ http_requests=list(requests) if requests else None,
309
+ http_responses=responses,
303
310
  )
304
311
  body = json.dumps(verification.to_dict())
305
312
  status, response_body = await self._request(
@@ -504,6 +511,180 @@ class AsyncMockServerClient:
504
511
  )
505
512
  return await self.upsert(expectation)
506
513
 
514
+ # -------------------------------------------------------------------
515
+ # Breakpoint matcher management
516
+ # -------------------------------------------------------------------
517
+
518
+ async def _ensure_breakpoint_websocket(self) -> MockServerWebSocketClient:
519
+ """Ensure a callback WS is connected for breakpoint use, returning it."""
520
+ # Serialise lazy creation so concurrent add_breakpoint coroutines
521
+ # (e.g. via asyncio.gather) cannot each create a duplicate breakpoint WS
522
+ # across the await in connect(). Creating/assigning the lock has no await
523
+ # between the check and the set, so it is atomic on the event loop.
524
+ lock = getattr(self, '_breakpoint_ws_lock', None)
525
+ if lock is None:
526
+ lock = asyncio.Lock()
527
+ self._breakpoint_ws_lock = lock # type: ignore[attr-defined]
528
+ async with lock:
529
+ # Reuse the first WS client tagged as breakpoint, or create one
530
+ for ws in self._websocket_clients:
531
+ if getattr(ws, '_is_breakpoint_ws', False):
532
+ return ws
533
+
534
+ ws_client = MockServerWebSocketClient()
535
+ await ws_client.connect(
536
+ self._host,
537
+ self._port,
538
+ self._context_path,
539
+ self._secure,
540
+ self._ca_cert_path,
541
+ tls_verify=self._tls_verify,
542
+ )
543
+ ws_client._is_breakpoint_ws = True # type: ignore[attr-defined]
544
+ ws_client._listen_task = asyncio.create_task(ws_client.listen())
545
+ self._websocket_clients.append(ws_client)
546
+ return ws_client
547
+
548
+ async def add_breakpoint(
549
+ self,
550
+ matcher: HttpRequest,
551
+ phases: list[str],
552
+ request_handler: Callable | None = None,
553
+ response_handler: Callable | None = None,
554
+ stream_frame_handler: Callable | None = None,
555
+ ) -> str:
556
+ """Register a breakpoint matcher with callback handlers.
557
+
558
+ The callback WebSocket is opened lazily on the first call and reused.
559
+
560
+ Args:
561
+ matcher: the request definition to match (same as expectation matcher)
562
+ phases: list of phase strings: "REQUEST", "RESPONSE",
563
+ "RESPONSE_STREAM", "INBOUND_STREAM"
564
+ request_handler: callable(HttpRequest) -> HttpRequest|HttpResponse
565
+ for REQUEST phase (optional)
566
+ response_handler: callable(HttpRequest, HttpResponse) -> HttpResponse
567
+ for RESPONSE phase (optional)
568
+ stream_frame_handler: callable(dict) -> dict for streaming phases
569
+ (optional). Receives PausedStreamFrameDTO dict,
570
+ returns StreamFrameDecisionDTO dict.
571
+
572
+ Returns:
573
+ The server-assigned breakpoint matcher id (UUID string).
574
+ """
575
+ if matcher is None:
576
+ raise ValueError("add_breakpoint requires a non-None matcher")
577
+ if not phases:
578
+ raise ValueError("add_breakpoint requires a non-empty phases list")
579
+
580
+ ws_client = await self._ensure_breakpoint_websocket()
581
+ client_id = ws_client.client_id
582
+
583
+ body = json.dumps({
584
+ "httpRequest": matcher.to_dict(),
585
+ "phases": phases,
586
+ "clientId": client_id,
587
+ })
588
+ status, response_body = await self._request(
589
+ "PUT", "/mockserver/breakpoint/matcher", body
590
+ )
591
+ if status >= 400:
592
+ raise MockServerError(
593
+ f"Failed to register breakpoint matcher (status={status}): {response_body}"
594
+ )
595
+
596
+ parsed = json.loads(response_body) if response_body else {}
597
+ breakpoint_id = parsed.get("id")
598
+ if not breakpoint_id:
599
+ raise MockServerError("Server did not return a breakpoint id")
600
+
601
+ # Install per-breakpoint-id handlers
602
+ if request_handler:
603
+ ws_client.set_breakpoint_request_handler(breakpoint_id, request_handler)
604
+ if response_handler:
605
+ ws_client.set_breakpoint_response_handler(breakpoint_id, response_handler)
606
+ if stream_frame_handler:
607
+ ws_client.set_breakpoint_stream_frame_handler(breakpoint_id, stream_frame_handler)
608
+
609
+ return breakpoint_id
610
+
611
+ async def add_request_breakpoint(
612
+ self,
613
+ matcher: HttpRequest,
614
+ request_handler: Callable,
615
+ ) -> str:
616
+ """Convenience: register a REQUEST-only breakpoint."""
617
+ return await self.add_breakpoint(
618
+ matcher, ["REQUEST"], request_handler=request_handler
619
+ )
620
+
621
+ async def add_request_and_response_breakpoint(
622
+ self,
623
+ matcher: HttpRequest,
624
+ request_handler: Callable,
625
+ response_handler: Callable,
626
+ ) -> str:
627
+ """Convenience: register a REQUEST+RESPONSE breakpoint."""
628
+ return await self.add_breakpoint(
629
+ matcher,
630
+ ["REQUEST", "RESPONSE"],
631
+ request_handler=request_handler,
632
+ response_handler=response_handler,
633
+ )
634
+
635
+ async def list_breakpoint_matchers(self) -> dict:
636
+ """List all registered breakpoint matchers.
637
+
638
+ Returns a dict: {"matchers": [{id, httpRequest, phases, clientId}, ...]}.
639
+ """
640
+ status, response_body = await self._request(
641
+ "GET", "/mockserver/breakpoint/matchers"
642
+ )
643
+ if status >= 400:
644
+ raise MockServerError(
645
+ f"Failed to list breakpoint matchers (status={status}): {response_body}"
646
+ )
647
+ return json.loads(response_body) if response_body else {}
648
+
649
+ async def remove_breakpoint_matcher(self, breakpoint_id: str) -> dict:
650
+ """Remove a breakpoint matcher by id.
651
+
652
+ Returns a dict: {"status": "removed", "id": "..."} or raises on 404.
653
+ """
654
+ if not breakpoint_id:
655
+ raise ValueError("remove_breakpoint_matcher requires a non-empty id")
656
+ body = json.dumps({"id": breakpoint_id})
657
+ status, response_body = await self._request(
658
+ "PUT", "/mockserver/breakpoint/matcher/remove", body
659
+ )
660
+ if status >= 400:
661
+ raise MockServerError(
662
+ f"Failed to remove breakpoint matcher (status={status}): {response_body}"
663
+ )
664
+ # Remove client-side handlers
665
+ for ws in self._websocket_clients:
666
+ if getattr(ws, '_is_breakpoint_ws', False):
667
+ ws.remove_breakpoint_handlers(breakpoint_id)
668
+ return json.loads(response_body) if response_body else {}
669
+
670
+ async def clear_breakpoint_matchers(self) -> dict:
671
+ """Clear all registered breakpoint matchers.
672
+
673
+ Returns a dict: {"status": "cleared", "count": N}.
674
+ """
675
+ status, response_body = await self._request(
676
+ "PUT", "/mockserver/breakpoint/matcher/clear"
677
+ )
678
+ if status >= 400:
679
+ raise MockServerError(
680
+ f"Failed to clear breakpoint matchers (status={status}): {response_body}"
681
+ )
682
+ # Clear client-side handlers
683
+ for ws in self._websocket_clients:
684
+ if getattr(ws, '_is_breakpoint_ws', False):
685
+ ws.clear_breakpoint_handlers()
686
+ return json.loads(response_body) if response_body else {}
687
+
507
688
  async def _register_websocket_callback(
508
689
  self,
509
690
  callback_type: str,
@@ -169,13 +169,19 @@ class MockServerClient:
169
169
 
170
170
  def verify(
171
171
  self,
172
- request: HttpRequest,
172
+ request: HttpRequest | None = None,
173
173
  times: VerificationTimes | None = None,
174
+ *,
175
+ response: HttpResponse | None = None,
174
176
  ) -> None:
175
- return self._run(self._async_client.verify(request, times))
177
+ return self._run(self._async_client.verify(request, times, response=response))
176
178
 
177
- def verify_sequence(self, *requests: HttpRequest) -> None:
178
- return self._run(self._async_client.verify_sequence(*requests))
179
+ def verify_sequence(
180
+ self,
181
+ *requests: HttpRequest,
182
+ responses: list[HttpResponse] | None = None,
183
+ ) -> None:
184
+ return self._run(self._async_client.verify_sequence(*requests, responses=responses))
179
185
 
180
186
  def verify_zero_interactions(self) -> None:
181
187
  return self._run(self._async_client.verify_zero_interactions())
@@ -237,6 +243,58 @@ class MockServerClient:
237
243
  self._async_client.mock_with_callback(request, callback, times, time_to_live)
238
244
  )
239
245
 
246
+ # -------------------------------------------------------------------
247
+ # Breakpoint matcher management
248
+ # -------------------------------------------------------------------
249
+
250
+ def add_breakpoint(
251
+ self,
252
+ matcher,
253
+ phases: list[str],
254
+ request_handler=None,
255
+ response_handler=None,
256
+ stream_frame_handler=None,
257
+ ) -> str:
258
+ """Register a breakpoint matcher with callback handlers.
259
+
260
+ Returns the server-assigned breakpoint matcher id (UUID string).
261
+ """
262
+ return self._run(
263
+ self._async_client.add_breakpoint(
264
+ matcher, phases, request_handler, response_handler, stream_frame_handler
265
+ )
266
+ )
267
+
268
+ def add_request_breakpoint(self, matcher, request_handler) -> str:
269
+ """Convenience: register a REQUEST-only breakpoint."""
270
+ return self._run(
271
+ self._async_client.add_request_breakpoint(matcher, request_handler)
272
+ )
273
+
274
+ def add_request_and_response_breakpoint(
275
+ self, matcher, request_handler, response_handler
276
+ ) -> str:
277
+ """Convenience: register a REQUEST+RESPONSE breakpoint."""
278
+ return self._run(
279
+ self._async_client.add_request_and_response_breakpoint(
280
+ matcher, request_handler, response_handler
281
+ )
282
+ )
283
+
284
+ def list_breakpoint_matchers(self) -> dict:
285
+ """List all registered breakpoint matchers."""
286
+ return self._run(self._async_client.list_breakpoint_matchers())
287
+
288
+ def remove_breakpoint_matcher(self, breakpoint_id: str) -> dict:
289
+ """Remove a breakpoint matcher by id."""
290
+ return self._run(
291
+ self._async_client.remove_breakpoint_matcher(breakpoint_id)
292
+ )
293
+
294
+ def clear_breakpoint_matchers(self) -> dict:
295
+ """Clear all registered breakpoint matchers."""
296
+ return self._run(self._async_client.clear_breakpoint_matchers())
297
+
240
298
  def mock_with_forward_callback(
241
299
  self,
242
300
  request: HttpRequest,