locust-cloud 1.20.7__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 (31) hide show
  1. locust_cloud-1.20.7/.github/workflows/daily-check.yml +54 -0
  2. locust_cloud-1.20.7/.github/workflows/tests.yml +62 -0
  3. locust_cloud-1.20.7/.gitignore +7 -0
  4. locust_cloud-1.20.7/.pre-commit-config.yaml +8 -0
  5. locust_cloud-1.20.7/.vscode/extensions.json +5 -0
  6. locust_cloud-1.20.7/.vscode/launch.json +14 -0
  7. locust_cloud-1.20.7/.vscode/settings.json +27 -0
  8. locust_cloud-1.20.7/LICENSE +21 -0
  9. locust_cloud-1.20.7/PKG-INFO +19 -0
  10. locust_cloud-1.20.7/README.md +3 -0
  11. locust_cloud-1.20.7/locust_cloud/__init__.py +3 -0
  12. locust_cloud-1.20.7/locust_cloud/apisession.py +112 -0
  13. locust_cloud-1.20.7/locust_cloud/args.py +240 -0
  14. locust_cloud-1.20.7/locust_cloud/cloud.py +194 -0
  15. locust_cloud-1.20.7/locust_cloud/common.py +47 -0
  16. locust_cloud-1.20.7/locust_cloud/docs/.gitignore +1 -0
  17. locust_cloud-1.20.7/locust_cloud/docs/1-first-run.rst +36 -0
  18. locust_cloud-1.20.7/locust_cloud/docs/2-examples.rst +159 -0
  19. locust_cloud-1.20.7/locust_cloud/docs/images/locust-cloud-screenshot.png +0 -0
  20. locust_cloud-1.20.7/locust_cloud/docs/locust-cloud.rst +7 -0
  21. locust_cloud-1.20.7/locust_cloud/input_events.py +120 -0
  22. locust_cloud-1.20.7/locust_cloud/web_login.py +83 -0
  23. locust_cloud-1.20.7/locust_cloud/websocket.py +209 -0
  24. locust_cloud-1.20.7/locustfile.py +32 -0
  25. locust_cloud-1.20.7/pyproject.toml +67 -0
  26. locust_cloud-1.20.7/testdata/extra.txt +1 -0
  27. locust_cloud-1.20.7/tests/args_test.py +128 -0
  28. locust_cloud-1.20.7/tests/cloud_test.py +127 -0
  29. locust_cloud-1.20.7/tests/web_login_test.py +86 -0
  30. locust_cloud-1.20.7/tests/websocket_test.py +157 -0
  31. locust_cloud-1.20.7/uv.lock +854 -0
@@ -0,0 +1,54 @@
1
+ name: Daily test run (api-dev)
2
+
3
+ on:
4
+ schedule: # 00:00, skipping Sunday and Monday
5
+ - cron: '0 0 * * 2-6'
6
+ workflow_dispatch:
7
+
8
+ env:
9
+ LOCUSTCLOUD_USERNAME: ${{ secrets.LOCUSTCLOUD_USERNAME }}
10
+ LOCUSTCLOUD_PASSWORD: ${{ secrets.LOCUSTCLOUD_PASSWORD }}
11
+ LOCUSTCLOUD_DEPLOYER_URL: https://api-dev.eu-north-1.locust.cloud/1
12
+ LOCUSTCLOUD_REGION: eu-north-1
13
+ LOCUSTCLOUD_NON_INTERACTIVE: 1
14
+ LOCUST_LOGLEVEL: DEBUG
15
+ SLACK_NOTIFICATIONS_WEBHOOK: ${{ secrets.SLACK_NOTIFICATIONS_WEBHOOK }}
16
+ PYTHONUNBUFFERED: 1 # ensure we see logs output right away
17
+
18
+ jobs:
19
+ locust_cloud_full_run:
20
+ runs-on: ubuntu-latest
21
+ timeout-minutes: 5
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ with:
25
+ fetch-depth: 0
26
+ fetch-tags: true
27
+ - uses: actions/setup-python@v5
28
+ with:
29
+ python-version: '3.11'
30
+ - uses: astral-sh/setup-uv@v2
31
+ - uses: actions/setup-node@v4
32
+ with:
33
+ node-version: 22.x
34
+ - run: uv venv --python 3.11
35
+ # any local changes would make hatch-vcs set a "local version" (+dev0...), so we ignore any uv.lock updates:
36
+ - run: git update-index --assume-unchanged uv.lock
37
+ - run: uv run locust-cloud --help
38
+ - run: uv run locust-cloud --image-tag master --profile status-checker --mock-server --autostart --autoquit 0 --run-time 1m --loglevel DEBUG --extra-files testdata |& tee output.txt
39
+ # check ok exit
40
+ - run: grep -m 1 '(exit code 0)' output.txt
41
+ # check extra files specified were available
42
+ - run: "grep -m 1 -- '--extra-files verification: pineapple' output.txt"
43
+ # check for errors
44
+ - run: bash -ec "! grep Traceback output.txt"
45
+ - run: bash -ec "! grep ERROR output.txt"
46
+ # Disabled the following test for now, because of incorrect warning about:
47
+ # You can't start a distributed test before at least one worker processes has connected
48
+ # - run: bash -ec "! grep WARNING output.txt"
49
+ - name: On failure, notify slack
50
+ if: failure()
51
+ run: curl -d "{\"text\":\"Failed run $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID\"}" $SLACK_NOTIFICATIONS_WEBHOOK
52
+ - name: Make sure to delete (only really needed for mock)
53
+ if: always()
54
+ run: uv tool run locust-cloud --delete
@@ -0,0 +1,62 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ workflow_dispatch:
6
+
7
+ permissions:
8
+ id-token: write
9
+ contents: read
10
+
11
+ env:
12
+ AWS_ACCOUNT_ID: 637423602143
13
+ AWS_REGION: eu-north-1
14
+
15
+ jobs:
16
+ build:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ with:
21
+ fetch-depth: 0
22
+ fetch-tags: true
23
+ - uses: astral-sh/setup-uv@v2
24
+ - run: uv venv --python 3.10
25
+ - run: uv run ruff check
26
+ - run: uv run ruff format --check
27
+ - run: uv run pyright
28
+ # This peculiar way of running pytest is to ensure monkey patching is done before pytest starts loading modules.
29
+ # Otherwise you may get "MonkeyPatchWarning: Monkey-patching ssl after ssl has already been imported" and infinite recursion
30
+ # https://github.com/pytest-dev/pytest/issues/6210
31
+ - run: uv run python -m gevent.monkey --module pytest
32
+ env:
33
+ LOCUSTCLOUD_USERNAME: ${{ secrets.LOCUSTCLOUD_USERNAME }}
34
+ LOCUSTCLOUD_PASSWORD: ${{ secrets.LOCUSTCLOUD_PASSWORD }}
35
+ # any local changes would make hatch-vcs set a "local version" (+dev0...), so we ignore any uv.lock updates:
36
+ - run: git update-index --assume-unchanged uv.lock
37
+ - run: uvx --from build pyproject-build --sdist --wheel --installer uv
38
+ - uses: actions/upload-artifact@v4
39
+ with:
40
+ name: dist-artifact
41
+ path: dist/*
42
+ # Ensure what customers will actually run does not rely on dev-dependencies
43
+ - run: rm -rf uv.lock .venv
44
+ - run: uv run --no-default-groups locust-cloud --help
45
+
46
+ publish_pypi:
47
+ name: Publish to PyPI
48
+ needs: [build]
49
+ if: github.repository_owner == 'locustcloud' && ( github.ref == 'refs/heads/master' || startsWith(github.event.ref, 'refs/tags') )
50
+ runs-on: ubuntu-latest
51
+ permissions:
52
+ id-token: write
53
+ steps:
54
+ - uses: actions/setup-python@v4
55
+ with:
56
+ python-version: "3.11"
57
+ - name: Download Python dist
58
+ uses: actions/download-artifact@v4
59
+ with:
60
+ name: dist-artifact
61
+ path: dist
62
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,7 @@
1
+ .DS_Store
2
+
3
+ venv
4
+ **/__pycache__
5
+ locust_cloud.egg*
6
+ .eggs
7
+ **_cache
@@ -0,0 +1,8 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ # pin the same version as in pyproject.toml
4
+ rev: v0.10.0
5
+ hooks:
6
+ - id: ruff
7
+ args: [--fix]
8
+ - id: ruff-format
@@ -0,0 +1,5 @@
1
+ {
2
+ "recommendations": [
3
+ "charliermarsh.ruff"
4
+ ]
5
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "version": "0.2.0",
3
+ "configurations": [
4
+ {
5
+ "name": "Run via uv",
6
+ "type": "debugpy",
7
+ "request": "launch",
8
+ "console": "integratedTerminal",
9
+ "program": "${workspaceFolder}/.venv/bin/locust-cloud",
10
+ "args": []
11
+ }
12
+ ]
13
+ }
14
+
@@ -0,0 +1,27 @@
1
+ {
2
+ "editor.formatOnSave": true,
3
+ "files.exclude": {
4
+ "locust_cloud.egg-info/**": true,
5
+ "dist/**": true,
6
+ "**/.tox/": true,
7
+ "**/__pycache__": true,
8
+ ".mypy_cache/**/*": true,
9
+ ".ruff_cache/**/*": true,
10
+ ".venv/": true,
11
+ "**/.eggs": true,
12
+ "**_cache": true
13
+ },
14
+ "[python]": {
15
+ "editor.formatOnSave": true,
16
+ "editor.codeActionsOnSave": {
17
+ "source.fixAll": "explicit",
18
+ "source.organizeImports": "explicit"
19
+ },
20
+ "editor.defaultFormatter": "charliermarsh.ruff"
21
+ },
22
+ "python.testing.pytestArgs": [
23
+ "tests"
24
+ ],
25
+ "python.testing.unittestEnabled": false,
26
+ "python.testing.pytestEnabled": true
27
+ }
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2024-present, Locust Technologies Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: locust-cloud
3
+ Version: 1.20.7
4
+ Summary: Locust Cloud
5
+ Project-URL: homepage, https://locust.cloud
6
+ Project-URL: repository, https://github.com/locustcloud/locust-cloud
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: configargparse>=1.5.5
11
+ Requires-Dist: gevent<25.0.0,>=24.10.1
12
+ Requires-Dist: platformdirs<5.0.0,>=4.3.6
13
+ Requires-Dist: python-socketio[client]==5.13.0
14
+ Requires-Dist: tomli>=1.1.0; python_version < '3.11'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Locust Cloud
18
+
19
+ See https://locust.cloud for general information on Locust Cloud, or https://docs.locust.cloud for usage instructions.
@@ -0,0 +1,3 @@
1
+ # Locust Cloud
2
+
3
+ See https://locust.cloud for general information on Locust Cloud, or https://docs.locust.cloud for usage instructions.
@@ -0,0 +1,3 @@
1
+ from locust_cloud.cloud import main
2
+
3
+ __all__ = ["main"]
@@ -0,0 +1,112 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ import time
5
+
6
+ import requests
7
+ from locust_cloud.common import VALID_REGIONS, __version__, get_api_url, read_cloud_config, write_cloud_config
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ unauthorized_message = "You need to log in again. Please run:\n locust --cloud --login"
12
+
13
+
14
+ class ApiSession(requests.Session):
15
+ def __init__(self, non_interactive: bool) -> None:
16
+ super().__init__()
17
+ self.non_interactive = non_interactive
18
+
19
+ if non_interactive:
20
+ username = os.getenv("LOCUSTCLOUD_USERNAME")
21
+ password = os.getenv("LOCUSTCLOUD_PASSWORD")
22
+ region = os.getenv("LOCUSTCLOUD_REGION")
23
+
24
+ if not all([username, password, region]):
25
+ print(
26
+ "Running with --non-interactive requires that LOCUSTCLOUD_USERNAME, LOCUSTCLOUD_PASSWORD and LOCUSTCLOUD_REGION environment variables are set."
27
+ )
28
+ sys.exit(1)
29
+
30
+ if region not in VALID_REGIONS:
31
+ print("Environment variable LOCUSTCLOUD_REGION needs to be set to one of", ", ".join(VALID_REGIONS))
32
+ sys.exit(1)
33
+
34
+ self.__configure_for_region(region)
35
+ response = requests.post(
36
+ self.__login_url,
37
+ json={"username": username, "password": password},
38
+ headers={"X-Client-Version": __version__},
39
+ )
40
+ if not response.ok:
41
+ print(f"Authentication failed: {response.text}")
42
+ sys.exit(1)
43
+
44
+ id_token = response.json()["cognito_client_id_token"]
45
+ user_sub_id = response.json()["user_sub_id"]
46
+ refresh_token = response.json()["refresh_token"]
47
+ id_token_expires = response.json()["id_token_expires"]
48
+ else:
49
+ config = read_cloud_config()
50
+
51
+ if config.refresh_token_expires < time.time() + 24 * 60 * 60:
52
+ print(unauthorized_message)
53
+ sys.exit(1)
54
+
55
+ assert config.region
56
+ self.__configure_for_region(config.region)
57
+ id_token = config.id_token
58
+ user_sub_id = config.user_sub_id
59
+ refresh_token = config.refresh_token
60
+ id_token_expires = config.id_token_expires
61
+
62
+ assert id_token
63
+
64
+ self.__user_sub_id = user_sub_id
65
+ self.__refresh_token = refresh_token
66
+ self.__id_token_expires = id_token_expires - 60 # Refresh 1 minute before expiry
67
+ self.headers["Authorization"] = f"Bearer {id_token}"
68
+ self.headers["X-Client-Version"] = __version__
69
+
70
+ def __configure_for_region(self, region: str) -> None:
71
+ self.region = region
72
+ self.api_url = get_api_url(region)
73
+ self.__login_url = f"{self.api_url}/auth/login"
74
+
75
+ logger.debug(f"Lambda url: {self.api_url}")
76
+
77
+ def __ensure_valid_authorization_header(self) -> None:
78
+ if self.__id_token_expires > time.time():
79
+ return
80
+ if not self.__user_sub_id and self.__refresh_token:
81
+ print(unauthorized_message)
82
+ sys.exit(1)
83
+
84
+ response = requests.post(
85
+ self.__login_url,
86
+ json={"user_sub_id": self.__user_sub_id, "refresh_token": self.__refresh_token},
87
+ headers={"X-Client-Version": __version__},
88
+ )
89
+
90
+ if not response.ok:
91
+ logger.error(f"Authentication failed: {response.text}")
92
+ sys.exit(1)
93
+
94
+ # TODO: Technically the /login endpoint can return a challenge for you
95
+ # to change your password.
96
+ # Now that we have a web based login flow we should force them to
97
+ # do a locust --cloud --login if we get that.
98
+
99
+ id_token = response.json()["cognito_client_id_token"]
100
+ id_token_expires = response.json()["id_token_expires"]
101
+ self.__id_token_expires = id_token_expires - 60 # Refresh 1 minute before expiry
102
+ self.headers["Authorization"] = f"Bearer {id_token}"
103
+
104
+ if not self.non_interactive:
105
+ config = read_cloud_config()
106
+ config.id_token = id_token
107
+ config.id_token_expires = id_token_expires
108
+ write_cloud_config(config)
109
+
110
+ def request(self, method, url, *args, **kwargs) -> requests.Response:
111
+ self.__ensure_valid_authorization_header()
112
+ return super().request(method, f"{self.api_url}{url}", *args, **kwargs)
@@ -0,0 +1,240 @@
1
+ import argparse
2
+ import base64
3
+ import gzip
4
+ import io
5
+ import os
6
+ import pathlib
7
+ import sys
8
+
9
+ if sys.version_info >= (3, 11):
10
+ import tomllib
11
+ else:
12
+ import tomli as tomllib
13
+
14
+ from argparse import ArgumentTypeError
15
+ from collections import OrderedDict
16
+ from collections.abc import Callable, Generator
17
+ from typing import IO, Any, cast
18
+ from zipfile import ZipFile
19
+
20
+ import configargparse
21
+
22
+ CWD = pathlib.Path.cwd()
23
+
24
+
25
+ class LocustTomlConfigParser(configargparse.TomlConfigParser):
26
+ def parse(self, stream: IO[str]) -> OrderedDict[str, Any]:
27
+ try:
28
+ config = tomllib.loads(stream.read())
29
+ except Exception as e:
30
+ raise configargparse.ConfigFileParserException(f"Couldn't parse TOML file: {e}")
31
+
32
+ result: OrderedDict[str, Any] = OrderedDict()
33
+
34
+ for section in self.sections:
35
+ data = configargparse.get_toml_section(config, section)
36
+ if data:
37
+ for key, value in data.items():
38
+ if isinstance(value, list):
39
+ result[key] = value
40
+ elif value is not None:
41
+ result[key] = str(value)
42
+ break
43
+
44
+ return result
45
+
46
+
47
+ def pipe(value: Any, *functions: Callable) -> Any:
48
+ for function in functions:
49
+ value = function(value)
50
+
51
+ return value
52
+
53
+
54
+ def valid_extra_files_path(file_path: str) -> pathlib.Path:
55
+ p = pathlib.Path(file_path).resolve()
56
+
57
+ if not CWD in p.parents:
58
+ raise ArgumentTypeError(f"Can only reference files under current working directory: {CWD}")
59
+ if not p.exists():
60
+ raise ArgumentTypeError(f"File not found: {file_path}")
61
+ return p
62
+
63
+
64
+ def transfer_encode(file_name: str, stream: IO[bytes]) -> dict[str, str]:
65
+ return {
66
+ "filename": file_name,
67
+ "data": pipe(
68
+ stream.read(),
69
+ gzip.compress,
70
+ base64.b64encode,
71
+ bytes.decode,
72
+ ),
73
+ }
74
+
75
+
76
+ def transfer_encoded_file(file_path: str) -> dict[str, str]:
77
+ try:
78
+ with open(file_path, "rb") as f:
79
+ return transfer_encode(os.path.basename(file_path), f)
80
+ except FileNotFoundError:
81
+ raise ArgumentTypeError(f"File not found: {file_path}")
82
+
83
+
84
+ def expanded(paths: list[pathlib.Path]) -> Generator[pathlib.Path, None, None]:
85
+ for path in paths:
86
+ if path.is_dir():
87
+ for root, _, file_names in os.walk(path):
88
+ for file_name in file_names:
89
+ yield pathlib.Path(root) / file_name
90
+ else:
91
+ yield path
92
+
93
+
94
+ def transfer_encoded_extra_files(paths: list[pathlib.Path]) -> dict[str, str]:
95
+ buffer = io.BytesIO()
96
+
97
+ with ZipFile(buffer, "w") as zf:
98
+ for path in set(expanded(paths)):
99
+ zf.write(path.relative_to(CWD))
100
+
101
+ buffer.seek(0)
102
+ return transfer_encode("extra-files.zip", buffer)
103
+
104
+
105
+ class MergeToTransferEncodedZip(argparse.Action):
106
+ def __call__(self, parser, namespace, values, option_string=None):
107
+ paths = cast(list[pathlib.Path], values)
108
+ value = transfer_encoded_extra_files(paths)
109
+ setattr(namespace, self.dest, value)
110
+
111
+
112
+ cloud_parser = configargparse.ArgumentParser(add_help=False)
113
+ cloud_parser.add_argument(
114
+ "--login",
115
+ action="store_true",
116
+ help="Launch an interactive session to authenticate your user.\nOnce completed your credentials will be stored and automatically refreshed for quite a long time.\nOnce those expire you will be prompted to perform another login.",
117
+ )
118
+ cloud_parser.add_argument(
119
+ "--logout",
120
+ action="store_true",
121
+ help="Removes the authentication credentials",
122
+ )
123
+ cloud_parser.add_argument(
124
+ "--delete",
125
+ action="store_true",
126
+ help="Delete a running cluster. Useful if locust-cloud was killed/disconnected or if there was an error.",
127
+ )
128
+ cloud_parser.add_argument(
129
+ "--requirements",
130
+ metavar="<filename>",
131
+ type=transfer_encoded_file,
132
+ help="Optional requirements.txt file that contains your external libraries.",
133
+ )
134
+ cloud_parser.add_argument(
135
+ "--non-interactive",
136
+ action="store_true",
137
+ default=False,
138
+ help="This can be set when, for example, running in a CI/CD environment to ensure no interactive steps while executing.\nRequires that LOCUSTCLOUD_USERNAME, LOCUSTCLOUD_PASSWORD and LOCUSTCLOUD_REGION environment variables are set.",
139
+ )
140
+ cloud_parser.add_argument(
141
+ "--workers",
142
+ metavar="<int>",
143
+ type=int,
144
+ help="Number of workers to use for the deployment. Defaults to number of users divided by 500, but the default may be customized for your account.",
145
+ default=None,
146
+ )
147
+ cloud_parser.add_argument(
148
+ "--image-tag",
149
+ type=str,
150
+ default=None,
151
+ help=configargparse.SUPPRESS, # overrides the locust-cloud docker image tag. for internal use
152
+ )
153
+ cloud_parser.add_argument(
154
+ "--mock-server",
155
+ action="store_true",
156
+ default=False,
157
+ help="Start a demo mock service and set --host parameter to point Locust towards it.",
158
+ )
159
+ cloud_parser.add_argument(
160
+ "--extra-files",
161
+ action=MergeToTransferEncodedZip,
162
+ nargs="*",
163
+ type=valid_extra_files_path,
164
+ help="A list of extra files or directories to upload. Space-separated, e.g. `--extra-files testdata.csv *.py my-directory/`.",
165
+ )
166
+ cloud_parser.add_argument(
167
+ "--testrun-tags",
168
+ nargs="*",
169
+ default=None,
170
+ help="A list of tags that can be used to filter testruns.",
171
+ )
172
+
173
+ combined_cloud_parser = configargparse.ArgumentParser(
174
+ parents=[cloud_parser],
175
+ default_config_files=[
176
+ "~/.cloud.conf",
177
+ "cloud.conf",
178
+ ],
179
+ auto_env_var_prefix="LOCUSTCLOUD_",
180
+ formatter_class=configargparse.RawTextHelpFormatter,
181
+ config_file_parser_class=configargparse.CompositeConfigParser(
182
+ [
183
+ LocustTomlConfigParser(["tool.locust"]),
184
+ configargparse.DefaultConfigFileParser,
185
+ ]
186
+ ),
187
+ description="""Launches a distributed Locust runs on locust.cloud infrastructure.
188
+
189
+ Example: locust --cloud -f my_locustfile.py --users 1000 ...""",
190
+ epilog="""Any parameters not listed here are forwarded to locust master unmodified, so go ahead and use things like --users, --host, --run-time, ...
191
+ Locust config can also be set using config file (~/.locust.conf, locust.conf, pyproject.toml, ~/.cloud.conf or cloud.conf).
192
+ Parameters specified on command line override env vars, which in turn override config files.""",
193
+ add_config_file_help=False,
194
+ add_env_var_help=False,
195
+ )
196
+ combined_cloud_parser.add_argument(
197
+ "-f",
198
+ "--locustfile",
199
+ metavar="<filename>",
200
+ default="locustfile.py",
201
+ help="The Python file that contains your test. Defaults to 'locustfile.py'.",
202
+ env_var="LOCUST_LOCUSTFILE",
203
+ type=transfer_encoded_file,
204
+ )
205
+ combined_cloud_parser.add_argument(
206
+ "-u",
207
+ "--users",
208
+ type=int,
209
+ default=1,
210
+ help="Number of users to launch. This is the same as the regular Locust argument, but also affects how many workers to launch.",
211
+ env_var="LOCUST_USERS",
212
+ )
213
+ combined_cloud_parser.add_argument(
214
+ "--loglevel",
215
+ "-L",
216
+ type=str.upper,
217
+ help="Set --loglevel DEBUG for extra info.",
218
+ choices=["DEBUG", "INFO", "WARNING", "ERROR"],
219
+ default="INFO",
220
+ )
221
+
222
+
223
+ def add_locust_cloud_argparse(parser):
224
+ cloud_group = parser.add_argument_group(
225
+ "Locust Cloud",
226
+ """Launches a distributed Locust run on locust.cloud infrastructure.
227
+
228
+ Example: locust --cloud -f my_locustfile.py --users 1000 ...""",
229
+ )
230
+
231
+ # This arguments is defined here because only makes sense when
232
+ # running from locust core
233
+ cloud_group.add_argument(
234
+ "--cloud",
235
+ action="store_true",
236
+ help="Run Locust in cloud mode.",
237
+ )
238
+
239
+ for action in cloud_parser._actions:
240
+ cloud_group._add_action(action)