locust-cloud 1.12.4__py3-none-any.whl → 1.13.0__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_cloud/cloud.py +244 -106
- {locust_cloud-1.12.4.dist-info → locust_cloud-1.13.0.dist-info}/METADATA +3 -4
- locust_cloud-1.13.0.dist-info/RECORD +5 -0
- locust_cloud/__init__.py +0 -133
- locust_cloud/auth.py +0 -443
- locust_cloud/credential_manager.py +0 -141
- locust_cloud/idle_exit.py +0 -38
- locust_cloud/socket_logging.py +0 -127
- locust_cloud/timescale/exporter.py +0 -313
- locust_cloud/timescale/queries.py +0 -321
- locust_cloud/timescale/query.py +0 -74
- locust_cloud/webui/.gitignore +0 -4
- locust_cloud/webui/.prettierrc +0 -9
- locust_cloud/webui/dist/assets/index-D3YieuNV.js +0 -329
- locust_cloud/webui/dist/index.html +0 -20
- locust_cloud/webui/eslint.config.mjs +0 -83
- locust_cloud/webui/index.html +0 -20
- locust_cloud/webui/package.json +0 -52
- locust_cloud/webui/tsconfig.json +0 -27
- locust_cloud/webui/tsconfig.tsbuildinfo +0 -1
- locust_cloud/webui/vite.config.ts +0 -9
- locust_cloud/webui/vitest.config.ts +0 -16
- locust_cloud/webui/yarn.lock +0 -5816
- locust_cloud-1.12.4.dist-info/RECORD +0 -25
- {locust_cloud-1.12.4.dist-info → locust_cloud-1.13.0.dist-info}/WHEEL +0 -0
- {locust_cloud-1.12.4.dist-info → locust_cloud-1.13.0.dist-info}/entry_points.txt +0 -0
locust_cloud/cloud.py
CHANGED
@@ -1,21 +1,29 @@
|
|
1
1
|
import base64
|
2
2
|
import gzip
|
3
|
+
import importlib.metadata
|
4
|
+
import json
|
3
5
|
import logging
|
4
6
|
import os
|
7
|
+
import pathlib
|
5
8
|
import sys
|
6
9
|
import threading
|
10
|
+
import time
|
7
11
|
import tomllib
|
8
12
|
import urllib.parse
|
13
|
+
import webbrowser
|
9
14
|
from argparse import Namespace
|
10
15
|
from collections import OrderedDict
|
16
|
+
from dataclasses import dataclass
|
11
17
|
from typing import IO, Any
|
12
18
|
|
13
19
|
import configargparse
|
20
|
+
import jwt
|
21
|
+
import platformdirs
|
14
22
|
import requests
|
15
23
|
import socketio
|
16
24
|
import socketio.exceptions
|
17
|
-
|
18
|
-
|
25
|
+
|
26
|
+
__version__ = importlib.metadata.version("locust-cloud")
|
19
27
|
|
20
28
|
|
21
29
|
class LocustTomlConfigParser(configargparse.TomlConfigParser):
|
@@ -49,7 +57,7 @@ parser = configargparse.ArgumentParser(
|
|
49
57
|
"cloud.conf",
|
50
58
|
],
|
51
59
|
auto_env_var_prefix="LOCUSTCLOUD_",
|
52
|
-
formatter_class=configargparse.
|
60
|
+
formatter_class=configargparse.RawTextHelpFormatter,
|
53
61
|
config_file_parser_class=configargparse.CompositeConfigParser(
|
54
62
|
[
|
55
63
|
LocustTomlConfigParser(["tool.locust"]),
|
@@ -108,36 +116,16 @@ advanced.add_argument(
|
|
108
116
|
help="Optional requirements.txt file that contains your external libraries.",
|
109
117
|
)
|
110
118
|
advanced.add_argument(
|
111
|
-
"--
|
112
|
-
|
113
|
-
default=
|
114
|
-
help="Sets the AWS region to use for the deployed cluster, e.g. us-east-1. It defaults to use AWS_DEFAULT_REGION env var, like AWS tools.",
|
115
|
-
)
|
116
|
-
parser.add_argument(
|
117
|
-
"--aws-access-key-id",
|
118
|
-
type=str,
|
119
|
-
help=configargparse.SUPPRESS,
|
120
|
-
env_var="AWS_ACCESS_KEY_ID",
|
121
|
-
default=None,
|
122
|
-
)
|
123
|
-
parser.add_argument(
|
124
|
-
"--aws-secret-access-key",
|
125
|
-
type=str,
|
126
|
-
help=configargparse.SUPPRESS,
|
127
|
-
env_var="AWS_SECRET_ACCESS_KEY",
|
128
|
-
default=None,
|
129
|
-
)
|
130
|
-
parser.add_argument(
|
131
|
-
"--username",
|
132
|
-
type=str,
|
119
|
+
"--login",
|
120
|
+
action="store_true",
|
121
|
+
default=False,
|
133
122
|
help=configargparse.SUPPRESS,
|
134
|
-
default=os.getenv("LOCUST_CLOUD_USERNAME", None), # backwards compatitibility for dmdb
|
135
123
|
)
|
136
|
-
|
137
|
-
"--
|
138
|
-
|
139
|
-
|
140
|
-
|
124
|
+
advanced.add_argument(
|
125
|
+
"--non-interactive",
|
126
|
+
action="store_true",
|
127
|
+
default=False,
|
128
|
+
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.",
|
141
129
|
)
|
142
130
|
parser.add_argument(
|
143
131
|
"--workers",
|
@@ -153,7 +141,7 @@ parser.add_argument(
|
|
153
141
|
parser.add_argument(
|
154
142
|
"--image-tag",
|
155
143
|
type=str,
|
156
|
-
default=
|
144
|
+
default=None,
|
157
145
|
help=configargparse.SUPPRESS, # overrides the locust-cloud docker image tag. for internal use
|
158
146
|
)
|
159
147
|
parser.add_argument(
|
@@ -169,6 +157,7 @@ parser.add_argument(
|
|
169
157
|
)
|
170
158
|
|
171
159
|
options, locust_options = parser.parse_known_args()
|
160
|
+
|
172
161
|
options: Namespace
|
173
162
|
locust_options: list
|
174
163
|
|
@@ -183,51 +172,222 @@ logging.getLogger("boto3").setLevel(logging.INFO)
|
|
183
172
|
logging.getLogger("requests").setLevel(logging.INFO)
|
184
173
|
logging.getLogger("urllib3").setLevel(logging.INFO)
|
185
174
|
|
175
|
+
cloud_conf_file = pathlib.Path(platformdirs.user_config_dir(appname="locust-cloud")) / "config"
|
176
|
+
valid_regions = ["us-east-1", "eu-north-1"]
|
186
177
|
|
187
|
-
api_url = os.environ.get("LOCUSTCLOUD_DEPLOYER_URL", f"https://api.{options.region}.locust.cloud/1")
|
188
178
|
|
179
|
+
def get_api_url(region):
|
180
|
+
return os.environ.get("LOCUSTCLOUD_DEPLOYER_URL", f"https://api.{region}.locust.cloud/1")
|
189
181
|
|
190
|
-
def main() -> None:
|
191
|
-
if options.version:
|
192
|
-
print(f"locust-cloud version {__version__}")
|
193
|
-
sys.exit(0)
|
194
182
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
183
|
+
@dataclass
|
184
|
+
class CloudConfig:
|
185
|
+
id_token: str | None = None
|
186
|
+
refresh_token: str | None = None
|
187
|
+
refresh_token_expires: int = 0
|
188
|
+
region: str | None = None
|
189
|
+
|
190
|
+
|
191
|
+
def read_cloud_config() -> CloudConfig:
|
192
|
+
if cloud_conf_file.exists():
|
193
|
+
with open(cloud_conf_file) as f:
|
194
|
+
return CloudConfig(**json.load(f))
|
195
|
+
|
196
|
+
return CloudConfig()
|
197
|
+
|
198
|
+
|
199
|
+
def write_cloud_config(config: CloudConfig) -> None:
|
200
|
+
cloud_conf_file.parent.mkdir(parents=True, exist_ok=True)
|
201
|
+
|
202
|
+
with open(cloud_conf_file, "w") as f:
|
203
|
+
json.dump(config.__dict__, f)
|
204
|
+
|
205
|
+
|
206
|
+
def web_login() -> None:
|
207
|
+
print("Enter the number for the region to authenticate against")
|
208
|
+
print()
|
209
|
+
for i, valid_region in enumerate(valid_regions, start=1):
|
210
|
+
print(f" {i}. {valid_region}")
|
211
|
+
print()
|
212
|
+
choice = input("> ")
|
213
|
+
try:
|
214
|
+
region_index = int(choice) - 1
|
215
|
+
assert 0 <= region_index < len(valid_regions)
|
216
|
+
except (ValueError, AssertionError):
|
217
|
+
print(f"Not a valid choice: '{choice}'")
|
199
218
|
sys.exit(1)
|
200
|
-
if options.region:
|
201
|
-
os.environ["AWS_DEFAULT_REGION"] = options.region
|
202
219
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
)
|
220
|
+
region = valid_regions[region_index]
|
221
|
+
|
222
|
+
try:
|
223
|
+
response = requests.post(f"{get_api_url(region)}/cli-auth")
|
224
|
+
response.raise_for_status()
|
225
|
+
response_data = response.json()
|
226
|
+
authentication_url = response_data["authentication_url"]
|
227
|
+
result_url = response_data["result_url"]
|
228
|
+
except Exception as e:
|
229
|
+
print("Something went wrong trying to authorize the locust-cloud CLI:", str(e))
|
207
230
|
sys.exit(1)
|
231
|
+
|
232
|
+
message = f"""
|
233
|
+
Attempting to automatically open the SSO authorization page in your default browser.
|
234
|
+
If the browser does not open or you wish to use a different device to authorize this request, open the following URL:
|
235
|
+
|
236
|
+
{authentication_url}
|
237
|
+
""".strip()
|
238
|
+
print()
|
239
|
+
print(message)
|
240
|
+
|
241
|
+
webbrowser.open_new_tab(authentication_url)
|
242
|
+
|
243
|
+
while True: # Should there be some kind of timeout?
|
244
|
+
response = requests.get(result_url)
|
245
|
+
|
246
|
+
if not response.ok:
|
247
|
+
print("Oh no!")
|
248
|
+
print(response.text)
|
249
|
+
sys.exit(1)
|
250
|
+
|
251
|
+
data = response.json()
|
252
|
+
|
253
|
+
if data["state"] == "pending":
|
254
|
+
time.sleep(1)
|
255
|
+
continue
|
256
|
+
elif data["state"] == "failed":
|
257
|
+
print(f"\nFailed to authorize CLI: {data['reason']}")
|
258
|
+
sys.exit(1)
|
259
|
+
elif data["state"] == "authorized":
|
260
|
+
print("\nAuthorization succeded")
|
261
|
+
break
|
262
|
+
else:
|
263
|
+
print("\nGot unexpected response when authorizing CLI")
|
264
|
+
sys.exit(1)
|
265
|
+
|
266
|
+
config = CloudConfig(
|
267
|
+
id_token=data["id_token"],
|
268
|
+
refresh_token=data["refresh_token"],
|
269
|
+
refresh_token_expires=data["refresh_token_expires"],
|
270
|
+
region=region,
|
271
|
+
)
|
272
|
+
write_cloud_config(config)
|
273
|
+
|
274
|
+
|
275
|
+
class ApiSession(requests.Session):
|
276
|
+
def __init__(self) -> None:
|
277
|
+
super().__init__()
|
278
|
+
|
279
|
+
if options.non_interactive:
|
280
|
+
username = os.getenv("LOCUSTCLOUD_USERNAME")
|
281
|
+
password = os.getenv("LOCUSTCLOUD_PASSWORD")
|
282
|
+
region = os.getenv("LOCUSTCLOUD_REGION")
|
283
|
+
|
284
|
+
if not all([username, password, region]):
|
285
|
+
print(
|
286
|
+
"Running with --non-interaction requires that LOCUSTCLOUD_USERNAME, LOCUSTCLOUD_PASSWORD and LOCUSTCLOUD_REGION environment variables are set."
|
287
|
+
)
|
288
|
+
sys.exit(1)
|
289
|
+
|
290
|
+
if region not in valid_regions:
|
291
|
+
print("Environment variable LOCUSTCLOUD_REGION needs to be set to one of", ", ".join(valid_regions))
|
292
|
+
sys.exit(1)
|
293
|
+
|
294
|
+
self.__configure_for_region(region)
|
295
|
+
response = requests.post(
|
296
|
+
self.__login_url,
|
297
|
+
json={"username": username, "password": password},
|
298
|
+
headers={"X-Client-Version": __version__},
|
299
|
+
)
|
300
|
+
if not response.ok:
|
301
|
+
print(f"Authentication failed: {response.text}")
|
302
|
+
sys.exit(1)
|
303
|
+
|
304
|
+
self.__refresh_token = response.json()["refresh_token"]
|
305
|
+
id_token = response.json()["cognito_client_id_token"]
|
306
|
+
|
307
|
+
else:
|
308
|
+
config = read_cloud_config()
|
309
|
+
|
310
|
+
if config.refresh_token_expires < time.time() + 24 * 60 * 60:
|
311
|
+
message = "You need to authenticate before proceeding. Please run:\n locust-cloud --login"
|
312
|
+
print(message)
|
313
|
+
sys.exit(1)
|
314
|
+
|
315
|
+
assert config.region
|
316
|
+
self.__configure_for_region(config.region)
|
317
|
+
self.__refresh_token = config.refresh_token
|
318
|
+
id_token = config.id_token
|
319
|
+
|
320
|
+
assert id_token
|
321
|
+
|
322
|
+
decoded = jwt.decode(id_token, options={"verify_signature": False})
|
323
|
+
self.__expiry_time = decoded["exp"] - 60 # Refresh 1 minute before expiry
|
324
|
+
self.headers["Authorization"] = f"Bearer {id_token}"
|
325
|
+
|
326
|
+
self.__sub = decoded["sub"]
|
327
|
+
self.headers["X-Client-Version"] = __version__
|
328
|
+
|
329
|
+
def __configure_for_region(self, region: str) -> None:
|
330
|
+
self.__region = region
|
331
|
+
self.api_url = get_api_url(region)
|
332
|
+
self.__login_url = f"{self.api_url}/auth/login"
|
333
|
+
|
334
|
+
logger.debug(f"Lambda url: {self.api_url}")
|
335
|
+
|
336
|
+
def __ensure_valid_authorization_header(self) -> None:
|
337
|
+
if self.__expiry_time > time.time():
|
338
|
+
return
|
339
|
+
|
340
|
+
logger.info(f"Authenticating ({self.__region}, v{__version__})")
|
341
|
+
|
342
|
+
response = requests.post(
|
343
|
+
self.__login_url,
|
344
|
+
json={"user_sub_id": self.__sub, "refresh_token": self.__refresh_token},
|
345
|
+
headers={"X-Client-Version": __version__},
|
346
|
+
)
|
347
|
+
|
348
|
+
if not response.ok:
|
349
|
+
logger.error(f"Authentication failed: {response.text}")
|
350
|
+
sys.exit(1)
|
351
|
+
|
352
|
+
# TODO: Technically the /login endpoint can return a challenge for you
|
353
|
+
# to change your password. Don't know how we should handle that
|
354
|
+
# in the cli.
|
355
|
+
|
356
|
+
id_token = response.json()["cognito_client_id_token"]
|
357
|
+
decoded = jwt.decode(id_token, options={"verify_signature": False})
|
358
|
+
self.__expiry_time = decoded["exp"] - 60 # Refresh 1 minute before expiry
|
359
|
+
self.headers["Authorization"] = f"Bearer {id_token}"
|
360
|
+
|
361
|
+
if not options.non_interactive:
|
362
|
+
config = read_cloud_config()
|
363
|
+
config.id_token = id_token
|
364
|
+
write_cloud_config(config)
|
365
|
+
|
366
|
+
def request(self, method, url, *args, **kwargs) -> requests.Response:
|
367
|
+
self.__ensure_valid_authorization_header()
|
368
|
+
return super().request(method, f"{self.api_url}{url}", *args, **kwargs)
|
369
|
+
|
370
|
+
|
371
|
+
def main() -> None:
|
372
|
+
if options.version:
|
373
|
+
print(f"locust-cloud version {__version__}")
|
374
|
+
sys.exit(0)
|
208
375
|
if not options.locustfile:
|
209
376
|
logger.error("A locustfile is required to run a test.")
|
210
377
|
sys.exit(1)
|
211
378
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
secret_key=options.aws_secret_access_key,
|
219
|
-
username=options.username,
|
220
|
-
password=options.password,
|
221
|
-
)
|
379
|
+
if options.login:
|
380
|
+
try:
|
381
|
+
web_login()
|
382
|
+
except KeyboardInterrupt:
|
383
|
+
pass
|
384
|
+
sys.exit()
|
222
385
|
|
223
|
-
|
224
|
-
cognito_client_id_token = credentials["cognito_client_id_token"]
|
225
|
-
aws_access_key_id = credentials.get("access_key")
|
226
|
-
aws_secret_access_key = credentials.get("secret_key")
|
227
|
-
aws_session_token = credentials.get("token", "")
|
386
|
+
session = ApiSession()
|
228
387
|
|
388
|
+
try:
|
229
389
|
if options.delete:
|
230
|
-
delete(
|
390
|
+
delete(session)
|
231
391
|
return
|
232
392
|
|
233
393
|
try:
|
@@ -249,47 +409,41 @@ def main() -> None:
|
|
249
409
|
|
250
410
|
logger.info("Deploying load generators")
|
251
411
|
locust_env_variables = [
|
252
|
-
{"name": env_variable, "value":
|
412
|
+
{"name": env_variable, "value": os.environ[env_variable]}
|
253
413
|
for env_variable in os.environ
|
254
414
|
if env_variable.startswith("LOCUST_")
|
255
|
-
and
|
256
|
-
in [
|
415
|
+
and env_variable
|
416
|
+
not in [
|
257
417
|
"LOCUST_LOCUSTFILE",
|
258
418
|
"LOCUST_USERS",
|
259
419
|
"LOCUST_WEB_HOST_DISPLAY_NAME",
|
260
420
|
"LOCUST_SKIP_MONKEY_PATCH",
|
261
421
|
]
|
262
|
-
and os.environ[env_variable]
|
263
422
|
]
|
264
|
-
deploy_endpoint = f"{api_url}/deploy"
|
265
423
|
payload = {
|
266
424
|
"locust_args": [
|
267
425
|
{"name": "LOCUST_USERS", "value": str(options.users)},
|
268
426
|
{"name": "LOCUST_FLAGS", "value": " ".join(locust_options)},
|
269
|
-
{"name": "LOCUSTCLOUD_DEPLOYER_URL", "value": api_url},
|
427
|
+
{"name": "LOCUSTCLOUD_DEPLOYER_URL", "value": session.api_url},
|
270
428
|
{"name": "LOCUSTCLOUD_PROFILE", "value": options.profile},
|
271
429
|
*locust_env_variables,
|
272
430
|
],
|
273
431
|
"locustfile": {"filename": options.locustfile, "data": locustfile_data},
|
274
432
|
"user_count": options.users,
|
275
|
-
"image_tag": options.image_tag,
|
276
433
|
"mock_server": options.mock_server,
|
277
434
|
}
|
435
|
+
|
436
|
+
if options.image_tag is not None:
|
437
|
+
payload["image_tag"] = options.image_tag
|
438
|
+
|
278
439
|
if options.workers is not None:
|
279
440
|
payload["worker_count"] = options.workers
|
441
|
+
|
280
442
|
if options.requirements:
|
281
443
|
payload["requirements"] = {"filename": options.requirements, "data": requirements_data}
|
282
|
-
|
283
|
-
"Authorization": f"Bearer {cognito_client_id_token}",
|
284
|
-
"Content-Type": "application/json",
|
285
|
-
"AWS_ACCESS_KEY_ID": aws_access_key_id,
|
286
|
-
"AWS_SECRET_ACCESS_KEY": aws_secret_access_key,
|
287
|
-
"AWS_SESSION_TOKEN": aws_session_token,
|
288
|
-
"X-Client-Version": __version__,
|
289
|
-
}
|
444
|
+
|
290
445
|
try:
|
291
|
-
|
292
|
-
response = requests.post(deploy_endpoint, json=payload, headers=headers)
|
446
|
+
response = session.post("/deploy", json=payload)
|
293
447
|
except requests.exceptions.RequestException as e:
|
294
448
|
logger.error(f"Failed to deploy the load generators: {e}")
|
295
449
|
sys.exit(1)
|
@@ -304,10 +458,9 @@ def main() -> None:
|
|
304
458
|
f"HTTP {response.status_code}/{response.reason} - Response: {response.text} - URL: {response.request.url}"
|
305
459
|
)
|
306
460
|
sys.exit(1)
|
307
|
-
|
308
|
-
logger.error(f"Credential error: {ce}")
|
309
|
-
sys.exit(1)
|
461
|
+
|
310
462
|
except KeyboardInterrupt:
|
463
|
+
# TODO: This would potentially leave a deployment running, combine with try-catch below?
|
311
464
|
logger.debug("Interrupted by user")
|
312
465
|
sys.exit(0)
|
313
466
|
|
@@ -318,10 +471,10 @@ def main() -> None:
|
|
318
471
|
shutdown_allowed.set()
|
319
472
|
reconnect_aborted = threading.Event()
|
320
473
|
connect_timeout = threading.Timer(2 * 60, reconnect_aborted.set)
|
474
|
+
sio = socketio.Client(handle_sigint=False)
|
321
475
|
|
322
476
|
try:
|
323
477
|
ws_connection_info = urllib.parse.urlparse(log_ws_url)
|
324
|
-
sio = socketio.Client(handle_sigint=False)
|
325
478
|
|
326
479
|
@sio.event
|
327
480
|
def connect():
|
@@ -364,38 +517,23 @@ def main() -> None:
|
|
364
517
|
|
365
518
|
except KeyboardInterrupt:
|
366
519
|
logger.debug("Interrupted by user")
|
367
|
-
delete(
|
520
|
+
delete(session)
|
368
521
|
shutdown_allowed.wait(timeout=90)
|
369
522
|
except Exception as e:
|
370
523
|
logger.exception(e)
|
371
|
-
delete(
|
524
|
+
delete(session)
|
372
525
|
sys.exit(1)
|
373
526
|
else:
|
374
|
-
delete(
|
527
|
+
delete(session)
|
375
528
|
finally:
|
376
529
|
sio.shutdown()
|
377
530
|
|
378
531
|
|
379
|
-
def delete(
|
532
|
+
def delete(session):
|
380
533
|
try:
|
381
534
|
logger.info("Tearing down Locust cloud...")
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
headers = {
|
386
|
-
"AWS_ACCESS_KEY_ID": refreshed_credentials.get("access_key", ""),
|
387
|
-
"AWS_SECRET_ACCESS_KEY": refreshed_credentials.get("secret_key", ""),
|
388
|
-
"Authorization": f"Bearer {refreshed_credentials.get('cognito_client_id_token', '')}",
|
389
|
-
"X-Client-Version": __version__,
|
390
|
-
}
|
391
|
-
|
392
|
-
token = refreshed_credentials.get("token")
|
393
|
-
if token:
|
394
|
-
headers["AWS_SESSION_TOKEN"] = token
|
395
|
-
|
396
|
-
response = requests.delete(
|
397
|
-
f"{api_url}/teardown",
|
398
|
-
headers=headers,
|
535
|
+
response = session.delete(
|
536
|
+
"/teardown",
|
399
537
|
)
|
400
538
|
|
401
539
|
if response.status_code == 200:
|
@@ -407,7 +545,7 @@ def delete(credential_manager):
|
|
407
545
|
except Exception as e:
|
408
546
|
logger.error(f"Could not automatically tear down Locust Cloud: {e.__class__.__name__}:{e}")
|
409
547
|
|
410
|
-
logger.info("Done! ✨")
|
548
|
+
logger.info("Done! ✨") # FIXME: Should probably not say it's done since at this point it could still be running
|
411
549
|
|
412
550
|
|
413
551
|
if __name__ == "__main__":
|
@@ -1,13 +1,12 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: locust-cloud
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.13.0
|
4
4
|
Summary: Locust Cloud
|
5
5
|
Project-URL: Homepage, https://locust.cloud
|
6
6
|
Requires-Python: >=3.11
|
7
7
|
Requires-Dist: boto3==1.34.125
|
8
|
-
Requires-Dist:
|
9
|
-
Requires-Dist:
|
10
|
-
Requires-Dist: psycopg[binary,pool]>=3.2.1
|
8
|
+
Requires-Dist: configargparse==1.7
|
9
|
+
Requires-Dist: platformdirs>=4.3.6
|
11
10
|
Requires-Dist: pyjwt>=2.0
|
12
11
|
Requires-Dist: python-socketio[client]==5.11.4
|
13
12
|
Description-Content-Type: text/markdown
|
@@ -0,0 +1,5 @@
|
|
1
|
+
locust_cloud/cloud.py,sha256=r0lWoP1QiT1Klkrecsr6LyPD_l1GMW2BFGRCRVkLowM,18124
|
2
|
+
locust_cloud-1.13.0.dist-info/METADATA,sha256=STWxyZOw0qE003r-Pf-8RU3KPFK3yjH_NsSo5ffoHh0,496
|
3
|
+
locust_cloud-1.13.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
4
|
+
locust_cloud-1.13.0.dist-info/entry_points.txt,sha256=PGyAb4e3aTsGS3N3VGShDl6VzJaXy7QwsEgsLOC7V00,57
|
5
|
+
locust_cloud-1.13.0.dist-info/RECORD,,
|
locust_cloud/__init__.py
DELETED
@@ -1,133 +0,0 @@
|
|
1
|
-
import importlib.metadata
|
2
|
-
import os
|
3
|
-
import sys
|
4
|
-
|
5
|
-
os.environ["LOCUST_SKIP_MONKEY_PATCH"] = "1"
|
6
|
-
|
7
|
-
from locust_cloud.socket_logging import setup_socket_logging
|
8
|
-
|
9
|
-
if os.environ.get("LOCUST_MODE_MASTER") == "1":
|
10
|
-
major, minor, *rest = os.environ["LOCUSTCLOUD_CLIENT_VERSION"].split(".")
|
11
|
-
|
12
|
-
if int(major) > 1 or int(major) == 1 and int(minor) >= 12:
|
13
|
-
setup_socket_logging()
|
14
|
-
|
15
|
-
__version__ = importlib.metadata.version("locust-cloud")
|
16
|
-
|
17
|
-
import logging
|
18
|
-
|
19
|
-
import configargparse
|
20
|
-
import locust.env
|
21
|
-
import psycopg
|
22
|
-
from locust import events
|
23
|
-
from locust.argument_parser import LocustArgumentParser
|
24
|
-
from locust_cloud.auth import register_auth
|
25
|
-
from locust_cloud.idle_exit import IdleExit
|
26
|
-
from locust_cloud.timescale.exporter import Exporter
|
27
|
-
from locust_cloud.timescale.query import register_query
|
28
|
-
from psycopg.conninfo import make_conninfo
|
29
|
-
from psycopg_pool import ConnectionPool
|
30
|
-
|
31
|
-
logger = logging.getLogger(__name__)
|
32
|
-
|
33
|
-
|
34
|
-
@events.init_command_line_parser.add_listener
|
35
|
-
def add_arguments(parser: LocustArgumentParser):
|
36
|
-
if not (os.environ.get("PGHOST")):
|
37
|
-
parser.add_argument_group(
|
38
|
-
"locust-cloud",
|
39
|
-
"locust-cloud disabled, because PGHOST was not set - this is normal for local runs",
|
40
|
-
)
|
41
|
-
return
|
42
|
-
|
43
|
-
try:
|
44
|
-
REGION = os.environ["AWS_DEFAULT_REGION"]
|
45
|
-
except KeyError:
|
46
|
-
logger.fatal("Missing AWS_DEFAULT_REGION env var")
|
47
|
-
sys.exit(1)
|
48
|
-
|
49
|
-
os.environ["LOCUST_BUILD_PATH"] = os.path.join(os.path.dirname(__file__), "webui/dist")
|
50
|
-
locust_cloud = parser.add_argument_group(
|
51
|
-
"locust-cloud",
|
52
|
-
"Arguments for use with Locust cloud",
|
53
|
-
)
|
54
|
-
# do not set
|
55
|
-
# used for sending the run id from master to workers
|
56
|
-
locust_cloud.add_argument(
|
57
|
-
"--run-id",
|
58
|
-
type=str,
|
59
|
-
env_var="LOCUSTCLOUD_RUN_ID",
|
60
|
-
help=configargparse.SUPPRESS,
|
61
|
-
)
|
62
|
-
locust_cloud.add_argument(
|
63
|
-
"--allow-signup",
|
64
|
-
env_var="LOCUSTCLOUD_ALLOW_SIGNUP",
|
65
|
-
help=configargparse.SUPPRESS,
|
66
|
-
default=False,
|
67
|
-
action="store_true",
|
68
|
-
)
|
69
|
-
locust_cloud.add_argument(
|
70
|
-
"--allow-forgot-password",
|
71
|
-
env_var="LOCUSTCLOUD_FORGOT_PASSWORD",
|
72
|
-
help=configargparse.SUPPRESS,
|
73
|
-
default=False,
|
74
|
-
action="store_true",
|
75
|
-
)
|
76
|
-
locust_cloud.add_argument(
|
77
|
-
"--graph-viewer",
|
78
|
-
env_var="LOCUSTCLOUD_GRAPH_VIEWER",
|
79
|
-
help=configargparse.SUPPRESS,
|
80
|
-
default=False,
|
81
|
-
action="store_true",
|
82
|
-
)
|
83
|
-
locust_cloud.add_argument(
|
84
|
-
"--deployer-url",
|
85
|
-
type=str,
|
86
|
-
env_var="LOCUSTCLOUD_DEPLOYER_URL",
|
87
|
-
help=configargparse.SUPPRESS,
|
88
|
-
default=f"https://api.{REGION}.locust.cloud/1",
|
89
|
-
)
|
90
|
-
locust_cloud.add_argument(
|
91
|
-
"--profile",
|
92
|
-
type=str,
|
93
|
-
env_var="LOCUSTCLOUD_PROFILE",
|
94
|
-
help=configargparse.SUPPRESS,
|
95
|
-
default=None,
|
96
|
-
)
|
97
|
-
|
98
|
-
|
99
|
-
def set_autocommit(conn: psycopg.Connection):
|
100
|
-
conn.autocommit = True
|
101
|
-
|
102
|
-
|
103
|
-
@events.init.add_listener
|
104
|
-
def on_locust_init(environment: locust.env.Environment, **_args):
|
105
|
-
if not (os.environ.get("PGHOST")):
|
106
|
-
return
|
107
|
-
|
108
|
-
conninfo = make_conninfo(
|
109
|
-
sslmode="require",
|
110
|
-
)
|
111
|
-
pool = ConnectionPool(
|
112
|
-
conninfo,
|
113
|
-
min_size=1,
|
114
|
-
max_size=20,
|
115
|
-
configure=set_autocommit,
|
116
|
-
check=ConnectionPool.check_connection,
|
117
|
-
)
|
118
|
-
pool.wait(timeout=10)
|
119
|
-
|
120
|
-
if not environment.parsed_options.graph_viewer:
|
121
|
-
IdleExit(environment)
|
122
|
-
Exporter(environment, pool)
|
123
|
-
|
124
|
-
if environment.web_ui:
|
125
|
-
environment.web_ui.template_args["locustVersion"] = locust.__version__
|
126
|
-
environment.web_ui.template_args["locustCloudVersion"] = __version__
|
127
|
-
environment.web_ui.template_args["webBasePath"] = environment.parsed_options.web_base_path
|
128
|
-
|
129
|
-
if environment.parsed_options.graph_viewer:
|
130
|
-
environment.web_ui.template_args["isGraphViewer"] = True
|
131
|
-
|
132
|
-
register_auth(environment)
|
133
|
-
register_query(environment, pool)
|