diff --git a/.vscode/launch.json b/.vscode/launch.json
index d0489760cd..c00e8cc44b 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -10,7 +10,7 @@
"gevent": true
},
{
- "name": "Run locust headless",
+ "name": "Run current locust scenario headless",
"type": "python",
"request": "launch",
"module": "locust",
@@ -24,7 +24,7 @@
"gevent": true
},
{
- "name": "Run locust, autostart",
+ "name": "Run current locust scenario, autostart",
"type": "python",
"request": "launch",
"module": "locust",
diff --git a/docs/developing-locust.rst b/docs/developing-locust.rst
index 094e39a96c..efe12090e3 100644
--- a/docs/developing-locust.rst
+++ b/docs/developing-locust.rst
@@ -33,6 +33,8 @@ If you install `pre-commit `_, linting and format check
Before you open a pull request, make sure all the tests work. And if you are adding a feature, make sure it is documented (in ``docs/*.rst``).
+If you're in a hurry or don't have access to a development environment, you can simply use `Codespaces `_, the github cloud development environment. On your fork page, just click on *Code* then on *Create codespace on *, and voila, your ready to code and test.
+
Testing your changes
====================
@@ -51,6 +53,11 @@ To only run a specific suite or specific test you can call `pytest `.
-Make sure you have enabled gevent in your debugger settings. In VS Code's ``launch.json`` it looks like this:
+Make sure you have enabled gevent in your debugger settings.
+
+Debugging Locust is quite easy with Vscode:
+
+- Place breakpoints
+- Select a python file or a scenario (ex: ```examples/basic.py``)
+- Check that the Poetry virtualenv is correctly detected (bottom right)
+- Open the action *Debug using launch.json*. You will have the choice between debugging the python file, the scenario with WebUI or in headless mode
+- It could be rerun with the F5 shortkey
+
+VS Code's ``launch.json`` looks like this:
.. literalinclude:: ../.vscode/launch.json
:language: json
diff --git a/locust/html.py b/locust/html.py
index 0f8a96a9ec..1f88839bac 100644
--- a/locust/html.py
+++ b/locust/html.py
@@ -12,7 +12,7 @@
from .runners import STATE_STOPPED, STATE_STOPPING, MasterRunner
from .stats import sort_stats, update_stats_history
from .user.inspectuser import get_ratio
-from .util.date import format_utc_timestamp
+from .util.date import format_duration, format_utc_timestamp
PERCENTILES_FOR_HTML_REPORT = [0.50, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99, 1.0]
DEFAULT_BUILD_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "webui", "dist")
@@ -36,6 +36,7 @@ def get_html_report(
if end_ts := stats.last_request_timestamp:
end_time = format_utc_timestamp(end_ts)
else:
+ end_ts = stats.start_time
end_time = start_time
host = None
@@ -88,6 +89,7 @@ def get_html_report(
],
"start_time": start_time,
"end_time": end_time,
+ "duration": format_duration(stats.start_time, end_ts),
"host": escape(str(host)),
"history": history,
"show_download_link": show_download_link,
diff --git a/locust/test/test_date.py b/locust/test/test_date.py
new file mode 100644
index 0000000000..756176dcf8
--- /dev/null
+++ b/locust/test/test_date.py
@@ -0,0 +1,86 @@
+from locust.util.date import format_duration, format_safe_timestamp, format_utc_timestamp
+
+from datetime import datetime
+
+import pytest
+
+dates_checks = [
+ {
+ "datetime": datetime(2023, 10, 1, 12, 0, 0),
+ "utc_timestamp": "2023-10-01T10:00:00Z",
+ "safe_timestamp": "2023-10-01-12h00",
+ "duration": "0 seconds",
+ },
+ {
+ "datetime": datetime(2023, 10, 1, 12, 0, 30),
+ "utc_timestamp": "2023-10-01T10:00:30Z",
+ "safe_timestamp": "2023-10-01-12h00",
+ "duration": "30 seconds",
+ },
+ {
+ "datetime": datetime(2023, 10, 1, 12, 45, 0),
+ "utc_timestamp": "2023-10-01T10:45:00Z",
+ "safe_timestamp": "2023-10-01-12h45",
+ "duration": "45 minutes",
+ },
+ {
+ "datetime": datetime(2023, 10, 1, 15, 0, 0),
+ "utc_timestamp": "2023-10-01T13:00:00Z",
+ "safe_timestamp": "2023-10-01-15h00",
+ "duration": "3 hours",
+ },
+ {
+ "datetime": datetime(2023, 10, 4, 12, 0, 0),
+ "utc_timestamp": "2023-10-04T10:00:00Z",
+ "safe_timestamp": "2023-10-04-12h00",
+ "duration": "3 days",
+ },
+ {
+ "datetime": datetime(2023, 10, 3, 15, 45, 30),
+ "utc_timestamp": "2023-10-03T13:45:30Z",
+ "safe_timestamp": "2023-10-03-15h45",
+ "duration": "2 days, 3 hours, 45 minutes and 30 seconds",
+ },
+ {
+ "datetime": datetime(2023, 10, 2, 13, 1, 1),
+ "utc_timestamp": "2023-10-02T11:01:01Z",
+ "safe_timestamp": "2023-10-02-13h01",
+ "duration": "1 day, 1 hour, 1 minute and 1 second",
+ },
+ {
+ "datetime": datetime(2023, 10, 1, 15, 30, 45),
+ "utc_timestamp": "2023-10-01T13:30:45Z",
+ "safe_timestamp": "2023-10-01-15h30",
+ "duration": "3 hours, 30 minutes and 45 seconds",
+ },
+ {
+ "datetime": datetime(2023, 10, 2, 12, 30, 45),
+ "utc_timestamp": "2023-10-02T10:30:45Z",
+ "safe_timestamp": "2023-10-02-12h30",
+ "duration": "1 day, 30 minutes and 45 seconds",
+ },
+ {
+ "datetime": datetime(2023, 10, 2, 12, 0, 45),
+ "utc_timestamp": "2023-10-02T10:00:45Z",
+ "safe_timestamp": "2023-10-02-12h00",
+ "duration": "1 day and 45 seconds",
+ },
+]
+
+
+@pytest.mark.parametrize("check", dates_checks)
+def test_format_utc_timestamp(check):
+ assert format_utc_timestamp(check["datetime"].timestamp()) == check["utc_timestamp"]
+
+
+@pytest.mark.parametrize("check", dates_checks)
+def test_format_safe_timestamp(check):
+ assert format_safe_timestamp(check["datetime"].timestamp()) == check["safe_timestamp"]
+
+
+@pytest.mark.parametrize("check", dates_checks)
+def test_format_duration(check):
+ global dates_checks
+ start_time = dates_checks[0]["datetime"].timestamp()
+ end_time = check["datetime"].timestamp()
+ assert format_duration(start_time, end_time) == check["duration"]
diff --git a/locust/test/test_main.py b/locust/test/test_main.py
index aab8916f38..cf562bf5d9 100644
--- a/locust/test/test_main.py
+++ b/locust/test/test_main.py
@@ -1181,6 +1181,9 @@ def test_html_report_option(self):
# make sure host appears in the report
self.assertIn("https://test.com/", html_report_content)
self.assertIn('"show_download_link": false', html_report_content)
+ self.assertRegex(html_report_content, r'"start_time": "\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z"')
+ self.assertRegex(html_report_content, r'"end_time": "\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z"')
+ self.assertRegex(html_report_content, r'"duration": "\d* seconds?"')
def test_run_with_userclass_picker(self):
with temporary_file(content=MOCK_LOCUSTFILE_CONTENT_A) as file1:
diff --git a/locust/util/date.py b/locust/util/date.py
index 20b7a26c6c..d29a761948 100644
--- a/locust/util/date.py
+++ b/locust/util/date.py
@@ -2,4 +2,22 @@
def format_utc_timestamp(unix_timestamp):
- return datetime.fromtimestamp(unix_timestamp, timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
+ return datetime.fromtimestamp(int(unix_timestamp), timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
+
+
+def format_safe_timestamp(unix_timestamp):
+ return datetime.fromtimestamp(int(unix_timestamp)).strftime("%Y-%m-%d-%Hh%M")
+
+
+def format_duration(start_time, end_time):
+ seconds = int(end_time) - int(start_time)
+ days = seconds // 86400
+ hours = (seconds % 86400) // 3600
+ minutes = (seconds % 3600) // 60
+ seconds = seconds % 60
+
+ time_parts = [(days, "day"), (hours, "hour"), (minutes, "minute"), (seconds, "second")]
+
+ parts = [f"{value} {label}{'s' if value != 1 else ''}" for value, label in time_parts if value > 0]
+
+ return " and ".join(filter(None, [", ".join(parts[:-1])] + parts[-1:])) if parts else "0 seconds"
diff --git a/locust/web.py b/locust/web.py
index 8b2377cba9..4dc58a0cb8 100644
--- a/locust/web.py
+++ b/locust/web.py
@@ -40,7 +40,7 @@
from .stats import StatsCSV, StatsCSVFileWriter, StatsErrorDict, sort_stats
from .user.inspectuser import get_ratio
from .util.cache import memoize
-from .util.date import format_utc_timestamp
+from .util.date import format_safe_timestamp
from .util.timespan import parse_timespan
if TYPE_CHECKING:
@@ -347,17 +347,25 @@ def stats_report() -> Response:
)
if request.args.get("download"):
res = app.make_response(res)
- res.headers["Content-Disposition"] = f"attachment;filename=report_{time()}.html"
+ host = f"_{self.environment.host}" if self.environment.host else ""
+ res.headers["Content-Disposition"] = (
+ f"attachment;filename=Locust_{format_safe_timestamp(self.environment.stats.start_time)}_"
+ + f"{self.environment.locustfile}{host}.html"
+ )
return res
def _download_csv_suggest_file_name(suggest_filename_prefix: str) -> str:
"""Generate csv file download attachment filename suggestion.
Arguments:
- suggest_filename_prefix: Prefix of the filename to suggest for saving the download. Will be appended with timestamp.
+ suggest_filename_prefix: Prefix of the filename to suggest for saving the download.
+ Will be appended with timestamp.
"""
-
- return f"{suggest_filename_prefix}_{time()}.csv"
+ host = f"_{self.environment.host}" if self.environment.host else ""
+ return (
+ f"Locust_{format_safe_timestamp(self.environment.stats.start_time)}_"
+ + f"{self.environment.locustfile}{host}_{suggest_filename_prefix}.csv"
+ )
def _download_csv_response(csv_data: str, filename_prefix: str) -> Response:
"""Generate csv file download response with 'csv_data'.
diff --git a/locust/webui/src/pages/HtmlReport.tsx b/locust/webui/src/pages/HtmlReport.tsx
index 4d7155143c..05520bee05 100644
--- a/locust/webui/src/pages/HtmlReport.tsx
+++ b/locust/webui/src/pages/HtmlReport.tsx
@@ -42,6 +42,7 @@ export default function HtmlReport({
showDownloadLink,
startTime,
endTime,
+ duration,
charts,
host,
exceptionsStatistics,
@@ -75,7 +76,7 @@ export default function HtmlReport({
During:
- {formatLocaleString(startTime)} - {formatLocaleString(endTime)}
+ {formatLocaleString(startTime)} - {formatLocaleString(endTime)} ({duration})
diff --git a/locust/webui/src/pages/tests/HtmlReport.test.tsx b/locust/webui/src/pages/tests/HtmlReport.test.tsx
index b814988b08..5bc7553896 100644
--- a/locust/webui/src/pages/tests/HtmlReport.test.tsx
+++ b/locust/webui/src/pages/tests/HtmlReport.test.tsx
@@ -26,7 +26,7 @@ describe('HtmlReport', () => {
getByText(
`${formatLocaleString(swarmReportMock.startTime)} - ${formatLocaleString(
swarmReportMock.endTime,
- )}`,
+ )} (${swarmReportMock.duration})`,
),
).toBeTruthy();
});
diff --git a/locust/webui/src/test/mocks/swarmState.mock.ts b/locust/webui/src/test/mocks/swarmState.mock.ts
index 92caac14a1..1f6b8e757f 100644
--- a/locust/webui/src/test/mocks/swarmState.mock.ts
+++ b/locust/webui/src/test/mocks/swarmState.mock.ts
@@ -35,7 +35,8 @@ export const swarmReportMock: IReport = {
locustfile: 'locustfile.py',
showDownloadLink: true,
startTime: '2024-02-26 12:13:26',
- endTime: '2024-02-26 12:13:26',
+ endTime: '2024-02-26 13:27:14',
+ duration: '1 hour, 13 minutes and 48 seconds',
host: 'http://0.0.0.0:8089/',
exceptionsStatistics: [],
requestsStatistics: [],
diff --git a/locust/webui/src/types/swarm.types.ts b/locust/webui/src/types/swarm.types.ts
index 08a4b14add..7c83fd8f0b 100644
--- a/locust/webui/src/types/swarm.types.ts
+++ b/locust/webui/src/types/swarm.types.ts
@@ -71,6 +71,7 @@ export interface IReport {
showDownloadLink: boolean;
startTime: string;
endTime: string;
+ duration: string;
host: string;
charts: ICharts;
requestsStatistics: ISwarmStat[];
diff --git a/poetry.lock b/poetry.lock
index 830fa196d8..e26c3923cc 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
[[package]]
name = "alabaster"
@@ -68,6 +68,10 @@ files = [
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"},
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"},
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"},
+ {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c"},
+ {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1"},
+ {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2"},
+ {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec"},
{file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"},
{file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"},
{file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"},
@@ -80,8 +84,14 @@ files = [
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"},
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"},
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"},
+ {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f"},
+ {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757"},
+ {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0"},
+ {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b"},
{file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"},
{file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"},
+ {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28"},
+ {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f"},
{file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"},
{file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"},
{file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"},
@@ -92,8 +102,24 @@ files = [
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"},
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"},
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"},
+ {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9"},
+ {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb"},
+ {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111"},
+ {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839"},
{file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"},
{file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"},
+ {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5"},
+ {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8"},
+ {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f"},
+ {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648"},
+ {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0"},
+ {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089"},
+ {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368"},
+ {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c"},
+ {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284"},
+ {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7"},
+ {file = "Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0"},
+ {file = "Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b"},
{file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"},
{file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"},
{file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"},
@@ -103,6 +129,10 @@ files = [
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"},
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"},
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"},
+ {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75"},
+ {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c"},
+ {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2"},
+ {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52"},
{file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"},
{file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"},
{file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"},
@@ -114,6 +144,10 @@ files = [
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"},
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"},
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"},
+ {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01"},
+ {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547"},
+ {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38"},
+ {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c"},
{file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"},
{file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"},
{file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"},
@@ -126,6 +160,10 @@ files = [
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"},
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"},
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"},
+ {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7"},
+ {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5"},
+ {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943"},
+ {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a"},
{file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"},
{file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"},
{file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"},
@@ -138,6 +176,10 @@ files = [
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"},
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"},
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"},
+ {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419"},
+ {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2"},
+ {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f"},
+ {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb"},
{file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"},
{file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"},
{file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"},
@@ -513,6 +555,20 @@ files = [
{file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"},
]
+[[package]]
+name = "exceptiongroup"
+version = "1.2.2"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
+ {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
[[package]]
name = "filelock"
version = "3.16.1"
@@ -872,6 +928,17 @@ perf = ["ipython"]
test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"]
type = ["pytest-mypy"]
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+]
+
[[package]]
name = "itsdangerous"
version = "2.2.0"
@@ -1632,6 +1699,28 @@ lxml = ">=2.1"
[package.extras]
test = ["pytest", "pytest-cov", "requests", "webob", "webtest"]
+[[package]]
+name = "pytest"
+version = "8.3.3"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
+ {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=1.5,<2"
+tomli = {version = ">=1", markers = "python_version < \"3.11\""}
+
+[package.extras]
+dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+
[[package]]
name = "pywin32"
version = "308"
@@ -2474,4 +2563,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.9"
-content-hash = "9e58a176725655fe7b74069776ba86492cdc74c7f34b9c2a1788da9fba924e8e"
+content-hash = "357f408cf26bab20fe0398440230e10c5cdb23756467e5ff936ba064f916ae42"
diff --git a/pyproject.toml b/pyproject.toml
index 5bed208d72..ccd9d24129 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -143,6 +143,7 @@ pre-commit = "^3.7.1"
ruff = "0.3.7"
tox = "^4.16.0"
twine = "^5.1.1"
+pytest = "^8.3.3"
[tool.poetry.group.test]
optional = true