locust 2.40.2.dev15__py3-none-any.whl → 2.40.3__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.
locust/_version.py CHANGED
@@ -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.2.dev15'
32
- __version_tuple__ = version_tuple = (2, 40, 2, 'dev15')
31
+ __version__ = version = '2.40.3'
32
+ __version_tuple__ = version_tuple = (2, 40, 3)
33
33
 
34
34
  __commit_id__ = commit_id = None
locust/clients.py CHANGED
@@ -233,7 +233,7 @@ class HttpSession(requests.Session):
233
233
  except (MissingSchema, InvalidSchema, InvalidURL):
234
234
  raise
235
235
  except RequestException as e:
236
- return ResponseContextManager(e)
236
+ return ResponseContextManager(e) # this is inconsistent, we should fix this some time
237
237
 
238
238
  def get(
239
239
  self, url: str | bytes, *, data: Any = None, json: Any = None, **kwargs: Unpack[RESTKwargs]
@@ -321,16 +321,23 @@ class ResponseContextManager(Response):
321
321
  """
322
322
 
323
323
  error: Exception | None = None
324
- _manual_result: bool | Exception | None
324
+ _manual_result: bool | Exception | None = None
325
325
  _entered: bool
326
326
  _request_event: EventHook
327
327
  request_meta: Mapping[str, Any]
328
328
  _catch_response: bool
329
329
 
330
330
  def raise_for_status(self) -> None:
331
- if self.error:
332
- raise self.error
333
- Response.raise_for_status(self)
331
+ """Wrapper around :py:meth:`Response.raise_for_status <requests.Response.raise_for_status>`
332
+ but also taking into account explicit failure()/success() calls.
333
+ """
334
+ __tracebackhide__ = True
335
+ if not self._manual_result:
336
+ if self.error:
337
+ raise self.error
338
+ Response.raise_for_status(self)
339
+ elif isinstance(self._manual_result, Exception):
340
+ raise self._manual_result
334
341
 
335
342
  def __init__(self, error: RequestException):
336
343
  """
@@ -340,6 +347,8 @@ class ResponseContextManager(Response):
340
347
  super().__init__()
341
348
  self.error = error
342
349
  self.status_code = 0
350
+ # this is a bit of an assumption but it is probably worth not to have to pass this around:
351
+ self._catch_response = True
343
352
  self.request = error.request # type: ignore
344
353
 
345
354
  @classmethod
@@ -161,7 +161,7 @@ class FastHttpSession:
161
161
  else:
162
162
  return f"{self.base_url}{path}"
163
163
 
164
- def _send_request_safe_mode(self, method: str, url: str, **kwargs):
164
+ def _send_request_safe_mode(self, method: str, url: str, **kwargs) -> FastResponse:
165
165
  """
166
166
  Send an HTTP request, and catch any exception that might occur due to either
167
167
  connection problems, or invalid HTTP status codes
@@ -170,7 +170,9 @@ class FastHttpSession:
170
170
  return self.client.urlopen(url, method=method, **kwargs)
171
171
  except FAILURE_EXCEPTIONS as e:
172
172
  if hasattr(e, "response"):
173
- r = e.response
173
+ # regular FastResponse object
174
+ resp = e.response
175
+ resp.error = e
174
176
  else:
175
177
  req = self.client._make_request(
176
178
  url,
@@ -179,9 +181,9 @@ class FastHttpSession:
179
181
  payload=kwargs.get("payload"),
180
182
  params=kwargs.get("params"),
181
183
  )
182
- r = ErrorResponse(url=url, request=req)
183
- r.error = e
184
- return r
184
+ # fake FastResponse object
185
+ resp = ErrorResponse(req, e)
186
+ return resp
185
187
 
186
188
  def request(
187
189
  self,
@@ -197,10 +199,9 @@ class FastHttpSession:
197
199
  allow_redirects: bool = True,
198
200
  context: dict = {},
199
201
  **kwargs,
200
- ) -> ResponseContextManager | FastResponse:
202
+ ) -> ResponseContextManager:
201
203
  """
202
- Send and HTTP request
203
- Returns :py:class:`locust.contrib.fasthttp.FastResponse` object.
204
+ Send an HTTP request
204
205
 
205
206
  :param method: method for the new :class:`Request` object.
206
207
  :param url: path that will be concatenated with the base host URL that has been specified.
@@ -282,30 +283,31 @@ class FastHttpSession:
282
283
  except HTTPParseError as e:
283
284
  request_meta["response_time"] = (time.perf_counter() - start_perf_counter) * 1000
284
285
  request_meta["exception"] = e
285
- self.request_event.fire(**request_meta)
286
- return response
286
+ rcm = ResponseContextManager(response, self.request_event, request_meta, catch_response)
287
+ if not catch_response:
288
+ rcm.__exit__(None, None, None)
289
+ return rcm
287
290
 
291
+ rcm = ResponseContextManager(response, self.request_event, request_meta, catch_response)
288
292
  # Record the consumed time
289
293
  # Note: This is intentionally placed after we record the content_size above, since
290
294
  # we'll then trigger fetching of the body (unless stream=True)
291
295
  request_meta["response_time"] = (time.perf_counter() - start_perf_counter) * 1000
292
296
 
293
- if catch_response:
294
- return ResponseContextManager(response, request_event=self.request_event, request_meta=request_meta)
295
- else:
296
- try:
297
- response.raise_for_status()
298
- except FAILURE_EXCEPTIONS as e:
299
- request_meta["exception"] = e
297
+ try:
298
+ response.raise_for_status()
299
+ except FAILURE_EXCEPTIONS as e:
300
+ request_meta["exception"] = e # type: ignore[assignment] # mypy, why are you so dumb..
300
301
 
301
- self.request_event.fire(**request_meta)
302
- return response
302
+ if not catch_response: # if not using with-block, report the request immediately
303
+ rcm.__exit__(None, None, None)
304
+ return rcm
303
305
 
304
- def delete(self, url: str, **kwargs: Unpack[RESTKwargs]) -> ResponseContextManager | FastResponse:
306
+ def delete(self, url: str, **kwargs: Unpack[RESTKwargs]) -> ResponseContextManager:
305
307
  """Sends a DELETE request"""
306
308
  return self.request("DELETE", url, **kwargs)
307
309
 
308
- def get(self, url: str, **kwargs: Unpack[RESTKwargs]) -> ResponseContextManager | FastResponse:
310
+ def get(self, url: str, **kwargs: Unpack[RESTKwargs]) -> ResponseContextManager:
309
311
  """Sends a GET request"""
310
312
  return self.request("GET", url, **kwargs)
311
313
 
@@ -320,29 +322,25 @@ class FastHttpSession:
320
322
  line, buffer = buffer.split("\n", 1)
321
323
  yield line
322
324
 
323
- def head(self, url: str, **kwargs: Unpack[RESTKwargs]) -> ResponseContextManager | FastResponse:
325
+ def head(self, url: str, **kwargs: Unpack[RESTKwargs]) -> ResponseContextManager:
324
326
  """Sends a HEAD request"""
325
327
  return self.request("HEAD", url, **kwargs)
326
328
 
327
- def options(self, url: str, **kwargs: Unpack[RESTKwargs]) -> ResponseContextManager | FastResponse:
329
+ def options(self, url: str, **kwargs: Unpack[RESTKwargs]) -> ResponseContextManager:
328
330
  """Sends a OPTIONS request"""
329
331
  return self.request("OPTIONS", url, **kwargs)
330
332
 
331
- def patch(
332
- self, url: str, data: str | dict | None = None, **kwargs: Unpack[PatchKwargs]
333
- ) -> ResponseContextManager | FastResponse:
333
+ def patch(self, url: str, data: str | dict | None = None, **kwargs: Unpack[PatchKwargs]) -> ResponseContextManager:
334
334
  """Sends a PATCH request"""
335
335
  return self.request("PATCH", url, data=data, **kwargs)
336
336
 
337
337
  def post(
338
338
  self, url: str, data: str | dict | None = None, json: Any = None, **kwargs: Unpack[PostKwargs]
339
- ) -> ResponseContextManager | FastResponse:
339
+ ) -> ResponseContextManager:
340
340
  """Sends a POST request"""
341
341
  return self.request("POST", url, data=data, json=json, **kwargs)
342
342
 
343
- def put(
344
- self, url: str, data: str | dict | None = None, **kwargs: Unpack[PutKwargs]
345
- ) -> ResponseContextManager | FastResponse:
343
+ def put(self, url: str, data: str | dict | None = None, **kwargs: Unpack[PutKwargs]) -> ResponseContextManager:
346
344
  """Sends a PUT request"""
347
345
  return self.request("PUT", url, data=data, **kwargs)
348
346
 
@@ -563,8 +561,8 @@ class FastResponse(CompatResponse):
563
561
 
564
562
  def raise_for_status(self):
565
563
  """Raise any connection errors that occurred during the request"""
566
- if hasattr(self, "error") and self.error:
567
- raise self.error
564
+ if error := getattr(self, "error", None):
565
+ raise error
568
566
 
569
567
  @property
570
568
  def status_code(self) -> int:
@@ -595,7 +593,7 @@ class FastResponse(CompatResponse):
595
593
  )
596
594
 
597
595
 
598
- class ErrorResponse:
596
+ class ErrorResponse(FastResponse): # we're really just pretending to be a FastResponse
599
597
  """
600
598
  This is used as a dummy response object when geventhttpclient raises an error
601
599
  that doesn't have a real Response object attached. E.g. a socket error or similar
@@ -608,9 +606,9 @@ class ErrorResponse:
608
606
  text: str | None = None
609
607
  request: CompatRequest
610
608
 
611
- def __init__(self, url: str, request: CompatRequest):
612
- self.url = url
609
+ def __init__(self, request: CompatRequest, error: Exception):
613
610
  self.request = request
611
+ self.error = error
614
612
 
615
613
  def raise_for_status(self):
616
614
  raise self.error
@@ -662,8 +660,10 @@ class ResponseContextManager(FastResponse):
662
660
  _manual_result = None
663
661
  _entered = False
664
662
 
665
- def __init__(self, response, request_event, request_meta):
663
+ def __init__(self, response, request_event, request_meta, catch_response: bool):
666
664
  # copy data from response to this object
665
+ # I wanted to change this to the approach from the HttpUser's ResponseContentManager.from_request,
666
+ # but it was such a mess
667
667
  self.__dict__ = response.__dict__
668
668
  try:
669
669
  self._cached_content = response._cached_content
@@ -671,16 +671,20 @@ class ResponseContextManager(FastResponse):
671
671
  pass
672
672
  self._request_event = request_event
673
673
  self.request_meta = request_meta
674
+ self._catch_response = catch_response
674
675
 
675
676
  def __enter__(self):
677
+ if not self._catch_response:
678
+ raise LocustError("In order to use a with-block for requests, you must also pass catch_response=True")
676
679
  self._entered = True
677
680
  return self
678
681
 
679
682
  def __exit__(self, exc, value, traceback):
680
683
  # if the user has already manually marked this response as failure or success
681
- # we can ignore the default behaviour of letting the response code determine the outcome
684
+ # we ignore the default behaviour of letting the response code determine the outcome
682
685
  if self._manual_result is not None:
683
686
  if self._manual_result is True:
687
+ self.request_meta["exception"] = None
684
688
  self._report_request()
685
689
  elif isinstance(self._manual_result, Exception):
686
690
  self.request_meta["exception"] = self._manual_result
@@ -743,6 +747,13 @@ class ResponseContextManager(FastResponse):
743
747
  exc = CatchResponseError(exc)
744
748
  self._manual_result = exc
745
749
 
750
+ def raise_for_status(self):
751
+ """Raise any connection errors that occurred during the request"""
752
+ if not self._manual_result:
753
+ super().raise_for_status()
754
+ elif isinstance(self._manual_result, Exception):
755
+ raise self._manual_result
756
+
746
757
 
747
758
  class RestResponseContextManager(ResponseContextManager):
748
759
  js: dict # This is technically an Optional, but I dont want to force everyone to check it
locust/exception.py CHANGED
@@ -28,6 +28,11 @@ class InterruptTaskSet(Exception):
28
28
 
29
29
 
30
30
  class StopUser(Exception):
31
+ """
32
+ This is only intended to be used internally in task.py.
33
+ Raising this in your locustfile in a full locust run may have unexpected results.
34
+ """
35
+
31
36
  pass
32
37
 
33
38
 
locust/user/users.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from locust.clients import HttpSession
4
- from locust.exception import LocustError, StopUser
4
+ from locust.exception import CatchResponseError, LocustError, StopUser
5
5
  from locust.user.task import (
6
6
  LOCUST_STATE_RUNNING,
7
7
  LOCUST_STATE_STOPPING,
@@ -302,5 +302,7 @@ class PytestUser(User):
302
302
  if isinstance(e, ValueError): # things like MissingSchema etc, lets not catch that
303
303
  raise
304
304
  logger.debug("%s\n%s", e, traceback.format_exc())
305
+ except CatchResponseError as e:
306
+ logger.debug("%s\n%s", e, traceback.format_exc())
305
307
  except ConnectionError as e:
306
308
  logger.debug("%s\n%s", e, traceback.format_exc())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: locust
3
- Version: 2.40.2.dev15
3
+ Version: 2.40.3
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
@@ -38,8 +38,7 @@ Requires-Dist: python-engineio>=4.12.2
38
38
  Requires-Dist: python-socketio[client]>=5.13.0
39
39
  Requires-Dist: pywin32; sys_platform == 'win32'
40
40
  Requires-Dist: pyzmq>=25.0.0
41
- Requires-Dist: requests>=2.26.0; python_version <= '3.11'
42
- Requires-Dist: requests>=2.32.2; python_version > '3.11'
41
+ Requires-Dist: requests>=2.32.2
43
42
  Requires-Dist: setuptools>=70.0.0
44
43
  Requires-Dist: tomli>=1.1.0; python_version < '3.11'
45
44
  Requires-Dist: typing-extensions>=4.6.0; python_version < '3.12'
@@ -1,13 +1,13 @@
1
1
  locust/__init__.py,sha256=HadpgGidiyCDPSKwkxrk1Qw6eB7dTmftNJVftuJzAiw,1876
2
2
  locust/__main__.py,sha256=vBQ82334kX06ImDbFlPFgiBRiLIinwNk3z8Khs6hd74,31
3
- locust/_version.py,sha256=1lUD_y3GdgwY5ZP5Qi49KZlF9bRAYb_27xyeCkXcsGk,721
3
+ locust/_version.py,sha256=9cUTLUlZ2HRDSemp7LUnZcGlLE6k-NI6R1w4zxPE-bY,706
4
4
  locust/argument_parser.py,sha256=t6mAoK9u13DxC9UH-alVqS6fFABFTyNWSJG89yQ4QQQ,33056
5
- locust/clients.py,sha256=Gj1rD73_G7G7f6bi0plDiN-GIDPTBng8pBY21cFozQc,20219
5
+ locust/clients.py,sha256=OfMTOT3LwijczdL3u6bpDwDNM2TDcPCIwXBelizPOyw,20776
6
6
  locust/debug.py,sha256=7CCm8bIg44uGH2wqBlo1rXBzV2VzwPicLxLewz8r5CQ,5099
7
7
  locust/dispatch.py,sha256=prdwtb9EoN4A9klgiKgWuwQmvFB8hEuFHOK6ot62AJI,16202
8
8
  locust/env.py,sha256=PypNHmEFBhGBk9dU6pZ2cL5L0TThYejaGWW16RO3ZNQ,13203
9
9
  locust/event.py,sha256=AWoeG2q11Vpupv1eW9Rf2N0EI4dDDAldKe6zY1ajC7I,8717
10
- locust/exception.py,sha256=jGgJ32ubuf4pWdlaVOkbh2Y0LlG0_DHi-lv3ib8ppOE,1791
10
+ locust/exception.py,sha256=XCQyJj5XHvJPgTWGhzgjjavoqVdyF0tqG2wj6WzmXvo,1954
11
11
  locust/html.py,sha256=18LlaL-NEQdthXVdS16gRAd4SOpW_bowOJKyuLvWrCY,4043
12
12
  locust/input_events.py,sha256=lqLDB2Ax-OQ7-vtYNkGjySjfaWVobBzqf0GxRwjcLcQ,3323
13
13
  locust/log.py,sha256=5E2ZUOa3V4sfCqa-470Gle1Ik9L5nxYitsjEB9tRwE0,3455
@@ -18,7 +18,7 @@ locust/shape.py,sha256=t-lwBS8LOjWcKXNL7j2U3zroIXJ1b0fazUwpRYQOKXw,1973
18
18
  locust/stats.py,sha256=qyoSKT0i7RunLDj5pMGqizK1Sp8bcqUsXwh2m4_DpR8,47203
19
19
  locust/web.py,sha256=pLYuocmx9gifJ4vsVDgGDYIPkQhQxI7kKjxoXcUajqM,31865
20
20
  locust/contrib/__init__.py,sha256=LtZN7MczpIAbZkN7PT2h8W2wgb9nBl6cDXbFCVsV4fo,290
21
- locust/contrib/fasthttp.py,sha256=c8MznjqbtYTKZPc20OC4GCiaENDCJmmNSNuHqtMhh2Q,29883
21
+ locust/contrib/fasthttp.py,sha256=Hz-DqTW2qxVWv-OWPIvEgX9CbAlcyPBlp0lAKcOjpbg,30721
22
22
  locust/contrib/milvus.py,sha256=YabgLd0lImzWupJFCm0OZAW-Nxeibwn91ldWpZ2irDo,12811
23
23
  locust/contrib/mongodb.py,sha256=1seUYgJOaNKwybYOP9PUEVhgl8hGy-G33f8lFj3R8W8,1246
24
24
  locust/contrib/oai.py,sha256=Ot3T8lp31ThckGbNps86oVvq6Vn845Eec0mxhDmONDE,2684
@@ -32,7 +32,7 @@ locust/user/inspectuser.py,sha256=KgrWHyE5jhK6or58R7soLRf-_st42AaQrR72qbiXw9E,26
32
32
  locust/user/markov_taskset.py,sha256=eESre6OacbP7nTzZFwxUe7TO4X4l7WqOAEETtDzsIfU,11784
33
33
  locust/user/sequential_taskset.py,sha256=SbrrGU9HV2nEWe6zQVtjymn8NgPISP7QSNoVdyoXjYg,2687
34
34
  locust/user/task.py,sha256=QnNDs8lEZGgMqBe8l_O4FKgn4WUL9r5WTRHWw88nO-w,17242
35
- locust/user/users.py,sha256=n2IE7MwIKj2JUgYU4_7Vv-cNk-NWDRhj62dQbxrGhIk,11204
35
+ locust/user/users.py,sha256=7Au0F6MnqQYAiuHYcXaVsm-BEhyigBq8PiON6RJDfKM,11342
36
36
  locust/user/wait_time.py,sha256=bGRKMVx4lom75sX3POYJUa1CPeME2bEAXG6CEgxSO5U,2675
37
37
  locust/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
38
  locust/util/cache.py,sha256=IxbpGawl0-hoWKvCrtksxjSLf2GbBBTVns06F7mFBkM,1062
@@ -56,8 +56,8 @@ locust/webui/dist/assets/terminal.gif,sha256=iw80LO2u0dnf4wpGfFJZauBeKTcSpw9iUfI
56
56
  locust/webui/dist/assets/testruns-dark.png,sha256=G4p2VZSBuuqF4neqUaPSshIp5OKQJ_Bvb69Luj6XuVs,125231
57
57
  locust/webui/dist/assets/testruns-light.png,sha256=JinGDiiBPOkhpfF-XCbmQqhRInqItrjrBTLKt5MlqVI,130301
58
58
  pytest_locust/plugin.py,sha256=WAyiRHLynXegbbX2DxIutPKO4PQRT6JdBFA7zbbaJgM,1469
59
- locust-2.40.2.dev15.dist-info/METADATA,sha256=Twi0mcXhfvVh7tKWHwHemQTdyVoDofdcAHnIYFD8JE0,9599
60
- locust-2.40.2.dev15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
61
- locust-2.40.2.dev15.dist-info/entry_points.txt,sha256=0uIHcQ71R1qaWhM_sd8uBUCCJgp7gJfGHUVMnJeZfcY,86
62
- locust-2.40.2.dev15.dist-info/licenses/LICENSE,sha256=5hnz-Vpj0Z3kSCQl0LzV2hT1TLc4LHcbpBp3Cy-EuyM,1110
63
- locust-2.40.2.dev15.dist-info/RECORD,,
59
+ locust-2.40.3.dist-info/METADATA,sha256=0TZx5QftsszuyRoq4P061KfB-h0Txp2sFmFKMx5xe6A,9510
60
+ locust-2.40.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
61
+ locust-2.40.3.dist-info/entry_points.txt,sha256=0uIHcQ71R1qaWhM_sd8uBUCCJgp7gJfGHUVMnJeZfcY,86
62
+ locust-2.40.3.dist-info/licenses/LICENSE,sha256=5hnz-Vpj0Z3kSCQl0LzV2hT1TLc4LHcbpBp3Cy-EuyM,1110
63
+ locust-2.40.3.dist-info/RECORD,,