locust 2.46.5.dev8__tar.gz → 2.46.6.dev4__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 (69) hide show
  1. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/PKG-INFO +1 -1
  2. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/__init__.py +2 -1
  3. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/_version.py +2 -2
  4. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/clients.py +4 -0
  5. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/contrib/fasthttp.py +3 -67
  6. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/user/users.py +79 -3
  7. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/user/wait_time.py +27 -0
  8. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/.gitignore +0 -0
  9. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/LICENSE +0 -0
  10. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/README.md +0 -0
  11. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/hatch_build.py +0 -0
  12. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/__main__.py +0 -0
  13. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/argument_parser.py +0 -0
  14. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/contrib/__init__.py +0 -0
  15. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/contrib/csv_request_logger.py +0 -0
  16. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/contrib/dns.py +0 -0
  17. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/contrib/milvus.py +0 -0
  18. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/contrib/mongodb.py +0 -0
  19. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/contrib/mqtt.py +0 -0
  20. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/contrib/oai.py +0 -0
  21. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/contrib/postgres.py +0 -0
  22. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/contrib/qdrant.py +0 -0
  23. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/contrib/socketio.py +0 -0
  24. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/debug.py +0 -0
  25. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/dispatch.py +0 -0
  26. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/env.py +0 -0
  27. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/event.py +0 -0
  28. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/exception.py +0 -0
  29. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/html.py +0 -0
  30. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/input_events.py +0 -0
  31. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/log.py +0 -0
  32. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/main.py +0 -0
  33. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/opentelemetry.py +0 -0
  34. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/py.typed +0 -0
  35. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/rpc/__init__.py +0 -0
  36. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/rpc/protocol.py +0 -0
  37. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/rpc/zmqrpc.py +0 -0
  38. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/runners.py +0 -0
  39. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/shape.py +0 -0
  40. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/stats.py +0 -0
  41. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/user/__init__.py +0 -0
  42. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/user/inspectuser.py +0 -0
  43. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/user/markov_taskset.py +0 -0
  44. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/user/sequential_taskset.py +0 -0
  45. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/user/task.py +0 -0
  46. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/util/__init__.py +0 -0
  47. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/util/cache.py +0 -0
  48. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/util/date.py +0 -0
  49. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/util/deprecation.py +0 -0
  50. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/util/directory.py +0 -0
  51. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/util/exception_handler.py +0 -0
  52. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/util/load_locustfile.py +0 -0
  53. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/util/rounding.py +0 -0
  54. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/util/timespan.py +0 -0
  55. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/util/url.py +0 -0
  56. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/web.py +0 -0
  57. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/webui/dist/assets/favicon-dark.png +0 -0
  58. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/webui/dist/assets/favicon-light.png +0 -0
  59. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/webui/dist/assets/graphs-dark.png +0 -0
  60. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/webui/dist/assets/graphs-light.png +0 -0
  61. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/webui/dist/assets/src-pSlzKFX1.js +0 -0
  62. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/webui/dist/assets/terminal.gif +0 -0
  63. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/webui/dist/assets/testruns-dark.png +0 -0
  64. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/webui/dist/assets/testruns-light.png +0 -0
  65. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/webui/dist/auth.html +0 -0
  66. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/webui/dist/index.html +0 -0
  67. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/locust/webui/dist/report.html +0 -0
  68. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/pyproject.toml +0 -0
  69. {locust-2.46.5.dev8 → locust-2.46.6.dev4}/pytest_locust/plugin.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: locust
3
- Version: 2.46.5.dev8
3
+ Version: 2.46.6.dev4
4
4
  Summary: Developer-friendly load testing framework
5
5
  Project-URL: Homepage, https://locust.cloud/
6
6
  Project-URL: Repository, https://github.com/locustio/locust
@@ -31,7 +31,7 @@ from .user.markov_taskset import MarkovTaskSet, transition, transitions
31
31
  from .user.sequential_taskset import SequentialTaskSet
32
32
  from .user.task import TaskSet, tag, task
33
33
  from .user.users import HttpUser, User
34
- from .user.wait_time import between, constant, constant_pacing, constant_throughput
34
+ from .user.wait_time import between, constant, constant_pacing, constant_throughput, poisson
35
35
 
36
36
  events = Events()
37
37
 
@@ -51,6 +51,7 @@ __all__ = (
51
51
  "constant",
52
52
  "constant_pacing",
53
53
  "constant_throughput",
54
+ "poisson",
54
55
  "events",
55
56
  "LoadTestShape",
56
57
  "run_single_user",
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.46.5.dev8'
22
- __version_tuple__ = version_tuple = (2, 46, 5, 'dev8')
21
+ __version__ = version = '2.46.6.dev4'
22
+ __version_tuple__ = version_tuple = (2, 46, 6, 'dev4')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -478,6 +478,10 @@ class ResponseContextManager(Response):
478
478
  self._manual_result = exc
479
479
 
480
480
 
481
+ class RestResponseContextManager(ResponseContextManager):
482
+ js: dict # This is technically an Optional, but I dont want to force everyone to check it
483
+
484
+
481
485
  class LocustHttpAdapter(HTTPAdapter):
482
486
  def __init__(self, pool_manager: PoolManager | None, *args, **kwargs):
483
487
  self.poolmanager = pool_manager
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from locust.exception import CatchResponseError, LocustError, ResponseError, StopTest
4
4
  from locust.user import User
5
+ from locust.user.users import RestMixin
5
6
  from locust.util.deprecation import DeprecatedFastHttpLocustClass as FastHttpLocust # noqa: F401
6
7
 
7
8
  import json
@@ -9,14 +10,11 @@ import json as unshadowed_json # some methods take a named parameter called jso
9
10
  import re
10
11
  import socket
11
12
  import time
12
- import traceback
13
13
  import zlib
14
14
  from base64 import b64encode
15
- from contextlib import contextmanager
16
15
  from http.cookiejar import CookieJar
17
- from json.decoder import JSONDecodeError
18
16
  from ssl import SSLError
19
- from typing import TYPE_CHECKING, cast
17
+ from typing import TYPE_CHECKING
20
18
  from urllib.parse import urlparse, urlunparse
21
19
 
22
20
  import gevent
@@ -355,7 +353,7 @@ class FastHttpSession:
355
353
  return self.request("PUT", url, data=data, **kwargs)
356
354
 
357
355
 
358
- class FastHttpUser(User):
356
+ class FastHttpUser(User, RestMixin["RestResponseContextManager"]):
359
357
  """
360
358
  FastHttpUser provides the same API as HttpUser, but uses geventhttpclient instead of python-requests
361
359
  as its underlying client. It uses considerably less CPU on the load generator, and should work
@@ -413,8 +411,6 @@ class FastHttpUser(User):
413
411
  abstract = True
414
412
  """Dont register this as a User class that can be run by itself"""
415
413
 
416
- _callstack_regex = re.compile(r' File "(\/.[^"]*)", line (\d*),(.*)')
417
-
418
414
  def __init__(self, environment) -> None:
419
415
  super().__init__(environment)
420
416
  if self.host is None:
@@ -443,66 +439,6 @@ class FastHttpUser(User):
443
439
  The client support cookies, and therefore keeps the session between HTTP requests.
444
440
  """
445
441
 
446
- @contextmanager
447
- def rest(self, method, url, headers: dict | None = None, **kwargs) -> Generator[RestResponseContextManager]:
448
- """
449
- A wrapper for self.client.request that:
450
-
451
- * Parses the JSON response to a dict called ``js`` in the response object. Marks the request as failed if the response was not valid JSON.
452
- * Defaults ``Content-Type`` and ``Accept`` headers to ``application/json``
453
- * Sets ``catch_response=True`` (so always use a :ref:`with-block <catch-response>`)
454
- * Catches any unhandled exceptions thrown inside your with-block, marking the sample as failed (instead of exiting the task immediately without even firing the request event)
455
- """
456
- headers = headers or {}
457
- if not ("Content-Type" in headers or "content-type" in headers):
458
- headers["Content-Type"] = "application/json"
459
- if not ("Accept" in headers or "accept" in headers):
460
- headers["Accept"] = "application/json"
461
- with self.client.request(method, url, catch_response=True, headers=headers, **kwargs) as r:
462
- resp = cast(RestResponseContextManager, r)
463
- resp.js = None # type: ignore
464
- if resp.content is None:
465
- resp.failure(str(resp.error))
466
- elif resp.text:
467
- try:
468
- resp.js = resp.json()
469
- except JSONDecodeError as e:
470
- resp.failure(
471
- f"Could not parse response as JSON. {resp.text[:250]}, response code {resp.status_code}, error {e}"
472
- )
473
- try:
474
- yield resp
475
- except AssertionError as e:
476
- if e.args:
477
- if e.args[0].endswith(","):
478
- short_resp = resp.text[:200] if resp.text else resp.text
479
- resp.failure(f"{e.args[0][:-1]}, response was {short_resp}")
480
- else:
481
- resp.failure(e.args[0])
482
- else:
483
- resp.failure("Assertion failed")
484
-
485
- except Exception as e:
486
- error_lines = []
487
- for l in traceback.format_exc().split("\n"):
488
- if m := self._callstack_regex.match(l):
489
- filename = re.sub(r"/((home|Users)/\w*)/", "~/", m.group(1))
490
- error_lines.append(filename + ":" + m.group(2) + m.group(3))
491
- short_resp = resp.text[:200] if resp.text else resp.text
492
- resp.failure(f"{e.__class__.__name__}: {e} at {', '.join(error_lines)}. Response was {short_resp}")
493
-
494
- @contextmanager
495
- def rest_(self, method, url, name=None, **kwargs) -> Generator[RestResponseContextManager]:
496
- """
497
- Some REST api:s use a timestamp as part of their query string (mainly to break through caches).
498
- This is a convenience method for that, appending a _=<timestamp> parameter automatically
499
- """
500
- separator = "&" if "?" in url else "?"
501
- if name is None:
502
- name = url + separator + "_=..."
503
- with self.rest(method, f"{url}{separator}_={int(time.time() * 1000)}", name=name, **kwargs) as resp:
504
- yield resp
505
-
506
442
 
507
443
  class FastRequest(CompatRequest):
508
444
  payload: str | None = None
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from locust.clients import HttpSession
3
+ from locust.clients import HttpSession, RestResponseContextManager
4
4
  from locust.exception import CatchResponseError, StopTest, StopUser
5
5
  from locust.user.task import (
6
6
  LOCUST_STATE_RUNNING,
@@ -14,11 +14,14 @@ from locust.user.wait_time import constant
14
14
  from locust.util import deprecation
15
15
 
16
16
  import logging
17
+ import re
17
18
  import sys
18
19
  import time
19
20
  import traceback
20
21
  from collections.abc import Callable
21
- from typing import TYPE_CHECKING, final
22
+ from contextlib import contextmanager
23
+ from json.decoder import JSONDecodeError
24
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar, final
22
25
 
23
26
  from gevent import GreenletExit, greenlet
24
27
  from gevent.pool import Group
@@ -32,6 +35,8 @@ else:
32
35
  from typing_extensions import override
33
36
 
34
37
  if TYPE_CHECKING:
38
+ from collections.abc import Generator
39
+
35
40
  import pytest
36
41
 
37
42
  logger = logging.getLogger(__name__)
@@ -250,7 +255,78 @@ class User(metaclass=UserMeta):
250
255
  return ".".join(filter(lambda x: x != "<locals>", (cls.__module__ + "." + cls.__qualname__).split(".")))
251
256
 
252
257
 
253
- class HttpUser(User):
258
+ RestResponseT = TypeVar("RestResponseT")
259
+
260
+
261
+ class RestMixin(Generic[RestResponseT]):
262
+ """
263
+ Provides the ``rest`` and ``rest_`` methods for User classes that have an HTTP ``client`` (HttpUser and FastHttpUser)
264
+ """
265
+
266
+ _callstack_regex = re.compile(r' File "(\/.[^"]*)", line (\d*),(.*)')
267
+
268
+ @contextmanager
269
+ def rest(self, method, url, headers: dict | None = None, **kwargs) -> Generator[RestResponseT]:
270
+ """
271
+ A wrapper for self.client.request that:
272
+
273
+ * Parses the JSON response to a dict called ``js`` in the response object. Marks the request as failed if the response was not valid JSON.
274
+ * Defaults ``Content-Type`` and ``Accept`` headers to ``application/json``
275
+ * Sets ``catch_response=True`` (so always use a :ref:`with-block <catch-response>`)
276
+ * Catches any unhandled exceptions thrown inside your with-block, marking the sample as failed (instead of exiting the task immediately without even firing the request event)
277
+ """
278
+ headers = headers or {}
279
+ if not ("Content-Type" in headers or "content-type" in headers):
280
+ headers["Content-Type"] = "application/json"
281
+ if not ("Accept" in headers or "accept" in headers):
282
+ headers["Accept"] = "application/json"
283
+ with self.client.request(method, url, catch_response=True, headers=headers, **kwargs) as r: # type: ignore[attr-defined]
284
+ resp: Any = r
285
+ resp.js = None
286
+ if resp.content is None:
287
+ resp.failure(str(resp.error))
288
+ elif resp.text:
289
+ try:
290
+ resp.js = resp.json()
291
+ except JSONDecodeError as e:
292
+ resp.failure(
293
+ f"Could not parse response as JSON. {resp.text[:250]}, response code {resp.status_code}, error {e}"
294
+ )
295
+ try:
296
+ yield resp
297
+ except AssertionError as e:
298
+ if e.args:
299
+ if e.args[0].endswith(","):
300
+ short_resp = resp.text[:200] if resp.text else resp.text
301
+ resp.failure(f"{e.args[0][:-1]}, response was {short_resp}")
302
+ else:
303
+ resp.failure(e.args[0])
304
+ else:
305
+ resp.failure("Assertion failed")
306
+
307
+ except Exception as e:
308
+ error_lines = []
309
+ for l in traceback.format_exc().split("\n"):
310
+ if m := self._callstack_regex.match(l):
311
+ filename = re.sub(r"/((home|Users)/\w*)/", "~/", m.group(1))
312
+ error_lines.append(filename + ":" + m.group(2) + m.group(3))
313
+ short_resp = resp.text[:200] if resp.text else resp.text
314
+ resp.failure(f"{e.__class__.__name__}: {e} at {', '.join(error_lines)}. Response was {short_resp}")
315
+
316
+ @contextmanager
317
+ def rest_(self, method, url, name=None, **kwargs) -> Generator[RestResponseT]:
318
+ """
319
+ Some REST api:s use a timestamp as part of their query string (mainly to break through caches).
320
+ This is a convenience method for that, appending a _=<timestamp> parameter automatically
321
+ """
322
+ separator = "&" if "?" in url else "?"
323
+ if name is None:
324
+ name = url + separator + "_=..."
325
+ with self.rest(method, f"{url}{separator}_={int(time.time() * 1000)}", name=name, **kwargs) as resp:
326
+ yield resp
327
+
328
+
329
+ class HttpUser(User, RestMixin[RestResponseContextManager]):
254
330
  """
255
331
  Represents an HTTP "user" which is to be spawned and attack the system that is to be load tested.
256
332
 
@@ -84,3 +84,30 @@ def constant_throughput(task_runs_per_second: float) -> Callable[["User"], float
84
84
  the next task.
85
85
  """
86
86
  return constant_pacing(1 / task_runs_per_second)
87
+
88
+
89
+ def poisson(rate: float) -> Callable[["User"], float]:
90
+ """
91
+ Returns a function that will return a random wait time sampled from an exponential
92
+ distribution with mean ``1/rate`` seconds. Tasks will therefore arrive according to a
93
+ Poisson process with mean rate ``rate`` tasks per second, which is a good approximation
94
+ of how real users arrive.
95
+
96
+ The average number of task runs per second will be ``rate``, but unlike
97
+ :py:func:`constant_throughput <locust.wait_time.constant_throughput>` the individual
98
+ intervals vary randomly, so bursts and quiet periods occur naturally.
99
+
100
+ Example::
101
+
102
+ class MyUser(User):
103
+ wait_time = poisson(2) # on average, each user runs 2 tasks per second
104
+ @task
105
+ def my_task(self):
106
+ ...
107
+
108
+ If you have multiple requests in a task your RPS will of course be higher than the
109
+ specified throughput.
110
+ """
111
+ if rate <= 0:
112
+ raise ValueError(f"poisson() requires a positive rate, got: {rate}")
113
+ return lambda instance: random.expovariate(rate)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes