locust 2.20.1.dev26__py3-none-any.whl → 2.20.2__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.
Files changed (64) hide show
  1. locust/__init__.py +7 -7
  2. locust/_version.py +2 -2
  3. locust/argument_parser.py +35 -18
  4. locust/clients.py +7 -6
  5. locust/contrib/fasthttp.py +42 -37
  6. locust/debug.py +14 -8
  7. locust/dispatch.py +25 -25
  8. locust/env.py +45 -37
  9. locust/event.py +13 -10
  10. locust/exception.py +0 -9
  11. locust/html.py +9 -8
  12. locust/input_events.py +7 -5
  13. locust/main.py +95 -47
  14. locust/rpc/protocol.py +4 -2
  15. locust/rpc/zmqrpc.py +6 -4
  16. locust/runners.py +69 -76
  17. locust/shape.py +7 -4
  18. locust/stats.py +73 -71
  19. locust/templates/index.html +8 -23
  20. locust/test/mock_logging.py +7 -5
  21. locust/test/test_debugging.py +4 -4
  22. locust/test/test_dispatch.py +10 -9
  23. locust/test/test_env.py +30 -1
  24. locust/test/test_fasthttp.py +9 -8
  25. locust/test/test_http.py +4 -3
  26. locust/test/test_interruptable_task.py +4 -4
  27. locust/test/test_load_locustfile.py +6 -5
  28. locust/test/test_locust_class.py +11 -5
  29. locust/test/test_log.py +5 -4
  30. locust/test/test_main.py +37 -7
  31. locust/test/test_parser.py +11 -15
  32. locust/test/test_runners.py +22 -21
  33. locust/test/test_sequential_taskset.py +3 -2
  34. locust/test/test_stats.py +16 -18
  35. locust/test/test_tags.py +3 -2
  36. locust/test/test_taskratio.py +3 -3
  37. locust/test/test_users.py +3 -3
  38. locust/test/test_util.py +3 -2
  39. locust/test/test_wait_time.py +3 -3
  40. locust/test/test_web.py +142 -27
  41. locust/test/test_zmqrpc.py +5 -3
  42. locust/test/testcases.py +7 -7
  43. locust/test/util.py +6 -7
  44. locust/user/__init__.py +1 -1
  45. locust/user/inspectuser.py +6 -5
  46. locust/user/sequential_taskset.py +3 -1
  47. locust/user/task.py +22 -27
  48. locust/user/users.py +17 -7
  49. locust/util/deprecation.py +0 -1
  50. locust/util/exception_handler.py +1 -1
  51. locust/util/load_locustfile.py +4 -2
  52. locust/web.py +102 -53
  53. locust/webui/dist/assets/auth-5e21717c.js.map +1 -0
  54. locust/webui/dist/assets/{index-01afe4fa.js → index-a83a5dd9.js} +85 -84
  55. locust/webui/dist/assets/index-a83a5dd9.js.map +1 -0
  56. locust/webui/dist/auth.html +20 -0
  57. locust/webui/dist/index.html +1 -1
  58. {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/METADATA +2 -2
  59. locust-2.20.2.dist-info/RECORD +105 -0
  60. locust-2.20.1.dev26.dist-info/RECORD +0 -102
  61. {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/LICENSE +0 -0
  62. {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/WHEEL +0 -0
  63. {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/entry_points.txt +0 -0
  64. {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/top_level.txt +0 -0
locust/env.py CHANGED
@@ -1,24 +1,18 @@
1
+ from __future__ import annotations
2
+
1
3
  from operator import methodcaller
2
- from typing import (
3
- Callable,
4
- Dict,
5
- List,
6
- Type,
7
- TypeVar,
8
- Optional,
9
- )
4
+ from typing import Callable, TypeVar
10
5
 
11
6
  from configargparse import Namespace
12
7
 
13
8
  from .event import Events
14
9
  from .exception import RunnerAlreadyExistsError
10
+ from .runners import LocalRunner, MasterRunner, Runner, WorkerRunner
11
+ from .shape import LoadTestShape
15
12
  from .stats import RequestStats, StatsCSV
16
- from .runners import Runner, LocalRunner, MasterRunner, WorkerRunner
17
- from .web import WebUI
18
13
  from .user import User
19
- from .user.task import filter_tasks_by_tags, TaskSet, TaskHolder
20
- from .shape import LoadTestShape
21
-
14
+ from .user.task import TaskHolder, TaskSet, filter_tasks_by_tags
15
+ from .web import WebUI
22
16
 
23
17
  RunnerType = TypeVar("RunnerType", bound=Runner)
24
18
 
@@ -27,27 +21,28 @@ class Environment:
27
21
  def __init__(
28
22
  self,
29
23
  *,
30
- user_classes: Optional[List[Type[User]]] = None,
31
- shape_class: Optional[LoadTestShape] = None,
32
- tags: Optional[List[str]] = None,
33
- locustfile: Optional[str] = None,
34
- exclude_tags: Optional[List[str]] = None,
35
- events: Optional[Events] = None,
36
- host: Optional[str] = None,
24
+ user_classes: list[type[User]] | None = None,
25
+ shape_class: LoadTestShape | None = None,
26
+ tags: list[str] | None = None,
27
+ locustfile: str | None = None,
28
+ exclude_tags: list[str] | None = None,
29
+ events: Events | None = None,
30
+ host: str | None = None,
37
31
  reset_stats=False,
38
- stop_timeout: Optional[float] = None,
32
+ stop_timeout: float | None = None,
39
33
  catch_exceptions=True,
40
- parsed_options: Optional[Namespace] = None,
41
- available_user_classes: Optional[Dict[str, User]] = None,
42
- available_shape_classes: Optional[Dict[str, LoadTestShape]] = None,
34
+ parsed_options: Namespace | None = None,
35
+ available_user_classes: dict[str, User] | None = None,
36
+ available_shape_classes: dict[str, LoadTestShape] | None = None,
37
+ available_user_tasks: dict[str, list[TaskSet | Callable]] | None = None,
43
38
  ):
44
- self.runner: Optional[Runner] = None
39
+ self.runner: Runner | None = None
45
40
  """Reference to the :class:`Runner <locust.runners.Runner>` instance"""
46
41
 
47
- self.web_ui: Optional[WebUI] = None
42
+ self.web_ui: WebUI | None = None
48
43
  """Reference to the WebUI instance"""
49
44
 
50
- self.process_exit_code: Optional[int] = None
45
+ self.process_exit_code: int | None = None
51
46
  """
52
47
  If set it'll be the exit code of the Locust process
53
48
  """
@@ -63,7 +58,7 @@ class Environment:
63
58
 
64
59
  self.locustfile = locustfile
65
60
  """Filename (not path) of locustfile"""
66
- self.user_classes: List[Type[User]] = user_classes or []
61
+ self.user_classes: list[type[User]] = user_classes or []
67
62
  """User classes that the runner will run"""
68
63
  self.shape_class = shape_class
69
64
  """A shape class to control the shape of the load test"""
@@ -98,6 +93,8 @@ class Environment:
98
93
  """List of the available User Classes to pick from in the UserClass Picker"""
99
94
  self.available_shape_classes = available_shape_classes
100
95
  """List of the available Shape Classes to pick from in the ShapeClass Picker"""
96
+ self.available_user_tasks = available_user_tasks
97
+ """List of the available Tasks per User Classes to pick from in the Task Picker"""
101
98
 
102
99
  self._remove_user_classes_with_weight_zero()
103
100
  self._validate_user_class_name_uniqueness()
@@ -105,7 +102,7 @@ class Environment:
105
102
 
106
103
  def _create_runner(
107
104
  self,
108
- runner_class: Type[RunnerType],
105
+ runner_class: type[RunnerType],
109
106
  *args,
110
107
  **kwargs,
111
108
  ) -> RunnerType:
@@ -159,10 +156,10 @@ class Environment:
159
156
  self,
160
157
  host="",
161
158
  port=8089,
162
- auth_credentials: Optional[str] = None,
163
- tls_cert: Optional[str] = None,
164
- tls_key: Optional[str] = None,
165
- stats_csv_writer: Optional[StatsCSV] = None,
159
+ web_login: bool = False,
160
+ tls_cert: str | None = None,
161
+ tls_key: str | None = None,
162
+ stats_csv_writer: StatsCSV | None = None,
166
163
  delayed_start=False,
167
164
  userclass_picker_is_active=False,
168
165
  modern_ui=False,
@@ -173,7 +170,7 @@ class Environment:
173
170
  :param host: Host/interface that the web server should accept connections to. Defaults to ""
174
171
  which means all interfaces
175
172
  :param port: Port that the web server should listen to
176
- :param auth_credentials: If provided (in format "username:password") basic auth will be enabled
173
+ :param web_login: If provided, an authentication page will protect the app
177
174
  :param tls_cert: An optional path (str) to a TLS cert. If this is provided the web UI will be
178
175
  served over HTTPS
179
176
  :param tls_key: An optional path (str) to a TLS private key. If this is provided the web UI will be
@@ -186,7 +183,7 @@ class Environment:
186
183
  self,
187
184
  host,
188
185
  port,
189
- auth_credentials=auth_credentials,
186
+ web_login=web_login,
190
187
  tls_cert=tls_cert,
191
188
  tls_key=tls_key,
192
189
  stats_csv_writer=stats_csv_writer,
@@ -196,6 +193,17 @@ class Environment:
196
193
  )
197
194
  return self.web_ui
198
195
 
196
+ def update_user_class(self, user_settings):
197
+ user_class_name = user_settings.get("user_class_name")
198
+ user_class = self.available_user_classes[user_class_name]
199
+ user_tasks = self.available_user_tasks[user_class_name]
200
+
201
+ for key, value in user_settings.items():
202
+ if key not in ["user_class_name", "tasks"]:
203
+ setattr(user_class, key, value)
204
+ if key == "tasks":
205
+ user_class.tasks = [task for task in user_tasks if task.__name__ in value]
206
+
199
207
  def _filter_tasks_by_tags(self) -> None:
200
208
  """
201
209
  Filter the tasks on all the user_classes recursively, according to the tags and
@@ -244,7 +252,7 @@ class Environment:
244
252
  """
245
253
  for u in self.user_classes:
246
254
  u.weight = 1
247
- user_tasks: List[TaskSet | Callable] = []
255
+ user_tasks: list[TaskSet | Callable] = []
248
256
  tasks_frontier = u.tasks
249
257
  while len(tasks_frontier) != 0:
250
258
  t = tasks_frontier.pop()
@@ -274,5 +282,5 @@ class Environment:
274
282
  )
275
283
 
276
284
  @property
277
- def user_classes_by_name(self) -> Dict[str, Type[User]]:
285
+ def user_classes_by_name(self) -> dict[str, type[User]]:
278
286
  return {u.__name__: u for u in self.user_classes}
locust/event.py CHANGED
@@ -1,10 +1,13 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
- from . import log
4
+ import time
3
5
  import traceback
4
6
  from contextlib import contextmanager
5
- from typing import Generator, Any, Dict
6
- import time
7
- from .exception import StopUser, RescheduleTask, RescheduleTaskImmediately, InterruptTaskSet
7
+ from typing import Any, Generator
8
+
9
+ from . import log
10
+ from .exception import InterruptTaskSet, RescheduleTask, RescheduleTaskImmediately, StopUser
8
11
 
9
12
 
10
13
  class EventHook:
@@ -52,7 +55,7 @@ class EventHook:
52
55
  @contextmanager
53
56
  def measure(
54
57
  self, request_type: str, name: str, response_length: int = 0, context=None
55
- ) -> Generator[Dict[str, Any], None, None]:
58
+ ) -> Generator[dict[str, Any], None, None]:
56
59
  """Convenience method for firing the event with automatically calculated response time and automatically marking the request as failed if an exception is raised (this is really only useful for the *request* event)
57
60
 
58
61
  Example usage (in a task):
@@ -230,11 +233,11 @@ class Events:
230
233
  """
231
234
 
232
235
  def __init__(self):
233
- # For backwarde compatibility use also values of class attributes
236
+ # For backward compatibility use also values of class attributes
234
237
  for name, value in vars(type(self)).items():
235
- if value == EventHook:
236
- setattr(self, name, value())
238
+ if value == "EventHook":
239
+ setattr(self, name, EventHook())
237
240
 
238
241
  for name, value in self.__annotations__.items():
239
- if value == EventHook:
240
- setattr(self, name, value())
242
+ if value == "EventHook":
243
+ setattr(self, name, EventHook())
locust/exception.py CHANGED
@@ -74,14 +74,5 @@ class RPCReceiveError(Exception):
74
74
  self.addr = addr
75
75
 
76
76
 
77
- class AuthCredentialsError(ValueError):
78
- """
79
- Exception when the auth credentials provided
80
- are not in the correct format
81
- """
82
-
83
- pass
84
-
85
-
86
77
  class RunnerAlreadyExistsError(Exception):
87
78
  pass
locust/html.py CHANGED
@@ -1,16 +1,17 @@
1
- from jinja2 import Environment, FileSystemLoader
2
- import os
1
+ import datetime
3
2
  import glob
3
+ import os
4
4
  import pathlib
5
- import datetime
6
- from itertools import chain
7
- from .stats import sort_stats
8
- from . import stats as stats_module
9
- from .user.inspectuser import get_ratio
10
5
  from html import escape
6
+ from itertools import chain
11
7
  from json import dumps
12
- from .runners import MasterRunner, STATE_STOPPED, STATE_STOPPING
13
8
 
9
+ from jinja2 import Environment, FileSystemLoader
10
+
11
+ from . import stats as stats_module
12
+ from .runners import STATE_STOPPED, STATE_STOPPING, MasterRunner
13
+ from .stats import sort_stats
14
+ from .user.inspectuser import get_ratio
14
15
 
15
16
  PERCENTILES_FOR_HTML_REPORT = [0.50, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99, 1.0]
16
17
 
locust/input_events.py CHANGED
@@ -1,18 +1,20 @@
1
- from typing import Dict, Callable
1
+ from __future__ import annotations
2
2
 
3
- import gevent
4
3
  import logging
5
4
  import os
6
5
  import sys
6
+ from typing import Callable
7
+
8
+ import gevent
7
9
 
8
10
  if os.name == "nt":
9
11
  from win32api import STD_INPUT_HANDLE
10
12
  from win32console import (
11
- GetStdHandle,
12
- KEY_EVENT,
13
13
  ENABLE_ECHO_INPUT,
14
14
  ENABLE_LINE_INPUT,
15
15
  ENABLE_PROCESSED_INPUT,
16
+ KEY_EVENT,
17
+ GetStdHandle,
16
18
  )
17
19
  else:
18
20
  import select
@@ -88,7 +90,7 @@ def get_poller():
88
90
  return UnixKeyPoller()
89
91
 
90
92
 
91
- def input_listener(key_to_func_map: Dict[str, Callable]):
93
+ def input_listener(key_to_func_map: dict[str, Callable]):
92
94
  def input_listener_func():
93
95
  try:
94
96
  poller = get_poller()
locust/main.py CHANGED
@@ -1,35 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import locust
4
+
5
+ import atexit
1
6
  import errno
7
+ import gc
8
+ import inspect
9
+ import json
2
10
  import logging
3
11
  import os
4
12
  import signal
5
13
  import sys
6
14
  import time
7
- import atexit
8
- import inspect
15
+ import traceback
16
+
9
17
  import gevent
10
- import locust
11
- from typing import Dict
12
- from . import log
18
+
19
+ from . import log, stats
13
20
  from .argument_parser import parse_locustfile_option, parse_options
14
21
  from .env import Environment
15
- from .log import setup_logging, greenlet_exception_logger
16
- from . import stats
22
+ from .html import get_html_report
23
+ from .input_events import input_listener
24
+ from .log import greenlet_exception_logger, setup_logging
17
25
  from .stats import (
26
+ StatsCSV,
27
+ StatsCSVFileWriter,
18
28
  print_error_report,
19
29
  print_percentile_stats,
20
30
  print_stats,
21
31
  print_stats_json,
22
- stats_printer,
23
32
  stats_history,
33
+ stats_printer,
24
34
  )
25
- from .stats import StatsCSV, StatsCSVFileWriter
26
35
  from .user.inspectuser import print_task_ratio, print_task_ratio_json
27
- from .util.timespan import parse_timespan
28
- from .exception import AuthCredentialsError
29
- from .input_events import input_listener
30
- from .html import get_html_report
31
36
  from .util.load_locustfile import load_locustfile
32
- import traceback
37
+ from .util.timespan import parse_timespan
33
38
 
34
39
  try:
35
40
  # import locust_plugins if it is installed, to allow it to register custom arguments etc
@@ -56,6 +61,7 @@ def create_environment(
56
61
  locustfile=None,
57
62
  available_user_classes=None,
58
63
  available_shape_classes=None,
64
+ available_user_tasks=None,
59
65
  ):
60
66
  """
61
67
  Create an Environment instance from options
@@ -70,6 +76,7 @@ def create_environment(
70
76
  parsed_options=options,
71
77
  available_user_classes=available_user_classes,
72
78
  available_shape_classes=available_shape_classes,
79
+ available_user_tasks=available_user_tasks,
73
80
  )
74
81
 
75
82
 
@@ -84,9 +91,10 @@ def main():
84
91
  locustfile = locustfiles[0] if locustfiles_length == 1 else None
85
92
 
86
93
  # Importing Locustfile(s) - setting available UserClasses and ShapeClasses to choose from in UI
87
- user_classes: Dict[str, locust.User] = {}
94
+ user_classes: dict[str, locust.User] = {}
88
95
  available_user_classes = {}
89
96
  available_shape_classes = {}
97
+ available_user_tasks = {}
90
98
  shape_class = None
91
99
  for _locustfile in locustfiles:
92
100
  docstring, _user_classes, shape_classes = load_locustfile(_locustfile)
@@ -118,6 +126,7 @@ def main():
118
126
 
119
127
  user_classes[key] = value
120
128
  available_user_classes[key] = value
129
+ available_user_tasks[key] = value.tasks or None
121
130
 
122
131
  if len(stats.PERCENTILES_TO_CHART) != 2:
123
132
  logging.error("stats.PERCENTILES_TO_CHART parameter should be 2 parameters \n")
@@ -137,6 +146,14 @@ def main():
137
146
  "stats.PERCENTILES_TO_CHART parameter need to be float and value between. 0 < percentile < 1 Eg 0.95\n"
138
147
  )
139
148
  sys.exit(1)
149
+
150
+ for percentile in stats.PERCENTILES_TO_STATISTICS:
151
+ if not is_valid_percentile(percentile):
152
+ logging.error(
153
+ "stats.PERCENTILES_TO_STATISTICS parameter need to be float and value between. 0 < percentile < 1 Eg 0.95\n"
154
+ )
155
+ sys.exit(1)
156
+
140
157
  # parse all command line options
141
158
  options = parse_options()
142
159
 
@@ -147,6 +164,12 @@ def main():
147
164
  sys.stderr.write("The --slave/--expect-slaves parameters have been renamed --worker/--expect-workers\n")
148
165
  sys.exit(1)
149
166
 
167
+ if options.web_auth:
168
+ sys.stderr.write(
169
+ "The --web-auth parameters has been replaced with --web-login. See https://docs.locust.io/en/stable/extending-locust.html#authentication for details\n"
170
+ )
171
+ sys.exit(1)
172
+
150
173
  if options.autoquit != -1 and not options.autostart:
151
174
  sys.stderr.write("--autoquit is only meaningful in combination with --autostart\n")
152
175
  sys.exit(1)
@@ -182,6 +205,9 @@ def main():
182
205
  "--master cannot be combined with --processes. Remove --master, as it is implicit as long as --worker is not set.\n"
183
206
  )
184
207
  sys.exit(1)
208
+ # Optimize copy-on-write-behavior to save some memory (aprx 26MB -> 15MB rss) in child processes
209
+ gc.collect() # avoid freezing garbage
210
+ gc.freeze() # move all objects to perm gen so ref counts dont get updated
185
211
  for _ in range(options.processes):
186
212
  child_pid = gevent.fork()
187
213
  if child_pid:
@@ -337,8 +363,36 @@ See https://github.com/locustio/locust/wiki/Installation#increasing-maximum-numb
337
363
  locustfile=locustfile_path,
338
364
  available_user_classes=available_user_classes,
339
365
  available_shape_classes=available_shape_classes,
366
+ available_user_tasks=available_user_tasks,
340
367
  )
341
368
 
369
+ if options.config_users:
370
+ for json_user_config in options.config_users:
371
+ try:
372
+ if json_user_config.endswith(".json"):
373
+ with open(json_user_config) as file:
374
+ user_config = json.load(file)
375
+ else:
376
+ user_config = json.loads(json_user_config)
377
+
378
+ def ensure_user_class_name(config):
379
+ if "user_class_name" not in config:
380
+ logger.error("The user config must specify a user_class_name")
381
+ sys.exit(-1)
382
+
383
+ if isinstance(user_config, list):
384
+ for config in user_config:
385
+ ensure_user_class_name(config)
386
+
387
+ environment.update_user_class(config)
388
+ else:
389
+ ensure_user_class_name(user_config)
390
+
391
+ environment.update_user_class(user_config)
392
+ except Exception as e:
393
+ logger.error(f"The --config-users arugment must be in valid JSON string or file: {e}")
394
+ sys.exit(-1)
395
+
342
396
  if (
343
397
  shape_class
344
398
  and not shape_class.use_common_options
@@ -412,39 +466,33 @@ See https://github.com/locustio/locust/wiki/Installation#increasing-maximum-numb
412
466
  if not options.headless and not options.worker:
413
467
  # spawn web greenlet
414
468
  protocol = "https" if options.tls_cert and options.tls_key else "http"
415
- try:
416
- if options.web_host == "*":
417
- # special check for "*" so that we're consistent with --master-bind-host
418
- web_host = ""
419
- else:
420
- web_host = options.web_host
421
- if web_host:
422
- logger.info(f"Starting web interface at {protocol}://{web_host}:{options.web_port}")
423
- else:
424
- if os.name == "nt":
425
- logger.info(
426
- f"Starting web interface at {protocol}://localhost:{options.web_port} (accepting connections from all network interfaces)"
427
- )
428
- else:
429
- logger.info(f"Starting web interface at {protocol}://0.0.0.0:{options.web_port}")
430
- if options.web_auth:
431
- logging.warning(
432
- "BasicAuth support is deprecated, it will be removed in a future release, unless someone reimplements it in a more modern way! See https://github.com/locustio/locust/issues/2517"
469
+
470
+ if options.web_host == "*":
471
+ # special check for "*" so that we're consistent with --master-bind-host
472
+ web_host = ""
473
+ else:
474
+ web_host = options.web_host
475
+ if web_host:
476
+ logger.info(f"Starting web interface at {protocol}://{web_host}:{options.web_port}")
477
+ else:
478
+ if os.name == "nt":
479
+ logger.info(
480
+ f"Starting web interface at {protocol}://localhost:{options.web_port} (accepting connections from all network interfaces)"
433
481
  )
434
- web_ui = environment.create_web_ui(
435
- host=web_host,
436
- port=options.web_port,
437
- auth_credentials=options.web_auth,
438
- tls_cert=options.tls_cert,
439
- tls_key=options.tls_key,
440
- stats_csv_writer=stats_csv_writer,
441
- delayed_start=True,
442
- userclass_picker_is_active=options.class_picker,
443
- modern_ui=options.modern_ui,
444
- )
445
- except AuthCredentialsError:
446
- logger.error("Credentials supplied with --web-auth should have the format: username:password")
447
- sys.exit(1)
482
+ else:
483
+ logger.info(f"Starting web interface at {protocol}://0.0.0.0:{options.web_port}")
484
+
485
+ web_ui = environment.create_web_ui(
486
+ host=web_host,
487
+ port=options.web_port,
488
+ web_login=options.web_login,
489
+ tls_cert=options.tls_cert,
490
+ tls_key=options.tls_key,
491
+ stats_csv_writer=stats_csv_writer,
492
+ delayed_start=True,
493
+ userclass_picker_is_active=options.class_picker,
494
+ modern_ui=options.modern_ui,
495
+ )
448
496
  else:
449
497
  web_ui = None
450
498
 
locust/rpc/protocol.py CHANGED
@@ -1,6 +1,8 @@
1
- import msgpack
1
+ from __future__ import annotations
2
+
2
3
  import datetime
3
- from typing import Type
4
+
5
+ import msgpack
4
6
 
5
7
  try:
6
8
  from bson import ObjectId # type: ignore
locust/rpc/zmqrpc.py CHANGED
@@ -1,9 +1,11 @@
1
- import zmq.green as zmq
2
- from .protocol import Message
1
+ from locust.exception import RPCError, RPCReceiveError, RPCSendError
3
2
  from locust.util.exception_handler import retry
4
- from locust.exception import RPCError, RPCSendError, RPCReceiveError
5
- import zmq.error as zmqerr
3
+
6
4
  import msgpack.exceptions as msgerr
5
+ import zmq.error as zmqerr
6
+ import zmq.green as zmq
7
+
8
+ from .protocol import Message
7
9
 
8
10
 
9
11
  class BaseSocket: