locust 2.38.1.dev4__py3-none-any.whl → 2.38.2.dev7__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
@@ -1,7 +1,14 @@
1
1
  # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
3
 
4
- __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
5
12
 
6
13
  TYPE_CHECKING = False
7
14
  if TYPE_CHECKING:
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
9
16
  from typing import Union
10
17
 
11
18
  VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
12
20
  else:
13
21
  VERSION_TUPLE = object
22
+ COMMIT_ID = object
14
23
 
15
24
  version: str
16
25
  __version__: str
17
26
  __version_tuple__: VERSION_TUPLE
18
27
  version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
19
30
 
20
- __version__ = version = '2.38.1.dev4'
21
- __version_tuple__ = version_tuple = (2, 38, 1, 'dev4')
31
+ __version__ = version = '2.38.2.dev7'
32
+ __version_tuple__ = version_tuple = (2, 38, 2, 'dev7')
33
+
34
+ __commit_id__ = commit_id = None
@@ -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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: locust
3
- Version: 2.38.1.dev4
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
@@ -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=uTueUC_r5h08wNJDaddN1-QEOPEeLyIZpdxIzqXbZ6I,526
3
+ locust/_version.py,sha256=s2j9gE6VWFZ3Bbw6sbjPzaEsKdu7tciZebcEag4lP4c,719
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
@@ -18,7 +18,8 @@ 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
20
  locust/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- locust/contrib/fasthttp.py,sha256=04rnf4kxRxVbzOyqDlb-io43dWIyMPMUwmQ7DOFo2Lo,29656
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
@@ -53,8 +54,8 @@ locust/webui/dist/assets/index-BjqxSg7R.js,sha256=3JyrKWfAg8LlTy2bxAJh73c6njNPhN
53
54
  locust/webui/dist/assets/terminal.gif,sha256=iw80LO2u0dnf4wpGfFJZauBeKTcSpw9iUfISXT2nEF4,75302
54
55
  locust/webui/dist/assets/testruns-dark.png,sha256=G4p2VZSBuuqF4neqUaPSshIp5OKQJ_Bvb69Luj6XuVs,125231
55
56
  locust/webui/dist/assets/testruns-light.png,sha256=JinGDiiBPOkhpfF-XCbmQqhRInqItrjrBTLKt5MlqVI,130301
56
- locust-2.38.1.dev4.dist-info/METADATA,sha256=LBqunPnbR6lVJHUB6eXzouDXc_60dzInTBZ6hbZpqjo,9403
57
- locust-2.38.1.dev4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
58
- locust-2.38.1.dev4.dist-info/entry_points.txt,sha256=RAdt8Ku-56m7bFjmdj-MBhbF6h4NX7tVODR9QNnOg0E,44
59
- locust-2.38.1.dev4.dist-info/licenses/LICENSE,sha256=5hnz-Vpj0Z3kSCQl0LzV2hT1TLc4LHcbpBp3Cy-EuyM,1110
60
- locust-2.38.1.dev4.dist-info/RECORD,,
57
+ locust-2.38.2.dev7.dist-info/METADATA,sha256=Y85K_sULv2Nnz46NJgs-rAr3QeH01cT0OOlBsy9LyoA,9476
58
+ locust-2.38.2.dev7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
59
+ locust-2.38.2.dev7.dist-info/entry_points.txt,sha256=RAdt8Ku-56m7bFjmdj-MBhbF6h4NX7tVODR9QNnOg0E,44
60
+ locust-2.38.2.dev7.dist-info/licenses/LICENSE,sha256=5hnz-Vpj0Z3kSCQl0LzV2hT1TLc4LHcbpBp3Cy-EuyM,1110
61
+ locust-2.38.2.dev7.dist-info/RECORD,,