locust 2.38.1.dev6__py3-none-any.whl → 2.38.2.dev21__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.
locust/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '2.38.1.dev6'
32
- __version_tuple__ = version_tuple = (2, 38, 1, 'dev6')
31
+ __version__ = version = '2.38.2.dev21'
32
+ __version_tuple__ = version_tuple = (2, 38, 2, 'dev21')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -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]`.
@@ -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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: locust
3
- Version: 2.38.1.dev6
3
+ Version: 2.38.2.dev21
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,7 @@ 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-socketio[client]==5.13.0
36
37
  Requires-Dist: pywin32; sys_platform == 'win32'
37
38
  Requires-Dist: pyzmq>=25.0.0
38
39
  Requires-Dist: requests>=2.26.0; python_version <= '3.11'
@@ -41,6 +42,8 @@ Requires-Dist: setuptools>=70.0.0
41
42
  Requires-Dist: tomli>=1.1.0; python_version < '3.11'
42
43
  Requires-Dist: typing-extensions>=4.6.0; python_version < '3.11'
43
44
  Requires-Dist: werkzeug>=2.0.0
45
+ Provides-Extra: milvus
46
+ Requires-Dist: pymilvus>=2.5.0; extra == 'milvus'
44
47
  Description-Content-Type: text/markdown
45
48
 
46
49
  # Locust
@@ -1,6 +1,6 @@
1
1
  locust/__init__.py,sha256=HadpgGidiyCDPSKwkxrk1Qw6eB7dTmftNJVftuJzAiw,1876
2
2
  locust/__main__.py,sha256=vBQ82334kX06ImDbFlPFgiBRiLIinwNk3z8Khs6hd74,31
3
- locust/_version.py,sha256=vJeMENoCSmLjhUlWAqdotYON5kM9KKXlT5SdPmoMZk8,719
3
+ locust/_version.py,sha256=p8zeYDV-RC8SI8ouqHrCtIHFcanHPr6XX8PXeHk5P4I,721
4
4
  locust/argument_parser.py,sha256=t6mAoK9u13DxC9UH-alVqS6fFABFTyNWSJG89yQ4QQQ,33056
5
5
  locust/clients.py,sha256=o-277lWQdpmPnoRTdf3IQVNPQT8LMFDtPtuxbLHQIIs,19286
6
6
  locust/debug.py,sha256=7CCm8bIg44uGH2wqBlo1rXBzV2VzwPicLxLewz8r5CQ,5099
@@ -17,11 +17,13 @@ locust/runners.py,sha256=niYmGsfOpxMfVmTXGod4MYTefpaZ2wirFlhqxRw5mq4,70617
17
17
  locust/shape.py,sha256=t-lwBS8LOjWcKXNL7j2U3zroIXJ1b0fazUwpRYQOKXw,1973
18
18
  locust/stats.py,sha256=qyoSKT0i7RunLDj5pMGqizK1Sp8bcqUsXwh2m4_DpR8,47203
19
19
  locust/web.py,sha256=HLFN9jUtKG3sMIKu_Xw9wtvTAFxXvzDHdtLtfb_JxUQ,31849
20
- locust/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ locust/contrib/__init__.py,sha256=LtZN7MczpIAbZkN7PT2h8W2wgb9nBl6cDXbFCVsV4fo,290
21
21
  locust/contrib/fasthttp.py,sha256=QfVpcdo5oKOO1rKLXeCeTJ4t57XU1lhIIWeUeeSJtIE,29650
22
+ locust/contrib/milvus.py,sha256=YabgLd0lImzWupJFCm0OZAW-Nxeibwn91ldWpZ2irDo,12811
22
23
  locust/contrib/mongodb.py,sha256=1seUYgJOaNKwybYOP9PUEVhgl8hGy-G33f8lFj3R8W8,1246
23
24
  locust/contrib/oai.py,sha256=Ot3T8lp31ThckGbNps86oVvq6Vn845Eec0mxhDmONDE,2684
24
25
  locust/contrib/postgres.py,sha256=OuMWnGYN10K65Tq2axVESEW25Y0g5gJb0rK90jkcCJg,1230
26
+ locust/contrib/socketio.py,sha256=PpXKHFTlQgFNNkdwU3N3AvJ18XERoQtxxTmOira0Lyo,3152
25
27
  locust/rpc/__init__.py,sha256=5YOu-58XSnt-oWWNATgXLTNdYoDkkngwHNXprxkWKSM,99
26
28
  locust/rpc/protocol.py,sha256=n-rb3GZQcAlldYDj4E4GuFGylYj_26GSS5U29meft5Y,1282
27
29
  locust/rpc/zmqrpc.py,sha256=tMeLQiLII8QP29lAHGZsj5Pf5FsTL-X4wM0DrtR3ALw,3214
@@ -53,8 +55,8 @@ locust/webui/dist/assets/index-BjqxSg7R.js,sha256=3JyrKWfAg8LlTy2bxAJh73c6njNPhN
53
55
  locust/webui/dist/assets/terminal.gif,sha256=iw80LO2u0dnf4wpGfFJZauBeKTcSpw9iUfISXT2nEF4,75302
54
56
  locust/webui/dist/assets/testruns-dark.png,sha256=G4p2VZSBuuqF4neqUaPSshIp5OKQJ_Bvb69Luj6XuVs,125231
55
57
  locust/webui/dist/assets/testruns-light.png,sha256=JinGDiiBPOkhpfF-XCbmQqhRInqItrjrBTLKt5MlqVI,130301
56
- locust-2.38.1.dev6.dist-info/METADATA,sha256=Q1Vto-dB1VQKED1Ut_Q7dnJ-RAfLcPL1b_3cPRajP9A,9403
57
- locust-2.38.1.dev6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
58
- locust-2.38.1.dev6.dist-info/entry_points.txt,sha256=RAdt8Ku-56m7bFjmdj-MBhbF6h4NX7tVODR9QNnOg0E,44
59
- locust-2.38.1.dev6.dist-info/licenses/LICENSE,sha256=5hnz-Vpj0Z3kSCQl0LzV2hT1TLc4LHcbpBp3Cy-EuyM,1110
60
- locust-2.38.1.dev6.dist-info/RECORD,,
58
+ locust-2.38.2.dev21.dist-info/METADATA,sha256=XpyoRPuFbgJt-ljIUQtbZpIDS_0nq5VALqWQVIbWshU,9524
59
+ locust-2.38.2.dev21.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
60
+ locust-2.38.2.dev21.dist-info/entry_points.txt,sha256=RAdt8Ku-56m7bFjmdj-MBhbF6h4NX7tVODR9QNnOg0E,44
61
+ locust-2.38.2.dev21.dist-info/licenses/LICENSE,sha256=5hnz-Vpj0Z3kSCQl0LzV2hT1TLc4LHcbpBp3Cy-EuyM,1110
62
+ locust-2.38.2.dev21.dist-info/RECORD,,