locust-cloud 1.14.5__py3-none-any.whl → 1.15.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,11 +1,14 @@
1
1
  import logging
2
2
  import os
3
3
  import sys
4
+ import webbrowser
5
+ from threading import Thread
4
6
 
5
7
  import requests
6
8
  from locust_cloud.apisession import ApiSession
7
9
  from locust_cloud.args import parse_known_args
8
10
  from locust_cloud.common import __version__
11
+ from locust_cloud.input_events import input_listener
9
12
  from locust_cloud.web_login import web_login
10
13
  from locust_cloud.websocket import SessionMismatchError, Websocket, WebsocketTimeout
11
14
 
@@ -13,11 +16,13 @@ logger = logging.getLogger(__name__)
13
16
 
14
17
 
15
18
  def configure_logging(loglevel: str) -> None:
16
- logging.basicConfig(
17
- format="[LOCUST-CLOUD] %(levelname)s: %(message)s",
18
- level=loglevel,
19
+ format = (
20
+ "[%(asctime)s] %(levelname)s: %(message)s"
21
+ if loglevel == "INFO"
22
+ else "[%(asctime)s] %(levelname)s/%(module)s: %(message)s"
19
23
  )
20
- # Restore log level for other libs. Yes, this can be done more nicely
24
+ logging.basicConfig(format=format, level=loglevel)
25
+ # Restore log level for other libs. Yes, this can probably be done more nicely
21
26
  logging.getLogger("requests").setLevel(logging.INFO)
22
27
  logging.getLogger("urllib3").setLevel(logging.INFO)
23
28
 
@@ -88,22 +93,30 @@ def main() -> None:
88
93
 
89
94
  try:
90
95
  response = session.post("/deploy", json=payload)
96
+ js = response.json()
91
97
  except requests.exceptions.RequestException as e:
92
98
  logger.error(f"Failed to deploy the load generators: {e}")
93
99
  sys.exit(1)
94
100
 
95
101
  if response.status_code != 200:
96
102
  try:
97
- logger.error(f"{response.json()['Message']} (HTTP {response.status_code}/{response.reason})")
103
+ logger.error(f"{js['Message']} (HTTP {response.status_code}/{response.reason})")
98
104
  except Exception:
99
105
  logger.error(
100
106
  f"HTTP {response.status_code}/{response.reason} - Response: {response.text} - URL: {response.request.url}"
101
107
  )
102
108
  sys.exit(1)
103
109
 
104
- log_ws_url = response.json()["log_ws_url"]
105
- session_id = response.json()["session_id"]
106
- logger.debug(f"Session ID is {session_id}")
110
+ log_ws_url = js["log_ws_url"]
111
+ session_id = js["session_id"]
112
+ webui_url = log_ws_url.replace("/socket-logs", "")
113
+
114
+ def open_ui():
115
+ webbrowser.open_new_tab(webui_url)
116
+
117
+ Thread(target=input_listener({"\r": open_ui, "\n": open_ui}), daemon=True).start()
118
+
119
+ # logger.debug(f"Session ID is {session_id}")
107
120
 
108
121
  logger.info("Waiting for load generators to be ready...")
109
122
  websocket.connect(
@@ -147,7 +160,7 @@ def delete(session):
147
160
  )
148
161
 
149
162
  if response.status_code == 200:
150
- logger.debug(response.json()["message"])
163
+ logger.debug(f"Response message from teardown: {response.json()['message']}")
151
164
  else:
152
165
  logger.info(
153
166
  f"Could not automatically tear down Locust Cloud: HTTP {response.status_code}/{response.reason} - Response: {response.text} - URL: {response.request.url}"
@@ -0,0 +1,120 @@
1
+ # mostly copied from locust core
2
+ from __future__ import annotations
3
+
4
+ import collections
5
+ import logging
6
+ import os
7
+ import sys
8
+ from collections.abc import Callable
9
+
10
+ import gevent
11
+
12
+ if os.name == "nt":
13
+ import pywintypes
14
+ from win32api import STD_INPUT_HANDLE
15
+ from win32console import (
16
+ ENABLE_ECHO_INPUT,
17
+ ENABLE_LINE_INPUT,
18
+ ENABLE_PROCESSED_INPUT,
19
+ KEY_EVENT,
20
+ GetStdHandle,
21
+ )
22
+ else:
23
+ import select
24
+ import termios
25
+ import tty
26
+
27
+
28
+ class InitError(Exception):
29
+ pass
30
+
31
+
32
+ class UnixKeyPoller:
33
+ def __init__(self):
34
+ if sys.stdin.isatty():
35
+ self.stdin = sys.stdin.fileno()
36
+ self.tattr = termios.tcgetattr(self.stdin)
37
+ tty.setcbreak(self.stdin, termios.TCSANOW)
38
+ else:
39
+ raise InitError("Terminal was not a tty. Keyboard input disabled")
40
+
41
+ def cleanup(self):
42
+ termios.tcsetattr(self.stdin, termios.TCSANOW, self.tattr)
43
+
44
+ def poll(_self):
45
+ dr, dw, de = select.select([sys.stdin], [], [], 0)
46
+ if not dr == []:
47
+ return sys.stdin.read(1)
48
+ return None
49
+
50
+
51
+ class WindowsKeyPoller:
52
+ def __init__(self):
53
+ if sys.stdin.isatty():
54
+ try:
55
+ self.read_handle = GetStdHandle(STD_INPUT_HANDLE) # type: ignore
56
+ self.read_handle.SetConsoleMode(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT) # type: ignore
57
+ self.cur_event_length = 0
58
+ self.cur_keys_length = 0
59
+ self.captured_chars = collections.deque()
60
+ except pywintypes.error: # type: ignore
61
+ raise InitError("Terminal says its a tty but we couldn't enable line input. Keyboard input disabled.")
62
+ else:
63
+ raise InitError("Terminal was not a tty. Keyboard input disabled")
64
+
65
+ def cleanup(self):
66
+ pass
67
+
68
+ def poll(self):
69
+ if self.captured_chars:
70
+ return self.captured_chars.popleft()
71
+
72
+ events_peek = self.read_handle.PeekConsoleInput(10000)
73
+
74
+ if not events_peek:
75
+ return None
76
+
77
+ if not len(events_peek) == self.cur_event_length:
78
+ for cur_event in events_peek[self.cur_event_length :]:
79
+ if cur_event.EventType == KEY_EVENT: # type: ignore
80
+ if ord(cur_event.Char) and cur_event.KeyDown:
81
+ cur_char = str(cur_event.Char)
82
+ self.captured_chars.append(cur_char)
83
+
84
+ self.cur_event_length = len(events_peek)
85
+
86
+ if self.captured_chars:
87
+ return self.captured_chars.popleft()
88
+ else:
89
+ return None
90
+
91
+
92
+ def get_poller():
93
+ if os.name == "nt":
94
+ return WindowsKeyPoller()
95
+ else:
96
+ return UnixKeyPoller()
97
+
98
+
99
+ def input_listener(key_to_func_map: dict[str, Callable]):
100
+ def input_listener_func():
101
+ try:
102
+ poller = get_poller()
103
+ except InitError as e:
104
+ logging.debug(e)
105
+ return
106
+
107
+ try:
108
+ while True:
109
+ if input := poller.poll():
110
+ for key in key_to_func_map:
111
+ if input == key:
112
+ key_to_func_map[key]()
113
+ else:
114
+ gevent.sleep(0.2)
115
+ except Exception as e:
116
+ logging.warning(f"Exception in keyboard input poller: {e}")
117
+ finally:
118
+ poller.cleanup()
119
+
120
+ return input_listener_func
locust_cloud/websocket.py CHANGED
@@ -64,7 +64,7 @@ class Websocket:
64
64
 
65
65
  self.__connect_timeout_timer = threading.Timer(timeout, _timeout)
66
66
  self.__connect_timeout_timer.daemon = True
67
- logger.debug(f"Setting websocket connection timeout to {timeout} seconds")
67
+ # logger.debug(f"Setting websocket connection timeout to {timeout} seconds")
68
68
  self.__connect_timeout_timer.start()
69
69
 
70
70
  def connect(self, url, *, auth) -> None:
@@ -110,7 +110,8 @@ class Websocket:
110
110
  __on_connect_error method), raise it.
111
111
  """
112
112
  timeout = self.wait_timeout if timeout else None
113
- logger.debug(f"Waiting for shutdown for {str(timeout)+'s' if timeout else 'ever'}")
113
+ if timeout: # not worth even debug logging if we dont have a timeout
114
+ logger.debug(f"Waiting for shutdown for {str(timeout) + 's' if timeout else 'ever'}")
114
115
  res = self.__shutdown_allowed.wait(timeout)
115
116
  if self.exception:
116
117
  raise self.exception
@@ -174,6 +175,7 @@ class Websocket:
174
175
 
175
176
  if shutdown:
176
177
  logger.debug("Got shutdown from locust master")
178
+ self.__connect_timeout_timer.cancel() # I dont know exactly why/if this is necessary but we had an issue in status-checker once
177
179
  if shutdown_message:
178
180
  print(shutdown_message)
179
181
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: locust-cloud
3
- Version: 1.14.5
3
+ Version: 1.15.2
4
4
  Summary: Locust Cloud
5
5
  Project-URL: Homepage, https://locust.cloud
6
6
  Requires-Python: >=3.11
@@ -0,0 +1,11 @@
1
+ locust_cloud/apisession.py,sha256=AoU0FGQbyH2qbaTmdyoIMBd_lwZimLtihK0gnpAV6c0,4091
2
+ locust_cloud/args.py,sha256=1SaMQ16f_3vaNqnAbjFiBL-6ietp1-qfV2LUjBk6b8k,7100
3
+ locust_cloud/cloud.py,sha256=OaUE0bnlRGgLTKhdDCMnonaT-h2pop_cAfcOFOLcwng,5581
4
+ locust_cloud/common.py,sha256=cFrDVKpi9OEmH6giOuj9HoIUFSBArixNtNHzZIgDvPE,992
5
+ locust_cloud/input_events.py,sha256=MyxccgboHByICuK6VpQCCJhZQqTZAacNmkSpw-gxBEw,3420
6
+ locust_cloud/web_login.py,sha256=1j2AQoEM6XVSDtE1q0Ryrs4jFEx07r9IQfZCoFAQXJg,2400
7
+ locust_cloud/websocket.py,sha256=9Q7nTFuAwVhgW74DlJNcHTZXOQ1drsXi8hX9ciZhWlQ,8998
8
+ locust_cloud-1.15.2.dist-info/METADATA,sha256=a79gziKM22Yf8cyno1E0GHw2PUPhyj6vDYxA7RoJqkE,497
9
+ locust_cloud-1.15.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ locust_cloud-1.15.2.dist-info/entry_points.txt,sha256=PGyAb4e3aTsGS3N3VGShDl6VzJaXy7QwsEgsLOC7V00,57
11
+ locust_cloud-1.15.2.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- locust_cloud/apisession.py,sha256=AoU0FGQbyH2qbaTmdyoIMBd_lwZimLtihK0gnpAV6c0,4091
2
- locust_cloud/args.py,sha256=1SaMQ16f_3vaNqnAbjFiBL-6ietp1-qfV2LUjBk6b8k,7100
3
- locust_cloud/cloud.py,sha256=Piq6J9pS6Tom1aoYwNmmt4Q9LrLFPKFZC6EqgOen3OE,5107
4
- locust_cloud/common.py,sha256=cFrDVKpi9OEmH6giOuj9HoIUFSBArixNtNHzZIgDvPE,992
5
- locust_cloud/web_login.py,sha256=1j2AQoEM6XVSDtE1q0Ryrs4jFEx07r9IQfZCoFAQXJg,2400
6
- locust_cloud/websocket.py,sha256=lnVRsk0goAHIDNz9cT5xkYAjHSB5aqFyjR_m3X48qRM,8771
7
- locust_cloud-1.14.5.dist-info/METADATA,sha256=o-6YQ6-rgvRy_3XU2bvM3NVKyhD5jXbzPrzPb30jZZE,497
8
- locust_cloud-1.14.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
- locust_cloud-1.14.5.dist-info/entry_points.txt,sha256=PGyAb4e3aTsGS3N3VGShDl6VzJaXy7QwsEgsLOC7V00,57
10
- locust_cloud-1.14.5.dist-info/RECORD,,