locust 2.37.15.dev5__tar.gz → 2.39.1.dev1__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 (65) hide show
  1. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/PKG-INFO +5 -1
  2. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/__init__.py +4 -0
  3. locust-2.39.1.dev1/locust/_version.py +34 -0
  4. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/argument_parser.py +1 -1
  5. locust-2.39.1.dev1/locust/contrib/__init__.py +5 -0
  6. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/contrib/fasthttp.py +1 -1
  7. locust-2.39.1.dev1/locust/contrib/milvus.py +407 -0
  8. locust-2.39.1.dev1/locust/contrib/socketio.py +95 -0
  9. locust-2.39.1.dev1/locust/user/markov_taskset.py +322 -0
  10. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/user/task.py +13 -2
  11. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/web.py +6 -0
  12. locust-2.37.15.dev5/locust/webui/dist/assets/index-Csul44Gr.js → locust-2.39.1.dev1/locust/webui/dist/assets/index-BjqxSg7R.js +26 -26
  13. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/webui/dist/auth.html +1 -1
  14. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/webui/dist/index.html +1 -1
  15. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/webui/dist/report.html +1 -1
  16. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/pyproject.toml +14 -0
  17. locust-2.37.15.dev5/locust/_version.py +0 -21
  18. locust-2.37.15.dev5/locust/util/__init__.py +0 -0
  19. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/.gitignore +0 -0
  20. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/LICENSE +0 -0
  21. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/README.md +0 -0
  22. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/hatch_build.py +0 -0
  23. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/__main__.py +0 -0
  24. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/clients.py +0 -0
  25. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/contrib/mongodb.py +0 -0
  26. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/contrib/oai.py +0 -0
  27. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/contrib/postgres.py +0 -0
  28. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/debug.py +0 -0
  29. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/dispatch.py +0 -0
  30. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/env.py +0 -0
  31. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/event.py +0 -0
  32. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/exception.py +0 -0
  33. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/html.py +0 -0
  34. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/input_events.py +0 -0
  35. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/log.py +0 -0
  36. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/main.py +0 -0
  37. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/py.typed +0 -0
  38. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/rpc/__init__.py +0 -0
  39. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/rpc/protocol.py +0 -0
  40. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/rpc/zmqrpc.py +0 -0
  41. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/runners.py +0 -0
  42. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/shape.py +0 -0
  43. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/stats.py +0 -0
  44. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/user/__init__.py +0 -0
  45. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/user/inspectuser.py +0 -0
  46. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/user/sequential_taskset.py +0 -0
  47. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/user/users.py +0 -0
  48. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/user/wait_time.py +0 -0
  49. {locust-2.37.15.dev5/locust/contrib → locust-2.39.1.dev1/locust/util}/__init__.py +0 -0
  50. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/util/cache.py +0 -0
  51. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/util/date.py +0 -0
  52. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/util/deprecation.py +0 -0
  53. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/util/directory.py +0 -0
  54. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/util/exception_handler.py +0 -0
  55. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/util/load_locustfile.py +0 -0
  56. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/util/rounding.py +0 -0
  57. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/util/timespan.py +0 -0
  58. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/util/url.py +0 -0
  59. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/webui/dist/assets/favicon-dark.png +0 -0
  60. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/webui/dist/assets/favicon-light.png +0 -0
  61. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/webui/dist/assets/graphs-dark.png +0 -0
  62. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/webui/dist/assets/graphs-light.png +0 -0
  63. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/webui/dist/assets/terminal.gif +0 -0
  64. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/webui/dist/assets/testruns-dark.png +0 -0
  65. {locust-2.37.15.dev5 → locust-2.39.1.dev1}/locust/webui/dist/assets/testruns-light.png +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: locust
3
- Version: 2.37.15.dev5
3
+ Version: 2.39.1.dev1
4
4
  Summary: Developer-friendly load testing framework
5
5
  Project-URL: homepage, https://locust.io/
6
6
  Project-URL: repository, https://github.com/locustio/locust
@@ -33,6 +33,8 @@ Requires-Dist: geventhttpclient>=2.3.1
33
33
  Requires-Dist: locust-cloud>=1.26.3
34
34
  Requires-Dist: msgpack>=1.0.0
35
35
  Requires-Dist: psutil>=5.9.1
36
+ Requires-Dist: python-engineio>=4.12.2
37
+ Requires-Dist: python-socketio[client]>=5.13.0
36
38
  Requires-Dist: pywin32; sys_platform == 'win32'
37
39
  Requires-Dist: pyzmq>=25.0.0
38
40
  Requires-Dist: requests>=2.26.0; python_version <= '3.11'
@@ -41,6 +43,8 @@ Requires-Dist: setuptools>=70.0.0
41
43
  Requires-Dist: tomli>=1.1.0; python_version < '3.11'
42
44
  Requires-Dist: typing-extensions>=4.6.0; python_version < '3.11'
43
45
  Requires-Dist: werkzeug>=2.0.0
46
+ Provides-Extra: milvus
47
+ Requires-Dist: pymilvus>=2.5.0; extra == 'milvus'
44
48
  Description-Content-Type: text/markdown
45
49
 
46
50
  # Locust
@@ -27,6 +27,7 @@ from .debug import run_single_user
27
27
  from .event import Events
28
28
  from .shape import LoadTestShape
29
29
  from .user import wait_time
30
+ from .user.markov_taskset import MarkovTaskSet, transition, transitions
30
31
  from .user.sequential_taskset import SequentialTaskSet
31
32
  from .user.task import TaskSet, tag, task
32
33
  from .user.users import HttpUser, User
@@ -36,6 +37,9 @@ events = Events()
36
37
 
37
38
  __all__ = (
38
39
  "SequentialTaskSet",
40
+ "MarkovTaskSet",
41
+ "transition",
42
+ "transitions",
39
43
  "wait_time",
40
44
  "task",
41
45
  "tag",
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '2.39.1.dev1'
32
+ __version_tuple__ = version_tuple = (2, 39, 1, 'dev1')
33
+
34
+ __commit_id__ = commit_id = None
@@ -216,7 +216,7 @@ Usage: locust [options] [UserClass ...]
216
216
 
217
217
  locust --headless -u 100 -t 20m --processes 4 MyHttpUser AnotherUser
218
218
 
219
- locust --headless -u 100 -r 10 -t 50 --print-stats --html "test_report_{u}_{r}_{t}.html" -H https://www.example.com
219
+ locust --headless -u 100 -r 10 -t 50 --print-stats --html "test_report_{u}_{r}_{t}.html"
220
220
  (The above run would generate an html file with the name "test_report_100_10_50.html")
221
221
 
222
222
  See documentation for more details, including how to set options using a file or environment variables: https://docs.locust.io/en/stable/configuration.html""",
@@ -0,0 +1,5 @@
1
+ # This directory contains modules that are experimental.
2
+
3
+ # Generally they should not have any significant issues, but they may change without a major version bump for locust.
4
+
5
+ # Some modules may require optional dependencies, typically installed with `pip install locust[name-of-module]`.
@@ -231,7 +231,7 @@ class FastHttpSession:
231
231
  elif self.auth_header:
232
232
  headers["Authorization"] = self.auth_header
233
233
  if "Accept-Encoding" not in headers and "accept-encoding" not in headers:
234
- headers["Accept-Encoding"] = "gzip, deflate, br, zstd"
234
+ headers["Accept-Encoding"] = "gzip, deflate, br"
235
235
 
236
236
  if not data and json is not None:
237
237
  data = unshadowed_json.dumps(json)
@@ -0,0 +1,407 @@
1
+ import gevent.monkey
2
+
3
+ gevent.monkey.patch_all()
4
+ import grpc.experimental.gevent as grpc_gevent
5
+
6
+ grpc_gevent.init_gevent()
7
+
8
+ from locust import User, events
9
+
10
+ import time
11
+ from abc import ABC, abstractmethod
12
+ from typing import Any
13
+
14
+ from pymilvus import CollectionSchema, MilvusClient
15
+ from pymilvus.milvus_client import IndexParams
16
+
17
+
18
+ class BaseClient(ABC):
19
+ @abstractmethod
20
+ def close(self) -> None:
21
+ pass
22
+
23
+ @abstractmethod
24
+ def create_collection(self, schema, index_params) -> None:
25
+ pass
26
+
27
+ @abstractmethod
28
+ def insert(self, data) -> dict[str, Any]:
29
+ pass
30
+
31
+ @abstractmethod
32
+ def upsert(self, data) -> dict[str, Any]:
33
+ pass
34
+
35
+ @abstractmethod
36
+ def search(
37
+ self,
38
+ data,
39
+ anns_field,
40
+ limit,
41
+ filter="",
42
+ search_params=None,
43
+ output_fields=None,
44
+ calculate_recall=False,
45
+ ground_truth=None,
46
+ ) -> dict[str, Any]:
47
+ pass
48
+
49
+ @abstractmethod
50
+ def hybrid_search(self, reqs, ranker, limit, output_fields=None) -> dict[str, Any]:
51
+ pass
52
+
53
+ @abstractmethod
54
+ def query(self, filter, output_fields=None) -> dict[str, Any]:
55
+ pass
56
+
57
+ @abstractmethod
58
+ def delete(self, filter) -> dict[str, Any]:
59
+ pass
60
+
61
+
62
+ class MilvusV2Client(BaseClient):
63
+ """Milvus v2 Python SDK Client Wrapper"""
64
+
65
+ def __init__(self, uri, collection_name, token="root:Milvus", db_name="default", timeout=60):
66
+ self.uri = uri
67
+ self.collection_name = collection_name
68
+ self.token = token
69
+ self.db_name = db_name
70
+ self.timeout = timeout
71
+
72
+ # Initialize MilvusClient v2
73
+ self.client = MilvusClient(
74
+ uri=self.uri,
75
+ token=self.token,
76
+ db_name=self.db_name,
77
+ timeout=self.timeout,
78
+ )
79
+
80
+ def close(self):
81
+ self.client.close()
82
+
83
+ def create_collection(self, schema, index_params):
84
+ self.client.create_collection(
85
+ collection_name=self.collection_name,
86
+ schema=schema,
87
+ index_params=index_params,
88
+ )
89
+
90
+ def insert(self, data):
91
+ start = time.time()
92
+ try:
93
+ result = self.client.insert(collection_name=self.collection_name, data=data)
94
+ total_time = (time.time() - start) * 1000
95
+ return {"success": True, "response_time": total_time, "result": result}
96
+ except Exception as e:
97
+ return {
98
+ "success": False,
99
+ "response_time": (time.time() - start) * 1000,
100
+ "exception": e,
101
+ }
102
+
103
+ def upsert(self, data):
104
+ start = time.time()
105
+ try:
106
+ result = self.client.upsert(collection_name=self.collection_name, data=data)
107
+ total_time = (time.time() - start) * 1000
108
+ return {"success": True, "response_time": total_time, "result": result}
109
+ except Exception as e:
110
+ return {
111
+ "success": False,
112
+ "response_time": (time.time() - start) * 1000,
113
+ "exception": e,
114
+ }
115
+
116
+ def search(
117
+ self,
118
+ data,
119
+ anns_field,
120
+ limit,
121
+ filter="",
122
+ search_params=None,
123
+ output_fields=None,
124
+ calculate_recall=False,
125
+ ground_truth=None,
126
+ ):
127
+ if output_fields is None:
128
+ output_fields = ["id"]
129
+
130
+ start = time.time()
131
+ try:
132
+ result = self.client.search(
133
+ collection_name=self.collection_name,
134
+ data=data,
135
+ anns_field=anns_field,
136
+ filter=filter,
137
+ limit=limit,
138
+ search_params=search_params,
139
+ output_fields=output_fields,
140
+ )
141
+ total_time = (time.time() - start) * 1000
142
+ empty = len(result) == 0 or all(len(r) == 0 for r in result)
143
+
144
+ # Prepare base result
145
+ search_result = {
146
+ "success": not empty,
147
+ "response_time": total_time,
148
+ "empty": empty,
149
+ "result": result,
150
+ }
151
+
152
+ # Calculate recall if requested
153
+ if calculate_recall and ground_truth is not None and not empty:
154
+ recall_value = self.get_recall(result, ground_truth, limit)
155
+ search_result["recall"] = recall_value
156
+
157
+ return search_result
158
+ except Exception as e:
159
+ return {
160
+ "success": False,
161
+ "response_time": (time.time() - start) * 1000,
162
+ "exception": e,
163
+ }
164
+
165
+ def hybrid_search(self, reqs, ranker, limit, output_fields=None):
166
+ if output_fields is None:
167
+ output_fields = ["id"]
168
+
169
+ start = time.time()
170
+ try:
171
+ result = self.client.hybrid_search(
172
+ collection_name=self.collection_name,
173
+ reqs=reqs,
174
+ ranker=ranker,
175
+ limit=limit,
176
+ output_fields=output_fields,
177
+ timeout=self.timeout,
178
+ )
179
+ total_time = (time.time() - start) * 1000
180
+ empty = len(result) == 0 or all(len(r) == 0 for r in result)
181
+
182
+ # Prepare base result
183
+ search_result = {
184
+ "success": not empty,
185
+ "response_time": total_time,
186
+ "empty": empty,
187
+ "result": result,
188
+ }
189
+
190
+ return search_result
191
+ except Exception as e:
192
+ return {
193
+ "success": False,
194
+ "response_time": (time.time() - start) * 1000,
195
+ "exception": e,
196
+ }
197
+
198
+ @staticmethod
199
+ def get_recall(search_results, ground_truth, limit=None):
200
+ """Calculate recall for V2 client search results."""
201
+ try:
202
+ # Extract IDs from V2 search results
203
+ retrieved_ids = []
204
+ if isinstance(search_results, list) and len(search_results) > 0:
205
+ # search_results[0] contains the search results for the first query
206
+ for hit in search_results[0] if isinstance(search_results[0], list) else search_results:
207
+ if isinstance(hit, dict) and "id" in hit:
208
+ retrieved_ids.append(hit["id"])
209
+ elif hasattr(hit, "get"):
210
+ retrieved_ids.append(hit.get("id"))
211
+
212
+ # Apply limit if specified
213
+ if limit is None:
214
+ limit = len(retrieved_ids)
215
+
216
+ if len(ground_truth) < limit:
217
+ raise ValueError(f"Ground truth length is less than limit: {len(ground_truth)} < {limit}")
218
+
219
+ # Calculate recall
220
+ ground_truth_set = set(ground_truth[:limit])
221
+ retrieved_set = set(retrieved_ids)
222
+ intersect = len(ground_truth_set.intersection(retrieved_set))
223
+ return intersect / len(ground_truth_set)
224
+
225
+ except Exception:
226
+ return 0.0
227
+
228
+ def query(self, filter, output_fields=None):
229
+ if output_fields is None:
230
+ output_fields = ["id"]
231
+
232
+ start = time.time()
233
+ try:
234
+ result = self.client.query(
235
+ collection_name=self.collection_name,
236
+ filter=filter,
237
+ output_fields=output_fields,
238
+ )
239
+ total_time = (time.time() - start) * 1000
240
+ empty = len(result) == 0
241
+ return {
242
+ "success": not empty,
243
+ "response_time": total_time,
244
+ "empty": empty,
245
+ "result": result,
246
+ }
247
+ except Exception as e:
248
+ return {
249
+ "success": False,
250
+ "response_time": (time.time() - start) * 1000,
251
+ "exception": e,
252
+ }
253
+
254
+ def delete(self, filter):
255
+ start = time.time()
256
+ try:
257
+ result = self.client.delete(collection_name=self.collection_name, filter=filter)
258
+ total_time = (time.time() - start) * 1000
259
+ return {"success": True, "response_time": total_time, "result": result}
260
+ except Exception as e:
261
+ return {
262
+ "success": False,
263
+ "response_time": (time.time() - start) * 1000,
264
+ "exception": e,
265
+ }
266
+
267
+
268
+ # ----------------------------------
269
+ # Locust User wrapper
270
+ # ----------------------------------
271
+
272
+
273
+ class MilvusUser(User):
274
+ """Locust User implementation for Milvus operations.
275
+
276
+ This class wraps the MilvusV2Client implementation and translates
277
+ client method results into Locust request events so that performance
278
+ statistics are collected properly.
279
+
280
+ Parameters
281
+ ----------
282
+ host : str
283
+ Milvus server URI, e.g. ``"http://localhost:19530"``.
284
+ collection_name : str
285
+ The name of the collection to operate on.
286
+ **client_kwargs
287
+ Additional keyword arguments forwarded to the client.
288
+ """
289
+
290
+ abstract = True
291
+
292
+ def __init__(
293
+ self,
294
+ environment,
295
+ uri: str = "http://localhost:19530",
296
+ token: str = "root:Milvus",
297
+ collection_name: str = "test_collection",
298
+ db_name: str = "default",
299
+ timeout: int = 60,
300
+ schema: CollectionSchema | None = None,
301
+ index_params: IndexParams | None = None,
302
+ **client_kwargs,
303
+ ):
304
+ super().__init__(environment)
305
+
306
+ if uri is None:
307
+ raise ValueError("'uri' must be provided for MilvusUser")
308
+ if collection_name is None:
309
+ raise ValueError("'collection_name' must be provided for MilvusUser")
310
+
311
+ self.client_type = "milvus"
312
+ self.client = MilvusV2Client(
313
+ uri=uri,
314
+ token=token,
315
+ collection_name=collection_name,
316
+ db_name=db_name,
317
+ timeout=timeout,
318
+ )
319
+ if schema is not None:
320
+ self.client.create_collection(schema=schema, index_params=index_params)
321
+
322
+ @staticmethod
323
+ def _fire_event(request_type: str, name: str, result: dict[str, Any]):
324
+ """Emit a Locust request event from a Milvus client result dict."""
325
+ response_time = int(result.get("response_time", 0))
326
+ events.request.fire(
327
+ request_type=f"{request_type}",
328
+ name=name,
329
+ response_time=response_time,
330
+ response_length=0,
331
+ exception=result.get("exception"),
332
+ )
333
+
334
+ @staticmethod
335
+ def _fire_recall_event(request_type: str, name: str, result: dict[str, Any]):
336
+ """Emit a Locust request event for recall metric using recall value instead of response time."""
337
+ recall_value = result.get("recall", 0.0)
338
+ # Use recall value as response_time for metric display (scaled by 100 for better visualization) percentage
339
+ response_time_as_recall = int(recall_value * 100)
340
+ events.request.fire(
341
+ request_type=f"{request_type}",
342
+ name=name,
343
+ response_time=response_time_as_recall,
344
+ response_length=result.get("retrieved_count", 0),
345
+ exception=result.get("exception"),
346
+ )
347
+
348
+ def insert(self, data):
349
+ result = self.client.insert(data)
350
+ self._fire_event(self.client_type, "insert", result)
351
+ return result
352
+
353
+ def upsert(self, data):
354
+ result = self.client.upsert(data)
355
+ self._fire_event(self.client_type, "upsert", result)
356
+ return result
357
+
358
+ def search(
359
+ self,
360
+ data,
361
+ anns_field,
362
+ limit,
363
+ filter="",
364
+ search_params=None,
365
+ output_fields=None,
366
+ calculate_recall=False,
367
+ ground_truth=None,
368
+ ):
369
+ result = self.client.search(
370
+ data,
371
+ anns_field,
372
+ limit,
373
+ filter=filter,
374
+ search_params=search_params,
375
+ output_fields=output_fields,
376
+ calculate_recall=calculate_recall,
377
+ ground_truth=ground_truth,
378
+ )
379
+ # Fire search event
380
+ self._fire_event(self.client_type, "search", result)
381
+
382
+ # Fire recall event if recall was calculated
383
+ if calculate_recall and "recall" in result:
384
+ self._fire_recall_event(self.client_type, "recall", result)
385
+
386
+ return result
387
+
388
+ def hybrid_search(self, reqs, ranker, limit, output_fields=None):
389
+ result = self.client.hybrid_search(reqs, ranker, limit, output_fields)
390
+ self._fire_event(self.client_type, "hybrid_search", result)
391
+ return result
392
+
393
+ def query(self, filter, output_fields=None):
394
+ result = self.client.query(
395
+ filter=filter,
396
+ output_fields=output_fields,
397
+ )
398
+ self._fire_event(self.client_type, "query", result)
399
+ return result
400
+
401
+ def delete(self, filter):
402
+ result = self.client.delete(filter)
403
+ self._fire_event(self.client_type, "delete", result)
404
+ return result
405
+
406
+ def on_stop(self):
407
+ self.client.close()
@@ -0,0 +1,95 @@
1
+ from locust import User
2
+
3
+ import gevent
4
+ import socketio
5
+
6
+
7
+ class SocketIOUser(User):
8
+ """
9
+ SocketIOUser wraps an instance of :class:`socketio.Client` to log requests.
10
+ See example in :gh:`examples/socketio/socketio_ex.py`.
11
+ """
12
+
13
+ abstract = True
14
+ options: dict = {}
15
+ """socketio.Client options, e.g. `{"reconnection_attempts": 1, "reconnection_delay": 2}`"""
16
+ client: socketio.Client
17
+ """The underlying :class:`socketio.Client` instance. Can be useful to call directly if you want to skip logging a requests."""
18
+
19
+ def __init__(self, *args, **kwargs):
20
+ super().__init__(*args, **kwargs)
21
+ self.client = socketio.Client(**self.options)
22
+ self.ws_greenlet = gevent.spawn(self.client.wait)
23
+ self.client.on("*", self.on_message)
24
+
25
+ #
26
+ def on_message(self, event: str, data: str) -> None:
27
+ """
28
+ This is the default handler for events. You can override it for custom behavior,
29
+ or even register separate handlers using self.client.on(event, handler)
30
+
31
+ Measuring response_time isn't obvious for for WebSockets. Sometimes a response time
32
+ can be inferred from the event data (if it contains a timestamp) or related to
33
+ a message that you sent. Override this method in your User class to do that.
34
+ """
35
+ self.environment.events.request.fire(
36
+ request_type="WSR",
37
+ name=event,
38
+ response_time=0,
39
+ response_length=len(data or []),
40
+ exception=None,
41
+ context={},
42
+ )
43
+
44
+ def connect(self, *args, **kwargs):
45
+ """
46
+ Wraps :meth:`socketio.Client.connect`.
47
+ """
48
+ with self.environment.events.request.measure("WS", "connect") as _:
49
+ self.client.connect(*args, **kwargs)
50
+
51
+ def send(self, name, data=None, namespace=None) -> None:
52
+ """
53
+ Wraps :meth:`socketio.Client.send`.
54
+ """
55
+ exception = None
56
+ try:
57
+ self.client.send(data, namespace)
58
+ except Exception as e:
59
+ exception = e
60
+ self.environment.events.request.fire(
61
+ request_type="WSS",
62
+ name=name,
63
+ response_time=0,
64
+ response_length=len(data or []),
65
+ exception=exception,
66
+ context={},
67
+ )
68
+
69
+ def emit(self, name, data=None, namespace=None, callback=None) -> None:
70
+ """
71
+ Wraps :meth:`socketio.Client.emit`.
72
+ """
73
+ exception = None
74
+ try:
75
+ self.client.emit(name, data, namespace, callback)
76
+ except Exception as e:
77
+ exception = e
78
+ self.environment.events.request.fire(
79
+ request_type="WSE",
80
+ name=name,
81
+ response_time=0,
82
+ response_length=len(data or []),
83
+ exception=exception,
84
+ context={},
85
+ )
86
+
87
+ def call(self, event, data=None, *args, **kwargs):
88
+ """
89
+ Wraps :meth:`socketio.Client.call`.
90
+ """
91
+ with self.environment.events.request.measure("WSC", event) as _:
92
+ return self.client.call(event, data, *args, **kwargs)
93
+
94
+ def on_stop(self):
95
+ self.client.disconnect()