locust 2.43.5.dev25__tar.gz → 2.43.5.dev31__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.43.5.dev25 → locust-2.43.5.dev31}/PKG-INFO +1 -1
  2. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/_version.py +2 -2
  3. locust-2.43.5.dev31/locust/contrib/csv_request_logger.py +187 -0
  4. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/oai.py +1 -1
  5. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/main.py +20 -12
  6. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/stats.py +1 -1
  7. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/user/inspectuser.py +1 -1
  8. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/web.py +1 -1
  9. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/pyproject.toml +1 -1
  10. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/.gitignore +0 -0
  11. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/LICENSE +0 -0
  12. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/README.md +0 -0
  13. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/hatch_build.py +0 -0
  14. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/__init__.py +0 -0
  15. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/__main__.py +0 -0
  16. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/argument_parser.py +0 -0
  17. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/clients.py +0 -0
  18. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/__init__.py +0 -0
  19. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/dns.py +0 -0
  20. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/fasthttp.py +0 -0
  21. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/milvus.py +0 -0
  22. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/mongodb.py +0 -0
  23. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/mqtt.py +0 -0
  24. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/postgres.py +0 -0
  25. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/qdrant.py +0 -0
  26. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/contrib/socketio.py +0 -0
  27. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/debug.py +0 -0
  28. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/dispatch.py +0 -0
  29. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/env.py +0 -0
  30. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/event.py +0 -0
  31. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/exception.py +0 -0
  32. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/html.py +0 -0
  33. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/input_events.py +0 -0
  34. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/log.py +0 -0
  35. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/opentelemetry.py +0 -0
  36. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/py.typed +0 -0
  37. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/rpc/__init__.py +0 -0
  38. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/rpc/protocol.py +0 -0
  39. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/rpc/zmqrpc.py +0 -0
  40. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/runners.py +0 -0
  41. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/shape.py +0 -0
  42. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/user/__init__.py +0 -0
  43. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/user/markov_taskset.py +0 -0
  44. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/user/sequential_taskset.py +0 -0
  45. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/user/task.py +0 -0
  46. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/user/users.py +0 -0
  47. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/user/wait_time.py +0 -0
  48. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/__init__.py +0 -0
  49. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/cache.py +0 -0
  50. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/date.py +0 -0
  51. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/deprecation.py +0 -0
  52. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/directory.py +0 -0
  53. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/exception_handler.py +0 -0
  54. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/load_locustfile.py +0 -0
  55. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/rounding.py +0 -0
  56. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/timespan.py +0 -0
  57. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/util/url.py +0 -0
  58. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/assets/favicon-dark.png +0 -0
  59. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/assets/favicon-light.png +0 -0
  60. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/assets/graphs-dark.png +0 -0
  61. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/assets/graphs-light.png +0 -0
  62. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/assets/src-lBIzfjkZ.js +0 -0
  63. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/assets/terminal.gif +0 -0
  64. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/assets/testruns-dark.png +0 -0
  65. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/assets/testruns-light.png +0 -0
  66. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/auth.html +0 -0
  67. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/index.html +0 -0
  68. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/locust/webui/dist/report.html +0 -0
  69. {locust-2.43.5.dev25 → locust-2.43.5.dev31}/pytest_locust/plugin.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: locust
3
- Version: 2.43.5.dev25
3
+ Version: 2.43.5.dev31
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
@@ -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.43.5.dev25'
22
- __version_tuple__ = version_tuple = (2, 43, 5, 'dev25')
21
+ __version__ = version = '2.43.5.dev31'
22
+ __version_tuple__ = version_tuple = (2, 43, 5, 'dev31')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,187 @@
1
+ """
2
+ Per-request CSV logger for Locust.
3
+
4
+ Hooks into the ``request`` event and appends one row per completed request to a
5
+ CSV file. Useful for post-run analysis that requires individual data-points
6
+ rather than the aggregated statistics provided by the built-in
7
+ ``--csv`` flag.
8
+
9
+ Usage::
10
+
11
+ from locust import HttpUser, task, events
12
+ from locust.contrib.csv_request_logger import CsvRequestLogger
13
+
14
+ logger = CsvRequestLogger("results/requests.csv")
15
+
16
+ @events.init.add_listener
17
+ def on_locust_init(environment, **kwargs):
18
+ logger.register(environment)
19
+
20
+ class MyUser(HttpUser):
21
+ @task
22
+ def index(self):
23
+ self.client.get("/")
24
+
25
+ The CSV file is created (or truncated) when :meth:`CsvRequestLogger.register`
26
+ is called and closed when the ``quitting`` event fires.
27
+
28
+ CSV columns
29
+ -----------
30
+ ``timestamp``
31
+ Unix timestamp (seconds, float) at which the request was sent.
32
+ ``request_type``
33
+ HTTP method or custom protocol name (e.g. ``GET``, ``POST``, ``WS``).
34
+ ``name``
35
+ URL path / request name as reported to Locust stats.
36
+ ``response_time_ms``
37
+ Response time in **milliseconds** (float, rounded to 2 dp).
38
+ ``response_length``
39
+ Response body size in bytes (int).
40
+ ``status_code``
41
+ HTTP status code (int). ``0`` when the request failed before a response
42
+ was received (e.g. connection error or custom failure).
43
+ ``exception``
44
+ String representation of the exception if the request failed, else empty.
45
+ """
46
+
47
+ import csv
48
+ import io
49
+ import logging
50
+ import time
51
+ from typing import TYPE_CHECKING, Any
52
+
53
+ if TYPE_CHECKING:
54
+ from locust.env import Environment
55
+ from locust.stats import CSVWriter
56
+
57
+ logger = logging.getLogger(__name__)
58
+
59
+ #: Column headers written to the CSV file.
60
+ CSV_COLUMNS = (
61
+ "timestamp",
62
+ "request_type",
63
+ "name",
64
+ "response_time_ms",
65
+ "response_length",
66
+ "status_code",
67
+ "exception",
68
+ )
69
+
70
+
71
+ def _status_code(response: Any, exception: Any) -> int:
72
+ """Extract the HTTP status code from a response object.
73
+
74
+ Returns ``0`` when no response is available (connection errors, custom
75
+ ``ResponseContextManager`` failures, non-HTTP protocols, etc.).
76
+ """
77
+ if exception is not None and response is None:
78
+ return 0
79
+ try:
80
+ code = int(response.status_code)
81
+ return code
82
+ except (AttributeError, TypeError, ValueError):
83
+ return 0
84
+
85
+
86
+ class CsvRequestLogger:
87
+ """Listens to Locust's ``request`` event and writes one CSV row per request.
88
+
89
+ Parameters
90
+ ----------
91
+ filepath:
92
+ Path to the output CSV file. Parent directories must already exist.
93
+ If the file exists it will be **overwritten** at the start of each run.
94
+ flush_interval:
95
+ Number of rows to buffer before flushing to disk. Use ``1`` for
96
+ immediate write-through (safest for crash recovery), or a larger value
97
+ for better performance on high-RPS tests. Defaults to ``1``.
98
+ """
99
+
100
+ def __init__(self, filepath: str, *, flush_interval: int = 1) -> None:
101
+ self.filepath = filepath
102
+ self.flush_interval = max(1, flush_interval)
103
+
104
+ self._filehandle: io.TextIOWrapper | None = None
105
+ self._writer: CSVWriter | None = None
106
+ self._pending: int = 0
107
+
108
+ # ------------------------------------------------------------------
109
+ # Public API
110
+ # ------------------------------------------------------------------
111
+
112
+ def register(self, environment: "Environment") -> None:
113
+ """Attach this logger to *environment*'s event hooks.
114
+
115
+ Must be called once, typically inside an ``@events.init`` listener.
116
+ """
117
+ environment.events.request.add_listener(self._on_request)
118
+ environment.events.quitting.add_listener(self._on_quitting)
119
+ self._open()
120
+ logger.debug("CsvRequestLogger: writing per-request log to %s", self.filepath)
121
+
122
+ def close(self) -> None:
123
+ """Flush and close the underlying file handle.
124
+
125
+ Safe to call multiple times.
126
+ """
127
+ if self._filehandle is not None and not self._filehandle.closed:
128
+ self._filehandle.flush()
129
+ self._filehandle.close()
130
+ logger.debug("CsvRequestLogger: closed %s", self.filepath)
131
+ self._filehandle = None
132
+ self._writer = None
133
+ self._pending = 0
134
+
135
+ # ------------------------------------------------------------------
136
+ # Internal helpers
137
+ # ------------------------------------------------------------------
138
+
139
+ def _open(self) -> None:
140
+ """Open (or re-open) the CSV file and write the header row."""
141
+ self.close()
142
+ self._filehandle = open(self.filepath, "w", newline="", encoding="utf-8")
143
+ self._writer = csv.writer(self._filehandle)
144
+ self._writer.writerow(CSV_COLUMNS)
145
+ self._filehandle.flush()
146
+
147
+ def _on_request(
148
+ self,
149
+ *,
150
+ request_type: str,
151
+ name: str,
152
+ response_time: float,
153
+ response_length: int,
154
+ exception: Any = None,
155
+ response: Any = None,
156
+ start_time: float | None = None,
157
+ context: Any = None,
158
+ **kwargs: Any,
159
+ ) -> None:
160
+ """Event handler — called by Locust for every completed request."""
161
+ if self._writer is None:
162
+ return
163
+
164
+ ts = start_time if start_time is not None else time.time()
165
+ status_code = _status_code(response, exception)
166
+ exc_str = str(exception) if exception is not None else ""
167
+
168
+ self._writer.writerow(
169
+ [
170
+ round(ts, 6),
171
+ request_type,
172
+ name,
173
+ round(response_time, 2),
174
+ response_length,
175
+ status_code,
176
+ exc_str,
177
+ ]
178
+ )
179
+
180
+ self._pending += 1
181
+ if self._pending >= self.flush_interval:
182
+ self._filehandle.flush() # type: ignore[union-attr]
183
+ self._pending = 0
184
+
185
+ def _on_quitting(self, **kwargs: Any) -> None:
186
+ """Flush and close when Locust shuts down."""
187
+ self.close()
@@ -33,7 +33,7 @@ class OpenAIClient(OpenAI):
33
33
  exception = e
34
34
  request_event.fire(
35
35
  request_type=response.request.method,
36
- name=self.request_name if self.request_name else response.url.path,
36
+ name=self.request_name or response.url.path,
37
37
  context={},
38
38
  response=response,
39
39
  exception=exception,
@@ -587,18 +587,26 @@ See https://github.com/locustio/locust/wiki/Installation#increasing-maximum-numb
587
587
  input_listener_greenlet = gevent.spawn(
588
588
  input_listener(
589
589
  {
590
- "w": lambda: runner.start(runner.user_count + 1, 100)
591
- if runner.state != "spawning"
592
- else logging.warning("Already spawning users, can't spawn more right now"),
593
- "W": lambda: runner.start(runner.user_count + 10, 100)
594
- if runner.state != "spawning"
595
- else logging.warning("Already spawning users, can't spawn more right now"),
596
- "s": lambda: runner.start(max(0, runner.user_count - 1), 100)
597
- if runner.state != "spawning"
598
- else logging.warning("Spawning users, can't stop right now"),
599
- "S": lambda: runner.start(max(0, runner.user_count - 10), 100)
600
- if runner.state != "spawning"
601
- else logging.warning("Spawning users, can't stop right now"),
590
+ "w": lambda: (
591
+ runner.start(runner.user_count + 1, 100)
592
+ if runner.state != "spawning"
593
+ else logging.warning("Already spawning users, can't spawn more right now")
594
+ ),
595
+ "W": lambda: (
596
+ runner.start(runner.user_count + 10, 100)
597
+ if runner.state != "spawning"
598
+ else logging.warning("Already spawning users, can't spawn more right now")
599
+ ),
600
+ "s": lambda: (
601
+ runner.start(max(0, runner.user_count - 1), 100)
602
+ if runner.state != "spawning"
603
+ else logging.warning("Spawning users, can't stop right now")
604
+ ),
605
+ "S": lambda: (
606
+ runner.start(max(0, runner.user_count - 10), 100)
607
+ if runner.state != "spawning"
608
+ else logging.warning("Spawning users, can't stop right now")
609
+ ),
602
610
  "\r": lambda: webbrowser.open_new_tab(url),
603
611
  "\n": lambda: webbrowser.open_new_tab(url),
604
612
  },
@@ -774,7 +774,7 @@ class StatsError:
774
774
  def _getattr(obj: StatsError, key: str, default: Any) -> Any:
775
775
  value = getattr(obj, key, default)
776
776
 
777
- if key in ["error"]:
777
+ if key == "error":
778
778
  value = StatsError.parse_error(value)
779
779
 
780
780
  return value
@@ -35,7 +35,7 @@ def _calc_distribution(user_classes, num_users):
35
35
  user_classes_count = {}
36
36
 
37
37
  for u in user_classes:
38
- count = u.fixed_count if u.fixed_count else (u.weight / total_weight) * weighted_count
38
+ count = u.fixed_count or (u.weight / total_weight) * weighted_count
39
39
  user_classes_count[u.__name__] = round(count)
40
40
 
41
41
  return user_classes_count
@@ -726,7 +726,7 @@ class WebUI:
726
726
  "is_distributed": is_distributed,
727
727
  "user_count": self.environment.runner.user_count,
728
728
  "version": version,
729
- "host": host if host else "",
729
+ "host": host or "",
730
730
  "history": request_stats.history if request_stats.num_requests > 0 else [],
731
731
  "override_host_warning": override_host_warning,
732
732
  "missing_host_warning": missing_host_warning,
@@ -82,7 +82,7 @@ test = [
82
82
  ]
83
83
  lint = [
84
84
  "pre-commit>=3.7.1,<4.0.0",
85
- "ruff==0.14.3",
85
+ "ruff==0.15.12",
86
86
  "mypy>=1.13.0,<1.15.0",
87
87
  "types-requests==2.32.4.20250809",
88
88
  ]
File without changes
File without changes
File without changes