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.
- locust_cloud-1.20.7/.github/workflows/daily-check.yml +54 -0
- locust_cloud-1.20.7/.github/workflows/tests.yml +62 -0
- locust_cloud-1.20.7/.gitignore +7 -0
- locust_cloud-1.20.7/.pre-commit-config.yaml +8 -0
- locust_cloud-1.20.7/.vscode/extensions.json +5 -0
- locust_cloud-1.20.7/.vscode/launch.json +14 -0
- locust_cloud-1.20.7/.vscode/settings.json +27 -0
- locust_cloud-1.20.7/LICENSE +21 -0
- locust_cloud-1.20.7/PKG-INFO +19 -0
- locust_cloud-1.20.7/README.md +3 -0
- locust_cloud-1.20.7/locust_cloud/__init__.py +3 -0
- locust_cloud-1.20.7/locust_cloud/apisession.py +112 -0
- locust_cloud-1.20.7/locust_cloud/args.py +240 -0
- locust_cloud-1.20.7/locust_cloud/cloud.py +194 -0
- locust_cloud-1.20.7/locust_cloud/common.py +47 -0
- locust_cloud-1.20.7/locust_cloud/docs/.gitignore +1 -0
- locust_cloud-1.20.7/locust_cloud/docs/1-first-run.rst +36 -0
- locust_cloud-1.20.7/locust_cloud/docs/2-examples.rst +159 -0
- locust_cloud-1.20.7/locust_cloud/docs/images/locust-cloud-screenshot.png +0 -0
- locust_cloud-1.20.7/locust_cloud/docs/locust-cloud.rst +7 -0
- locust_cloud-1.20.7/locust_cloud/input_events.py +120 -0
- locust_cloud-1.20.7/locust_cloud/web_login.py +83 -0
- locust_cloud-1.20.7/locust_cloud/websocket.py +209 -0
- locust_cloud-1.20.7/locustfile.py +32 -0
- locust_cloud-1.20.7/pyproject.toml +67 -0
- locust_cloud-1.20.7/testdata/extra.txt +1 -0
- locust_cloud-1.20.7/tests/args_test.py +128 -0
- locust_cloud-1.20.7/tests/cloud_test.py +127 -0
- locust_cloud-1.20.7/tests/web_login_test.py +86 -0
- locust_cloud-1.20.7/tests/websocket_test.py +157 -0
- 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,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,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)
|