locust 2.40.6.dev8__tar.gz → 2.40.6.dev18__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.40.6.dev8 → locust-2.40.6.dev18}/PKG-INFO +4 -1
  2. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/_version.py +2 -2
  3. locust-2.40.6.dev18/locust/contrib/mqtt.py +462 -0
  4. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/pyproject.toml +6 -7
  5. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/.gitignore +0 -0
  6. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/LICENSE +0 -0
  7. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/README.md +0 -0
  8. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/hatch_build.py +0 -0
  9. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/__init__.py +0 -0
  10. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/__main__.py +0 -0
  11. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/argument_parser.py +0 -0
  12. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/clients.py +0 -0
  13. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/contrib/__init__.py +0 -0
  14. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/contrib/fasthttp.py +0 -0
  15. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/contrib/milvus.py +0 -0
  16. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/contrib/mongodb.py +0 -0
  17. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/contrib/oai.py +0 -0
  18. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/contrib/postgres.py +0 -0
  19. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/contrib/socketio.py +0 -0
  20. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/debug.py +0 -0
  21. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/dispatch.py +0 -0
  22. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/env.py +0 -0
  23. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/event.py +0 -0
  24. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/exception.py +0 -0
  25. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/html.py +0 -0
  26. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/input_events.py +0 -0
  27. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/log.py +0 -0
  28. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/main.py +0 -0
  29. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/py.typed +0 -0
  30. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/rpc/__init__.py +0 -0
  31. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/rpc/protocol.py +0 -0
  32. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/rpc/zmqrpc.py +0 -0
  33. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/runners.py +0 -0
  34. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/shape.py +0 -0
  35. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/stats.py +0 -0
  36. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/user/__init__.py +0 -0
  37. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/user/inspectuser.py +0 -0
  38. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/user/markov_taskset.py +0 -0
  39. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/user/sequential_taskset.py +0 -0
  40. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/user/task.py +0 -0
  41. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/user/users.py +0 -0
  42. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/user/wait_time.py +0 -0
  43. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/util/__init__.py +0 -0
  44. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/util/cache.py +0 -0
  45. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/util/date.py +0 -0
  46. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/util/deprecation.py +0 -0
  47. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/util/directory.py +0 -0
  48. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/util/exception_handler.py +0 -0
  49. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/util/load_locustfile.py +0 -0
  50. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/util/rounding.py +0 -0
  51. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/util/timespan.py +0 -0
  52. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/util/url.py +0 -0
  53. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/web.py +0 -0
  54. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/webui/dist/assets/favicon-dark.png +0 -0
  55. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/webui/dist/assets/favicon-light.png +0 -0
  56. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/webui/dist/assets/graphs-dark.png +0 -0
  57. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/webui/dist/assets/graphs-light.png +0 -0
  58. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/webui/dist/assets/index-aozIzkOV.js +0 -0
  59. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/webui/dist/assets/terminal.gif +0 -0
  60. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/webui/dist/assets/testruns-dark.png +0 -0
  61. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/webui/dist/assets/testruns-light.png +0 -0
  62. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/webui/dist/auth.html +0 -0
  63. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/webui/dist/index.html +0 -0
  64. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/locust/webui/dist/report.html +0 -0
  65. {locust-2.40.6.dev8 → locust-2.40.6.dev18}/pytest_locust/plugin.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: locust
3
- Version: 2.40.6.dev8
3
+ Version: 2.40.6.dev18
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
@@ -32,6 +32,7 @@ Requires-Dist: gevent<25.8.1,>=24.10.1
32
32
  Requires-Dist: geventhttpclient>=2.3.1
33
33
  Requires-Dist: locust-cloud>=1.27.0
34
34
  Requires-Dist: msgpack>=1.0.0
35
+ Requires-Dist: paho-mqtt>=2.1.0
35
36
  Requires-Dist: psutil>=5.9.1
36
37
  Requires-Dist: pytest<9.0.0,>=8.3.3
37
38
  Requires-Dist: python-engineio>=4.12.2
@@ -45,6 +46,8 @@ Requires-Dist: typing-extensions>=4.6.0; python_version < '3.12'
45
46
  Requires-Dist: werkzeug>=2.0.0
46
47
  Provides-Extra: milvus
47
48
  Requires-Dist: pymilvus>=2.5.0; extra == 'milvus'
49
+ Provides-Extra: mqtt
50
+ Requires-Dist: paho-mqtt>=2.1.0; extra == 'mqtt'
48
51
  Description-Content-Type: text/markdown
49
52
 
50
53
  # 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.40.6.dev8'
32
- __version_tuple__ = version_tuple = (2, 40, 6, 'dev8')
31
+ __version__ = version = '2.40.6.dev18'
32
+ __version_tuple__ = version_tuple = (2, 40, 6, 'dev18')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -0,0 +1,462 @@
1
+ from __future__ import annotations
2
+
3
+ from locust import User
4
+ from locust.env import Environment
5
+
6
+ import random
7
+ import time
8
+ import typing
9
+
10
+ import paho.mqtt.client as mqtt
11
+
12
+ if typing.TYPE_CHECKING:
13
+ from paho.mqtt.client import MQTTMessageInfo
14
+ from paho.mqtt.enums import MQTTProtocolVersion
15
+ from paho.mqtt.properties import Properties
16
+ from paho.mqtt.reasoncodes import ReasonCode
17
+ from paho.mqtt.subscribeoptions import SubscribeOptions
18
+
19
+
20
+ # A SUBACK response for MQTT can only contain 0x00, 0x01, 0x02, or 0x80. 0x80
21
+ # indicates a failure to subscribe.
22
+ #
23
+ # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Figure_3.26_-
24
+ SUBACK_FAILURE = 0x80
25
+ REQUEST_TYPE = "MQTT"
26
+
27
+
28
+ def _generate_random_id(
29
+ length: int,
30
+ alphabet: str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
31
+ ):
32
+ """Generate a random ID from the given alphabet.
33
+
34
+ Args:
35
+ length: the number of random characters to generate.
36
+ alphabet: the pool of random characters to choose from.
37
+ """
38
+ return "".join(random.choice(alphabet) for _ in range(length))
39
+
40
+
41
+ def _generate_mqtt_event_name(event_type: str, qos: int, topic: str):
42
+ """Generate a name to identify publish/subscribe tasks.
43
+
44
+ This will be used to ultimately identify tasks in the Locust web console.
45
+ This will identify publish/subscribe tasks with their QoS & associated
46
+ topic.
47
+
48
+ Examples:
49
+ publish:0:my/topic
50
+ subscribe:1:my/other/topic
51
+
52
+ Args:
53
+ event_type: The type of MQTT event (subscribe or publish)
54
+ qos: The quality-of-service associated with this event
55
+ topic: The MQTT topic associated with this event
56
+ """
57
+ return f"{event_type}:{qos}:{topic}"
58
+
59
+
60
+ class PublishedMessageContext(typing.NamedTuple):
61
+ """Stores metadata about outgoing published messages."""
62
+
63
+ qos: int
64
+ topic: str
65
+ start_time: float
66
+ payload_size: int
67
+
68
+
69
+ class MqttClient(mqtt.Client):
70
+ def __init__(
71
+ self,
72
+ *args,
73
+ environment: Environment,
74
+ client_id: str | None = None,
75
+ protocol: MQTTProtocolVersion = mqtt.MQTTv311,
76
+ **kwargs,
77
+ ):
78
+ """Initializes a paho.mqtt.Client for use in Locust swarms.
79
+
80
+ This class passes most args & kwargs through to the underlying
81
+ paho.mqtt constructor.
82
+
83
+ Args:
84
+ environment: the Locust environment with which to associate events.
85
+ client_id: the MQTT Client ID to use in connecting to the broker.
86
+ If not set, one will be randomly generated.
87
+ protocol: the MQTT protocol version.
88
+ defaults to MQTT v3.11.
89
+ """
90
+ # If a client ID is not provided, this class will randomly generate an ID
91
+ # of the form: `locust-[0-9a-zA-Z]{16}` (i.e., `locust-` followed by 16
92
+ # random characters, so that the resulting client ID does not exceed the
93
+ # specification limit of 23 characters).
94
+
95
+ # This is done in this wrapper class so that this locust client can
96
+ # self-identify when firing requests, since some versions of MQTT will
97
+ # have the broker assign IDs to clients that do not provide one: in this
98
+ # case, there is no way to retrieve the client ID.
99
+
100
+ # See https://github.com/eclipse/paho.mqtt.python/issues/237
101
+ if not client_id:
102
+ client_id = f"locust-{_generate_random_id(16)}"
103
+
104
+ super().__init__(*args, **kwargs)
105
+ self.environment = environment
106
+ # we need to set client_id in case the broker assigns one to us
107
+ self.client_id = client_id
108
+
109
+ self.on_publish = self._on_publish_cb # type: ignore[assignment]
110
+
111
+ if protocol == mqtt.MQTTv5:
112
+ self.on_disconnect = self._on_disconnect_cb_v5
113
+ self.on_connect = self._on_connect_cb_v5
114
+ self.on_subscribe = self._on_subscribe_cb_v5
115
+ else:
116
+ self.on_disconnect = self._on_disconnect_cb_v3x # type: ignore[assignment]
117
+ self.on_connect = self._on_connect_cb_v3x # type: ignore[assignment]
118
+ self.on_subscribe = self._on_subscribe_cb_v3x # type: ignore[assignment]
119
+
120
+ self._publish_requests: dict[int, PublishedMessageContext] = {}
121
+ self._subscribe_requests: dict[int, tuple[int, str, float]] = {}
122
+
123
+ def _generate_event_name(self, event_type: str, qos: int, topic: str):
124
+ return _generate_mqtt_event_name(event_type, qos, topic)
125
+
126
+ def _on_publish_cb(
127
+ self,
128
+ client: mqtt.Client,
129
+ userdata: typing.Any,
130
+ mid: int,
131
+ ):
132
+ cb_time = time.time()
133
+ try:
134
+ request_context = self._publish_requests.pop(mid)
135
+ except KeyError:
136
+ # we shouldn't hit this block of code
137
+ self.environment.events.request.fire(
138
+ request_type=REQUEST_TYPE,
139
+ name="publish",
140
+ response_time=0,
141
+ response_length=0,
142
+ exception=AssertionError(f"Could not find message data for mid '{mid}' in _on_publish_cb."),
143
+ context={
144
+ "client_id": self.client_id,
145
+ "mid": mid,
146
+ },
147
+ )
148
+ else:
149
+ # fire successful publish event
150
+ self.environment.events.request.fire(
151
+ request_type=REQUEST_TYPE,
152
+ name=self._generate_event_name("publish", request_context.qos, request_context.topic),
153
+ response_time=(cb_time - request_context.start_time) * 1000,
154
+ response_length=request_context.payload_size,
155
+ exception=None,
156
+ context={
157
+ "client_id": self.client_id,
158
+ **request_context._asdict(),
159
+ },
160
+ )
161
+
162
+ def _on_subscribe_cb_v3x(
163
+ self,
164
+ client: mqtt.Client,
165
+ userdata: typing.Any,
166
+ mid: int,
167
+ granted_qos: list[int],
168
+ ):
169
+ cb_time = time.time()
170
+ try:
171
+ qos, topic, start_time = self._subscribe_requests.pop(mid)
172
+ except KeyError:
173
+ # we shouldn't hit this block of code
174
+ self.environment.events.request.fire(
175
+ request_type=REQUEST_TYPE,
176
+ name="subscribe",
177
+ response_time=0,
178
+ response_length=0,
179
+ exception=AssertionError(f"Could not find message data for mid '{mid}' in _on_subscribe_cb."),
180
+ context={
181
+ "client_id": self.client_id,
182
+ "mid": mid,
183
+ },
184
+ )
185
+ else:
186
+ if SUBACK_FAILURE in granted_qos:
187
+ self.environment.events.request.fire(
188
+ request_type=REQUEST_TYPE,
189
+ name=self._generate_event_name("subscribe", qos, topic),
190
+ response_time=(cb_time - start_time) * 1000,
191
+ response_length=0,
192
+ exception=AssertionError(f"Broker returned an error response during subscription: {granted_qos}"),
193
+ context={
194
+ "client_id": self.client_id,
195
+ "qos": qos,
196
+ "topic": topic,
197
+ "start_time": start_time,
198
+ },
199
+ )
200
+ else:
201
+ # fire successful subscribe event
202
+ self.environment.events.request.fire(
203
+ request_type=REQUEST_TYPE,
204
+ name=self._generate_event_name("subscribe", qos, topic),
205
+ response_time=(cb_time - start_time) * 1000,
206
+ response_length=0,
207
+ exception=None,
208
+ context={
209
+ "client_id": self.client_id,
210
+ "qos": qos,
211
+ "topic": topic,
212
+ "start_time": start_time,
213
+ },
214
+ )
215
+
216
+ # pylint: disable=unused-argument
217
+ def _on_subscribe_cb_v5(
218
+ self,
219
+ client: mqtt.Client,
220
+ userdata: typing.Any,
221
+ mid: int,
222
+ reason_codes: list[ReasonCode],
223
+ properties: Properties,
224
+ ) -> None:
225
+ granted_qos = [rc.value for rc in reason_codes]
226
+ return self._on_subscribe_cb_v3x(client, userdata, mid, granted_qos)
227
+
228
+ def _on_disconnect_cb(
229
+ self,
230
+ client: mqtt.Client,
231
+ userdata: typing.Any,
232
+ rc: int | ReasonCode,
233
+ ):
234
+ if rc != 0:
235
+ self.environment.events.request.fire(
236
+ request_type=REQUEST_TYPE,
237
+ name="disconnect",
238
+ response_time=0,
239
+ response_length=0,
240
+ exception=rc,
241
+ context={
242
+ "client_id": self.client_id,
243
+ },
244
+ )
245
+ else:
246
+ self.environment.events.request.fire(
247
+ request_type=REQUEST_TYPE,
248
+ name="disconnect",
249
+ response_time=0,
250
+ response_length=0,
251
+ exception=None,
252
+ context={
253
+ "client_id": self.client_id,
254
+ },
255
+ )
256
+
257
+ def _on_disconnect_cb_v3x(
258
+ self,
259
+ client: mqtt.Client,
260
+ userdata: typing.Any,
261
+ rc: int,
262
+ ):
263
+ return self._on_disconnect_cb(client, userdata, rc)
264
+
265
+ # pylint: disable=unused-argument
266
+ def _on_disconnect_cb_v5(
267
+ self,
268
+ client: mqtt.Client,
269
+ userdata: typing.Any,
270
+ disconnect_flags: mqtt.DisconnectFlags,
271
+ reasoncode: ReasonCode,
272
+ properties: Properties | None,
273
+ ) -> None:
274
+ return self._on_disconnect_cb(client, userdata, reasoncode)
275
+
276
+ def _on_connect_cb(
277
+ self,
278
+ client: mqtt.Client,
279
+ userdata: typing.Any,
280
+ flags: dict[str, int],
281
+ rc: int | ReasonCode,
282
+ ):
283
+ if rc != 0:
284
+ self.environment.events.request.fire(
285
+ request_type=REQUEST_TYPE,
286
+ name="connect",
287
+ response_time=0,
288
+ response_length=0,
289
+ exception=Exception(str(rc)),
290
+ context={
291
+ "client_id": self.client_id,
292
+ },
293
+ )
294
+ else:
295
+ self.environment.events.request.fire(
296
+ request_type=REQUEST_TYPE,
297
+ name="connect",
298
+ response_time=0,
299
+ response_length=0,
300
+ exception=None,
301
+ context={
302
+ "client_id": self.client_id,
303
+ },
304
+ )
305
+
306
+ def _on_connect_cb_v3x(
307
+ self,
308
+ client: mqtt.Client,
309
+ userdata: typing.Any,
310
+ flags: dict[str, int],
311
+ rc: int,
312
+ ):
313
+ return self._on_connect_cb(client, userdata, flags, rc)
314
+
315
+ # pylint: disable=unused-argument
316
+ def _on_connect_cb_v5(
317
+ self,
318
+ client: mqtt.Client,
319
+ userdata: typing.Any,
320
+ connect_flags: mqtt.ConnectFlags,
321
+ reasoncode: ReasonCode,
322
+ properties: Properties | None,
323
+ ) -> None:
324
+ self._on_connect_cb(client, userdata, {}, reasoncode)
325
+
326
+ def publish(
327
+ self,
328
+ topic: str,
329
+ payload: mqtt.PayloadType | None = None,
330
+ qos: int = 0,
331
+ retain: bool = False,
332
+ properties: Properties | None = None,
333
+ ) -> MQTTMessageInfo:
334
+ """Publish a message to the MQTT broker.
335
+
336
+ This method wraps the underlying paho-mqtt client's method in order to
337
+ set up & fire Locust events.
338
+ """
339
+ request_context = PublishedMessageContext(
340
+ qos=qos,
341
+ topic=topic,
342
+ start_time=time.time(),
343
+ payload_size=len(payload) if payload and not isinstance(payload, (int, float)) else 0,
344
+ )
345
+
346
+ publish_info = super().publish(topic, payload=payload, qos=qos, retain=retain, properties=properties)
347
+
348
+ if publish_info.rc != mqtt.MQTT_ERR_SUCCESS:
349
+ self.environment.events.request.fire(
350
+ request_type=REQUEST_TYPE,
351
+ name=self._generate_event_name("publish", request_context.qos, request_context.topic),
352
+ response_time=0,
353
+ response_length=0,
354
+ exception=publish_info.rc,
355
+ context={
356
+ "client_id": self.client_id,
357
+ **request_context._asdict(),
358
+ },
359
+ )
360
+ else:
361
+ # store this for use in the on_publish callback
362
+ self._publish_requests[publish_info.mid] = request_context
363
+
364
+ return publish_info
365
+
366
+ def subscribe(
367
+ self,
368
+ topic: str
369
+ | tuple[str, int]
370
+ | tuple[str, SubscribeOptions]
371
+ | list[tuple[str, int]]
372
+ | list[tuple[str, SubscribeOptions]],
373
+ qos: int = 0,
374
+ options: SubscribeOptions | None = None,
375
+ properties: Properties | None = None,
376
+ ) -> tuple[mqtt.MQTTErrorCode, int | None]:
377
+ """Subscribe to a given topic.
378
+
379
+ This method wraps the underlying paho-mqtt client's method in order to
380
+ set up & fire Locust events.
381
+ """
382
+ start_time = time.time()
383
+ subscribe_topic = topic if isinstance(topic, str) else topic[0][0]
384
+
385
+ result, mid = super().subscribe(topic, qos, options, properties) # type: ignore[arg-type]
386
+
387
+ if result != mqtt.MQTT_ERR_SUCCESS:
388
+ self.environment.events.request.fire(
389
+ request_type=REQUEST_TYPE,
390
+ name=self._generate_event_name("subscribe", qos, subscribe_topic),
391
+ response_time=0,
392
+ response_length=0,
393
+ exception=result,
394
+ context={
395
+ "client_id": self.client_id,
396
+ "qos": qos,
397
+ "topic": subscribe_topic,
398
+ "start_time": start_time,
399
+ },
400
+ )
401
+ else:
402
+ if mid is None:
403
+ # QoS 0 subscriptions do not have a mid, so we'll just fire a success event immediately
404
+ self.environment.events.request.fire(
405
+ request_type=REQUEST_TYPE,
406
+ name=self._generate_event_name("subscribe", qos, subscribe_topic),
407
+ response_time=(time.time() - start_time) * 1000,
408
+ response_length=0,
409
+ exception=None,
410
+ context={
411
+ "client_id": self.client_id,
412
+ "qos": qos,
413
+ "topic": subscribe_topic,
414
+ "start_time": start_time,
415
+ },
416
+ )
417
+ else:
418
+ self._subscribe_requests[mid] = (qos, subscribe_topic, start_time)
419
+
420
+ return result, mid
421
+
422
+
423
+ class MqttUser(User):
424
+ abstract = True
425
+
426
+ host = "localhost"
427
+ port = 1883
428
+ transport = "tcp"
429
+ ws_path = "/mqtt"
430
+ tls_context = None
431
+ client_cls: type[MqttClient] = MqttClient
432
+ client_id = None
433
+ username = None
434
+ password = None
435
+ protocol = mqtt.MQTTv311
436
+
437
+ def __init__(self, environment: Environment):
438
+ super().__init__(environment)
439
+ self.client: MqttClient = self.client_cls(
440
+ environment=self.environment,
441
+ transport=self.transport,
442
+ client_id=self.client_id,
443
+ protocol=self.protocol,
444
+ )
445
+
446
+ if self.tls_context:
447
+ self.client.tls_set_context(self.tls_context)
448
+
449
+ if self.transport == "websockets" and self.ws_path:
450
+ self.client.ws_set_options(path=self.ws_path)
451
+
452
+ if self.username and self.password:
453
+ self.client.username_pw_set(
454
+ username=self.username,
455
+ password=self.password,
456
+ )
457
+
458
+ self.client.connect_async(
459
+ host=self.host, # type: ignore
460
+ port=self.port,
461
+ )
462
+ self.client.loop_start()
@@ -51,12 +51,12 @@ dependencies = [
51
51
  "python-socketio[client]>=5.13.0",
52
52
  "python-engineio>=4.12.2",
53
53
  "pytest>=8.3.3,<9.0.0",
54
+ "paho-mqtt>=2.1.0",
54
55
  ]
55
56
 
56
57
  [project.optional-dependencies]
57
- milvus = [
58
- "pymilvus>=2.5.0",
59
- ]
58
+ milvus = ["pymilvus>=2.5.0"]
59
+ mqtt = ["paho-mqtt>=2.1.0"]
60
60
 
61
61
  [project.urls]
62
62
  homepage = "https://locust.io/"
@@ -103,11 +103,10 @@ docs = [
103
103
  # these optional dependencies are needed to build the some contrib modules
104
104
  "pymilvus>=2.5.0",
105
105
  "psycopg[binary]>=3.2.1",
106
- "pymongo>=4.8.0"
107
- ]
108
- milvus = [
109
- "pymilvus>=2.5.0",
106
+ "pymongo>=4.8.0",
110
107
  ]
108
+ milvus = ["pymilvus>=2.5.0"]
109
+ mqtt = ["paho-mqtt>=2.1.0"]
111
110
 
112
111
  [project.scripts]
113
112
  locust = "locust.main:main"
File without changes
File without changes
File without changes