mockserver-client 5.16.0__py3-none-any.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.
@@ -0,0 +1,314 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import uuid
7
+ from typing import Any, Callable
8
+
9
+ import websockets
10
+ import websockets.client
11
+ import websockets.exceptions
12
+
13
+ from mockserver.exceptions import MockServerCallbackError, MockServerWebSocketError
14
+ from mockserver.models import (
15
+ HttpRequest,
16
+ HttpRequestAndHttpResponse,
17
+ HttpResponse,
18
+ KeyToMultiValue,
19
+ )
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ WEB_SOCKET_CORRELATION_ID_HEADER_NAME = "WebSocketCorrelationId"
24
+ CLIENT_REGISTRATION_ID_HEADER = "X-CLIENT-REGISTRATION-ID"
25
+
26
+ WEBSOCKET_PATH = "/_mockserver_callback_websocket"
27
+
28
+ TYPE_HTTP_REQUEST = "org.mockserver.model.HttpRequest"
29
+ TYPE_HTTP_RESPONSE = "org.mockserver.model.HttpResponse"
30
+ TYPE_HTTP_REQUEST_AND_RESPONSE = "org.mockserver.model.HttpRequestAndHttpResponse"
31
+ TYPE_CLIENT_ID_DTO = "org.mockserver.serialization.model.WebSocketClientIdDTO"
32
+ TYPE_ERROR_DTO = "org.mockserver.serialization.model.WebSocketErrorDTO"
33
+
34
+ MAX_RECONNECT_ATTEMPTS = 3
35
+
36
+
37
+ def _extract_correlation_id(request: HttpRequest) -> str | None:
38
+ if request.headers is None:
39
+ return None
40
+ for header in request.headers:
41
+ if header.name == WEB_SOCKET_CORRELATION_ID_HEADER_NAME:
42
+ if header.values:
43
+ return header.values[0]
44
+ return None
45
+
46
+
47
+ def _add_correlation_id_header(
48
+ message: HttpResponse | HttpRequest, correlation_id: str
49
+ ) -> HttpResponse | HttpRequest:
50
+ if message.headers is None:
51
+ message.headers = []
52
+ for header in message.headers:
53
+ if header.name == WEB_SOCKET_CORRELATION_ID_HEADER_NAME:
54
+ header.values = [correlation_id]
55
+ return message
56
+ message.headers.append(
57
+ KeyToMultiValue(name=WEB_SOCKET_CORRELATION_ID_HEADER_NAME, values=[correlation_id])
58
+ )
59
+ return message
60
+
61
+
62
+ def _build_ws_message(type_name: str, value_dict: dict) -> str:
63
+ return json.dumps({
64
+ "type": type_name,
65
+ "value": json.dumps(value_dict),
66
+ })
67
+
68
+
69
+ def _build_error_message(error_msg: str, correlation_id: str) -> str:
70
+ return json.dumps({
71
+ "type": TYPE_ERROR_DTO,
72
+ "value": json.dumps({
73
+ "message": error_msg,
74
+ "webSocketCorrelationId": correlation_id,
75
+ }),
76
+ })
77
+
78
+
79
+ def _clean_context_path(context_path: str) -> str:
80
+ if not context_path:
81
+ return ""
82
+ if not context_path.startswith("/"):
83
+ return "/" + context_path
84
+ return context_path
85
+
86
+
87
+ class MockServerWebSocketClient:
88
+ def __init__(self) -> None:
89
+ self._ws: websockets.client.WebSocketClientProtocol | None = None
90
+ self._client_id: str | None = None
91
+ self._response_callback: Callable | None = None
92
+ self._forward_callback: Callable | None = None
93
+ self._forward_response_callback: Callable | None = None
94
+ self._stopped = False
95
+ self._listen_task: asyncio.Task | None = None
96
+ self._host: str = ""
97
+ self._port: int = 0
98
+ self._context_path: str = ""
99
+ self._secure: bool = False
100
+ self._ssl_context: Any = None
101
+
102
+ @property
103
+ def is_connected(self) -> bool:
104
+ return self._ws is not None and self._ws.open
105
+
106
+ @property
107
+ def client_id(self) -> str | None:
108
+ return self._client_id
109
+
110
+ async def connect(
111
+ self,
112
+ host: str,
113
+ port: int,
114
+ context_path: str = "",
115
+ secure: bool = False,
116
+ ca_cert_path: str | None = None,
117
+ client_id: str | None = None,
118
+ tls_verify: bool = True,
119
+ ) -> str:
120
+ self._host = host
121
+ self._port = port
122
+ self._context_path = context_path
123
+ self._secure = secure
124
+
125
+ if secure:
126
+ import ssl
127
+ self._ssl_context = ssl.create_default_context()
128
+ if ca_cert_path:
129
+ self._ssl_context.load_verify_locations(ca_cert_path)
130
+ elif not tls_verify:
131
+ self._ssl_context.check_hostname = False
132
+ self._ssl_context.verify_mode = ssl.CERT_NONE
133
+
134
+ registration_id = client_id or str(uuid.uuid4())
135
+
136
+ await self._do_connect(registration_id)
137
+ return self._client_id
138
+
139
+ async def _do_connect(self, registration_id: str) -> None:
140
+ scheme = "wss" if self._secure else "ws"
141
+ path = _clean_context_path(self._context_path) + WEBSOCKET_PATH
142
+ uri = f"{scheme}://{self._host}:{self._port}{path}"
143
+
144
+ extra_headers = {CLIENT_REGISTRATION_ID_HEADER: registration_id}
145
+
146
+ self._ws = await websockets.client.connect(
147
+ uri,
148
+ additional_headers=extra_headers,
149
+ ssl=self._ssl_context if self._secure else None,
150
+ open_timeout=10,
151
+ ping_interval=20,
152
+ ping_timeout=20,
153
+ close_timeout=5,
154
+ )
155
+
156
+ registration_msg = await asyncio.wait_for(self._ws.recv(), timeout=10.0)
157
+ parsed = json.loads(registration_msg)
158
+ if parsed.get("type") == TYPE_CLIENT_ID_DTO:
159
+ value = json.loads(parsed["value"])
160
+ self._client_id = value["clientId"]
161
+ else:
162
+ raise MockServerWebSocketError(
163
+ f"Expected WebSocketClientIdDTO but received: {parsed.get('type')}"
164
+ )
165
+
166
+ def register_response_callback(self, callback_fn: Callable) -> None:
167
+ self._response_callback = callback_fn
168
+
169
+ def register_forward_callback(
170
+ self,
171
+ forward_fn: Callable,
172
+ response_fn: Callable | None = None,
173
+ ) -> None:
174
+ self._forward_callback = forward_fn
175
+ self._forward_response_callback = response_fn
176
+
177
+ async def listen(self) -> None:
178
+ reconnect_attempts = 0
179
+ while not self._stopped:
180
+ try:
181
+ async for raw_message in self._ws:
182
+ await self._handle_message(raw_message)
183
+ except websockets.exceptions.ConnectionClosed:
184
+ if self._stopped:
185
+ break
186
+ reconnect_attempts += 1
187
+ if reconnect_attempts > MAX_RECONNECT_ATTEMPTS:
188
+ logger.error("Max reconnect attempts reached, giving up")
189
+ break
190
+ logger.warning(
191
+ "WebSocket disconnected, reconnecting (attempt %d/%d)",
192
+ reconnect_attempts,
193
+ MAX_RECONNECT_ATTEMPTS,
194
+ )
195
+ try:
196
+ await self._do_connect(self._client_id or str(uuid.uuid4()))
197
+ reconnect_attempts = 0
198
+ except Exception:
199
+ logger.exception("Reconnection failed")
200
+ await asyncio.sleep(1.0)
201
+ except Exception:
202
+ if self._stopped:
203
+ break
204
+ logger.exception("Unexpected error in WebSocket listen loop")
205
+ break
206
+
207
+ async def _handle_message(self, raw_message: str) -> None:
208
+ parsed = json.loads(raw_message)
209
+ msg_type = parsed.get("type")
210
+ msg_value = parsed.get("value")
211
+
212
+ if msg_type == TYPE_CLIENT_ID_DTO:
213
+ value = json.loads(msg_value)
214
+ self._client_id = value["clientId"]
215
+ return
216
+
217
+ if msg_type == TYPE_HTTP_REQUEST:
218
+ request = HttpRequest.from_dict(json.loads(msg_value))
219
+ correlation_id = _extract_correlation_id(request)
220
+
221
+ if self._forward_callback is not None:
222
+ await self._handle_forward_request(request, correlation_id)
223
+ elif self._response_callback is not None:
224
+ await self._handle_response_request(request, correlation_id)
225
+ return
226
+
227
+ if msg_type == TYPE_HTTP_REQUEST_AND_RESPONSE:
228
+ req_and_resp = HttpRequestAndHttpResponse.from_dict(json.loads(msg_value))
229
+ correlation_id = _extract_correlation_id(req_and_resp.http_request)
230
+
231
+ if self._forward_response_callback is not None:
232
+ await self._handle_forward_response(req_and_resp, correlation_id)
233
+ return
234
+
235
+ logger.warning("Received unhandled WebSocket message type: %s", msg_type)
236
+
237
+ async def _handle_response_request(
238
+ self, request: HttpRequest, correlation_id: str | None
239
+ ) -> None:
240
+ try:
241
+ result = self._response_callback(request)
242
+ if asyncio.iscoroutine(result) or asyncio.isfuture(result):
243
+ result = await result
244
+ if not isinstance(result, HttpResponse):
245
+ raise MockServerCallbackError(
246
+ f"Response callback must return HttpResponse, got {type(result)}"
247
+ )
248
+ if correlation_id:
249
+ _add_correlation_id_header(result, correlation_id)
250
+ msg = _build_ws_message(TYPE_HTTP_RESPONSE, result.to_dict())
251
+ await self._ws.send(msg)
252
+ except Exception as exc:
253
+ logger.exception("Error in response callback")
254
+ if correlation_id:
255
+ error_msg = _build_error_message(str(exc), correlation_id)
256
+ await self._ws.send(error_msg)
257
+
258
+ async def _handle_forward_request(
259
+ self, request: HttpRequest, correlation_id: str | None
260
+ ) -> None:
261
+ try:
262
+ result = self._forward_callback(request)
263
+ if asyncio.iscoroutine(result) or asyncio.isfuture(result):
264
+ result = await result
265
+ if not isinstance(result, HttpRequest):
266
+ raise MockServerCallbackError(
267
+ f"Forward callback must return HttpRequest, got {type(result)}"
268
+ )
269
+ if correlation_id:
270
+ _add_correlation_id_header(result, correlation_id)
271
+ msg = _build_ws_message(TYPE_HTTP_REQUEST, result.to_dict())
272
+ await self._ws.send(msg)
273
+ except Exception as exc:
274
+ logger.exception("Error in forward callback")
275
+ if correlation_id:
276
+ error_msg = _build_error_message(str(exc), correlation_id)
277
+ await self._ws.send(error_msg)
278
+
279
+ async def _handle_forward_response(
280
+ self,
281
+ req_and_resp: HttpRequestAndHttpResponse,
282
+ correlation_id: str | None,
283
+ ) -> None:
284
+ try:
285
+ result = self._forward_response_callback(
286
+ req_and_resp.http_request, req_and_resp.http_response
287
+ )
288
+ if asyncio.iscoroutine(result) or asyncio.isfuture(result):
289
+ result = await result
290
+ if not isinstance(result, HttpResponse):
291
+ raise MockServerCallbackError(
292
+ f"Forward response callback must return HttpResponse, got {type(result)}"
293
+ )
294
+ if correlation_id:
295
+ _add_correlation_id_header(result, correlation_id)
296
+ msg = _build_ws_message(TYPE_HTTP_RESPONSE, result.to_dict())
297
+ await self._ws.send(msg)
298
+ except Exception as exc:
299
+ logger.exception("Error in forward response callback")
300
+ if correlation_id:
301
+ error_msg = _build_error_message(str(exc), correlation_id)
302
+ await self._ws.send(error_msg)
303
+
304
+ async def close(self) -> None:
305
+ self._stopped = True
306
+ if self._listen_task and not self._listen_task.done():
307
+ self._listen_task.cancel()
308
+ try:
309
+ await self._listen_task
310
+ except asyncio.CancelledError:
311
+ pass
312
+ if self._ws and self._ws.open:
313
+ await self._ws.close()
314
+ self._ws = None
@@ -0,0 +1,291 @@
1
+ Metadata-Version: 2.4
2
+ Name: mockserver-client
3
+ Version: 5.16.0
4
+ Summary: Official Python client for MockServer — create expectations, verify requests, and register dynamic callbacks via WebSocket
5
+ Author-email: James Bloom <jamesdbloom@gmail.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://www.mock-server.com
8
+ Project-URL: Repository, https://github.com/mock-server/mockserver-monorepo
9
+ Project-URL: Documentation, https://www.mock-server.com/mock_server/getting_started.html
10
+ Project-URL: Changelog, https://www.mock-server.com/mock_server/changelog.html
11
+ Project-URL: Bug Tracker, https://github.com/mock-server/mockserver-monorepo/issues
12
+ Keywords: MockServer,HTTP Mock,Proxy,Testing,API,WebSocket,Callback,MCP,AI
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Testing
22
+ Classifier: Topic :: Internet :: WWW/HTTP
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: websockets>=15.0.1
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.4.2; extra == "dev"
28
+ Requires-Dist: pytest-asyncio>=1.2.0; extra == "dev"
29
+ Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
30
+
31
+ # MockServer Python Client
32
+
33
+ Python client for [MockServer](https://www.mock-server.com) with full WebSocket callback support.
34
+
35
+ ## Features
36
+
37
+ - **Full REST API**: Create expectations, verify requests, clear/reset, retrieve recorded data
38
+ - **Response Callbacks**: Register Python functions that dynamically generate responses via WebSocket
39
+ - **Forward Callbacks**: Modify requests before they are forwarded to the real server
40
+ - **Forward+Response Callbacks**: Modify both the forwarded request and the response
41
+ - **Fluent API**: `client.when(request).respond(callback)` — mirrors the Java client
42
+ - **Async + Sync**: Native asyncio API with a synchronous wrapper for non-async code
43
+ - **Minimal Dependencies**: Only `websockets` (for callback support)
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install mockserver-client
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ ### Synchronous API
54
+
55
+ ```python
56
+ from mockserver import MockServerClient, HttpRequest, HttpResponse
57
+
58
+ client = MockServerClient("localhost", 1080)
59
+
60
+ # Static expectation
61
+ client.when(
62
+ HttpRequest.request("/api/users").with_method("GET")
63
+ ).respond(
64
+ HttpResponse.response('{"users": []}', status_code=200)
65
+ )
66
+
67
+ # Verify
68
+ client.verify(
69
+ HttpRequest.request("/api/users").with_method("GET"),
70
+ VerificationTimes.at_least(1)
71
+ )
72
+
73
+ # Clean up
74
+ client.reset()
75
+ client.close()
76
+ ```
77
+
78
+ ### Context Manager
79
+
80
+ ```python
81
+ with MockServerClient("localhost", 1080) as client:
82
+ client.when(
83
+ HttpRequest.request("/hello")
84
+ ).respond(
85
+ HttpResponse.response("world")
86
+ )
87
+ ```
88
+
89
+ ### Async API
90
+
91
+ ```python
92
+ import asyncio
93
+ from mockserver import AsyncMockServerClient, HttpRequest, HttpResponse
94
+
95
+ async def main():
96
+ async with AsyncMockServerClient("localhost", 1080) as client:
97
+ await client.when(
98
+ HttpRequest.request("/api/data")
99
+ ).respond(
100
+ HttpResponse.response('{"key": "value"}')
101
+ )
102
+
103
+ asyncio.run(main())
104
+ ```
105
+
106
+ ## Response Callbacks
107
+
108
+ Register a Python function that generates responses dynamically when matching requests arrive:
109
+
110
+ ```python
111
+ from mockserver import MockServerClient, HttpRequest, HttpResponse
112
+
113
+ def handle_request(request):
114
+ if request.method == "POST":
115
+ return HttpResponse.response("created", status_code=201)
116
+ return HttpResponse.not_found_response()
117
+
118
+ client = MockServerClient("localhost", 1080)
119
+ client.mock_with_callback(
120
+ HttpRequest.request("/api/callback"),
121
+ handle_request
122
+ )
123
+ ```
124
+
125
+ Or with the fluent API:
126
+
127
+ ```python
128
+ client.when(
129
+ HttpRequest.request("/api/callback")
130
+ ).respond(handle_request)
131
+ ```
132
+
133
+ ## Forward Callbacks
134
+
135
+ Modify requests before they are forwarded to the real server:
136
+
137
+ ```python
138
+ def modify_request(request):
139
+ return request.with_header("X-Forwarded", "true").with_path("/modified" + request.path)
140
+
141
+ client.mock_with_forward_callback(
142
+ HttpRequest.request("/proxy/.*"),
143
+ modify_request
144
+ )
145
+ ```
146
+
147
+ ## Forward+Response Callbacks
148
+
149
+ Modify both the forwarded request and the response:
150
+
151
+ ```python
152
+ def modify_request(request):
153
+ return request.with_header("X-Proxied", "true")
154
+
155
+ def modify_response(request, response):
156
+ return response.with_header("X-Modified", "true")
157
+
158
+ client.mock_with_forward_callback(
159
+ HttpRequest.request("/proxy/.*"),
160
+ modify_request,
161
+ modify_response
162
+ )
163
+ ```
164
+
165
+ ## Verification
166
+
167
+ ```python
168
+ from mockserver import VerificationTimes
169
+
170
+ # Verify a request was received at least once
171
+ client.verify(
172
+ HttpRequest.request("/api/users").with_method("GET"),
173
+ VerificationTimes.at_least(1)
174
+ )
175
+
176
+ # Verify exact count
177
+ client.verify(
178
+ HttpRequest.request("/api/users"),
179
+ VerificationTimes.exactly(3)
180
+ )
181
+
182
+ # Verify request sequence (order matters)
183
+ client.verify_sequence(
184
+ HttpRequest.request("/first"),
185
+ HttpRequest.request("/second"),
186
+ HttpRequest.request("/third"),
187
+ )
188
+
189
+ # Verify no interactions
190
+ client.verify_zero_interactions()
191
+ ```
192
+
193
+ ## Retrieval
194
+
195
+ ```python
196
+ # Get recorded requests
197
+ requests = client.retrieve_recorded_requests(
198
+ HttpRequest.request("/api/.*")
199
+ )
200
+
201
+ # Get active expectations
202
+ expectations = client.retrieve_active_expectations()
203
+
204
+ # Get log messages
205
+ logs = client.retrieve_log_messages()
206
+ ```
207
+
208
+ ## Control
209
+
210
+ ```python
211
+ # Clear specific expectations
212
+ client.clear(HttpRequest.request("/api/users"))
213
+
214
+ # Clear by type
215
+ client.clear(HttpRequest.request("/api/users"), clear_type="LOG")
216
+
217
+ # Reset everything
218
+ client.reset()
219
+
220
+ # Bind additional ports
221
+ client.bind(1081, 1082)
222
+
223
+ # Check if running
224
+ if client.has_started():
225
+ print("MockServer is running")
226
+
227
+ # Stop
228
+ client.stop()
229
+ ```
230
+
231
+ ## TLS Support
232
+
233
+ ```python
234
+ # Uses system trust store (default — verifies certificates)
235
+ client = MockServerClient("localhost", 1080, secure=True)
236
+
237
+ # Custom CA certificate
238
+ client = MockServerClient(
239
+ "localhost", 1080,
240
+ secure=True,
241
+ ca_cert_path="/path/to/ca.pem"
242
+ )
243
+
244
+ # Disable certificate verification (testing only — NOT recommended for production)
245
+ client = MockServerClient(
246
+ "localhost", 1080,
247
+ secure=True,
248
+ tls_verify=False
249
+ )
250
+ ```
251
+
252
+ ## Domain Model
253
+
254
+ All domain model classes support builder-style chaining:
255
+
256
+ ```python
257
+ request = (
258
+ HttpRequest.request("/api/users")
259
+ .with_method("POST")
260
+ .with_header("Content-Type", "application/json")
261
+ .with_header("Authorization", "Bearer token")
262
+ .with_body('{"name": "test"}')
263
+ .with_query_param("page", "1")
264
+ .with_secure(True)
265
+ )
266
+
267
+ response = (
268
+ HttpResponse.response()
269
+ .with_status_code(201)
270
+ .with_header("Location", "/api/users/1")
271
+ .with_body('{"id": 1, "name": "test"}')
272
+ .with_delay(Delay(time_unit="SECONDS", value=1))
273
+ )
274
+ ```
275
+
276
+ ## Requirements
277
+
278
+ - Python 3.9+
279
+ - `websockets` >= 12.0 (for callback support)
280
+
281
+ ## License
282
+
283
+ Apache 2.0
284
+
285
+ ## AI Assistant Integration
286
+
287
+ MockServer includes a built-in [MCP](https://modelcontextprotocol.io) (Model Context Protocol) server that enables AI coding assistants to create expectations, verify requests, and debug HTTP traffic programmatically.
288
+
289
+ - **MCP Endpoint:** `http://localhost:1080/mockserver/mcp`
290
+ - **AI Documentation:** [llms.txt](https://www.mock-server.com/llms.txt)
291
+ - **Setup Guide:** [AI Integration](https://www.mock-server.com/mock_server/ai_mcp_setup.html)
@@ -0,0 +1,11 @@
1
+ mockserver/__init__.py,sha256=RtSodGAfkPBxKhCyWiYRjUi5WVf-XLfIDrP3fpWHyKk,1875
2
+ mockserver/async_client.py,sha256=mabjFlRI8eC59FCCx2c2G9uAFCDsSkbcCTEaDjmzm0s,15005
3
+ mockserver/client.py,sha256=etC8LV7qmFsnmWCkRzQfzj4dYaWVSaySDrdsxIvdFbc,6858
4
+ mockserver/exceptions.py,sha256=aWrjzXgYgAHzLdRDhbcEFu1-Fojcd7bkH6UDy82Hlsw,339
5
+ mockserver/fluent.py,sha256=HFa4mqtw2u_EWooS0xWlk8y9aSXk--U6F7z1UqUHWYA,4534
6
+ mockserver/models.py,sha256=7IKvHzp16BOKQTJnUiJNnzvmLl7OSRB8QvheF0SRR5s,46520
7
+ mockserver/websocket_client.py,sha256=p366vgMmoDfKOGWXOoab81yvXzVYZzS8Q5SmmpqpQSo,11397
8
+ mockserver_client-5.16.0.dist-info/METADATA,sha256=4LLGgu7f5wjwx6YTkQnQkuu-ulN__IrhyQnS-3cStyY,7545
9
+ mockserver_client-5.16.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
10
+ mockserver_client-5.16.0.dist-info/top_level.txt,sha256=MvCCh3LtK2uqvpmaxJVmKv3b1tlbpVFOCK8NFeDBJkU,11
11
+ mockserver_client-5.16.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ mockserver