mockserver-client 7.1.0__tar.gz → 7.2.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 (33) hide show
  1. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/PKG-INFO +2 -2
  2. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/README.md +1 -1
  3. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/mockserver/__init__.py +88 -0
  4. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/mockserver/async_client.py +368 -7
  5. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/mockserver/client.py +150 -1
  6. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/mockserver/fluent.py +31 -3
  7. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/mockserver/launcher.py +27 -2
  8. mockserver_client-7.2.0/mockserver/llm.py +1021 -0
  9. mockserver_client-7.2.0/mockserver/mcp.py +434 -0
  10. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/mockserver/models.py +363 -4
  11. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/mockserver/websocket_client.py +42 -14
  12. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/mockserver_client.egg-info/PKG-INFO +2 -2
  13. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/mockserver_client.egg-info/SOURCES.txt +5 -0
  14. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/pyproject.toml +1 -1
  15. mockserver_client-7.2.0/tests/test_async_client.py +882 -0
  16. mockserver_client-7.2.0/tests/test_auth_tls.py +205 -0
  17. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/tests/test_client.py +363 -2
  18. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/tests/test_fluent.py +37 -2
  19. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/tests/test_launcher.py +28 -0
  20. mockserver_client-7.2.0/tests/test_llm.py +579 -0
  21. mockserver_client-7.2.0/tests/test_mcp.py +257 -0
  22. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/tests/test_models.py +185 -1
  23. mockserver_client-7.1.0/tests/test_async_client.py +0 -475
  24. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/mockserver/exceptions.py +0 -0
  25. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/mockserver_client.egg-info/dependency_links.txt +0 -0
  26. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/mockserver_client.egg-info/requires.txt +0 -0
  27. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/mockserver_client.egg-info/top_level.txt +0 -0
  28. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/setup.cfg +0 -0
  29. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/tests/test_breakpoint.py +0 -0
  30. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/tests/test_exceptions.py +0 -0
  31. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/tests/test_init.py +0 -0
  32. {mockserver_client-7.1.0 → mockserver_client-7.2.0}/tests/test_integration.py +0 -0
  33. {mockserver_client-7.1.0 → mockserver_client-7.2.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.1.0
3
+ Version: 7.2.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
@@ -352,7 +352,7 @@ launcher_path = ensure_binary() # returns Path to the launcher executable
352
352
  ```python
353
353
  from mockserver.launcher import start
354
354
 
355
- server = start(port=1080, version="7.1.0")
355
+ server = start(port=1080, version="7.2.0")
356
356
  # ...
357
357
  server.stop()
358
358
  ```
@@ -322,7 +322,7 @@ launcher_path = ensure_binary() # returns Path to the launcher executable
322
322
  ```python
323
323
  from mockserver.launcher import start
324
324
 
325
- server = start(port=1080, version="7.1.0")
325
+ server = start(port=1080, version="7.2.0")
326
326
  # ...
327
327
  server.stop()
328
328
  ```
@@ -3,6 +3,8 @@ from mockserver.models import (
3
3
  BinaryResponse,
4
4
  Body,
5
5
  ConnectionOptions,
6
+ CrossProtocolScenario,
7
+ CrossProtocolTrigger,
6
8
  Delay,
7
9
  DelayDistribution,
8
10
  DnsRecord,
@@ -26,12 +28,18 @@ from mockserver.models import (
26
28
  HttpSseResponse,
27
29
  HttpTemplate,
28
30
  HttpWebSocketResponse,
31
+ JsonPathBody,
29
32
  JsonRpcBody,
30
33
  KeyToMultiValue,
34
+ LoadProfile,
35
+ LoadScenario,
36
+ LoadStage,
37
+ LoadStep,
31
38
  OpenAPIDefinition,
32
39
  OpenAPIExpectation,
33
40
  Ports,
34
41
  RequestDefinition,
42
+ ResponseMode,
35
43
  SocketAddress,
36
44
  SseEvent,
37
45
  TimeToLive,
@@ -41,6 +49,37 @@ from mockserver.models import (
41
49
  VerificationTimes,
42
50
  WebSocketMessage,
43
51
  )
52
+ from mockserver.llm import (
53
+ Completion,
54
+ ConversationPredicates,
55
+ EmbeddingResponse,
56
+ HttpLlmResponse,
57
+ IsolationSource,
58
+ LlmChaosProfile,
59
+ LlmConversationBuilder,
60
+ LlmFailoverBuilder,
61
+ LlmMockBuilder,
62
+ NormalizationOptions,
63
+ Provider,
64
+ Role,
65
+ StreamingPhysics,
66
+ ToolUse,
67
+ TurnBuilder,
68
+ Usage,
69
+ completion,
70
+ conversation,
71
+ cookie,
72
+ embedding,
73
+ header,
74
+ llm_failover,
75
+ llm_mock,
76
+ llm_response,
77
+ query_parameter,
78
+ streaming_physics,
79
+ time_to_first_token,
80
+ tool_use,
81
+ usage,
82
+ )
44
83
  from mockserver.exceptions import (
45
84
  MockServerCallbackError,
46
85
  MockServerConnectionError,
@@ -49,6 +88,13 @@ from mockserver.exceptions import (
49
88
  MockServerWebSocketError,
50
89
  )
51
90
  from mockserver.client import MockServerClient
91
+ from mockserver.mcp import (
92
+ McpMockBuilder,
93
+ McpPromptBuilder,
94
+ McpResourceBuilder,
95
+ McpToolBuilder,
96
+ mcp_mock,
97
+ )
52
98
  from mockserver.async_client import AsyncMockServerClient
53
99
  from mockserver.launcher import (
54
100
  MockServerProcess,
@@ -65,6 +111,8 @@ __all__ = [
65
111
  "BinaryResponse",
66
112
  "Body",
67
113
  "ConnectionOptions",
114
+ "CrossProtocolScenario",
115
+ "CrossProtocolTrigger",
68
116
  "Delay",
69
117
  "DelayDistribution",
70
118
  "DnsRecord",
@@ -88,8 +136,47 @@ __all__ = [
88
136
  "HttpSseResponse",
89
137
  "HttpTemplate",
90
138
  "HttpWebSocketResponse",
139
+ "JsonPathBody",
91
140
  "JsonRpcBody",
92
141
  "KeyToMultiValue",
142
+ "LoadProfile",
143
+ "LoadScenario",
144
+ "LoadStage",
145
+ "LoadStep",
146
+ "Completion",
147
+ "ConversationPredicates",
148
+ "EmbeddingResponse",
149
+ "HttpLlmResponse",
150
+ "IsolationSource",
151
+ "LlmChaosProfile",
152
+ "LlmConversationBuilder",
153
+ "LlmFailoverBuilder",
154
+ "LlmMockBuilder",
155
+ "NormalizationOptions",
156
+ "Provider",
157
+ "Role",
158
+ "StreamingPhysics",
159
+ "ToolUse",
160
+ "TurnBuilder",
161
+ "Usage",
162
+ "completion",
163
+ "conversation",
164
+ "cookie",
165
+ "embedding",
166
+ "header",
167
+ "llm_failover",
168
+ "llm_mock",
169
+ "llm_response",
170
+ "query_parameter",
171
+ "streaming_physics",
172
+ "time_to_first_token",
173
+ "tool_use",
174
+ "usage",
175
+ "McpMockBuilder",
176
+ "McpPromptBuilder",
177
+ "McpResourceBuilder",
178
+ "McpToolBuilder",
179
+ "mcp_mock",
93
180
  "MockServerCallbackError",
94
181
  "MockServerClient",
95
182
  "MockServerConnectionError",
@@ -100,6 +187,7 @@ __all__ = [
100
187
  "OpenAPIExpectation",
101
188
  "Ports",
102
189
  "RequestDefinition",
190
+ "ResponseMode",
103
191
  "SocketAddress",
104
192
  "SseEvent",
105
193
  "TimeToLive",
@@ -23,6 +23,7 @@ from mockserver.models import (
23
23
  HttpRequest,
24
24
  HttpRequestAndHttpResponse,
25
25
  HttpResponse,
26
+ LoadScenario,
26
27
  OpenAPIExpectation,
27
28
  Ports,
28
29
  TimeToLive,
@@ -36,6 +37,59 @@ from mockserver.websocket_client import MockServerWebSocketClient
36
37
  logger = logging.getLogger(__name__)
37
38
 
38
39
 
40
+ class AsyncScenarioHandle:
41
+ """An async handle to a single named stateful scenario on the server.
42
+
43
+ Obtained via :meth:`AsyncMockServerClient.scenario`. Wraps the
44
+ ``/mockserver/scenario/{name}`` control-plane endpoints. Each method returns
45
+ the server's JSON response as a dict (with ``scenarioName`` and
46
+ ``currentState`` keys).
47
+ """
48
+
49
+ def __init__(self, client: AsyncMockServerClient, name: str) -> None:
50
+ self._client = client
51
+ self._name = name
52
+
53
+ @property
54
+ def name(self) -> str:
55
+ return self._name
56
+
57
+ async def state(self) -> str | None:
58
+ """GET the current state of this scenario (``None`` if not yet set)."""
59
+ result = await self._client._scenario_request(
60
+ "GET", f"/mockserver/scenario/{urllib.parse.quote(self._name, safe="")}"
61
+ )
62
+ return result.get("currentState")
63
+
64
+ async def set(
65
+ self,
66
+ state: str,
67
+ transition_after_ms: int | None = None,
68
+ next_state: str | None = None,
69
+ ) -> dict:
70
+ """PUT to set this scenario's state, optionally scheduling a timed
71
+ transition to ``next_state`` after ``transition_after_ms`` milliseconds.
72
+ """
73
+ payload: dict = {"state": state}
74
+ if transition_after_ms is not None:
75
+ payload["transitionAfterMs"] = transition_after_ms
76
+ if next_state is not None:
77
+ payload["nextState"] = next_state
78
+ return await self._client._scenario_request(
79
+ "PUT",
80
+ f"/mockserver/scenario/{urllib.parse.quote(self._name, safe="")}",
81
+ json.dumps(payload),
82
+ )
83
+
84
+ async def trigger(self, new_state: str) -> dict:
85
+ """PUT an external trigger advancing this scenario to ``new_state``."""
86
+ return await self._client._scenario_request(
87
+ "PUT",
88
+ f"/mockserver/scenario/{urllib.parse.quote(self._name, safe="")}/trigger",
89
+ json.dumps({"newState": new_state}),
90
+ )
91
+
92
+
39
93
  class AsyncMockServerClient:
40
94
  def __init__(
41
95
  self,
@@ -45,6 +99,10 @@ class AsyncMockServerClient:
45
99
  secure: bool = False,
46
100
  ca_cert_path: str | None = None,
47
101
  tls_verify: bool = True,
102
+ client_cert_path: str | None = None,
103
+ client_key_path: str | None = None,
104
+ control_plane_bearer_token: str | None = None,
105
+ control_plane_bearer_token_supplier: Callable[[], str] | None = None,
48
106
  ) -> None:
49
107
  self._host = host
50
108
  self._port = port
@@ -52,6 +110,10 @@ class AsyncMockServerClient:
52
110
  self._secure = secure
53
111
  self._ca_cert_path = ca_cert_path
54
112
  self._tls_verify = tls_verify
113
+ self._client_cert_path = client_cert_path
114
+ self._client_key_path = client_key_path
115
+ self._control_plane_bearer_token = control_plane_bearer_token
116
+ self._control_plane_bearer_token_supplier = control_plane_bearer_token_supplier
55
117
  self._websocket_clients: list[MockServerWebSocketClient] = []
56
118
 
57
119
  scheme = "https" if secure else "http"
@@ -68,30 +130,57 @@ class AsyncMockServerClient:
68
130
  elif not tls_verify:
69
131
  self._ssl_context.check_hostname = False
70
132
  self._ssl_context.verify_mode = ssl.CERT_NONE
133
+ if client_cert_path:
134
+ # Load the client certificate chain (and private key) for mTLS.
135
+ self._ssl_context.load_cert_chain(
136
+ certfile=client_cert_path, keyfile=client_key_path
137
+ )
138
+
139
+ def _control_plane_bearer(self) -> str | None:
140
+ """Resolve the control-plane bearer token for the current request.
141
+
142
+ The supplier (if configured) is evaluated per request so rotating /
143
+ short-lived tokens are picked up; otherwise the static token is used.
144
+ """
145
+ if self._control_plane_bearer_token_supplier is not None:
146
+ return self._control_plane_bearer_token_supplier()
147
+ return self._control_plane_bearer_token
71
148
 
72
149
  async def _request(
73
150
  self,
74
151
  method: str,
75
152
  path: str,
76
- body: str | None = None,
153
+ body: str | bytes | None = None,
77
154
  query_params: dict[str, str] | None = None,
155
+ content_type: str = "application/json; charset=utf-8",
78
156
  ) -> tuple[int, str]:
79
157
  url = f"{self._base_url}{path}"
80
158
  if query_params:
81
159
  url = f"{url}?{urllib.parse.urlencode(query_params)}"
82
160
 
83
- data = body.encode("utf-8") if body else None
161
+ if isinstance(body, bytes):
162
+ data = body
163
+ elif body:
164
+ data = body.encode("utf-8")
165
+ else:
166
+ data = None
84
167
  req = urllib.request.Request(url, data=data, method=method)
85
- req.add_header("Content-Type", "application/json; charset=utf-8")
168
+ req.add_header("Content-Type", content_type)
169
+ bearer = self._control_plane_bearer()
170
+ if bearer:
171
+ req.add_header("Authorization", f"Bearer {bearer}")
86
172
 
87
173
  def _do_request() -> tuple[int, str]:
88
174
  try:
89
- response = urllib.request.urlopen(
175
+ with urllib.request.urlopen(
90
176
  req, context=self._ssl_context, timeout=60
91
- )
92
- return response.status, response.read().decode("utf-8")
177
+ ) as response:
178
+ return response.status, response.read().decode("utf-8")
93
179
  except urllib.error.HTTPError as e:
94
- return e.code, e.read().decode("utf-8")
180
+ # HTTPError is itself a closeable, response-like object; reading
181
+ # its body without closing leaks the underlying socket/fd.
182
+ with e:
183
+ return e.code, e.read().decode("utf-8")
95
184
  except socket.timeout as e:
96
185
  raise MockServerConnectionError(
97
186
  f"Request to MockServer at {self._base_url} timed out: {e}"
@@ -227,6 +316,53 @@ class AsyncMockServerClient:
227
316
  )
228
317
  return json.loads(response_body) if response_body else {}
229
318
 
319
+ async def upload_grpc_descriptor(self, descriptor_set_bytes: bytes) -> None:
320
+ """Upload a compiled protobuf descriptor set so gRPC requests can be matched.
321
+
322
+ *descriptor_set_bytes* must be the raw bytes of a
323
+ ``FileDescriptorSet`` (e.g. the output of
324
+ ``protoc --descriptor_set_out=... --include_imports``). The bytes are
325
+ sent verbatim (not base64-encoded) as the request body.
326
+ """
327
+ if not descriptor_set_bytes:
328
+ raise MockServerError("descriptor set bytes must not be empty")
329
+ status, response_body = await self._request(
330
+ "PUT",
331
+ "/mockserver/grpc/descriptors",
332
+ descriptor_set_bytes,
333
+ content_type="application/octet-stream",
334
+ )
335
+ if status >= 400:
336
+ raise MockServerError(
337
+ f"Failed to upload gRPC descriptor (status={status}): {response_body}"
338
+ )
339
+
340
+ async def retrieve_grpc_services(self) -> list[dict]:
341
+ """Retrieve the gRPC services registered from uploaded descriptor sets.
342
+
343
+ Returns a list of service dicts, each with a ``name`` and a list of
344
+ ``methods`` (``name``, ``inputType``, ``outputType``,
345
+ ``clientStreaming``, ``serverStreaming``).
346
+ """
347
+ status, response_body = await self._request(
348
+ "PUT", "/mockserver/grpc/services"
349
+ )
350
+ if status >= 400:
351
+ raise MockServerError(
352
+ f"Failed to retrieve gRPC services (status={status}): {response_body}"
353
+ )
354
+ return json.loads(response_body) if response_body else []
355
+
356
+ async def clear_grpc_descriptors(self) -> None:
357
+ """Clear all uploaded gRPC descriptor sets and registered services."""
358
+ status, response_body = await self._request(
359
+ "PUT", "/mockserver/grpc/clear"
360
+ )
361
+ if status >= 400:
362
+ raise MockServerError(
363
+ f"Failed to clear gRPC descriptors (status={status}): {response_body}"
364
+ )
365
+
230
366
  async def set_service_chaos(
231
367
  self, host: str, chaos: HttpChaosProfile, ttl_millis: int | None = None
232
368
  ) -> dict:
@@ -282,6 +418,188 @@ class AsyncMockServerClient:
282
418
  )
283
419
  return json.loads(response_body) if response_body else {}
284
420
 
421
+ @staticmethod
422
+ def _load_generation_disabled_error(action: str, response_body: str) -> MockServerError:
423
+ return MockServerError(
424
+ f"Failed to {action}: load generation is disabled "
425
+ "(start MockServer with loadGenerationEnabled to enable it) "
426
+ f"(status=403): {response_body}"
427
+ )
428
+
429
+ async def load_scenario(self, scenario: LoadScenario | dict) -> dict:
430
+ """Register (load) a load-injection scenario in the server-side registry.
431
+
432
+ *scenario* is a :class:`LoadScenario` (or an equivalent dict) and must
433
+ carry a unique ``name``. Registration adds the scenario in the ``LOADED``
434
+ state but does **not** start generating load; it is allowed even when the
435
+ server was started without ``loadGenerationEnabled``.
436
+
437
+ Returns the JSON ``{"name": ..., "state": ...}`` for the registered
438
+ scenario. Use :meth:`start_load_scenarios` to begin generating load.
439
+ """
440
+ payload = scenario.to_dict() if hasattr(scenario, "to_dict") else scenario
441
+ body = json.dumps(payload)
442
+ status, response_body = await self._request(
443
+ "PUT", "/mockserver/loadScenario", body
444
+ )
445
+ if status >= 400:
446
+ raise MockServerError(
447
+ f"Failed to register load scenario (status={status}): {response_body}"
448
+ )
449
+ return json.loads(response_body) if response_body else {}
450
+
451
+ async def load_scenarios(self) -> dict:
452
+ """List every registered load scenario.
453
+
454
+ Returns ``{"scenarios": [{"name", "state", "definition", "status"?}, ...]}``.
455
+ """
456
+ status, response_body = await self._request("GET", "/mockserver/loadScenario")
457
+ if status >= 400:
458
+ raise MockServerError(
459
+ f"Failed to list load scenarios (status={status}): {response_body}"
460
+ )
461
+ return json.loads(response_body) if response_body else {}
462
+
463
+ async def get_load_scenario(self, name: str) -> dict:
464
+ """Retrieve a single registered load scenario by *name*.
465
+
466
+ Raises :class:`MockServerError` if the scenario does not exist (``404``).
467
+ """
468
+ status, response_body = await self._request(
469
+ "GET", f"/mockserver/loadScenario/{urllib.parse.quote(name, safe='')}"
470
+ )
471
+ if status == 404:
472
+ raise MockServerError(
473
+ f"Load scenario '{name}' not found (status=404): {response_body}"
474
+ )
475
+ if status >= 400:
476
+ raise MockServerError(
477
+ f"Failed to get load scenario '{name}' (status={status}): {response_body}"
478
+ )
479
+ return json.loads(response_body) if response_body else {}
480
+
481
+ async def delete_load_scenario(self, name: str) -> dict:
482
+ """Remove a single registered load scenario by *name* (stops it if running)."""
483
+ status, response_body = await self._request(
484
+ "DELETE", f"/mockserver/loadScenario/{urllib.parse.quote(name, safe='')}"
485
+ )
486
+ if status == 404:
487
+ raise MockServerError(
488
+ f"Load scenario '{name}' not found (status=404): {response_body}"
489
+ )
490
+ if status >= 400:
491
+ raise MockServerError(
492
+ f"Failed to delete load scenario '{name}' (status={status}): {response_body}"
493
+ )
494
+ return json.loads(response_body) if response_body else {}
495
+
496
+ async def clear_load_scenarios(self) -> dict:
497
+ """Remove every registered load scenario (stopping any that are running)."""
498
+ status, response_body = await self._request(
499
+ "DELETE", "/mockserver/loadScenario"
500
+ )
501
+ if status >= 400:
502
+ raise MockServerError(
503
+ f"Failed to clear load scenarios (status={status}): {response_body}"
504
+ )
505
+ return json.loads(response_body) if response_body else {}
506
+
507
+ async def start_load_scenarios(self, names: str | list[str]) -> dict:
508
+ """Start one or more registered load scenarios.
509
+
510
+ *names* may be a single scenario name or a list of names. The server must
511
+ have been started with ``loadGenerationEnabled``; otherwise this raises
512
+ :class:`MockServerError` reporting the ``403`` response. Honours each
513
+ scenario's ``startDelayMillis``.
514
+
515
+ Returns ``{"started": [{"name", "state"}, ...], "status": ...}``.
516
+ """
517
+ if isinstance(names, str):
518
+ names = [names]
519
+ body = json.dumps({"names": list(names)})
520
+ status, response_body = await self._request(
521
+ "PUT", "/mockserver/loadScenario/start", body
522
+ )
523
+ if status == 403:
524
+ raise self._load_generation_disabled_error(
525
+ "start load scenarios", response_body
526
+ )
527
+ if status == 404:
528
+ raise MockServerError(
529
+ f"Unknown load scenario (status=404): {response_body}"
530
+ )
531
+ if status >= 400:
532
+ raise MockServerError(
533
+ f"Failed to start load scenarios (status={status}): {response_body}"
534
+ )
535
+ return json.loads(response_body) if response_body else {}
536
+
537
+ async def _scenario_request(
538
+ self, method: str, path: str, body: str | None = None
539
+ ) -> dict:
540
+ status, response_body = await self._request(method, path, body)
541
+ if status >= 400:
542
+ raise MockServerError(
543
+ f"Scenario request failed (status={status}): {response_body}"
544
+ )
545
+ return json.loads(response_body) if response_body else {}
546
+
547
+ def scenario(self, name: str) -> AsyncScenarioHandle:
548
+ """Return a handle to the named stateful scenario, wrapping the
549
+ ``/mockserver/scenario/{name}`` control-plane endpoints.
550
+ """
551
+ return AsyncScenarioHandle(self, name)
552
+
553
+ async def scenarios(self) -> list[dict]:
554
+ """List every known scenario and its current state.
555
+
556
+ Returns a list of dicts each with ``scenarioName`` and ``currentState``.
557
+ """
558
+ result = await self._scenario_request("GET", "/mockserver/scenario")
559
+ return result.get("scenarios", [])
560
+
561
+ async def stop_load_scenarios(self, names: str | list[str] | None = None) -> dict:
562
+ """Stop running load scenarios.
563
+
564
+ *names* may be a single name, a list of names, or ``None`` to stop every
565
+ running scenario.
566
+
567
+ Returns ``{"stopped": [...], "status": ...}``.
568
+ """
569
+ if names is None:
570
+ payload: dict = {}
571
+ elif isinstance(names, str):
572
+ payload = {"names": [names]}
573
+ else:
574
+ payload = {"names": list(names)}
575
+ body = json.dumps(payload)
576
+ status, response_body = await self._request(
577
+ "PUT", "/mockserver/loadScenario/stop", body
578
+ )
579
+ if status >= 400:
580
+ raise MockServerError(
581
+ f"Failed to stop load scenarios (status={status}): {response_body}"
582
+ )
583
+ return json.loads(response_body) if response_body else {}
584
+
585
+ async def run_load_scenario(self, scenario: LoadScenario | dict) -> dict:
586
+ """Convenience: register *scenario* then immediately start it.
587
+
588
+ Requires the server to have been started with ``loadGenerationEnabled``
589
+ (the start step responds ``403`` otherwise). Returns the start result
590
+ (``{"started": [...], "status": ...}``).
591
+ """
592
+ registered = await self.load_scenario(scenario)
593
+ name = registered.get("name") if isinstance(registered, dict) else None
594
+ if not name:
595
+ payload = scenario.to_dict() if hasattr(scenario, "to_dict") else scenario
596
+ name = payload.get("name") if isinstance(payload, dict) else None
597
+ if not name:
598
+ raise MockServerError(
599
+ "Cannot run load scenario: scenario has no name to start"
600
+ )
601
+ return await self.start_load_scenarios(name)
602
+
285
603
  async def verify(
286
604
  self,
287
605
  request: HttpRequest | None = None,
@@ -385,6 +703,49 @@ class AsyncMockServerClient:
385
703
  return [Expectation.from_dict(e) for e in parsed]
386
704
  return []
387
705
 
706
+ async def retrieve_expectations_as_code(
707
+ self, fmt: str = "java", request: HttpRequest | None = None
708
+ ) -> str:
709
+ """Retrieve the active expectations as MockServer SDK setup code.
710
+
711
+ Returns the builder code that recreates the expectations, generated in
712
+ the requested language (``fmt`` is one of ``java``, ``javascript``,
713
+ ``python``, ``go``, ``csharp``, ``ruby``, ``rust`` or ``php``).
714
+ """
715
+ body = json.dumps(request.to_dict()) if request else ""
716
+ status, response_body = await self._request(
717
+ "PUT",
718
+ "/mockserver/retrieve",
719
+ body,
720
+ {"type": "ACTIVE_EXPECTATIONS", "format": fmt.upper()},
721
+ )
722
+ if status >= 400:
723
+ raise MockServerError(
724
+ f"Failed to retrieve expectations as code (status={status}): {response_body}"
725
+ )
726
+ return response_body or ""
727
+
728
+ async def retrieve_recorded_expectations_as_code(
729
+ self, fmt: str = "java", request: HttpRequest | None = None
730
+ ) -> str:
731
+ """Retrieve the recorded (proxied) request/response pairs as MockServer
732
+ SDK setup code, generated in the requested language (``fmt`` is one of
733
+ ``java``, ``javascript``, ``python``, ``go``, ``csharp``, ``ruby``,
734
+ ``rust`` or ``php``).
735
+ """
736
+ body = json.dumps(request.to_dict()) if request else ""
737
+ status, response_body = await self._request(
738
+ "PUT",
739
+ "/mockserver/retrieve",
740
+ body,
741
+ {"type": "RECORDED_EXPECTATIONS", "format": fmt.upper()},
742
+ )
743
+ if status >= 400:
744
+ raise MockServerError(
745
+ f"Failed to retrieve recorded expectations as code (status={status}): {response_body}"
746
+ )
747
+ return response_body or ""
748
+
388
749
  async def retrieve_recorded_requests_and_responses(
389
750
  self, request: HttpRequest | None = None
390
751
  ) -> list[HttpRequestAndHttpResponse]: