locust-cloud 1.12.4__py3-none-any.whl → 1.14.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.
@@ -1,15 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: locust-cloud
3
- Version: 1.12.4
3
+ Version: 1.14.0
4
4
  Summary: Locust Cloud
5
5
  Project-URL: Homepage, https://locust.cloud
6
6
  Requires-Python: >=3.11
7
- Requires-Dist: boto3==1.34.125
8
- Requires-Dist: gevent-websocket==0.10.1
9
- Requires-Dist: locust>=2.32.5.dev10
10
- Requires-Dist: psycopg[binary,pool]>=3.2.1
7
+ Requires-Dist: configargparse==1.7
8
+ Requires-Dist: platformdirs>=4.3.6
11
9
  Requires-Dist: pyjwt>=2.0
12
10
  Requires-Dist: python-socketio[client]==5.11.4
11
+ Requires-Dist: requests==2.32.3
13
12
  Description-Content-Type: text/markdown
14
13
 
15
14
  # Locust Cloud
@@ -0,0 +1,5 @@
1
+ locust_cloud/cloud.py,sha256=H_bATzG3Hyw8BRw0HmgLKTl5kX4y9KQuKCVTtzSAdjA,25466
2
+ locust_cloud-1.14.0.dist-info/METADATA,sha256=mZOGdXwLoyzOO3TeABH2eKsnw6RTz2d5Av0POdyvaIc,497
3
+ locust_cloud-1.14.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
4
+ locust_cloud-1.14.0.dist-info/entry_points.txt,sha256=PGyAb4e3aTsGS3N3VGShDl6VzJaXy7QwsEgsLOC7V00,57
5
+ locust_cloud-1.14.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)
locust_cloud/auth.py DELETED
@@ -1,443 +0,0 @@
1
- import logging
2
- import os
3
- from datetime import UTC, datetime, timedelta
4
- from typing import Any, TypedDict, cast
5
-
6
- import locust.env
7
- import requests
8
- import werkzeug
9
- from flask import Blueprint, redirect, request, session, url_for
10
- from flask_login import UserMixin, login_required, login_user, logout_user
11
- from locust.html import render_template_from
12
- from locust_cloud import __version__
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
-
17
- class Credentials(TypedDict):
18
- user_sub_id: str
19
- refresh_token: str
20
-
21
-
22
- class AuthUser(UserMixin):
23
- def __init__(self, user_sub_id: str):
24
- self.user_sub_id = user_sub_id
25
-
26
- def get_id(self):
27
- return self.user_sub_id
28
-
29
-
30
- def set_credentials(username: str, credentials: Credentials, response: werkzeug.wrappers.response.Response):
31
- if not credentials.get("user_sub_id"):
32
- return response
33
-
34
- user_sub_id = credentials["user_sub_id"]
35
- refresh_token = credentials["refresh_token"]
36
-
37
- response.set_cookie("username", username, expires=datetime.now(tz=UTC) + timedelta(days=365))
38
- response.set_cookie("user_token", refresh_token, expires=datetime.now(tz=UTC) + timedelta(days=365))
39
- response.set_cookie("user_sub_id", user_sub_id, expires=datetime.now(tz=UTC) + timedelta(days=365))
40
-
41
- return response
42
-
43
-
44
- def register_auth(environment: locust.env.Environment):
45
- environment.web_ui.app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "") + os.getenv("CUSTOMER_ID", "")
46
- environment.web_ui.app.debug = False
47
-
48
- web_base_path = environment.parsed_options.web_base_path
49
- auth_blueprint = Blueprint("locust_cloud_auth", __name__, url_prefix=web_base_path)
50
-
51
- def load_user(user_sub_id: str):
52
- username = request.cookies.get("username")
53
- refresh_token = request.cookies.get("user_token")
54
-
55
- if refresh_token:
56
- environment.web_ui.template_args["username"] = username
57
- return AuthUser(user_sub_id)
58
-
59
- return None
60
-
61
- environment.web_ui.login_manager.user_loader(load_user)
62
- environment.web_ui.auth_args = cast(
63
- Any,
64
- {
65
- "username_password_callback": f"{web_base_path}/authenticate",
66
- },
67
- )
68
-
69
- environment.web_ui.auth_args["auth_providers"] = []
70
- if environment.parsed_options.allow_signup:
71
- environment.web_ui.auth_args["auth_providers"].append(
72
- {"label": "Sign Up", "callback_url": f"{web_base_path}/signup"}
73
- )
74
- if environment.parsed_options.allow_forgot_password:
75
- environment.web_ui.auth_args["auth_providers"].append(
76
- {"label": "Forgot Password?", "callback_url": f"{web_base_path}/forgot-password"}
77
- )
78
-
79
- @auth_blueprint.route("/authenticate", methods=["POST"])
80
- def login_submit():
81
- username = request.form.get("username", "")
82
- password = request.form.get("password")
83
-
84
- try:
85
- auth_response = requests.post(
86
- f"{environment.parsed_options.deployer_url}/auth/login",
87
- json={"username": username, "password": password},
88
- headers={"X-Client-Version": __version__},
89
- )
90
-
91
- auth_response.raise_for_status()
92
-
93
- credentials = auth_response.json()
94
-
95
- if credentials.get("challenge_session"):
96
- session["challenge_session"] = credentials.get("challenge_session")
97
- session["username"] = username
98
-
99
- session["auth_error"] = ""
100
-
101
- return redirect(url_for("locust_cloud_auth.password_reset"))
102
- if os.getenv("CUSTOMER_ID", "") and credentials.get("customer_id") != os.getenv("CUSTOMER_ID", ""):
103
- session["auth_error"] = "Invalid login for this deployment"
104
- return redirect(url_for("locust.login"))
105
-
106
- if not credentials.get("user_sub_id"):
107
- session["auth_error"] = "Unknown error during authentication, check logs and/or contact support"
108
- return redirect(url_for("locust.login"))
109
-
110
- response = redirect(url_for("locust.index"))
111
- response = set_credentials(username, credentials, response)
112
- login_user(AuthUser(credentials["user_sub_id"]))
113
-
114
- return response
115
- except requests.exceptions.HTTPError as e:
116
- if e.response.status_code == 401:
117
- session["auth_error"] = "Invalid username or password"
118
- else:
119
- logger.error(f"Unknown response from auth: {e.response.status_code} {e.response.text}")
120
-
121
- session["auth_error"] = "Unknown error during authentication, check logs and/or contact support"
122
-
123
- return redirect(url_for("locust.login"))
124
-
125
- @auth_blueprint.route("/signup")
126
- def signup():
127
- if not environment.parsed_options.allow_signup:
128
- return redirect(url_for("locust.login"))
129
-
130
- if session.get("username"):
131
- sign_up_args = {
132
- "custom_form": {
133
- "inputs": [
134
- {
135
- "label": "Confirmation Code",
136
- "name": "confirmation_code",
137
- "is_required": True,
138
- },
139
- ],
140
- "callback_url": f"{web_base_path}/confirm-signup",
141
- "submit_button_text": "Confirm Email",
142
- },
143
- }
144
- else:
145
- sign_up_args = {
146
- "custom_form": {
147
- "inputs": [
148
- {
149
- "label": "Username",
150
- "name": "username",
151
- "is_required": True,
152
- "type": "email",
153
- },
154
- {
155
- "label": "Full Name",
156
- "name": "customer_name",
157
- "is_required": True,
158
- },
159
- {
160
- "label": "Password",
161
- "name": "password",
162
- "is_secret": True,
163
- "is_required": True,
164
- },
165
- {
166
- "label": "Access Code",
167
- "name": "access_code",
168
- "is_required": True,
169
- },
170
- {
171
- "label": "I consent to:\n\n1. Only test your own website/service or our example target\n\n2. Only use locust-cloud for its intended purpose: to load test other sites/services.\n\n3. Not attempt to circumvent your account limitations (e.g. max user count or max request count)\n\n4. Not use personal data (real names, addresses etc) in your tests.",
172
- "name": "consent",
173
- "default_value": False,
174
- "is_required": True,
175
- },
176
- ],
177
- "callback_url": f"{web_base_path}/create-account",
178
- "submit_button_text": "Sign Up",
179
- },
180
- }
181
-
182
- if session.get("auth_info"):
183
- sign_up_args["info"] = session["auth_info"]
184
- if session.get("auth_sign_up_error"):
185
- sign_up_args["error"] = session["auth_sign_up_error"]
186
-
187
- return render_template_from(
188
- "auth.html",
189
- auth_args=sign_up_args,
190
- )
191
-
192
- @auth_blueprint.route("/create-account", methods=["POST"])
193
- def create_account():
194
- if not environment.parsed_options.allow_signup:
195
- return redirect(url_for("locust.login"))
196
-
197
- session["auth_sign_up_error"] = ""
198
- session["auth_info"] = ""
199
-
200
- username = request.form.get("username", "")
201
- customer_name = request.form.get("customer_name", "")
202
- password = request.form.get("password")
203
- access_code = request.form.get("access_code")
204
-
205
- try:
206
- auth_response = requests.post(
207
- f"{environment.parsed_options.deployer_url}/auth/signup",
208
- json={"username": username, "password": password, "access_code": access_code},
209
- )
210
-
211
- auth_response.raise_for_status()
212
-
213
- session["user_sub_id"] = auth_response.json().get("user_sub_id")
214
- session["username"] = username
215
- session["customer_name"] = customer_name
216
- session["auth_info"] = (
217
- "Please check your email and enter the confirmation code. If you didn't get a code after one minute, you can [request a new one](/resend-code)"
218
- )
219
-
220
- return redirect(url_for("locust_cloud_auth.signup"))
221
- except requests.exceptions.HTTPError as e:
222
- message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
223
- session["auth_info"] = ""
224
- session["auth_sign_up_error"] = message
225
-
226
- return redirect(url_for("locust_cloud_auth.signup"))
227
-
228
- @auth_blueprint.route("/resend-code")
229
- def resend_code():
230
- if not session.get("username"):
231
- session["auth_sign_up_error"] = "An unexpected error occured. Please try again."
232
- return redirect(url_for("locust_cloud_auth.signup"))
233
-
234
- try:
235
- auth_response = requests.post(
236
- f"{environment.parsed_options.deployer_url}/auth/resend-confirmation",
237
- json={"username": session.get("username")},
238
- )
239
-
240
- auth_response.raise_for_status()
241
-
242
- session["auth_sign_up_error"] = ""
243
- session["auth_info"] = "Confirmation code sent, please check your email."
244
-
245
- return redirect(url_for("locust_cloud_auth.signup"))
246
- except requests.exceptions.HTTPError as e:
247
- message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
248
- session["auth_info"] = ""
249
- session["auth_sign_up_error"] = message
250
-
251
- return redirect(url_for("locust_cloud_auth.signup"))
252
-
253
- @auth_blueprint.route("/confirm-signup", methods=["POST"])
254
- def confirm_signup():
255
- if not environment.parsed_options.allow_signup:
256
- return redirect(url_for("locust.login"))
257
- if not session.get("user_sub_id"):
258
- session["auth_sign_up_error"] = "An unexpected error occured. Please try again."
259
- return redirect(url_for("locust_cloud_auth.signup"))
260
-
261
- session["auth_sign_up_error"] = ""
262
- confirmation_code = request.form.get("confirmation_code")
263
-
264
- try:
265
- auth_response = requests.post(
266
- f"{environment.parsed_options.deployer_url}/auth/confirm-signup",
267
- json={
268
- "username": session.get("username"),
269
- "customer_name": session.get("customer_name"),
270
- "user_sub_id": session["user_sub_id"],
271
- "confirmation_code": confirmation_code,
272
- },
273
- )
274
-
275
- auth_response.raise_for_status()
276
-
277
- session["username"] = None
278
- session["auth_info"] = "Account created successfully!"
279
- session["auth_sign_up_error"] = ""
280
-
281
- return redirect("https://docs.locust.cloud/")
282
- except requests.exceptions.HTTPError as e:
283
- message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
284
- session["auth_info"] = ""
285
- session["auth_sign_up_error"] = message
286
-
287
- return redirect(url_for("locust_cloud_auth.signup"))
288
-
289
- @auth_blueprint.route("/forgot-password")
290
- def forgot_password():
291
- if not environment.parsed_options.allow_forgot_password:
292
- return redirect(url_for("locust.login"))
293
-
294
- forgot_password_args = {
295
- "custom_form": {
296
- "inputs": [
297
- {
298
- "label": "Username",
299
- "name": "username",
300
- "is_required": True,
301
- "type": "email",
302
- },
303
- ],
304
- "callback_url": f"{web_base_path}/send-forgot-password",
305
- "submit_button_text": "Reset Password",
306
- },
307
- "info": "Enter your email and we will send a code to reset your password",
308
- }
309
-
310
- if session.get("auth_error"):
311
- forgot_password_args["error"] = session["auth_error"]
312
-
313
- return render_template_from("auth.html", auth_args=forgot_password_args)
314
-
315
- @auth_blueprint.route("/send-forgot-password", methods=["POST"])
316
- def send_forgot_password():
317
- if not environment.parsed_options.allow_forgot_password:
318
- return redirect(url_for("locust.login"))
319
-
320
- try:
321
- username = request.form.get("username", "")
322
-
323
- auth_response = requests.post(
324
- f"{environment.parsed_options.deployer_url}/auth/forgot-password",
325
- json={"username": username},
326
- )
327
-
328
- auth_response.raise_for_status()
329
-
330
- session["username"] = username
331
-
332
- return redirect(url_for("locust_cloud_auth.password_reset"))
333
- except requests.exceptions.HTTPError as e:
334
- message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
335
- session["auth_info"] = ""
336
- session["auth_error"] = message
337
-
338
- return redirect(url_for("locust_cloud_auth.forgot_password"))
339
-
340
- @auth_blueprint.route("/password-reset")
341
- def password_reset():
342
- if not environment.parsed_options.allow_forgot_password and not session.get("challenge_session"):
343
- return redirect(url_for("locust.login"))
344
-
345
- if session.get("challenge_session"):
346
- reset_password_args = {
347
- "custom_form": {
348
- "inputs": [
349
- {
350
- "label": "New Password",
351
- "name": "new_password",
352
- "is_required": True,
353
- "is_secret": True,
354
- },
355
- ],
356
- "callback_url": f"{web_base_path}/confirm-reset-password",
357
- "submit_button_text": "Set Password",
358
- },
359
- "info": "You must set a new password",
360
- }
361
- else:
362
- reset_password_args = {
363
- "custom_form": {
364
- "inputs": [
365
- {
366
- "label": "Confirmation Code",
367
- "name": "confirmation_code",
368
- "is_required": True,
369
- },
370
- {
371
- "label": "New Password",
372
- "name": "new_password",
373
- "is_required": True,
374
- "is_secret": True,
375
- },
376
- ],
377
- "callback_url": f"{web_base_path}/confirm-reset-password",
378
- "submit_button_text": "Reset Password",
379
- },
380
- "info": "Enter your the confirmation code that was sent to your email",
381
- }
382
-
383
- if session.get("auth_error"):
384
- reset_password_args["error"] = session["auth_error"]
385
-
386
- return render_template_from("auth.html", auth_args=reset_password_args)
387
-
388
- @auth_blueprint.route("/confirm-reset-password", methods=["POST"])
389
- def confirm_reset_password():
390
- if not environment.parsed_options.allow_forgot_password and not session.get("challenge_session"):
391
- return redirect(url_for("locust.login"))
392
-
393
- try:
394
- username = session["username"]
395
- confirmation_code = request.form.get("confirmation_code")
396
- new_password = request.form.get("new_password")
397
-
398
- auth_response = requests.post(
399
- f"{environment.parsed_options.deployer_url}/auth/password-reset",
400
- json={
401
- "username": username,
402
- "confirmation_code": confirmation_code,
403
- "new_password": new_password,
404
- "challenge_session": session.get("challenge_session"),
405
- },
406
- )
407
-
408
- auth_response.raise_for_status()
409
-
410
- session["username"] = ""
411
- session["auth_error"] = ""
412
-
413
- if session.get("challenge_session"):
414
- session["challenge_session"] = ""
415
-
416
- return redirect(url_for("locust_cloud_auth.password_reset_success"))
417
-
418
- session["auth_info"] = "Password reset successfully! Please login"
419
-
420
- return redirect(url_for("locust.login"))
421
- except requests.exceptions.HTTPError as e:
422
- message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
423
- session["auth_info"] = ""
424
- session["auth_error"] = message
425
-
426
- return redirect(url_for("locust_cloud_auth.password_reset"))
427
-
428
- @auth_blueprint.route("/password-reset-success")
429
- def password_reset_success():
430
- return render_template_from(
431
- "auth.html",
432
- auth_args={
433
- "info": "Password successfully set! Please review the [documentation](https://docs.locust.cloud/) and start your first testrun! If you have already ran some tests, you may also [login](/login)"
434
- },
435
- )
436
-
437
- @auth_blueprint.route("/logout", methods=["POST"])
438
- @login_required
439
- def logout():
440
- logout_user()
441
- return redirect(url_for("locust.login"))
442
-
443
- environment.web_ui.app.register_blueprint(auth_blueprint)