locust-cloud 1.14.0__py3-none-any.whl → 1.14.2__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 CHANGED
@@ -1,6 +1,7 @@
1
1
  import base64
2
2
  import gzip
3
3
  import importlib.metadata
4
+ import io
4
5
  import json
5
6
  import logging
6
7
  import os
@@ -11,10 +12,12 @@ import time
11
12
  import tomllib
12
13
  import urllib.parse
13
14
  import webbrowser
14
- from argparse import Namespace
15
+ from argparse import ArgumentTypeError, Namespace
15
16
  from collections import OrderedDict
17
+ from collections.abc import Generator
16
18
  from dataclasses import dataclass
17
19
  from typing import IO, Any
20
+ from zipfile import ZipFile
18
21
 
19
22
  import configargparse
20
23
  import jwt
@@ -24,6 +27,7 @@ import socketio
24
27
  import socketio.exceptions
25
28
 
26
29
  __version__ = importlib.metadata.version("locust-cloud")
30
+ CWD = pathlib.Path.cwd()
27
31
 
28
32
 
29
33
  class LocustTomlConfigParser(configargparse.TomlConfigParser):
@@ -48,6 +52,51 @@ class LocustTomlConfigParser(configargparse.TomlConfigParser):
48
52
  return result
49
53
 
50
54
 
55
+ def valid_extra_files_path(file_path: str) -> pathlib.Path:
56
+ p = pathlib.Path(file_path).resolve()
57
+
58
+ if not CWD in p.parents:
59
+ raise ArgumentTypeError(f"Can only reference files under current working directory: {CWD}")
60
+ if not p.exists():
61
+ raise ArgumentTypeError(f"File not found: {file_path}")
62
+ return p
63
+
64
+
65
+ def transfer_encode(file_name: str, stream: IO[bytes]) -> dict[str, str]:
66
+ return {
67
+ "filename": file_name,
68
+ "data": base64.b64encode(gzip.compress(stream.read())).decode(),
69
+ }
70
+
71
+
72
+ def transfer_encoded_file(file_path: str) -> dict[str, str]:
73
+ try:
74
+ with open(file_path, "rb") as f:
75
+ return transfer_encode(file_path, f)
76
+ except FileNotFoundError:
77
+ raise ArgumentTypeError(f"File not found: {file_path}")
78
+
79
+
80
+ def transfer_encoded_extra_files(paths: list[pathlib.Path]) -> dict[str, str]:
81
+ def expanded(paths: list[pathlib.Path]) -> Generator[pathlib.Path, None, None]:
82
+ for path in paths:
83
+ if path.is_dir():
84
+ for root, _, file_names in os.walk(path):
85
+ for file_name in file_names:
86
+ yield pathlib.Path(root) / file_name
87
+ else:
88
+ yield path
89
+
90
+ buffer = io.BytesIO()
91
+
92
+ with ZipFile(buffer, "w") as zf:
93
+ for path in set(expanded(paths)):
94
+ zf.write(path.relative_to(CWD))
95
+
96
+ buffer.seek(0)
97
+ return transfer_encode("extra-files.zip", buffer)
98
+
99
+
51
100
  parser = configargparse.ArgumentParser(
52
101
  default_config_files=[
53
102
  "~/.locust.conf",
@@ -93,6 +142,7 @@ parser.add_argument(
93
142
  default="locustfile.py",
94
143
  help="The Python file that contains your test. Defaults to 'locustfile.py'.",
95
144
  env_var="LOCUST_LOCUSTFILE",
145
+ type=transfer_encoded_file,
96
146
  )
97
147
  parser.add_argument(
98
148
  "-u",
@@ -112,14 +162,14 @@ advanced.add_argument(
112
162
  )
113
163
  advanced.add_argument(
114
164
  "--requirements",
115
- type=str,
165
+ type=transfer_encoded_file,
116
166
  help="Optional requirements.txt file that contains your external libraries.",
117
167
  )
118
168
  advanced.add_argument(
119
169
  "--login",
120
170
  action="store_true",
121
171
  default=False,
122
- help=configargparse.SUPPRESS,
172
+ 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 expires you will be prompted to perform another login.",
123
173
  )
124
174
  advanced.add_argument(
125
175
  "--non-interactive",
@@ -155,11 +205,15 @@ parser.add_argument(
155
205
  type=str,
156
206
  help="Set a profile to group the testruns together",
157
207
  )
208
+ parser.add_argument(
209
+ "--extra-files",
210
+ nargs="*",
211
+ type=valid_extra_files_path,
212
+ help="A list of extra files or directories to upload. Space-separated, e.g. --extra-files testdata.csv *.py my-directory/",
213
+ )
158
214
 
159
- options, locust_options = parser.parse_known_args()
160
-
161
- options: Namespace
162
- locust_options: list
215
+ parsed_args: tuple[Namespace, list[str]] = parser.parse_known_args()
216
+ options, locust_options = parsed_args
163
217
 
164
218
  logging.basicConfig(
165
219
  format="[LOCUST-CLOUD] %(levelname)s: %(message)s",
@@ -281,7 +335,7 @@ class ApiSession(requests.Session):
281
335
 
282
336
  if not all([username, password, region]):
283
337
  print(
284
- "Running with --non-interaction requires that LOCUSTCLOUD_USERNAME, LOCUSTCLOUD_PASSWORD and LOCUSTCLOUD_REGION environment variables are set."
338
+ "Running with --non-interactive requires that LOCUSTCLOUD_USERNAME, LOCUSTCLOUD_PASSWORD and LOCUSTCLOUD_REGION environment variables are set."
285
339
  )
286
340
  sys.exit(1)
287
341
 
@@ -348,8 +402,9 @@ class ApiSession(requests.Session):
348
402
  sys.exit(1)
349
403
 
350
404
  # TODO: Technically the /login endpoint can return a challenge for you
351
- # to change your password. Don't know how we should handle that
352
- # in the cli.
405
+ # to change your password.
406
+ # Now that we have a web based login flow we should force them to
407
+ # do a locust-cloud --login if we get that.
353
408
 
354
409
  id_token = response.json()["cognito_client_id_token"]
355
410
  decoded = jwt.decode(id_token, options={"verify_signature": False})
@@ -587,23 +642,6 @@ def main() -> None:
587
642
  sys.exit()
588
643
 
589
644
  try:
590
- try:
591
- with open(options.locustfile, "rb") as f:
592
- locustfile_data = base64.b64encode(gzip.compress(f.read())).decode()
593
- except FileNotFoundError:
594
- logger.error(f"File not found: {options.locustfile}")
595
- sys.exit(1)
596
-
597
- requirements_data = None
598
-
599
- if options.requirements:
600
- try:
601
- with open(options.requirements, "rb") as f:
602
- requirements_data = base64.b64encode(gzip.compress(f.read())).decode()
603
- except FileNotFoundError:
604
- logger.error(f"File not found: {options.requirements}")
605
- sys.exit(1)
606
-
607
645
  logger.info("Deploying load generators")
608
646
  locust_env_variables = [
609
647
  {"name": env_variable, "value": os.environ[env_variable]}
@@ -625,7 +663,7 @@ def main() -> None:
625
663
  {"name": "LOCUSTCLOUD_PROFILE", "value": options.profile},
626
664
  *locust_env_variables,
627
665
  ],
628
- "locustfile": {"filename": options.locustfile, "data": locustfile_data},
666
+ "locustfile": options.locustfile,
629
667
  "user_count": options.users,
630
668
  "mock_server": options.mock_server,
631
669
  }
@@ -637,7 +675,10 @@ def main() -> None:
637
675
  payload["worker_count"] = options.workers
638
676
 
639
677
  if options.requirements:
640
- payload["requirements"] = {"filename": options.requirements, "data": requirements_data}
678
+ payload["requirements"] = options.requirements
679
+
680
+ if options.extra_files:
681
+ payload["extra_files"] = transfer_encoded_extra_files(options.extra_files)
641
682
 
642
683
  try:
643
684
  response = session.post("/deploy", json=payload)
@@ -658,7 +699,7 @@ def main() -> None:
658
699
  session_id = response.json()["session_id"]
659
700
  logger.debug(f"Session ID is {session_id}")
660
701
 
661
- logger.info("Waiting for pods to be ready...")
702
+ logger.info("Waiting for load generators to be ready...")
662
703
  websocket.connect(
663
704
  log_ws_url,
664
705
  auth=session_id,
@@ -708,8 +749,6 @@ def delete(session):
708
749
  except Exception as e:
709
750
  logger.error(f"Could not automatically tear down Locust Cloud: {e.__class__.__name__}:{e}")
710
751
 
711
- logger.info("Done! ✨") # FIXME: Should probably not say it's done since at this point it could still be running
712
-
713
752
 
714
753
  if __name__ == "__main__":
715
754
  main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: locust-cloud
3
- Version: 1.14.0
3
+ Version: 1.14.2
4
4
  Summary: Locust Cloud
5
5
  Project-URL: Homepage, https://locust.cloud
6
6
  Requires-Python: >=3.11
@@ -0,0 +1,5 @@
1
+ locust_cloud/cloud.py,sha256=ro6oWsd6SEjBHDUWH7vrnvo0wY1Ljz8zeiJwhlaIF-g,26873
2
+ locust_cloud-1.14.2.dist-info/METADATA,sha256=it51qVDFVV6aTB0b3bAuIvF9xfIVdx6Wk6UB31PLzhs,497
3
+ locust_cloud-1.14.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
4
+ locust_cloud-1.14.2.dist-info/entry_points.txt,sha256=PGyAb4e3aTsGS3N3VGShDl6VzJaXy7QwsEgsLOC7V00,57
5
+ locust_cloud-1.14.2.dist-info/RECORD,,
@@ -1,5 +0,0 @@
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,,