locust 2.38.1.dev6__tar.gz → 2.38.2.dev7__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 (62) hide show
  1. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/PKG-INFO +3 -1
  2. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/_version.py +2 -2
  3. locust-2.38.2.dev7/locust/contrib/milvus.py +407 -0
  4. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/pyproject.toml +8 -0
  5. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/.gitignore +0 -0
  6. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/LICENSE +0 -0
  7. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/README.md +0 -0
  8. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/hatch_build.py +0 -0
  9. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/__init__.py +0 -0
  10. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/__main__.py +0 -0
  11. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/argument_parser.py +0 -0
  12. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/clients.py +0 -0
  13. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/contrib/__init__.py +0 -0
  14. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/contrib/fasthttp.py +0 -0
  15. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/contrib/mongodb.py +0 -0
  16. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/contrib/oai.py +0 -0
  17. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/contrib/postgres.py +0 -0
  18. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/debug.py +0 -0
  19. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/dispatch.py +0 -0
  20. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/env.py +0 -0
  21. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/event.py +0 -0
  22. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/exception.py +0 -0
  23. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/html.py +0 -0
  24. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/input_events.py +0 -0
  25. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/log.py +0 -0
  26. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/main.py +0 -0
  27. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/py.typed +0 -0
  28. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/rpc/__init__.py +0 -0
  29. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/rpc/protocol.py +0 -0
  30. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/rpc/zmqrpc.py +0 -0
  31. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/runners.py +0 -0
  32. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/shape.py +0 -0
  33. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/stats.py +0 -0
  34. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/user/__init__.py +0 -0
  35. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/user/inspectuser.py +0 -0
  36. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/user/markov_taskset.py +0 -0
  37. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/user/sequential_taskset.py +0 -0
  38. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/user/task.py +0 -0
  39. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/user/users.py +0 -0
  40. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/user/wait_time.py +0 -0
  41. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/util/__init__.py +0 -0
  42. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/util/cache.py +0 -0
  43. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/util/date.py +0 -0
  44. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/util/deprecation.py +0 -0
  45. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/util/directory.py +0 -0
  46. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/util/exception_handler.py +0 -0
  47. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/util/load_locustfile.py +0 -0
  48. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/util/rounding.py +0 -0
  49. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/util/timespan.py +0 -0
  50. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/util/url.py +0 -0
  51. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/web.py +0 -0
  52. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/webui/dist/assets/favicon-dark.png +0 -0
  53. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/webui/dist/assets/favicon-light.png +0 -0
  54. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/webui/dist/assets/graphs-dark.png +0 -0
  55. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/webui/dist/assets/graphs-light.png +0 -0
  56. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/webui/dist/assets/index-BjqxSg7R.js +0 -0
  57. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/webui/dist/assets/terminal.gif +0 -0
  58. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/webui/dist/assets/testruns-dark.png +0 -0
  59. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/webui/dist/assets/testruns-light.png +0 -0
  60. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/webui/dist/auth.html +0 -0
  61. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/webui/dist/index.html +0 -0
  62. {locust-2.38.1.dev6 → locust-2.38.2.dev7}/locust/webui/dist/report.html +0 -0
@@ -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.dev7
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
@@ -41,6 +41,8 @@ Requires-Dist: setuptools>=70.0.0
41
41
  Requires-Dist: tomli>=1.1.0; python_version < '3.11'
42
42
  Requires-Dist: typing-extensions>=4.6.0; python_version < '3.11'
43
43
  Requires-Dist: werkzeug>=2.0.0
44
+ Provides-Extra: milvus
45
+ Requires-Dist: pymilvus>=2.5.0; extra == 'milvus'
44
46
  Description-Content-Type: text/markdown
45
47
 
46
48
  # Locust
@@ -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.dev7'
32
+ __version_tuple__ = version_tuple = (2, 38, 2, 'dev7')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -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()
@@ -52,6 +52,11 @@ dependencies = [
52
52
  "gevent>=24.10.1,<26.0.0",
53
53
  ]
54
54
 
55
+ [project.optional-dependencies]
56
+ milvus = [
57
+ "pymilvus>=2.5.0",
58
+ ]
59
+
55
60
  [project.urls]
56
61
  homepage = "https://locust.io/"
57
62
  repository = "https://github.com/locustio/locust"
@@ -96,6 +101,9 @@ docs = [
96
101
  "sphinxcontrib-serializinghtml==1.1.10",
97
102
  "sphinxcontrib-googleanalytics>=0.4",
98
103
  ]
104
+ milvus = [
105
+ "pymilvus>=2.5.0",
106
+ ]
99
107
 
100
108
  [project.scripts]
101
109
  locust = "locust.main:main"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes