scratchattach 2.1.15b0__py3-none-any.whl → 3.0.0b1__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.
- cli/__about__.py +1 -0
- cli/__init__.py +26 -0
- cli/cmd/__init__.py +4 -0
- cli/cmd/group.py +127 -0
- cli/cmd/login.py +60 -0
- cli/cmd/profile.py +7 -0
- cli/cmd/sessions.py +5 -0
- cli/context.py +142 -0
- cli/db.py +66 -0
- cli/namespace.py +14 -0
- {scratchattach/cloud → cloud}/_base.py +112 -87
- {scratchattach/cloud → cloud}/cloud.py +16 -16
- {scratchattach/editor → editor}/__init__.py +2 -1
- {scratchattach/editor → editor}/asset.py +26 -14
- {scratchattach/editor → editor}/backpack_json.py +3 -5
- {scratchattach/editor → editor}/base.py +2 -4
- {scratchattach/editor → editor}/block.py +27 -22
- {scratchattach/editor → editor}/blockshape.py +1 -1
- {scratchattach/editor → editor}/build_defaulting.py +2 -2
- editor/commons.py +145 -0
- {scratchattach/editor → editor}/field.py +1 -1
- {scratchattach/editor → editor}/inputs.py +6 -3
- {scratchattach/editor → editor}/meta.py +10 -7
- {scratchattach/editor → editor}/monitor.py +10 -8
- {scratchattach/editor → editor}/mutation.py +68 -11
- {scratchattach/editor → editor}/pallete.py +1 -3
- {scratchattach/editor → editor}/prim.py +4 -0
- {scratchattach/editor → editor}/project.py +118 -16
- {scratchattach/editor → editor}/sprite.py +25 -15
- {scratchattach/editor → editor}/vlb.py +2 -2
- {scratchattach/eventhandlers → eventhandlers}/_base.py +1 -0
- {scratchattach/eventhandlers → eventhandlers}/cloud_events.py +26 -6
- {scratchattach/eventhandlers → eventhandlers}/cloud_recorder.py +4 -4
- {scratchattach/eventhandlers → eventhandlers}/cloud_requests.py +139 -54
- {scratchattach/eventhandlers → eventhandlers}/cloud_server.py +6 -3
- {scratchattach/eventhandlers → eventhandlers}/cloud_storage.py +1 -2
- eventhandlers/filterbot.py +163 -0
- other/other_apis.py +598 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/METADATA +7 -11
- scratchattach-3.0.0b1.dist-info/RECORD +79 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/WHEEL +1 -1
- scratchattach-3.0.0b1.dist-info/entry_points.txt +2 -0
- scratchattach-3.0.0b1.dist-info/top_level.txt +7 -0
- {scratchattach/site → site}/_base.py +32 -5
- site/activity.py +426 -0
- {scratchattach/site → site}/alert.py +4 -5
- {scratchattach/site → site}/backpack_asset.py +2 -1
- {scratchattach/site → site}/classroom.py +80 -73
- {scratchattach/site → site}/cloud_activity.py +43 -29
- {scratchattach/site → site}/comment.py +86 -100
- {scratchattach/site → site}/forum.py +8 -4
- site/placeholder.py +132 -0
- {scratchattach/site → site}/project.py +228 -122
- {scratchattach/site → site}/session.py +156 -71
- {scratchattach/site → site}/studio.py +139 -46
- site/typed_dicts.py +151 -0
- {scratchattach/site → site}/user.py +511 -215
- {scratchattach/utils → utils}/commons.py +12 -4
- {scratchattach/utils → utils}/encoder.py +7 -4
- {scratchattach/utils → utils}/enums.py +1 -0
- {scratchattach/utils → utils}/exceptions.py +36 -2
- utils/optional_async.py +154 -0
- utils/requests.py +306 -0
- scratchattach/__init__.py +0 -29
- scratchattach/editor/commons.py +0 -273
- scratchattach/eventhandlers/filterbot.py +0 -161
- scratchattach/other/other_apis.py +0 -284
- scratchattach/site/activity.py +0 -382
- scratchattach/utils/requests.py +0 -93
- scratchattach-2.1.15b0.dist-info/RECORD +0 -66
- scratchattach-2.1.15b0.dist-info/top_level.txt +0 -1
- {scratchattach/cloud → cloud}/__init__.py +0 -0
- {scratchattach/editor → editor}/code_translation/__init__.py +0 -0
- {scratchattach/editor → editor}/code_translation/parse.py +0 -0
- {scratchattach/editor → editor}/comment.py +0 -0
- {scratchattach/editor → editor}/extension.py +0 -0
- {scratchattach/editor → editor}/twconfig.py +0 -0
- {scratchattach/eventhandlers → eventhandlers}/__init__.py +0 -0
- {scratchattach/eventhandlers → eventhandlers}/combine.py +0 -0
- {scratchattach/eventhandlers → eventhandlers}/message_events.py +0 -0
- {scratchattach/other → other}/__init__.py +0 -0
- {scratchattach/other → other}/project_json_capabilities.py +0 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
- {scratchattach/site → site}/__init__.py +0 -0
- {scratchattach/site → site}/browser_cookie3_stub.py +0 -0
- {scratchattach/site → site}/browser_cookies.py +0 -0
- {scratchattach/utils → utils}/__init__.py +0 -0
|
@@ -3,11 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import ssl
|
|
5
5
|
import time
|
|
6
|
+
import warnings
|
|
6
7
|
from typing import Optional, Union, TypeVar, Generic, TYPE_CHECKING, Any
|
|
7
8
|
from abc import ABC, abstractmethod, ABCMeta
|
|
8
9
|
from threading import Lock
|
|
9
10
|
from collections.abc import Iterator
|
|
10
|
-
import warnings
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from _typeshed import SupportsRead
|
|
@@ -18,23 +18,25 @@ else:
|
|
|
18
18
|
def read(self) -> T:
|
|
19
19
|
pass
|
|
20
20
|
|
|
21
|
-
class SupportsClose(ABC):
|
|
22
|
-
@abstractmethod
|
|
23
|
-
def close(self) -> None:
|
|
24
|
-
pass
|
|
25
21
|
|
|
26
22
|
import websocket
|
|
27
23
|
|
|
28
24
|
from scratchattach.site import session
|
|
29
25
|
from scratchattach.eventhandlers import cloud_recorder
|
|
30
26
|
from scratchattach.utils import exceptions
|
|
31
|
-
from scratchattach.eventhandlers.cloud_requests import CloudRequests
|
|
27
|
+
from scratchattach.eventhandlers.cloud_requests import CloudRequests, RespondOrder
|
|
32
28
|
from scratchattach.eventhandlers.cloud_events import CloudEvents
|
|
33
29
|
from scratchattach.eventhandlers.cloud_storage import CloudStorage
|
|
34
30
|
from scratchattach.site import cloud_activity
|
|
35
31
|
|
|
32
|
+
class SupportsClose(ABC):
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def close(self) -> None:
|
|
35
|
+
pass
|
|
36
|
+
|
|
36
37
|
T = TypeVar("T")
|
|
37
38
|
|
|
39
|
+
|
|
38
40
|
class EventStream(SupportsRead[Iterator[dict[str, Any]]], SupportsClose):
|
|
39
41
|
"""
|
|
40
42
|
Allows you to stream events
|
|
@@ -45,7 +47,7 @@ class AnyCloud(ABC, Generic[T]):
|
|
|
45
47
|
Represents a cloud that is not necessarily using a websocket.
|
|
46
48
|
"""
|
|
47
49
|
active_connection: bool
|
|
48
|
-
|
|
50
|
+
var_sets_since_first: int
|
|
49
51
|
_session: Optional[session.Session]
|
|
50
52
|
|
|
51
53
|
@abstractmethod
|
|
@@ -58,6 +60,7 @@ class AnyCloud(ABC, Generic[T]):
|
|
|
58
60
|
|
|
59
61
|
def reconnect(self):
|
|
60
62
|
self.disconnect()
|
|
63
|
+
time.sleep(0.1)
|
|
61
64
|
self.connect()
|
|
62
65
|
|
|
63
66
|
@abstractmethod
|
|
@@ -65,17 +68,20 @@ class AnyCloud(ABC, Generic[T]):
|
|
|
65
68
|
pass
|
|
66
69
|
|
|
67
70
|
@abstractmethod
|
|
68
|
-
def set_var(self, variable: str, value: T) -> None:
|
|
71
|
+
def set_var(self, variable: str, value: T, *, max_retries : int = 2) -> None:
|
|
69
72
|
"""
|
|
70
73
|
Sets a cloud variable.
|
|
71
74
|
|
|
72
75
|
Args:
|
|
73
76
|
variable (str): The name of the cloud variable that should be set (provided without the cloud emoji)
|
|
74
77
|
value (Any): The value the cloud variable should be set to
|
|
78
|
+
|
|
79
|
+
Kwargs:
|
|
80
|
+
max_retries (int) : Maximum number of times to retry setting the var if setting fails before raising an exception
|
|
75
81
|
"""
|
|
76
82
|
|
|
77
83
|
@abstractmethod
|
|
78
|
-
def set_vars(self, var_value_dict: dict[str, T], *, intelligent_waits: bool = True):
|
|
84
|
+
def set_vars(self, var_value_dict: dict[str, T], *, intelligent_waits: bool = True, max_retries : int = 2):
|
|
79
85
|
"""
|
|
80
86
|
Sets multiple cloud variables at once (works for an unlimited amount of variables).
|
|
81
87
|
|
|
@@ -84,31 +90,67 @@ class AnyCloud(ABC, Generic[T]):
|
|
|
84
90
|
|
|
85
91
|
Kwargs:
|
|
86
92
|
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
|
|
93
|
+
max_retries (int) : Maximum number of times to retry setting the var if setting fails before raising an exception
|
|
87
94
|
"""
|
|
88
95
|
|
|
89
96
|
@abstractmethod
|
|
90
|
-
def get_var(self, var, *, recorder_initial_values=
|
|
97
|
+
def get_var(self, var, *, recorder_initial_values: Optional[dict[str, Any]] = None) -> T:
|
|
91
98
|
pass
|
|
92
99
|
|
|
93
100
|
@abstractmethod
|
|
94
|
-
def get_all_vars(self, *, recorder_initial_values=
|
|
101
|
+
def get_all_vars(self, *, recorder_initial_values: Optional[dict[str, Any]] = None) -> dict[str, T]:
|
|
95
102
|
pass
|
|
96
103
|
|
|
97
104
|
def events(self) -> CloudEvents:
|
|
98
105
|
return CloudEvents(self)
|
|
99
106
|
|
|
100
|
-
def requests(self, *, no_packet_loss: bool = False, used_cloud_vars: list[str] =
|
|
101
|
-
respond_order=
|
|
107
|
+
def requests(self, *, no_packet_loss: bool = False, used_cloud_vars: Optional[list[str]] = None,
|
|
108
|
+
respond_order=RespondOrder.RECEIVE, debug: bool = False) -> CloudRequests:
|
|
109
|
+
used_cloud_vars = used_cloud_vars or ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
|
102
110
|
return CloudRequests(self, used_cloud_vars=used_cloud_vars, no_packet_loss=no_packet_loss,
|
|
103
111
|
respond_order=respond_order, debug=debug)
|
|
104
112
|
|
|
105
|
-
def storage(self, *, no_packet_loss: bool = False, used_cloud_vars: list[str] =
|
|
113
|
+
def storage(self, *, no_packet_loss: bool = False, used_cloud_vars: Optional[list[str]] = None) -> CloudStorage:
|
|
114
|
+
used_cloud_vars = used_cloud_vars or ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
|
106
115
|
return CloudStorage(self, used_cloud_vars=used_cloud_vars, no_packet_loss=no_packet_loss)
|
|
107
116
|
|
|
108
117
|
@abstractmethod
|
|
109
118
|
def create_event_stream(self) -> EventStream:
|
|
110
119
|
pass
|
|
111
120
|
|
|
121
|
+
class DummyCloud(AnyCloud[Any]):
|
|
122
|
+
class DummyEventStream(EventStream):
|
|
123
|
+
def read(self, length = ...):
|
|
124
|
+
return iter(())
|
|
125
|
+
|
|
126
|
+
def close(self):
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
def connect(self):
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
def disconnect(self):
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
def create_event_stream(self) -> EventStream:
|
|
136
|
+
return self.DummyEventStream()
|
|
137
|
+
|
|
138
|
+
def _enforce_ratelimit(self, *, n: int) -> None:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
def set_var(self, variable: str, value: T, *, max_retries : int = 2) -> None:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
def set_vars(self, var_value_dict: dict[str, T], *, intelligent_waits: bool = True, max_retries : int = 2):
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
def get_var(self, var, *, recorder_initial_values: Optional[dict[str, Any]] = None) -> Any:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
def get_all_vars(self, *, recorder_initial_values: Optional[dict[str, Any]] = None) -> dict[str, Any]:
|
|
151
|
+
return {}
|
|
152
|
+
|
|
153
|
+
|
|
112
154
|
class WebSocketEventStream(EventStream):
|
|
113
155
|
packets_left: list[Union[str, bytes]]
|
|
114
156
|
source_cloud: BaseCloud
|
|
@@ -172,7 +214,7 @@ class BaseCloud(AnyCloud[Union[str, int]]):
|
|
|
172
214
|
- must first call the constructor of the super class: super().__init__()
|
|
173
215
|
- must then set some attributes
|
|
174
216
|
|
|
175
|
-
Attributes that must be specified in the __init__ function a class inheriting from this one:
|
|
217
|
+
Attributes that must be specified in the __init__ function of a class inheriting from this one:
|
|
176
218
|
project_id: Project id of the cloud variables
|
|
177
219
|
|
|
178
220
|
cloud_host: URL of the websocket server ("wss://..." or "ws://...")
|
|
@@ -199,6 +241,9 @@ class BaseCloud(AnyCloud[Union[str, int]]):
|
|
|
199
241
|
|
|
200
242
|
print_connect_messages: Whether to print a message on every connect to the cloud server. Defaults to False.
|
|
201
243
|
"""
|
|
244
|
+
|
|
245
|
+
_PACKET_FAILURE_SLEEPDURATIONS = (0.1, 0.2, 1.5)
|
|
246
|
+
|
|
202
247
|
project_id: Optional[Union[str, int]]
|
|
203
248
|
cloud_host: str
|
|
204
249
|
ws_shortterm_ratelimit: float
|
|
@@ -213,6 +258,12 @@ class BaseCloud(AnyCloud[Union[str, int]]):
|
|
|
213
258
|
ws_timeout: Optional[int]
|
|
214
259
|
websocket: websocket.WebSocket
|
|
215
260
|
event_stream: Optional[EventStream] = None
|
|
261
|
+
|
|
262
|
+
recorder: Optional[cloud_recorder.CloudRecorder]
|
|
263
|
+
|
|
264
|
+
first_var_set: float
|
|
265
|
+
last_var_set: float
|
|
266
|
+
var_sets_since_first: int
|
|
216
267
|
|
|
217
268
|
def __init__(self, *, project_id: Optional[Union[int, str]] = None, _session=None):
|
|
218
269
|
|
|
@@ -223,9 +274,9 @@ class BaseCloud(AnyCloud[Union[str, int]]):
|
|
|
223
274
|
self.websocket = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE})
|
|
224
275
|
self.recorder = None # A CloudRecorder object that records cloud activity for the values to be retrieved later,
|
|
225
276
|
# which will be saved in this attribute as soon as .get_var is called
|
|
226
|
-
self.first_var_set = 0
|
|
227
|
-
self.last_var_set = 0
|
|
228
|
-
self.
|
|
277
|
+
self.first_var_set = 0.0
|
|
278
|
+
self.last_var_set = 0.0
|
|
279
|
+
self.var_sets_since_first = 0
|
|
229
280
|
|
|
230
281
|
# Set default values for attributes that save configurations specific to the represented cloud:
|
|
231
282
|
# (These attributes can be specifically in the constructors of classes inheriting from this base class)
|
|
@@ -248,57 +299,26 @@ class BaseCloud(AnyCloud[Union[str, int]]):
|
|
|
248
299
|
raise exceptions.Unauthenticated(
|
|
249
300
|
"You need to use session.connect_cloud (NOT get_cloud) in order to perform this operation.")
|
|
250
301
|
|
|
251
|
-
def
|
|
302
|
+
def _send_recursive(self, data : str, *, current_depth: int = 0, max_depth=0):
|
|
252
303
|
try:
|
|
253
|
-
self.websocket.send(
|
|
304
|
+
self.websocket.send(data)
|
|
254
305
|
except Exception:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
try:
|
|
259
|
-
self.websocket.send(json.dumps(packet) + "\n")
|
|
260
|
-
except Exception:
|
|
261
|
-
time.sleep(0.2)
|
|
306
|
+
if current_depth < max_depth:
|
|
307
|
+
sleep_duration = self._PACKET_FAILURE_SLEEPDURATIONS[min(current_depth, len(self._PACKET_FAILURE_SLEEPDURATIONS)-1)]
|
|
308
|
+
time.sleep(sleep_duration)
|
|
262
309
|
self.connect()
|
|
263
|
-
time.sleep(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
self.active_connection = False
|
|
274
|
-
raise exceptions.CloudConnectionError(f"Sending packet failed three times in a row: {packet}")
|
|
275
|
-
|
|
276
|
-
def _send_packet_list(self, packet_list):
|
|
310
|
+
time.sleep(sleep_duration)
|
|
311
|
+
self._send_recursive(data, current_depth=current_depth+1, max_depth=max_depth)
|
|
312
|
+
else:
|
|
313
|
+
self.active_connection = False
|
|
314
|
+
raise exceptions.CloudConnectionError(f"Sending packet failed {max_depth+1} tries: {data}")
|
|
315
|
+
|
|
316
|
+
def _send_packet(self, packet, *, max_retries=2):
|
|
317
|
+
self._send_recursive(json.dumps(packet) + "\n", max_depth=max_retries)
|
|
318
|
+
|
|
319
|
+
def _send_packet_list(self, packet_list, *, max_retries=2):
|
|
277
320
|
packet_string = "".join([json.dumps(packet) + "\n" for packet in packet_list])
|
|
278
|
-
|
|
279
|
-
self.websocket.send(packet_string)
|
|
280
|
-
except Exception:
|
|
281
|
-
time.sleep(0.1)
|
|
282
|
-
self.connect()
|
|
283
|
-
time.sleep(0.1)
|
|
284
|
-
try:
|
|
285
|
-
self.websocket.send(packet_string)
|
|
286
|
-
except Exception:
|
|
287
|
-
time.sleep(0.2)
|
|
288
|
-
self.connect()
|
|
289
|
-
time.sleep(0.2)
|
|
290
|
-
try:
|
|
291
|
-
self.websocket.send(packet_string)
|
|
292
|
-
except Exception:
|
|
293
|
-
time.sleep(1.6)
|
|
294
|
-
self.connect()
|
|
295
|
-
time.sleep(1.4)
|
|
296
|
-
try:
|
|
297
|
-
self.websocket.send(packet_string)
|
|
298
|
-
except Exception:
|
|
299
|
-
self.active_connection = False
|
|
300
|
-
raise exceptions.CloudConnectionError(
|
|
301
|
-
f"Sending packet list failed four times in a row: {packet_list}")
|
|
321
|
+
self._send_recursive(packet_string, max_depth=max_retries)
|
|
302
322
|
|
|
303
323
|
def _handshake(self):
|
|
304
324
|
packet = {"method": "handshake", "user": self.username, "project_id": self.project_id}
|
|
@@ -348,23 +368,27 @@ class BaseCloud(AnyCloud[Union[str, int]]):
|
|
|
348
368
|
def _enforce_ratelimit(self, *, n):
|
|
349
369
|
# n is the amount of variables being set
|
|
350
370
|
if (time.time() - self.first_var_set) / (
|
|
351
|
-
self.
|
|
352
|
-
self.
|
|
371
|
+
self.var_sets_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
|
|
372
|
+
self.var_sets_since_first = 0
|
|
353
373
|
self.first_var_set = time.time()
|
|
354
374
|
|
|
355
375
|
wait_time = self.ws_shortterm_ratelimit * n
|
|
356
376
|
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
|
|
357
377
|
wait_time = self.ws_longterm_ratelimit * n
|
|
358
|
-
|
|
359
|
-
|
|
378
|
+
sleep_time = self.last_var_set + wait_time - time.time()
|
|
379
|
+
if sleep_time > 0:
|
|
380
|
+
time.sleep(sleep_time)
|
|
360
381
|
|
|
361
|
-
def set_var(self, variable, value):
|
|
382
|
+
def set_var(self, variable, value, *, max_retries: int = 2):
|
|
362
383
|
"""
|
|
363
384
|
Sets a cloud variable.
|
|
364
385
|
|
|
365
386
|
Args:
|
|
366
387
|
variable (str): The name of the cloud variable that should be set (provided without the cloud emoji)
|
|
367
388
|
value (str): The value the cloud variable should be set to
|
|
389
|
+
|
|
390
|
+
Kwargs:
|
|
391
|
+
max_retries (int) : Maximum number of times to retry setting the var if setting fails before raising an exception
|
|
368
392
|
"""
|
|
369
393
|
self._assert_valid_value(value)
|
|
370
394
|
if not isinstance(variable, str):
|
|
@@ -374,7 +398,7 @@ class BaseCloud(AnyCloud[Union[str, int]]):
|
|
|
374
398
|
self.connect()
|
|
375
399
|
self._enforce_ratelimit(n=1)
|
|
376
400
|
|
|
377
|
-
self.
|
|
401
|
+
self.var_sets_since_first += 1
|
|
378
402
|
|
|
379
403
|
packet = {
|
|
380
404
|
"method": "set",
|
|
@@ -383,10 +407,10 @@ class BaseCloud(AnyCloud[Union[str, int]]):
|
|
|
383
407
|
"user": self.username,
|
|
384
408
|
"project_id": self.project_id,
|
|
385
409
|
}
|
|
386
|
-
self._send_packet(packet)
|
|
410
|
+
self._send_packet(packet, max_retries=max_retries)
|
|
387
411
|
self.last_var_set = time.time()
|
|
388
412
|
|
|
389
|
-
def set_vars(self, var_value_dict, *, intelligent_waits=True):
|
|
413
|
+
def set_vars(self, var_value_dict, *, intelligent_waits=True, max_retries: int = 2):
|
|
390
414
|
"""
|
|
391
415
|
Sets multiple cloud variables at once (works for an unlimited amount of variables).
|
|
392
416
|
|
|
@@ -395,13 +419,14 @@ class BaseCloud(AnyCloud[Union[str, int]]):
|
|
|
395
419
|
|
|
396
420
|
Kwargs:
|
|
397
421
|
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
|
|
422
|
+
max_retries (int) : Maximum number of times to retry setting the var if setting fails before raising an exception
|
|
398
423
|
"""
|
|
399
424
|
if not self.active_connection:
|
|
400
425
|
self.connect()
|
|
401
426
|
if intelligent_waits:
|
|
402
|
-
self._enforce_ratelimit(n=len(
|
|
427
|
+
self._enforce_ratelimit(n=len(var_value_dict))
|
|
403
428
|
|
|
404
|
-
self.
|
|
429
|
+
self.var_sets_since_first += len(var_value_dict)
|
|
405
430
|
|
|
406
431
|
packet_list = []
|
|
407
432
|
for variable in var_value_dict:
|
|
@@ -418,27 +443,27 @@ class BaseCloud(AnyCloud[Union[str, int]]):
|
|
|
418
443
|
"project_id": self.project_id,
|
|
419
444
|
}
|
|
420
445
|
packet_list.append(packet)
|
|
421
|
-
self._send_packet_list(packet_list)
|
|
446
|
+
self._send_packet_list(packet_list, max_retries=max_retries)
|
|
422
447
|
self.last_var_set = time.time()
|
|
423
448
|
|
|
424
|
-
def
|
|
425
|
-
var = "☁ "+var.removeprefix("☁ ")
|
|
449
|
+
def _assert_recorder_running(self, *, recorder_initial_values: dict[str, Any]) -> cloud_recorder.CloudRecorder:
|
|
426
450
|
if self.recorder is None:
|
|
427
451
|
self.recorder = cloud_recorder.CloudRecorder(self, initial_values=recorder_initial_values)
|
|
428
452
|
self.recorder.start()
|
|
429
453
|
start_time = time.time()
|
|
430
454
|
while not (self.recorder.cloud_values != {} or start_time < time.time() - 5):
|
|
431
455
|
time.sleep(0.01)
|
|
432
|
-
return self.recorder
|
|
456
|
+
return self.recorder
|
|
433
457
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
458
|
+
|
|
459
|
+
def get_var(self, var, *, recorder_initial_values: Optional[dict[str, Any]] = None):
|
|
460
|
+
var = "☁ "+var.removeprefix("☁ ")
|
|
461
|
+
recorder = self._assert_recorder_running(recorder_initial_values=recorder_initial_values or {})
|
|
462
|
+
return recorder.get_var(var)
|
|
463
|
+
|
|
464
|
+
def get_all_vars(self, *, recorder_initial_values: Optional[dict[str, Any]] = None):
|
|
465
|
+
recorder = self._assert_recorder_running(recorder_initial_values=recorder_initial_values or {})
|
|
466
|
+
return recorder.get_all_vars()
|
|
442
467
|
|
|
443
468
|
def create_event_stream(self):
|
|
444
469
|
if self.event_stream:
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
"""v2 ready: ScratchCloud, TwCloud and CustomCloud classes"""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
|
|
4
5
|
import warnings
|
|
6
|
+
from typing import Optional, Any
|
|
5
7
|
|
|
6
8
|
from websocket import WebSocketBadStatusException
|
|
7
9
|
|
|
8
10
|
from ._base import BaseCloud
|
|
9
|
-
from typing import Type
|
|
10
11
|
from scratchattach.utils.requests import requests
|
|
11
12
|
from scratchattach.utils import exceptions, commons
|
|
12
13
|
from scratchattach.site import cloud_activity
|
|
13
14
|
|
|
14
|
-
|
|
15
15
|
class ScratchCloud(BaseCloud):
|
|
16
16
|
def __init__(self, *, project_id, _session=None):
|
|
17
17
|
super().__init__()
|
|
@@ -32,15 +32,15 @@ class ScratchCloud(BaseCloud):
|
|
|
32
32
|
try:
|
|
33
33
|
super().connect()
|
|
34
34
|
except WebSocketBadStatusException as e:
|
|
35
|
-
raise exceptions.CloudConnectionError(
|
|
35
|
+
raise exceptions.CloudConnectionError("Error: Scratch's Cloud system may be down. Please try again later.") from e
|
|
36
36
|
|
|
37
|
-
def set_var(self, variable, value):
|
|
37
|
+
def set_var(self, variable, value, *, max_retries : int = 2):
|
|
38
38
|
self._assert_auth() # Setting a cloud var requires a login to the Scratch website
|
|
39
|
-
super().set_var(variable, value)
|
|
40
|
-
|
|
41
|
-
def set_vars(self, var_value_dict, *, intelligent_waits=True):
|
|
39
|
+
super().set_var(variable, value, max_retries=max_retries)
|
|
40
|
+
|
|
41
|
+
def set_vars(self, var_value_dict, *, intelligent_waits=True, max_retries : int = 2):
|
|
42
42
|
self._assert_auth()
|
|
43
|
-
super().set_vars(var_value_dict, intelligent_waits=intelligent_waits)
|
|
43
|
+
super().set_vars(var_value_dict, intelligent_waits=intelligent_waits, max_retries=max_retries)
|
|
44
44
|
|
|
45
45
|
def logs(self, *, filter_by_var_named=None, limit=100, offset=0) -> list[cloud_activity.CloudActivity]:
|
|
46
46
|
"""
|
|
@@ -63,7 +63,7 @@ class ScratchCloud(BaseCloud):
|
|
|
63
63
|
except Exception as e:
|
|
64
64
|
raise exceptions.FetchError(str(e))
|
|
65
65
|
|
|
66
|
-
def get_var(self, var, *, use_logs=False):
|
|
66
|
+
def get_var(self, var, *, recorder_initial_values: Optional[dict[str, Any]] = None, use_logs=False):
|
|
67
67
|
var = var.removeprefix("☁ ")
|
|
68
68
|
if self._session is None or use_logs:
|
|
69
69
|
filtered = self.logs(limit=100, filter_by_var_named="☁ "+var)
|
|
@@ -75,9 +75,9 @@ class ScratchCloud(BaseCloud):
|
|
|
75
75
|
initial_values = self.get_all_vars(use_logs=True)
|
|
76
76
|
return super().get_var("☁ "+var, recorder_initial_values=initial_values)
|
|
77
77
|
else:
|
|
78
|
-
return super().get_var("☁ "+var)
|
|
78
|
+
return super().get_var("☁ "+var, recorder_initial_values=recorder_initial_values)
|
|
79
79
|
|
|
80
|
-
def get_all_vars(self, *, use_logs=False):
|
|
80
|
+
def get_all_vars(self, *, recorder_initial_values: Optional[dict[str, Any]] = None, use_logs=False):
|
|
81
81
|
if self._session is None or use_logs:
|
|
82
82
|
logs = self.logs(limit=100)
|
|
83
83
|
logs.reverse()
|
|
@@ -90,7 +90,7 @@ class ScratchCloud(BaseCloud):
|
|
|
90
90
|
initial_values = self.get_all_vars(use_logs=True)
|
|
91
91
|
return super().get_all_vars(recorder_initial_values=initial_values)
|
|
92
92
|
else:
|
|
93
|
-
return super().get_all_vars()
|
|
93
|
+
return super().get_all_vars(recorder_initial_values=recorder_initial_values)
|
|
94
94
|
|
|
95
95
|
def events(self, *, use_logs=False):
|
|
96
96
|
if self._session is None or use_logs:
|
|
@@ -134,7 +134,7 @@ class CustomCloud(BaseCloud):
|
|
|
134
134
|
# If even more customization is needed, the developer can create a class inheriting from cloud._base.BaseCloud to override functions like .set_var etc.
|
|
135
135
|
|
|
136
136
|
|
|
137
|
-
def get_cloud(project_id, *, CloudClass:
|
|
137
|
+
def get_cloud(project_id, *, CloudClass: type[BaseCloud] = ScratchCloud) -> BaseCloud:
|
|
138
138
|
"""
|
|
139
139
|
Connects to a cloud (by default Scratch's cloud) as logged out user.
|
|
140
140
|
|
|
@@ -153,8 +153,8 @@ def get_cloud(project_id, *, CloudClass:Type[BaseCloud]=ScratchCloud) -> BaseClo
|
|
|
153
153
|
Type[scratchattach.cloud._base.BaseCloud]: An object representing the cloud of a project. Can be of any class inheriting from BaseCloud.
|
|
154
154
|
"""
|
|
155
155
|
warnings.warn(
|
|
156
|
-
"
|
|
157
|
-
exceptions.
|
|
156
|
+
"To set Scratch cloud variables, use session.connect_cloud instead of get_cloud",
|
|
157
|
+
exceptions.CloudAuthenticationWarning
|
|
158
158
|
)
|
|
159
159
|
return CloudClass(project_id=project_id)
|
|
160
160
|
|
|
@@ -171,7 +171,7 @@ def get_scratch_cloud(project_id):
|
|
|
171
171
|
"""
|
|
172
172
|
warnings.warn(
|
|
173
173
|
"To set Scratch cloud variables, use session.connect_scratch_cloud instead of get_scratch_cloud",
|
|
174
|
-
exceptions.
|
|
174
|
+
exceptions.CloudAuthenticationWarning
|
|
175
175
|
)
|
|
176
176
|
return ScratchCloud(project_id=project_id)
|
|
177
177
|
|
|
@@ -3,9 +3,10 @@ scratchattach.editor (sbeditor v2) - a library for all things sb3
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from .asset import Asset, Costume, Sound
|
|
6
|
+
from .blockshape import BlockShapes
|
|
6
7
|
from .project import Project
|
|
7
8
|
from .extension import Extensions, Extension
|
|
8
|
-
from .mutation import Mutation, Argument, parse_proc_code
|
|
9
|
+
from .mutation import Mutation, Argument, ArgumentType, parse_proc_code, construct_proccode, ArgTypes
|
|
9
10
|
from .meta import Meta, set_meta_platform
|
|
10
11
|
from .sprite import Sprite
|
|
11
12
|
from .block import Block
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
|
-
from hashlib import md5
|
|
4
|
+
from hashlib import md5, sha256
|
|
5
5
|
import requests
|
|
6
6
|
|
|
7
7
|
from . import base, commons, sprite, build_defaulting
|
|
@@ -11,21 +11,22 @@ from typing import Optional
|
|
|
11
11
|
@dataclass(init=True, repr=True)
|
|
12
12
|
class AssetFile:
|
|
13
13
|
"""
|
|
14
|
-
Represents the file information for an asset
|
|
14
|
+
Represents the file information for an asset (not the asset metdata)
|
|
15
15
|
- stores the filename, data, and md5 hash
|
|
16
16
|
"""
|
|
17
17
|
filename: str
|
|
18
|
-
_data: bytes = field(repr=False,
|
|
18
|
+
_data: Optional[bytes] = field(repr=False, default=None)
|
|
19
19
|
_md5: str = field(repr=False, default_factory=str)
|
|
20
20
|
|
|
21
21
|
@property
|
|
22
|
-
def data(self):
|
|
22
|
+
def data(self) -> bytes:
|
|
23
23
|
"""
|
|
24
24
|
Return the contents of the asset file, as bytes
|
|
25
25
|
"""
|
|
26
26
|
if self._data is None:
|
|
27
27
|
# Download and cache
|
|
28
28
|
rq = requests.get(f"https://assets.scratch.mit.edu/internalapi/asset/{self.filename}/get/")
|
|
29
|
+
# print(f"Downloaded {url}")
|
|
29
30
|
if rq.status_code != 200:
|
|
30
31
|
raise ValueError(f"Can't download asset {self.filename}\nIs not uploaded to scratch! Response: {rq.text}")
|
|
31
32
|
|
|
@@ -33,16 +34,23 @@ class AssetFile:
|
|
|
33
34
|
|
|
34
35
|
return self._data
|
|
35
36
|
|
|
37
|
+
@data.setter
|
|
38
|
+
def data(self, data: bytes):
|
|
39
|
+
self._data = data
|
|
40
|
+
|
|
36
41
|
@property
|
|
37
|
-
def md5(self):
|
|
42
|
+
def md5(self) -> str:
|
|
38
43
|
"""
|
|
39
|
-
Compute/retrieve the md5
|
|
44
|
+
Compute/retrieve the md5 hex-digest of the asset file data
|
|
40
45
|
"""
|
|
41
46
|
if self._md5 is None:
|
|
42
47
|
self._md5 = md5(self.data).hexdigest()
|
|
43
48
|
|
|
44
49
|
return self._md5
|
|
45
50
|
|
|
51
|
+
@property
|
|
52
|
+
def sha256(self) -> str:
|
|
53
|
+
return sha256(self.data).hexdigest()
|
|
46
54
|
|
|
47
55
|
class Asset(base.SpriteSubComponent):
|
|
48
56
|
def __init__(self,
|
|
@@ -50,7 +58,7 @@ class Asset(base.SpriteSubComponent):
|
|
|
50
58
|
file_name: str = "b7853f557e4426412e64bb3da6531a99.svg",
|
|
51
59
|
_sprite: commons.SpriteInput = build_defaulting.SPRITE_DEFAULT):
|
|
52
60
|
"""
|
|
53
|
-
Represents a generic asset. Can be a sound or
|
|
61
|
+
Represents a generic asset, with metadata. Can be a sound or a costume.
|
|
54
62
|
https://en.scratch-wiki.info/wiki/Scratch_File_Format#Assets
|
|
55
63
|
"""
|
|
56
64
|
try:
|
|
@@ -71,14 +79,14 @@ class Asset(base.SpriteSubComponent):
|
|
|
71
79
|
@property
|
|
72
80
|
def folder(self):
|
|
73
81
|
"""
|
|
74
|
-
Get the folder name of this asset, based on the asset name. Uses the
|
|
82
|
+
Get the folder name of this asset, based on the asset name. Uses the TurboWarp syntax
|
|
75
83
|
"""
|
|
76
84
|
return commons.get_folder_name(self.name)
|
|
77
85
|
|
|
78
86
|
@property
|
|
79
87
|
def name_nfldr(self):
|
|
80
88
|
"""
|
|
81
|
-
Get the asset name after removing the folder name
|
|
89
|
+
Get the asset name after removing the folder name. Uses the TurboWarp syntax
|
|
82
90
|
"""
|
|
83
91
|
return commons.get_name_nofldr(self.name)
|
|
84
92
|
|
|
@@ -95,14 +103,16 @@ class Asset(base.SpriteSubComponent):
|
|
|
95
103
|
"""
|
|
96
104
|
Get the exact file name, as it would be within an sb3 file
|
|
97
105
|
equivalent to the md5ext value using in scratch project JSON
|
|
106
|
+
|
|
107
|
+
(alias for file_name)
|
|
98
108
|
"""
|
|
99
109
|
return self.file_name
|
|
100
110
|
|
|
101
111
|
@property
|
|
102
112
|
def parent(self):
|
|
103
113
|
"""
|
|
104
|
-
Return the project that this asset is attached to. If there is no attached project,
|
|
105
|
-
try returning the attached sprite
|
|
114
|
+
Return the project (body) that this asset is attached to. If there is no attached project,
|
|
115
|
+
try returning the attached sprite instead.
|
|
106
116
|
"""
|
|
107
117
|
if self.project is None:
|
|
108
118
|
return self.sprite
|
|
@@ -194,8 +204,8 @@ class Costume(Asset):
|
|
|
194
204
|
|
|
195
205
|
bitmap_resolution = data.get("bitmapResolution")
|
|
196
206
|
|
|
197
|
-
rotation_center_x = data
|
|
198
|
-
rotation_center_y = data
|
|
207
|
+
rotation_center_x = data.get("rotationCenterX", 0)
|
|
208
|
+
rotation_center_y = data.get("rotationCenterY", 0)
|
|
199
209
|
return Costume(_asset_load.name, _asset_load.file_name,
|
|
200
210
|
|
|
201
211
|
bitmap_resolution, rotation_center_x, rotation_center_y)
|
|
@@ -206,10 +216,12 @@ class Costume(Asset):
|
|
|
206
216
|
"""
|
|
207
217
|
_json = super().to_json()
|
|
208
218
|
_json.update({
|
|
209
|
-
"bitmapResolution": self.bitmap_resolution,
|
|
210
219
|
"rotationCenterX": self.rotation_center_x,
|
|
211
220
|
"rotationCenterY": self.rotation_center_y
|
|
212
221
|
})
|
|
222
|
+
if self.bitmap_resolution is not None:
|
|
223
|
+
_json["bitmapResolution"] = self.bitmap_resolution
|
|
224
|
+
|
|
213
225
|
return _json
|
|
214
226
|
|
|
215
227
|
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Module to deal with the backpack's weird JSON format, by overriding with new load methods
|
|
2
|
+
Module to deal with the backpack's weird JSON format, by overriding editor classes with new load methods
|
|
3
3
|
"""
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
from . import block, prim, field, inputs, mutation, sprite
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def parse_prim_fields(_fields: dict[str]) -> tuple[str | None, str | None, str | None]:
|
|
9
|
+
def parse_prim_fields(_fields: dict[str, dict[str, str]]) -> tuple[str | None, str | None, str | None]:
|
|
10
10
|
"""
|
|
11
11
|
Function for reading the fields in a backpack **primitive**
|
|
12
12
|
"""
|
|
13
13
|
for key, value in _fields.items():
|
|
14
|
-
key: str
|
|
15
|
-
value: dict[str, str]
|
|
16
14
|
prim_value, prim_name, prim_id = (None,) * 3
|
|
17
15
|
if key == "NUM":
|
|
18
16
|
prim_value = value.get("value")
|
|
@@ -103,7 +101,7 @@ def load_script(_script_data: list[dict]) -> sprite.Sprite:
|
|
|
103
101
|
"""
|
|
104
102
|
Loads a script into a sprite from the backpack JSON format
|
|
105
103
|
:param _script_data: Backpack script JSON data
|
|
106
|
-
:return: a
|
|
104
|
+
:return: a Sprite object containing the script
|
|
107
105
|
"""
|
|
108
106
|
# Using a sprite since it simplifies things, e.g. local global loading
|
|
109
107
|
_blockchain = sprite.Sprite()
|
|
@@ -52,7 +52,7 @@ class JSONSerializable(Base, ABC):
|
|
|
52
52
|
|
|
53
53
|
def save_json(self, name: str = ''):
|
|
54
54
|
"""
|
|
55
|
-
Save a json
|
|
55
|
+
Save json to a file. Adds '.json' for you.
|
|
56
56
|
"""
|
|
57
57
|
data = self.to_json()
|
|
58
58
|
with open(f"{self.__class__.__name__.lower()}{name}.json", "w") as f:
|
|
@@ -92,9 +92,7 @@ class SpriteSubComponent(JSONSerializable, ABC):
|
|
|
92
92
|
sprite: module_sprite.Sprite
|
|
93
93
|
def __init__(self, _sprite: "commons.SpriteInput" = build_defaulting.SPRITE_DEFAULT):
|
|
94
94
|
if _sprite is build_defaulting.SPRITE_DEFAULT:
|
|
95
|
-
|
|
96
|
-
assert retrieved_sprite is not None, "You don't have any sprites."
|
|
97
|
-
_sprite = retrieved_sprite
|
|
95
|
+
_sprite = build_defaulting.current_sprite()
|
|
98
96
|
self.sprite = _sprite
|
|
99
97
|
|
|
100
98
|
@property
|