scratchattach 2.1.9__py3-none-any.whl → 2.1.10a1__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.
Files changed (59) hide show
  1. scratchattach/__init__.py +28 -25
  2. scratchattach/cloud/__init__.py +2 -0
  3. scratchattach/cloud/_base.py +454 -282
  4. scratchattach/cloud/cloud.py +171 -168
  5. scratchattach/editor/__init__.py +21 -0
  6. scratchattach/editor/asset.py +199 -0
  7. scratchattach/editor/backpack_json.py +117 -0
  8. scratchattach/editor/base.py +142 -0
  9. scratchattach/editor/block.py +507 -0
  10. scratchattach/editor/blockshape.py +353 -0
  11. scratchattach/editor/build_defaulting.py +47 -0
  12. scratchattach/editor/comment.py +74 -0
  13. scratchattach/editor/commons.py +243 -0
  14. scratchattach/editor/extension.py +43 -0
  15. scratchattach/editor/field.py +90 -0
  16. scratchattach/editor/inputs.py +132 -0
  17. scratchattach/editor/meta.py +106 -0
  18. scratchattach/editor/monitor.py +175 -0
  19. scratchattach/editor/mutation.py +317 -0
  20. scratchattach/editor/pallete.py +91 -0
  21. scratchattach/editor/prim.py +170 -0
  22. scratchattach/editor/project.py +273 -0
  23. scratchattach/editor/sbuild.py +2837 -0
  24. scratchattach/editor/sprite.py +586 -0
  25. scratchattach/editor/twconfig.py +113 -0
  26. scratchattach/editor/vlb.py +134 -0
  27. scratchattach/eventhandlers/_base.py +99 -92
  28. scratchattach/eventhandlers/cloud_events.py +110 -103
  29. scratchattach/eventhandlers/cloud_recorder.py +26 -21
  30. scratchattach/eventhandlers/cloud_requests.py +460 -452
  31. scratchattach/eventhandlers/cloud_server.py +246 -244
  32. scratchattach/eventhandlers/cloud_storage.py +135 -134
  33. scratchattach/eventhandlers/combine.py +29 -27
  34. scratchattach/eventhandlers/filterbot.py +160 -159
  35. scratchattach/eventhandlers/message_events.py +41 -40
  36. scratchattach/other/other_apis.py +284 -212
  37. scratchattach/other/project_json_capabilities.py +475 -546
  38. scratchattach/site/_base.py +64 -46
  39. scratchattach/site/activity.py +414 -122
  40. scratchattach/site/backpack_asset.py +118 -84
  41. scratchattach/site/classroom.py +430 -142
  42. scratchattach/site/cloud_activity.py +107 -103
  43. scratchattach/site/comment.py +220 -190
  44. scratchattach/site/forum.py +400 -399
  45. scratchattach/site/project.py +806 -787
  46. scratchattach/site/session.py +1134 -867
  47. scratchattach/site/studio.py +611 -609
  48. scratchattach/site/user.py +835 -837
  49. scratchattach/utils/commons.py +243 -148
  50. scratchattach/utils/encoder.py +157 -156
  51. scratchattach/utils/enums.py +197 -190
  52. scratchattach/utils/exceptions.py +233 -206
  53. scratchattach/utils/requests.py +67 -59
  54. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info}/METADATA +155 -146
  55. scratchattach-2.1.10a1.dist-info/RECORD +62 -0
  56. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info}/WHEEL +1 -1
  57. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info/licenses}/LICENSE +21 -21
  58. scratchattach-2.1.9.dist-info/RECORD +0 -40
  59. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info}/top_level.txt +0 -0
@@ -1,282 +1,454 @@
1
- from abc import ABC, abstractmethod
2
-
3
- import websocket
4
- import json
5
- import time
6
- from ..utils import exceptions
7
- import warnings
8
- from ..eventhandlers import cloud_recorder
9
- import ssl
10
-
11
- class BaseCloud(ABC):
12
-
13
- """
14
- Base class for a project's cloud variables. Represents a cloud.
15
-
16
- When inheriting from this class, the __init__ function of the inherited class ...
17
-
18
- - must first call the constructor of the super class: super().__init__()
19
-
20
- - must then set some attributes
21
-
22
- Attributes that must be specified in the __init__ function a class inheriting from this one:
23
-
24
- :self.project_id: Project id of the cloud variables
25
-
26
- :self.cloud_host: URL of the websocket server ("wss://..." or "ws://...")
27
-
28
- Attributes that can, but don't have to be specified in the __init__ function:
29
-
30
- :self._session: Either None or a site.session.Session object. Defaults to None.
31
-
32
- :self.ws_shortterm_ratelimit: The wait time between cloud variable sets. Defaults to 0.1
33
-
34
- :self.ws_longterm_ratelimit: The amount of cloud variable set that can be performed long-term without ever getting ratelimited
35
-
36
- :self.allow_non_numeric: Whether non-numeric cloud variable values are allowed. Defaults to False
37
-
38
- :self.length_limit: Length limit for cloud variable values. Defaults to 100000
39
-
40
- :self.username: The username to send during handshake. Defaults to "scratchattach"
41
-
42
- :self.header: The header to send. Defaults to None
43
-
44
- :self.cookie: The cookie to send. Defaults to None
45
-
46
- :self.origin: The origin to send. Defaults to None
47
-
48
- :self.print_connect_messages: Whether to print a message on every connect to the cloud server. Defaults to False.
49
- """
50
-
51
- def __init__(self, *, _session=None):
52
-
53
- # Required internal attributes that every object representing a cloud needs to have (no matter what cloud is represented):
54
- self._session = _session
55
- self.active_connection = False #whether a connection to a cloud variable server is currently established
56
- self.websocket = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE})
57
- self.recorder = None # A CloudRecorder object that records cloud activity for the values to be retrieved later will be saved in this attribute as soon as .get_var is called
58
- self.first_var_set = 0
59
- self.last_var_set = 0
60
- self.var_stets_since_first = 0
61
-
62
- # Set default values for attributes that save configurations specific to the represented cloud:
63
- # (These attributes can be specifically in the constructors of classes inheriting from this base class)
64
- self.ws_shortterm_ratelimit = 0.06667
65
- self.ws_longterm_ratelimit = 0.1
66
- self.ws_timeout = 3 # Timeout for send operations (after the timeout, the connection will be renewed and the operation will be retried 3 times)
67
- self.allow_non_numeric = False
68
- self.length_limit = 100000
69
- self.username = "scratchattach"
70
- self.header = None
71
- self.cookie = None
72
- self.origin = None
73
- self.print_connect_message = False
74
-
75
- def _assert_auth(self):
76
- if self._session is None:
77
- raise exceptions.Unauthenticated(
78
- "You need to use session.connect_cloud (NOT get_cloud) in order to perform this operation.")
79
-
80
- def _send_packet(self, packet):
81
- try:
82
- self.websocket.send(json.dumps(packet) + "\n")
83
- except Exception:
84
- time.sleep(0.1)
85
- self.connect()
86
- time.sleep(0.1)
87
- try:
88
- self.websocket.send(json.dumps(packet) + "\n")
89
- except Exception:
90
- time.sleep(0.2)
91
- self.connect()
92
- time.sleep(0.2)
93
- try:
94
- self.websocket.send(json.dumps(packet) + "\n")
95
- except Exception:
96
- time.sleep(1.6)
97
- self.connect()
98
- time.sleep(1.4)
99
- try:
100
- self.websocket.send(json.dumps(packet) + "\n")
101
- except Exception:
102
- self.active_connection = False
103
- raise exceptions.ConnectionError(f"Sending packet failed three times in a row: {packet}")
104
-
105
- def _send_packet_list(self, packet_list):
106
- packet_string = "".join([json.dumps(packet) + "\n" for packet in packet_list])
107
- try:
108
- self.websocket.send(packet_string)
109
- except Exception:
110
- time.sleep(0.1)
111
- self.connect()
112
- time.sleep(0.1)
113
- try:
114
- self.websocket.send(packet_string)
115
- except Exception:
116
- time.sleep(0.2)
117
- self.connect()
118
- time.sleep(0.2)
119
- try:
120
- self.websocket.send(packet_string)
121
- except Exception:
122
- time.sleep(1.6)
123
- self.connect()
124
- time.sleep(1.4)
125
- try:
126
- self.websocket.send(packet_string)
127
- except Exception:
128
- self.active_connection = False
129
- raise exceptions.ConnectionError(f"Sending packet list failed four times in a row: {packet_list}")
130
-
131
- def _handshake(self):
132
- packet = {"method": "handshake", "user": self.username, "project_id": self.project_id}
133
- self._send_packet(packet)
134
-
135
- def connect(self):
136
- self.websocket = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE})
137
- self.websocket.connect(
138
- self.cloud_host,
139
- cookie=self.cookie,
140
- origin=self.origin,
141
- enable_multithread=True,
142
- timeout = self.ws_timeout,
143
- header = self.header
144
- )
145
- self._handshake()
146
- self.active_connection = True
147
- if self.print_connect_message:
148
- print("Connected to cloud server ", self.cloud_host)
149
-
150
- def disconnect(self):
151
- self.active_connection = False
152
- if self.recorder is not None:
153
- self.recorder.stop()
154
- self.recorder.source_cloud.disconnect()
155
- self.recorder = None
156
- try:
157
- self.websocket.close()
158
- except Exception:
159
- pass
160
-
161
- def reconnect(self):
162
- self.disconnect()
163
- self.connect()
164
-
165
- def _assert_valid_value(self, value):
166
- if not (value in [True, False, float('inf'), -float('inf')]):
167
- value = str(value)
168
- if len(value) > self.length_limit:
169
- raise(exceptions.InvalidCloudValue(
170
- f"Value exceeds length limit: {str(value)}"
171
- ))
172
- if not self.allow_non_numeric:
173
- x = value.replace(".", "")
174
- x = x.replace("-", "")
175
- if not (x.isnumeric() or x == ""):
176
- raise(exceptions.InvalidCloudValue(
177
- "Value not numeric"
178
- ))
179
-
180
- def _enforce_ratelimit(self, *, n):
181
- # n is the amount of variables being set
182
- if (time.time() - self.first_var_set) / (self.var_stets_since_first+1) > self.ws_longterm_ratelimit: # if the average delay between cloud variable sets has been bigger than the long-term rate-limit, cloud variables can be set fast (wait time smaller than long-term rate limit) again
183
- self.var_stets_since_first = 0
184
- self.first_var_set = time.time()
185
-
186
- wait_time = self.ws_shortterm_ratelimit * n
187
- if time.time() - self.first_var_set > 25: # if cloud variables have been continously set fast (wait time smaller than long-term rate limit) for 25 seconds, they should be set slow now (wait time = long-term rate limit) to avoid getting rate-limited
188
- wait_time = self.ws_longterm_ratelimit * n
189
- while self.last_var_set + wait_time >= time.time():
190
- time.sleep(0.001)
191
-
192
-
193
- def set_var(self, variable, value):
194
- """
195
- Sets a cloud variable.
196
-
197
- Args:
198
- variable (str): The name of the cloud variable that should be set (provided without the cloud emoji)
199
- value (str): The value the cloud variable should be set to
200
- """
201
- self._assert_valid_value(value)
202
- if not isinstance(variable, str):
203
- raise ValueError("cloud var name must be a string")
204
- if not self.active_connection:
205
- self.connect()
206
- self._enforce_ratelimit(n=1)
207
-
208
- self.var_stets_since_first += 1
209
-
210
- packet = {
211
- "method": "set",
212
- "name": "☁ " + variable,
213
- "value": value,
214
- "user": self.username,
215
- "project_id": self.project_id,
216
- }
217
- self._send_packet(packet)
218
- self.last_var_set = time.time()
219
-
220
- def set_vars(self, var_value_dict, *, intelligent_waits=True):
221
- """
222
- Sets multiple cloud variables at once (works for an unlimited amount of variables).
223
-
224
- Args:
225
- var_value_dict (dict): variable:value dictionary with the variables / values to set. The dict should like this: {"var1":"value1", "var2":"value2", ...}
226
-
227
- Kwargs:
228
- intelligent_waits (boolean): When enabled, the method will automatically decide how long to wait before performing this cloud variable set, to make sure no rate limits are triggered
229
- """
230
- if not self.active_connection:
231
- self.connect()
232
- if intelligent_waits:
233
- self._enforce_ratelimit(n=len(list(var_value_dict.keys())))
234
-
235
- self.var_stets_since_first += len(list(var_value_dict.keys()))
236
-
237
- packet_list = []
238
- for variable in var_value_dict:
239
- value = var_value_dict[variable]
240
- self._assert_valid_value(value)
241
- if not isinstance(variable, str):
242
- raise ValueError("cloud var name must be a string")
243
- packet = {
244
- "method": "set",
245
- "name": "☁ " + variable,
246
- "value": value,
247
- "user": self.username,
248
- "project_id": self.project_id,
249
- }
250
- packet_list.append(packet)
251
- self._send_packet_list(packet_list)
252
- self.last_var_set = time.time()
253
-
254
- def get_var(self, var, *, recorder_initial_values={}):
255
- if self.recorder is None:
256
- self.recorder = cloud_recorder.CloudRecorder(self, initial_values=recorder_initial_values)
257
- self.recorder.start()
258
- start_time = time.time()
259
- while not (self.recorder.cloud_values != {} or start_time < time.time() -5):
260
- time.sleep(0.01)
261
- return self.recorder.get_var(var)
262
-
263
- def get_all_vars(self, *, recorder_initial_values={}):
264
- if self.recorder is None:
265
- self.recorder = cloud_recorder.CloudRecorder(self, initial_values=recorder_initial_values)
266
- self.recorder.start()
267
- start_time = time.time()
268
- while not (self.recorder.cloud_values != {} or start_time < time.time() -5):
269
- time.sleep(0.01)
270
- return self.recorder.get_all_vars()
271
-
272
- def events(self):
273
- from ..eventhandlers.cloud_events import CloudEvents
274
- return CloudEvents(self)
275
-
276
- def requests(self, *, no_packet_loss=False, used_cloud_vars=["1", "2", "3", "4", "5", "6", "7", "8", "9"], respond_order="receive"):
277
- from ..eventhandlers.cloud_requests import CloudRequests
278
- return CloudRequests(self, used_cloud_vars=used_cloud_vars, no_packet_loss=no_packet_loss, respond_order=respond_order)
279
-
280
- def storage(self, *, no_packet_loss=False, used_cloud_vars=["1", "2", "3", "4", "5", "6", "7", "8", "9"]):
281
- from ..eventhandlers.cloud_storage import CloudStorage
282
- return CloudStorage(self, used_cloud_vars=used_cloud_vars, no_packet_loss=no_packet_loss)
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import ssl
5
+ import time
6
+ from typing import Optional, Union, TypeVar, Generic, TYPE_CHECKING, Any
7
+ from abc import ABC, abstractmethod, ABCMeta
8
+ from threading import Lock
9
+ from collections.abc import Iterator
10
+
11
+ if TYPE_CHECKING:
12
+ from _typeshed import SupportsRead
13
+ else:
14
+ T = TypeVar("T")
15
+ class SupportsRead(ABC, Generic[T]):
16
+ @abstractmethod
17
+ def read(self) -> T:
18
+ pass
19
+
20
+ class SupportsClose(ABC):
21
+ @abstractmethod
22
+ def close(self) -> None:
23
+ pass
24
+
25
+ import websocket
26
+
27
+ from ..site import session
28
+ from ..eventhandlers import cloud_recorder
29
+ from ..utils import exceptions
30
+ from ..eventhandlers.cloud_requests import CloudRequests
31
+ from ..eventhandlers.cloud_events import CloudEvents
32
+ from ..eventhandlers.cloud_storage import CloudStorage
33
+ from ..site import cloud_activity
34
+
35
+ T = TypeVar("T")
36
+
37
+ class EventStream(SupportsRead[Iterator[dict[str, Any]]], SupportsClose):
38
+ """
39
+ Allows you to stream events
40
+ """
41
+
42
+ class AnyCloud(ABC, Generic[T]):
43
+ """
44
+ Represents a cloud that is not necessarily using a websocket.
45
+ """
46
+ active_connection: bool
47
+ var_stets_since_first: int
48
+ _session: Optional[session.Session]
49
+
50
+ @abstractmethod
51
+ def connect(self):
52
+ pass
53
+
54
+ @abstractmethod
55
+ def disconnect(self):
56
+ pass
57
+
58
+ def reconnect(self):
59
+ self.disconnect()
60
+ self.connect()
61
+
62
+ @abstractmethod
63
+ def _enforce_ratelimit(self, *, n: int) -> None:
64
+ pass
65
+
66
+ @abstractmethod
67
+ def set_var(self, variable: str, value: T) -> None:
68
+ """
69
+ Sets a cloud variable.
70
+
71
+ Args:
72
+ variable (str): The name of the cloud variable that should be set (provided without the cloud emoji)
73
+ value (Any): The value the cloud variable should be set to
74
+ """
75
+
76
+ @abstractmethod
77
+ def set_vars(self, var_value_dict: dict[str, T], *, intelligent_waits: bool = True):
78
+ """
79
+ Sets multiple cloud variables at once (works for an unlimited amount of variables).
80
+
81
+ Args:
82
+ var_value_dict (dict): variable:value dictionary with the variables / values to set. The dict should like this: {"var1":"value1", "var2":"value2", ...}
83
+
84
+ Kwargs:
85
+ intelligent_waits (boolean): When enabled, the method will automatically decide how long to wait before performing this cloud variable set, to make sure no rate limits are triggered
86
+ """
87
+
88
+ @abstractmethod
89
+ def get_var(self, var, *, recorder_initial_values={}) -> T:
90
+ pass
91
+
92
+ @abstractmethod
93
+ def get_all_vars(self, *, recorder_initial_values={}) -> dict[str, T]:
94
+ pass
95
+
96
+ def events(self) -> CloudEvents:
97
+ return CloudEvents(self)
98
+
99
+ def requests(self, *, no_packet_loss: bool = False, used_cloud_vars: list[str] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"],
100
+ respond_order="receive", debug: bool = False) -> CloudRequests:
101
+ return CloudRequests(self, used_cloud_vars=used_cloud_vars, no_packet_loss=no_packet_loss,
102
+ respond_order=respond_order, debug=debug)
103
+
104
+ def storage(self, *, no_packet_loss: bool = False, used_cloud_vars: list[str] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]) -> CloudStorage:
105
+ return CloudStorage(self, used_cloud_vars=used_cloud_vars, no_packet_loss=no_packet_loss)
106
+
107
+ @abstractmethod
108
+ def create_event_stream(self) -> EventStream:
109
+ pass
110
+
111
+ class WebSocketEventStream(EventStream):
112
+ packets_left: list[Union[str, bytes]]
113
+ source_cloud: BaseCloud
114
+ reading: Lock
115
+ def __init__(self, cloud: BaseCloud):
116
+ super().__init__()
117
+ self.source_cloud = type(cloud)(project_id=cloud.project_id)
118
+ self.source_cloud._session = cloud._session
119
+ self.source_cloud.cookie = cloud.cookie
120
+ self.source_cloud.header = cloud.header
121
+ self.source_cloud.origin = cloud.origin
122
+ self.source_cloud.username = cloud.username
123
+ self.source_cloud.ws_timeout = None # No timeout -> allows continous listening
124
+ self.reading = Lock()
125
+ self.source_cloud.connect()
126
+ self.packets_left = []
127
+
128
+ def receive_new(self, non_blocking: bool = False):
129
+ if non_blocking:
130
+ self.source_cloud.websocket.settimeout(0)
131
+ try:
132
+ received = self.source_cloud.websocket.recv().splitlines()
133
+ self.packets_left.extend(received)
134
+ except Exception:
135
+ pass
136
+ return
137
+ self.source_cloud.websocket.settimeout(None)
138
+ received = self.source_cloud.websocket.recv().splitlines()
139
+ self.packets_left.extend(received)
140
+
141
+ def read(self, amount: int = -1) -> Iterator[dict[str, Any]]:
142
+ i = 0
143
+ with self.reading:
144
+ try:
145
+ self.receive_new(True)
146
+ while (self.packets_left and amount == -1) or (amount != -1 and i < amount):
147
+ if not self.packets_left and amount != -1:
148
+ self.receive_new()
149
+ yield json.loads(self.packets_left.pop(0))
150
+ i += 1
151
+ except Exception:
152
+ self.source_cloud.reconnect()
153
+ self.receive_new(True)
154
+ while (self.packets_left and amount == -1) or (amount != -1 and i < amount):
155
+ if not self.packets_left and amount != -1:
156
+ self.receive_new()
157
+ yield json.loads(self.packets_left.pop(0))
158
+ i += 1
159
+
160
+ def close(self) -> None:
161
+ self.source_cloud.disconnect()
162
+
163
+ class BaseCloud(AnyCloud[Union[str, int]]):
164
+ """
165
+ Base class for a project's cloud variables. Represents a cloud.
166
+
167
+ When inheriting from this class, the __init__ function of the inherited class:
168
+ - must first call the constructor of the super class: super().__init__()
169
+ - must then set some attributes
170
+
171
+ Attributes that must be specified in the __init__ function a class inheriting from this one:
172
+ project_id: Project id of the cloud variables
173
+
174
+ cloud_host: URL of the websocket server ("wss://..." or "ws://...")
175
+
176
+ Attributes that can, but don't have to be specified in the __init__ function:
177
+
178
+ _session: Either None or a scratchattach.site.session.Session object. Defaults to None.
179
+
180
+ ws_shortterm_ratelimit: The wait time between cloud variable sets. Defaults to 0.1
181
+
182
+ ws_longterm_ratelimit: The amount of cloud variable set that can be performed long-term without ever getting ratelimited
183
+
184
+ allow_non_numeric: Whether non-numeric cloud variable values are allowed. Defaults to False
185
+
186
+ length_limit: Length limit for cloud variable values. Defaults to 100000
187
+
188
+ username: The username to send during handshake. Defaults to "scratchattach"
189
+
190
+ header: The header to send. Defaults to None
191
+
192
+ cookie: The cookie to send. Defaults to None
193
+
194
+ origin: The origin to send. Defaults to None
195
+
196
+ print_connect_messages: Whether to print a message on every connect to the cloud server. Defaults to False.
197
+ """
198
+ project_id: Optional[Union[str, int]]
199
+ cloud_host: str
200
+ ws_shortterm_ratelimit: float
201
+ ws_longterm_ratelimit: float
202
+ allow_non_numeric: bool
203
+ length_limit: int
204
+ username: str
205
+ header: Optional[dict]
206
+ cookie: Optional[dict]
207
+ origin: Optional[str]
208
+ print_connect_message: bool
209
+ ws_timeout: Optional[int]
210
+ websocket: websocket.WebSocket
211
+ event_stream: Optional[EventStream] = None
212
+
213
+ def __init__(self, *, project_id: Optional[Union[int, str]] = None, _session=None):
214
+
215
+ # Required internal attributes that every object representing a cloud needs to have (no matter what cloud is represented):
216
+ self._session = _session
217
+ self.active_connection = False #whether a connection to a cloud variable server is currently established
218
+
219
+ self.websocket = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE})
220
+ self.recorder = None # A CloudRecorder object that records cloud activity for the values to be retrieved later,
221
+ # which will be saved in this attribute as soon as .get_var is called
222
+ self.first_var_set = 0
223
+ self.last_var_set = 0
224
+ self.var_stets_since_first = 0
225
+
226
+ # Set default values for attributes that save configurations specific to the represented cloud:
227
+ # (These attributes can be specifically in the constructors of classes inheriting from this base class)
228
+ self.ws_shortterm_ratelimit = 0.06667
229
+ self.ws_longterm_ratelimit = 0.1
230
+ self.ws_timeout = 3 # Timeout for send operations (after the timeout,
231
+ # the connection will be renewed and the operation will be retried 3 times)
232
+ self.allow_non_numeric = False
233
+ self.length_limit = 100000
234
+ self.username = "scratchattach"
235
+ self.header = None
236
+ self.cookie = None
237
+ self.origin = None
238
+ self.print_connect_message = False
239
+
240
+ self.project_id = project_id
241
+
242
+ def _assert_auth(self):
243
+ if self._session is None:
244
+ raise exceptions.Unauthenticated(
245
+ "You need to use session.connect_cloud (NOT get_cloud) in order to perform this operation.")
246
+
247
+ def _send_packet(self, packet):
248
+ try:
249
+ self.websocket.send(json.dumps(packet) + "\n")
250
+ except Exception:
251
+ time.sleep(0.1)
252
+ self.connect()
253
+ time.sleep(0.1)
254
+ try:
255
+ self.websocket.send(json.dumps(packet) + "\n")
256
+ except Exception:
257
+ time.sleep(0.2)
258
+ self.connect()
259
+ time.sleep(0.2)
260
+ try:
261
+ self.websocket.send(json.dumps(packet) + "\n")
262
+ except Exception:
263
+ time.sleep(1.6)
264
+ self.connect()
265
+ time.sleep(1.4)
266
+ try:
267
+ self.websocket.send(json.dumps(packet) + "\n")
268
+ except Exception:
269
+ self.active_connection = False
270
+ raise exceptions.CloudConnectionError(f"Sending packet failed three times in a row: {packet}")
271
+
272
+ def _send_packet_list(self, packet_list):
273
+ packet_string = "".join([json.dumps(packet) + "\n" for packet in packet_list])
274
+ try:
275
+ self.websocket.send(packet_string)
276
+ except Exception:
277
+ time.sleep(0.1)
278
+ self.connect()
279
+ time.sleep(0.1)
280
+ try:
281
+ self.websocket.send(packet_string)
282
+ except Exception:
283
+ time.sleep(0.2)
284
+ self.connect()
285
+ time.sleep(0.2)
286
+ try:
287
+ self.websocket.send(packet_string)
288
+ except Exception:
289
+ time.sleep(1.6)
290
+ self.connect()
291
+ time.sleep(1.4)
292
+ try:
293
+ self.websocket.send(packet_string)
294
+ except Exception:
295
+ self.active_connection = False
296
+ raise exceptions.CloudConnectionError(
297
+ f"Sending packet list failed four times in a row: {packet_list}")
298
+
299
+ def _handshake(self):
300
+ packet = {"method": "handshake", "user": self.username, "project_id": self.project_id}
301
+ self._send_packet(packet)
302
+
303
+ def connect(self):
304
+ self.websocket = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE})
305
+ self.websocket.connect(
306
+ self.cloud_host,
307
+ cookie=self.cookie,
308
+ origin=self.origin,
309
+ enable_multithread=True,
310
+ timeout=self.ws_timeout,
311
+ header=self.header
312
+ )
313
+ self._handshake()
314
+ self.active_connection = True
315
+ if self.print_connect_message:
316
+ print("Connected to cloud server ", self.cloud_host)
317
+
318
+ def disconnect(self):
319
+ self.active_connection = False
320
+ if self.recorder is not None:
321
+ self.recorder.stop()
322
+ self.recorder.disconnect()
323
+ self.recorder = None
324
+ try:
325
+ self.websocket.close()
326
+ except Exception:
327
+ pass
328
+
329
+ def _assert_valid_value(self, value):
330
+ if not (value in [True, False, float('inf'), -float('inf')]):
331
+ value = str(value)
332
+ if len(value) > self.length_limit:
333
+ raise (exceptions.InvalidCloudValue(
334
+ f"Value exceeds length limit: {str(value)}"
335
+ ))
336
+ if not self.allow_non_numeric:
337
+ x = value.replace(".", "")
338
+ x = x.replace("-", "")
339
+ if not (x.isnumeric() or x == ""):
340
+ raise (exceptions.InvalidCloudValue(
341
+ "Value not numeric"
342
+ ))
343
+
344
+ def _enforce_ratelimit(self, *, n):
345
+ # n is the amount of variables being set
346
+ if (time.time() - self.first_var_set) / (
347
+ self.var_stets_since_first + 1) > self.ws_longterm_ratelimit: # if the average delay between cloud variable sets has been bigger than the long-term rate-limit, cloud variables can be set fast (wait time smaller than long-term rate limit) again
348
+ self.var_stets_since_first = 0
349
+ self.first_var_set = time.time()
350
+
351
+ wait_time = self.ws_shortterm_ratelimit * n
352
+ if time.time() - self.first_var_set > 25: # if cloud variables have been continously set fast (wait time smaller than long-term rate limit) for 25 seconds, they should be set slow now (wait time = long-term rate limit) to avoid getting rate-limited
353
+ wait_time = self.ws_longterm_ratelimit * n
354
+ while self.last_var_set + wait_time >= time.time():
355
+ time.sleep(0.001)
356
+
357
+ def set_var(self, variable, value):
358
+ """
359
+ Sets a cloud variable.
360
+
361
+ Args:
362
+ variable (str): The name of the cloud variable that should be set (provided without the cloud emoji)
363
+ value (str): The value the cloud variable should be set to
364
+ """
365
+ self._assert_valid_value(value)
366
+ if not isinstance(variable, str):
367
+ raise ValueError("cloud var name must be a string")
368
+ variable = variable.removeprefix("☁ ")
369
+ if not self.active_connection:
370
+ self.connect()
371
+ self._enforce_ratelimit(n=1)
372
+
373
+ self.var_stets_since_first += 1
374
+
375
+ packet = {
376
+ "method": "set",
377
+ "name": "☁ " + variable,
378
+ "value": value,
379
+ "user": self.username,
380
+ "project_id": self.project_id,
381
+ }
382
+ self._send_packet(packet)
383
+ self.last_var_set = time.time()
384
+
385
+ def set_vars(self, var_value_dict, *, intelligent_waits=True):
386
+ """
387
+ Sets multiple cloud variables at once (works for an unlimited amount of variables).
388
+
389
+ Args:
390
+ var_value_dict (dict): variable:value dictionary with the variables / values to set. The dict should like this: {"var1":"value1", "var2":"value2", ...}
391
+
392
+ Kwargs:
393
+ intelligent_waits (boolean): When enabled, the method will automatically decide how long to wait before performing this cloud variable set, to make sure no rate limits are triggered
394
+ """
395
+ if not self.active_connection:
396
+ self.connect()
397
+ if intelligent_waits:
398
+ self._enforce_ratelimit(n=len(list(var_value_dict.keys())))
399
+
400
+ self.var_stets_since_first += len(list(var_value_dict.keys()))
401
+
402
+ packet_list = []
403
+ for variable in var_value_dict:
404
+ value = var_value_dict[variable]
405
+ variable = variable.removeprefix("☁ ")
406
+ self._assert_valid_value(value)
407
+ if not isinstance(variable, str):
408
+ raise ValueError("cloud var name must be a string")
409
+ packet = {
410
+ "method": "set",
411
+ "name": "☁ " + variable,
412
+ "value": value,
413
+ "user": self.username,
414
+ "project_id": self.project_id,
415
+ }
416
+ packet_list.append(packet)
417
+ self._send_packet_list(packet_list)
418
+ self.last_var_set = time.time()
419
+
420
+ def get_var(self, var, *, recorder_initial_values={}):
421
+ var = "☁ "+var.removeprefix("☁ ")
422
+ if self.recorder is None:
423
+ self.recorder = cloud_recorder.CloudRecorder(self, initial_values=recorder_initial_values)
424
+ self.recorder.start()
425
+ start_time = time.time()
426
+ while not (self.recorder.cloud_values != {} or start_time < time.time() - 5):
427
+ time.sleep(0.01)
428
+ return self.recorder.get_var(var)
429
+
430
+ def get_all_vars(self, *, recorder_initial_values={}):
431
+ if self.recorder is None:
432
+ self.recorder = cloud_recorder.CloudRecorder(self, initial_values=recorder_initial_values)
433
+ self.recorder.start()
434
+ start_time = time.time()
435
+ while not (self.recorder.cloud_values != {} or start_time < time.time() - 5):
436
+ time.sleep(0.01)
437
+ return self.recorder.get_all_vars()
438
+
439
+ def create_event_stream(self):
440
+ if self.event_stream:
441
+ raise ValueError("Cloud already has an event stream.")
442
+ self.event_stream = WebSocketEventStream(self)
443
+ return self.event_stream
444
+
445
+ class LogCloudMeta(ABCMeta):
446
+ def __instancecheck__(cls, instance) -> bool:
447
+ if hasattr(instance, "logs"):
448
+ return isinstance(instance, BaseCloud)
449
+ return False
450
+
451
+ class LogCloud(BaseCloud, metaclass=LogCloudMeta):
452
+ @abstractmethod
453
+ def logs(self, *, filter_by_var_named: Optional[str] = None, limit: int = 100, offset: int = 0) -> list[cloud_activity.CloudActivity]:
454
+ pass