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.
- scratchattach/__init__.py +28 -25
- scratchattach/cloud/__init__.py +2 -0
- scratchattach/cloud/_base.py +454 -282
- scratchattach/cloud/cloud.py +171 -168
- scratchattach/editor/__init__.py +21 -0
- scratchattach/editor/asset.py +199 -0
- scratchattach/editor/backpack_json.py +117 -0
- scratchattach/editor/base.py +142 -0
- scratchattach/editor/block.py +507 -0
- scratchattach/editor/blockshape.py +353 -0
- scratchattach/editor/build_defaulting.py +47 -0
- scratchattach/editor/comment.py +74 -0
- scratchattach/editor/commons.py +243 -0
- scratchattach/editor/extension.py +43 -0
- scratchattach/editor/field.py +90 -0
- scratchattach/editor/inputs.py +132 -0
- scratchattach/editor/meta.py +106 -0
- scratchattach/editor/monitor.py +175 -0
- scratchattach/editor/mutation.py +317 -0
- scratchattach/editor/pallete.py +91 -0
- scratchattach/editor/prim.py +170 -0
- scratchattach/editor/project.py +273 -0
- scratchattach/editor/sbuild.py +2837 -0
- scratchattach/editor/sprite.py +586 -0
- scratchattach/editor/twconfig.py +113 -0
- scratchattach/editor/vlb.py +134 -0
- scratchattach/eventhandlers/_base.py +99 -92
- scratchattach/eventhandlers/cloud_events.py +110 -103
- scratchattach/eventhandlers/cloud_recorder.py +26 -21
- scratchattach/eventhandlers/cloud_requests.py +460 -452
- scratchattach/eventhandlers/cloud_server.py +246 -244
- scratchattach/eventhandlers/cloud_storage.py +135 -134
- scratchattach/eventhandlers/combine.py +29 -27
- scratchattach/eventhandlers/filterbot.py +160 -159
- scratchattach/eventhandlers/message_events.py +41 -40
- scratchattach/other/other_apis.py +284 -212
- scratchattach/other/project_json_capabilities.py +475 -546
- scratchattach/site/_base.py +64 -46
- scratchattach/site/activity.py +414 -122
- scratchattach/site/backpack_asset.py +118 -84
- scratchattach/site/classroom.py +430 -142
- scratchattach/site/cloud_activity.py +107 -103
- scratchattach/site/comment.py +220 -190
- scratchattach/site/forum.py +400 -399
- scratchattach/site/project.py +806 -787
- scratchattach/site/session.py +1134 -867
- scratchattach/site/studio.py +611 -609
- scratchattach/site/user.py +835 -837
- scratchattach/utils/commons.py +243 -148
- scratchattach/utils/encoder.py +157 -156
- scratchattach/utils/enums.py +197 -190
- scratchattach/utils/exceptions.py +233 -206
- scratchattach/utils/requests.py +67 -59
- {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info}/METADATA +155 -146
- scratchattach-2.1.10a1.dist-info/RECORD +62 -0
- {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info}/WHEEL +1 -1
- {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info/licenses}/LICENSE +21 -21
- scratchattach-2.1.9.dist-info/RECORD +0 -40
- {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info}/top_level.txt +0 -0
scratchattach/cloud/_base.py
CHANGED
|
@@ -1,282 +1,454 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import time
|
|
6
|
-
from
|
|
7
|
-
import
|
|
8
|
-
from
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
:
|
|
47
|
-
|
|
48
|
-
:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
self.
|
|
60
|
-
self.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
self.websocket.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
self.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
self.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
"
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
def
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|