locust 2.29.1.dev34__py3-none-any.whl → 2.29.1.dev39__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 (48) hide show
  1. locust/_version.py +6 -2
  2. locust/contrib/fasthttp.py +55 -18
  3. locust/dispatch.py +7 -6
  4. locust/event.py +32 -0
  5. locust/runners.py +13 -22
  6. locust/stats.py +4 -17
  7. locust/user/sequential_taskset.py +8 -6
  8. locust/webui/dist/assets/{index-84c63e70.js → index-e9ad42b4.js} +2 -2
  9. locust/webui/dist/auth.html +1 -1
  10. locust/webui/dist/index.html +1 -1
  11. {locust-2.29.1.dev34.dist-info → locust-2.29.1.dev39.dist-info}/METADATA +31 -26
  12. locust-2.29.1.dev39.dist-info/RECORD +49 -0
  13. locust-2.29.1.dev39.dist-info/WHEEL +4 -0
  14. locust-2.29.1.dev39.dist-info/entry_points.txt +3 -0
  15. locust/test/__init__.py +0 -15
  16. locust/test/fake_module1_for_env_test.py +0 -7
  17. locust/test/fake_module2_for_env_test.py +0 -7
  18. locust/test/mock_locustfile.py +0 -56
  19. locust/test/mock_logging.py +0 -28
  20. locust/test/test_debugging.py +0 -39
  21. locust/test/test_dispatch.py +0 -4170
  22. locust/test/test_env.py +0 -283
  23. locust/test/test_fasthttp.py +0 -785
  24. locust/test/test_http.py +0 -325
  25. locust/test/test_interruptable_task.py +0 -48
  26. locust/test/test_load_locustfile.py +0 -228
  27. locust/test/test_locust_class.py +0 -831
  28. locust/test/test_log.py +0 -237
  29. locust/test/test_main.py +0 -2264
  30. locust/test/test_old_wait_api.py +0 -0
  31. locust/test/test_parser.py +0 -450
  32. locust/test/test_runners.py +0 -4342
  33. locust/test/test_sequential_taskset.py +0 -137
  34. locust/test/test_stats.py +0 -866
  35. locust/test/test_tags.py +0 -440
  36. locust/test/test_taskratio.py +0 -94
  37. locust/test/test_users.py +0 -69
  38. locust/test/test_util.py +0 -33
  39. locust/test/test_wait_time.py +0 -79
  40. locust/test/test_web.py +0 -1257
  41. locust/test/test_zmqrpc.py +0 -58
  42. locust/test/testcases.py +0 -248
  43. locust/test/util.py +0 -88
  44. locust-2.29.1.dev34.dist-info/RECORD +0 -79
  45. locust-2.29.1.dev34.dist-info/WHEEL +0 -5
  46. locust-2.29.1.dev34.dist-info/entry_points.txt +0 -2
  47. locust-2.29.1.dev34.dist-info/top_level.txt +0 -1
  48. {locust-2.29.1.dev34.dist-info → locust-2.29.1.dev39.dist-info}/LICENSE +0 -0
locust/_version.py CHANGED
@@ -3,6 +3,7 @@
3
3
  TYPE_CHECKING = False
4
4
  if TYPE_CHECKING:
5
5
  from typing import Tuple, Union
6
+
6
7
  VERSION_TUPLE = Tuple[Union[int, str], ...]
7
8
  else:
8
9
  VERSION_TUPLE = object
@@ -12,5 +13,8 @@ __version__: str
12
13
  __version_tuple__: VERSION_TUPLE
13
14
  version_tuple: VERSION_TUPLE
14
15
 
15
- __version__ = version = '2.29.1.dev34'
16
- __version_tuple__ = version_tuple = (2, 29, 1, 'dev34')
16
+
17
+ __version__ = "2.29.1.dev39"
18
+ version = __version__
19
+ __version_tuple__ = (2, 29, 1, "dev39")
20
+ version_tuple = __version_tuple__
@@ -12,12 +12,11 @@ import socket
12
12
  import time
13
13
  import traceback
14
14
  from base64 import b64encode
15
- from collections.abc import Generator
16
15
  from contextlib import contextmanager
17
16
  from http.cookiejar import CookieJar
18
17
  from json.decoder import JSONDecodeError
19
18
  from ssl import SSLError
20
- from typing import Any, Callable, cast
19
+ from typing import TYPE_CHECKING, cast
21
20
  from urllib.parse import urlparse, urlunparse
22
21
 
23
22
  import gevent
@@ -32,6 +31,36 @@ from geventhttpclient.useragent import CompatRequest, CompatResponse, Connection
32
31
  # borrow requests's content-type header parsing
33
32
  from requests.utils import get_encoding_from_headers
34
33
 
34
+ if TYPE_CHECKING:
35
+ import sys
36
+ from collections.abc import Callable, Generator
37
+ from typing import TypedDict
38
+
39
+ if sys.version_info >= (3, 11):
40
+ from typing import Unpack
41
+ else:
42
+ from typing_extensions import Unpack
43
+
44
+ class PostKwargs(TypedDict, total=False):
45
+ name: str | None
46
+ catch_response: bool
47
+ stream: bool
48
+ headers: dict | None
49
+ auth: tuple[str | bytes, str | bytes] | None
50
+ allow_redirects: bool
51
+ context: dict
52
+
53
+ class PutKwargs(PostKwargs, total=False):
54
+ json: dict | None
55
+
56
+ class PatchKwargs(PostKwargs, total=False):
57
+ json: dict | None
58
+
59
+ class RESTKwargs(PostKwargs, total=False):
60
+ data: str | dict | None
61
+ json: dict | None
62
+
63
+
35
64
  # Monkey patch geventhttpclient.useragent.CompatRequest so that Cookiejar works with Python >= 3.3
36
65
  # More info: https://github.com/requests/requests/pull/871
37
66
  CompatRequest.unverifiable = False
@@ -85,7 +114,7 @@ class FastHttpSession:
85
114
  client_pool: HTTPClientPool | None = None,
86
115
  ssl_context_factory: Callable | None = None,
87
116
  **kwargs,
88
- ):
117
+ ) -> None:
89
118
  self.environment = environment
90
119
  self.base_url = base_url
91
120
  self.cookiejar = CookieJar()
@@ -117,14 +146,14 @@ class FastHttpSession:
117
146
  # store authentication header (we construct this by using _basic_auth_str() function from requests.auth)
118
147
  self.auth_header = _construct_basic_auth_str(parsed_url.username, parsed_url.password)
119
148
 
120
- def _build_url(self, path):
149
+ def _build_url(self, path: str) -> str:
121
150
  """prepend url with hostname unless it's already an absolute URL"""
122
151
  if absolute_http_url_regexp.match(path):
123
152
  return path
124
153
  else:
125
154
  return f"{self.base_url}{path}"
126
155
 
127
- def _send_request_safe_mode(self, method, url, **kwargs):
156
+ def _send_request_safe_mode(self, method: str, url: str, **kwargs):
128
157
  """
129
158
  Send an HTTP request, and catch any exception that might occur due to either
130
159
  connection problems, or invalid HTTP status codes
@@ -155,9 +184,9 @@ class FastHttpSession:
155
184
  catch_response: bool = False,
156
185
  stream: bool = False,
157
186
  headers: dict | None = None,
158
- auth=None,
187
+ auth: tuple[str | bytes, str | bytes] | None = None,
159
188
  json: dict | None = None,
160
- allow_redirects=True,
189
+ allow_redirects: bool = True,
161
190
  context: dict = {},
162
191
  **kwargs,
163
192
  ) -> ResponseContextManager | FastResponse:
@@ -187,6 +216,7 @@ class FastHttpSession:
187
216
  and can instead be consumed by accessing the stream attribute on the Response object.
188
217
  Another side effect of setting stream to True is that the time for downloading the response
189
218
  content will not be accounted for in the request time that is reported by Locust.
219
+ :param allow_redirects: (optional) Set to True by default.
190
220
  """
191
221
  # prepend url with hostname unless it's already an absolute URL
192
222
  built_url = self._build_url(url)
@@ -250,7 +280,7 @@ class FastHttpSession:
250
280
  # Record the consumed time
251
281
  # Note: This is intentionally placed after we record the content_size above, since
252
282
  # we'll then trigger fetching of the body (unless stream=True)
253
- request_meta["response_time"] = int((time.perf_counter() - start_perf_counter) * 1000)
283
+ request_meta["response_time"] = (time.perf_counter() - start_perf_counter) * 1000
254
284
 
255
285
  if catch_response:
256
286
  return ResponseContextManager(response, environment=self.environment, request_meta=request_meta)
@@ -263,30 +293,37 @@ class FastHttpSession:
263
293
  self.environment.events.request.fire(**request_meta)
264
294
  return response
265
295
 
266
- def delete(self, url, **kwargs):
296
+ def delete(self, url: str, **kwargs: Unpack[RESTKwargs]) -> ResponseContextManager | FastResponse:
297
+ """Sends a DELETE request"""
267
298
  return self.request("DELETE", url, **kwargs)
268
299
 
269
- def get(self, url, **kwargs):
300
+ def get(self, url: str, **kwargs: Unpack[RESTKwargs]) -> ResponseContextManager | FastResponse:
270
301
  """Sends a GET request"""
271
302
  return self.request("GET", url, **kwargs)
272
303
 
273
- def head(self, url, **kwargs):
304
+ def head(self, url: str, **kwargs: Unpack[RESTKwargs]) -> ResponseContextManager | FastResponse:
274
305
  """Sends a HEAD request"""
275
306
  return self.request("HEAD", url, **kwargs)
276
307
 
277
- def options(self, url, **kwargs):
308
+ def options(self, url: str, **kwargs: Unpack[RESTKwargs]) -> ResponseContextManager | FastResponse:
278
309
  """Sends a OPTIONS request"""
279
310
  return self.request("OPTIONS", url, **kwargs)
280
311
 
281
- def patch(self, url, data=None, **kwargs):
282
- """Sends a POST request"""
312
+ def patch(
313
+ self, url: str, data: str | dict | None = None, **kwargs: Unpack[PatchKwargs]
314
+ ) -> ResponseContextManager | FastResponse:
315
+ """Sends a PATCH request"""
283
316
  return self.request("PATCH", url, data=data, **kwargs)
284
317
 
285
- def post(self, url, data=None, **kwargs):
318
+ def post(
319
+ self, url: str, data: str | dict | None = None, json: dict | None = None, **kwargs: Unpack[PostKwargs]
320
+ ) -> ResponseContextManager | FastResponse:
286
321
  """Sends a POST request"""
287
- return self.request("POST", url, data=data, **kwargs)
322
+ return self.request("POST", url, data=data, json=json, **kwargs)
288
323
 
289
- def put(self, url, data=None, **kwargs):
324
+ def put(
325
+ self, url: str, data: str | dict | None = None, **kwargs: Unpack[PutKwargs]
326
+ ) -> ResponseContextManager | FastResponse:
290
327
  """Sends a PUT request"""
291
328
  return self.request("PUT", url, data=data, **kwargs)
292
329
 
@@ -446,7 +483,7 @@ class FastResponse(CompatResponse):
446
483
 
447
484
  _response: HTTPSocketPoolResponse | None = None
448
485
 
449
- encoding: str | None = None
486
+ encoding: str | float | None = None
450
487
  """In some cases setting the encoding explicitly is needed. If so, do it before calling .text"""
451
488
 
452
489
  request: FastRequest | None = None
locust/dispatch.py CHANGED
@@ -90,13 +90,13 @@ class UsersDispatcher(Iterator):
90
90
  assert len(user_classes) > 0
91
91
  assert len(set(self._user_classes)) == len(self._user_classes)
92
92
 
93
- self._target_user_count: int = None
93
+ self._target_user_count: int = 0
94
94
 
95
- self._spawn_rate: float = None
95
+ self._spawn_rate: float = 0.0
96
96
 
97
- self._user_count_per_dispatch_iteration: int = None
97
+ self._user_count_per_dispatch_iteration: int = 0
98
98
 
99
- self._wait_between_dispatch: float = None
99
+ self._wait_between_dispatch: float = 0.0
100
100
 
101
101
  self._initial_users_on_workers = {
102
102
  worker_node.id: {user_class.__name__: 0 for user_class in self._user_classes}
@@ -107,7 +107,8 @@ class UsersDispatcher(Iterator):
107
107
 
108
108
  self._current_user_count = self.get_current_user_count()
109
109
 
110
- self._dispatcher_generator: Generator[dict[str, dict[str, int]], None, None] = None
110
+ self._dispatcher_generator: Generator[dict[str, dict[str, int]], None, None] = None # type: ignore
111
+ # a generator is assigned (in new_dispatch()) to _dispatcher_generator before it's used
111
112
 
112
113
  self._user_generator = self._user_gen()
113
114
 
@@ -131,7 +132,7 @@ class UsersDispatcher(Iterator):
131
132
  return sum(map(sum, map(dict.values, self._users_on_workers.values())))
132
133
 
133
134
  @property
134
- def dispatch_in_progress(self):
135
+ def dispatch_in_progress(self) -> bool:
135
136
  return self._dispatch_in_progress
136
137
 
137
138
  @property
locust/event.py CHANGED
@@ -235,6 +235,38 @@ class Events:
235
235
  Fired when the CPU usage exceeds runners.CPU_WARNING_THRESHOLD (90% by default)
236
236
  """
237
237
 
238
+ heartbeat_sent: EventHook
239
+ """
240
+ Fired when a heartbeat is sent by master to a worker.
241
+
242
+ Event arguments:
243
+
244
+ :param client_id: worker client id
245
+ :param timestamp: time in seconds since the epoch (float) when the event occured
246
+ """
247
+
248
+ heartbeat_received: EventHook
249
+ """
250
+ Fired when a heartbeat is received by a worker from master.
251
+
252
+ Event arguments:
253
+
254
+ :param client_id: worker client id
255
+ :param timestamp: time in seconds since the epoch (float) when the event occured
256
+ """
257
+
258
+ usage_monitor: EventHook
259
+ """
260
+ Fired every runners.CPU_MONITOR_INTERVAL (5.0 seconds by default) with information about
261
+ current CPU and memory usage.
262
+
263
+ Event arguments:
264
+
265
+ :param environment: locust environment
266
+ :param cpu_usage: current CPU usage in percent
267
+ :param memory_usage: current memory usage (RSS) in bytes
268
+ """
269
+
238
270
  def __init__(self):
239
271
  # For backward compatibility use also values of class attributes
240
272
  for name, value in vars(type(self)).items():
locust/runners.py CHANGED
@@ -15,19 +15,9 @@ import traceback
15
15
  from abc import abstractmethod
16
16
  from collections import defaultdict
17
17
  from collections.abc import Iterator, MutableMapping, ValuesView
18
- from operator import (
19
- itemgetter,
20
- methodcaller,
21
- )
18
+ from operator import itemgetter, methodcaller
22
19
  from types import TracebackType
23
- from typing import (
24
- TYPE_CHECKING,
25
- Any,
26
- Callable,
27
- NoReturn,
28
- TypedDict,
29
- cast,
30
- )
20
+ from typing import TYPE_CHECKING, Any, Callable, NoReturn, TypedDict, cast
31
21
  from uuid import uuid4
32
22
 
33
23
  import gevent
@@ -40,15 +30,8 @@ from . import argument_parser
40
30
  from .dispatch import UsersDispatcher
41
31
  from .exception import RPCError, RPCReceiveError, RPCSendError
42
32
  from .log import get_logs, greenlet_exception_logger
43
- from .rpc import (
44
- Message,
45
- rpc,
46
- )
47
- from .stats import (
48
- RequestStats,
49
- StatsError,
50
- setup_distributed_stats_event_listeners,
51
- )
33
+ from .rpc import Message, rpc
34
+ from .stats import RequestStats, StatsError, setup_distributed_stats_event_listeners
52
35
 
53
36
  if TYPE_CHECKING:
54
37
  from . import User
@@ -106,7 +89,7 @@ class Runner:
106
89
  self.spawning_greenlet: gevent.Greenlet | None = None
107
90
  self.shape_greenlet: gevent.Greenlet | None = None
108
91
  self.shape_last_tick: tuple[int, float] | tuple[int, float, list[type[User]] | None] | None = None
109
- self.current_cpu_usage: int = 0
92
+ self.current_cpu_usage: float = 0.0
110
93
  self.cpu_warning_emitted: bool = False
111
94
  self.worker_cpu_warning_emitted: bool = False
112
95
  self.current_memory_usage: int = 0
@@ -308,6 +291,10 @@ class Runner:
308
291
  f"CPU usage above {CPU_WARNING_THRESHOLD}%! This may constrain your throughput and may even give inconsistent response time measurements! See https://docs.locust.io/en/stable/running-distributed.html for how to distribute the load over multiple CPU cores or machines"
309
292
  )
310
293
  self.cpu_warning_emitted = True
294
+
295
+ self.environment.events.usage_monitor.fire(
296
+ environment=self.environment, cpu_usage=self.current_cpu_usage, memory_usage=self.current_memory_usage
297
+ )
311
298
  gevent.sleep(CPU_MONITOR_INTERVAL)
312
299
 
313
300
  @abstractmethod
@@ -1102,6 +1089,7 @@ class MasterRunner(DistributedRunner):
1102
1089
  )
1103
1090
  if "current_memory_usage" in msg.data:
1104
1091
  c.memory_usage = msg.data["current_memory_usage"]
1092
+ self.environment.events.heartbeat_sent.fire(client_id=msg.node_id, timestamp=time.time())
1105
1093
  self.server.send_to_client(Message("heartbeat", None, msg.node_id))
1106
1094
  else:
1107
1095
  logging.debug(f"Got heartbeat message from unknown worker {msg.node_id}")
@@ -1399,6 +1387,9 @@ class WorkerRunner(DistributedRunner):
1399
1387
  self.reset_connection()
1400
1388
  elif msg.type == "heartbeat":
1401
1389
  self.last_heartbeat_timestamp = time.time()
1390
+ self.environment.events.heartbeat_received.fire(
1391
+ client_id=msg.node_id, timestamp=self.last_heartbeat_timestamp
1392
+ )
1402
1393
  elif msg.type == "update_user_class":
1403
1394
  self.environment.update_user_class(msg.data)
1404
1395
  elif msg.type == "spawning_complete":
locust/stats.py CHANGED
@@ -9,25 +9,12 @@ import signal
9
9
  import time
10
10
  from abc import abstractmethod
11
11
  from collections import OrderedDict, defaultdict, namedtuple
12
- from collections import (
13
- OrderedDict as OrderedDictType,
14
- )
15
12
  from collections.abc import Iterable
16
13
  from copy import copy
17
14
  from html import escape
18
15
  from itertools import chain
19
- from tempfile import NamedTemporaryFile
20
16
  from types import FrameType
21
- from typing import (
22
- TYPE_CHECKING,
23
- Any,
24
- Callable,
25
- NoReturn,
26
- Protocol,
27
- TypedDict,
28
- TypeVar,
29
- cast,
30
- )
17
+ from typing import TYPE_CHECKING, Any, Callable, NoReturn, Protocol, TypedDict, TypeVar, cast
31
18
 
32
19
  import gevent
33
20
 
@@ -319,12 +306,12 @@ class StatsEntry:
319
306
  A {response_time => count} dict that holds the response time distribution of all
320
307
  the requests.
321
308
 
322
- The keys (the response time in ms) are rounded to store 1, 2, ... 9, 10, 20. .. 90,
323
- 100, 200 .. 900, 1000, 2000 ... 9000, in order to save memory.
309
+ The keys (the response time in ms) are rounded to store 1, 2, ... 98, 99, 100, 110, 120, ... 980, 990, 1000,
310
+ 1100, 1200, ... 9800, 9900, 10_000, 11_000, 12_000 ... in order to save memory.
324
311
 
325
312
  This dict is used to calculate the median and percentile response times.
326
313
  """
327
- self.response_times_cache: OrderedDictType[int, CachedResponseTimes] | None = None
314
+ self.response_times_cache: OrderedDict[int, CachedResponseTimes] | None = None
328
315
  """
329
316
  If use_response_times_cache is set to True, this will be a {timestamp => CachedResponseTimes()}
330
317
  OrderedDict that holds a copy of the response_times dict for each of the last 20 seconds.
@@ -1,6 +1,6 @@
1
1
  from locust.exception import LocustError
2
2
 
3
- import logging
3
+ from itertools import cycle
4
4
 
5
5
  from .task import TaskSet, TaskSetMeta
6
6
 
@@ -26,8 +26,12 @@ class SequentialTaskSetMeta(TaskSetMeta):
26
26
  # compared to methods declared with @task
27
27
  if isinstance(value, list):
28
28
  new_tasks.extend(value)
29
+ elif isinstance(value, dict):
30
+ for task, weight in value.items():
31
+ for _ in range(weight):
32
+ new_tasks.append(task)
29
33
  else:
30
- raise ValueError("On SequentialTaskSet the task attribute can only be set to a list")
34
+ raise ValueError("The 'tasks' attribute can only be set to list or dict")
31
35
 
32
36
  if "locust_task_weight" in dir(value):
33
37
  # method decorated with @task
@@ -52,13 +56,11 @@ class SequentialTaskSet(TaskSet, metaclass=SequentialTaskSetMeta):
52
56
 
53
57
  def __init__(self, *args, **kwargs):
54
58
  super().__init__(*args, **kwargs)
55
- self._task_index = 0
59
+ self._task_cycle = cycle(self.tasks)
56
60
 
57
61
  def get_next_task(self):
58
62
  if not self.tasks:
59
63
  raise LocustError(
60
64
  "No tasks defined. Use the @task decorator or set the 'tasks' attribute of the SequentialTaskSet"
61
65
  )
62
- task = self.tasks[self._task_index % len(self.tasks)]
63
- self._task_index += 1
64
- return task
66
+ return next(self._task_cycle)