scratchattach 3.0.0b0__py3-none-any.whl → 3.0.0b2__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 (80) hide show
  1. scratchattach/cli/__about__.py +1 -0
  2. scratchattach/cli/__init__.py +26 -0
  3. scratchattach/cli/cmd/__init__.py +4 -0
  4. scratchattach/cli/cmd/group.py +127 -0
  5. scratchattach/cli/cmd/login.py +60 -0
  6. scratchattach/cli/cmd/profile.py +7 -0
  7. scratchattach/cli/cmd/sessions.py +5 -0
  8. scratchattach/cli/context.py +142 -0
  9. scratchattach/cli/db.py +66 -0
  10. scratchattach/cli/namespace.py +14 -0
  11. scratchattach/cloud/__init__.py +2 -0
  12. scratchattach/cloud/_base.py +483 -0
  13. scratchattach/cloud/cloud.py +183 -0
  14. scratchattach/editor/__init__.py +22 -0
  15. scratchattach/editor/asset.py +265 -0
  16. scratchattach/editor/backpack_json.py +115 -0
  17. scratchattach/editor/base.py +191 -0
  18. scratchattach/editor/block.py +584 -0
  19. scratchattach/editor/blockshape.py +357 -0
  20. scratchattach/editor/build_defaulting.py +51 -0
  21. scratchattach/editor/code_translation/__init__.py +0 -0
  22. scratchattach/editor/code_translation/parse.py +177 -0
  23. scratchattach/editor/comment.py +80 -0
  24. scratchattach/editor/commons.py +145 -0
  25. scratchattach/editor/extension.py +50 -0
  26. scratchattach/editor/field.py +99 -0
  27. scratchattach/editor/inputs.py +138 -0
  28. scratchattach/editor/meta.py +117 -0
  29. scratchattach/editor/monitor.py +185 -0
  30. scratchattach/editor/mutation.py +381 -0
  31. scratchattach/editor/pallete.py +88 -0
  32. scratchattach/editor/prim.py +174 -0
  33. scratchattach/editor/project.py +381 -0
  34. scratchattach/editor/sprite.py +609 -0
  35. scratchattach/editor/twconfig.py +114 -0
  36. scratchattach/editor/vlb.py +134 -0
  37. scratchattach/eventhandlers/__init__.py +0 -0
  38. scratchattach/eventhandlers/_base.py +101 -0
  39. scratchattach/eventhandlers/cloud_events.py +130 -0
  40. scratchattach/eventhandlers/cloud_recorder.py +26 -0
  41. scratchattach/eventhandlers/cloud_requests.py +544 -0
  42. scratchattach/eventhandlers/cloud_server.py +249 -0
  43. scratchattach/eventhandlers/cloud_storage.py +135 -0
  44. scratchattach/eventhandlers/combine.py +30 -0
  45. scratchattach/eventhandlers/filterbot.py +163 -0
  46. scratchattach/eventhandlers/message_events.py +42 -0
  47. scratchattach/other/__init__.py +0 -0
  48. scratchattach/other/other_apis.py +598 -0
  49. scratchattach/other/project_json_capabilities.py +475 -0
  50. scratchattach/site/__init__.py +0 -0
  51. scratchattach/site/_base.py +93 -0
  52. scratchattach/site/activity.py +426 -0
  53. scratchattach/site/alert.py +226 -0
  54. scratchattach/site/backpack_asset.py +119 -0
  55. scratchattach/site/browser_cookie3_stub.py +17 -0
  56. scratchattach/site/browser_cookies.py +61 -0
  57. scratchattach/site/classroom.py +454 -0
  58. scratchattach/site/cloud_activity.py +121 -0
  59. scratchattach/site/comment.py +228 -0
  60. scratchattach/site/forum.py +436 -0
  61. scratchattach/site/placeholder.py +132 -0
  62. scratchattach/site/project.py +932 -0
  63. scratchattach/site/session.py +1323 -0
  64. scratchattach/site/studio.py +704 -0
  65. scratchattach/site/typed_dicts.py +151 -0
  66. scratchattach/site/user.py +1252 -0
  67. scratchattach/utils/__init__.py +0 -0
  68. scratchattach/utils/commons.py +263 -0
  69. scratchattach/utils/encoder.py +161 -0
  70. scratchattach/utils/enums.py +237 -0
  71. scratchattach/utils/exceptions.py +277 -0
  72. scratchattach/utils/optional_async.py +154 -0
  73. scratchattach/utils/requests.py +306 -0
  74. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/METADATA +1 -1
  75. scratchattach-3.0.0b2.dist-info/RECORD +81 -0
  76. scratchattach-3.0.0b0.dist-info/RECORD +0 -8
  77. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/WHEEL +0 -0
  78. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/entry_points.txt +0 -0
  79. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/licenses/LICENSE +0 -0
  80. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,483 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import ssl
5
+ import time
6
+ import warnings
7
+ from typing import Optional, Union, TypeVar, Generic, TYPE_CHECKING, Any
8
+ from abc import ABC, abstractmethod, ABCMeta
9
+ from threading import Lock
10
+ from collections.abc import Iterator
11
+
12
+ if TYPE_CHECKING:
13
+ from _typeshed import SupportsRead
14
+ else:
15
+ T = TypeVar("T")
16
+ class SupportsRead(ABC, Generic[T]):
17
+ @abstractmethod
18
+ def read(self) -> T:
19
+ pass
20
+
21
+
22
+ import websocket
23
+
24
+ from scratchattach.site import session
25
+ from scratchattach.eventhandlers import cloud_recorder
26
+ from scratchattach.utils import exceptions
27
+ from scratchattach.eventhandlers.cloud_requests import CloudRequests, RespondOrder
28
+ from scratchattach.eventhandlers.cloud_events import CloudEvents
29
+ from scratchattach.eventhandlers.cloud_storage import CloudStorage
30
+ from scratchattach.site import cloud_activity
31
+
32
+ class SupportsClose(ABC):
33
+ @abstractmethod
34
+ def close(self) -> None:
35
+ pass
36
+
37
+ T = TypeVar("T")
38
+
39
+
40
+ class EventStream(SupportsRead[Iterator[dict[str, Any]]], SupportsClose):
41
+ """
42
+ Allows you to stream events
43
+ """
44
+
45
+ class AnyCloud(ABC, Generic[T]):
46
+ """
47
+ Represents a cloud that is not necessarily using a websocket.
48
+ """
49
+ active_connection: bool
50
+ var_sets_since_first: int
51
+ _session: Optional[session.Session]
52
+
53
+ @abstractmethod
54
+ def connect(self):
55
+ pass
56
+
57
+ @abstractmethod
58
+ def disconnect(self):
59
+ pass
60
+
61
+ def reconnect(self):
62
+ self.disconnect()
63
+ time.sleep(0.1)
64
+ self.connect()
65
+
66
+ @abstractmethod
67
+ def _enforce_ratelimit(self, *, n: int) -> None:
68
+ pass
69
+
70
+ @abstractmethod
71
+ def set_var(self, variable: str, value: T, *, max_retries : int = 2) -> None:
72
+ """
73
+ Sets a cloud variable.
74
+
75
+ Args:
76
+ variable (str): The name of the cloud variable that should be set (provided without the cloud emoji)
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
81
+ """
82
+
83
+ @abstractmethod
84
+ def set_vars(self, var_value_dict: dict[str, T], *, intelligent_waits: bool = True, max_retries : int = 2):
85
+ """
86
+ Sets multiple cloud variables at once (works for an unlimited amount of variables).
87
+
88
+ Args:
89
+ var_value_dict (dict): variable:value dictionary with the variables / values to set. The dict should like this: {"var1":"value1", "var2":"value2", ...}
90
+
91
+ Kwargs:
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
94
+ """
95
+
96
+ @abstractmethod
97
+ def get_var(self, var, *, recorder_initial_values: Optional[dict[str, Any]] = None) -> T:
98
+ pass
99
+
100
+ @abstractmethod
101
+ def get_all_vars(self, *, recorder_initial_values: Optional[dict[str, Any]] = None) -> dict[str, T]:
102
+ pass
103
+
104
+ def events(self) -> CloudEvents:
105
+ return CloudEvents(self)
106
+
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"]
110
+ return CloudRequests(self, used_cloud_vars=used_cloud_vars, no_packet_loss=no_packet_loss,
111
+ respond_order=respond_order, debug=debug)
112
+
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"]
115
+ return CloudStorage(self, used_cloud_vars=used_cloud_vars, no_packet_loss=no_packet_loss)
116
+
117
+ @abstractmethod
118
+ def create_event_stream(self) -> EventStream:
119
+ pass
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
+
154
+ class WebSocketEventStream(EventStream):
155
+ packets_left: list[Union[str, bytes]]
156
+ source_cloud: BaseCloud
157
+ reading: Lock
158
+ def __init__(self, cloud: BaseCloud):
159
+ super().__init__()
160
+ self.source_cloud = type(cloud)(project_id=cloud.project_id)
161
+ self.source_cloud._session = cloud._session
162
+ self.source_cloud.cookie = cloud.cookie
163
+ self.source_cloud.header = cloud.header
164
+ self.source_cloud.origin = cloud.origin
165
+ self.source_cloud.username = cloud.username
166
+ self.source_cloud.ws_timeout = None # No timeout -> allows continous listening
167
+ self.reading = Lock()
168
+ try:
169
+ self.source_cloud.connect()
170
+ except exceptions.CloudConnectionError:
171
+ warnings.warn("Initial cloud connection attempt failed, retrying...", exceptions.UnexpectedWebsocketEventWarning)
172
+ self.packets_left = []
173
+
174
+ def receive_new(self, non_blocking: bool = False):
175
+ if non_blocking:
176
+ self.source_cloud.websocket.settimeout(0)
177
+ try:
178
+ received = self.source_cloud.websocket.recv().splitlines()
179
+ self.packets_left.extend(received)
180
+ except Exception:
181
+ pass
182
+ return
183
+ self.source_cloud.websocket.settimeout(None)
184
+ received = self.source_cloud.websocket.recv().splitlines()
185
+ self.packets_left.extend(received)
186
+
187
+ def read(self, amount: int = -1) -> Iterator[dict[str, Any]]:
188
+ i = 0
189
+ with self.reading:
190
+ try:
191
+ self.receive_new(amount != -1)
192
+ while (self.packets_left and amount == -1) or (amount != -1 and i < amount):
193
+ if not self.packets_left and amount != -1:
194
+ self.receive_new()
195
+ yield json.loads(self.packets_left.pop(0))
196
+ i += 1
197
+ except Exception:
198
+ self.source_cloud.reconnect()
199
+ self.receive_new(amount != -1)
200
+ while (self.packets_left and amount == -1) or (amount != -1 and i < amount):
201
+ if not self.packets_left and amount != -1:
202
+ self.receive_new()
203
+ yield json.loads(self.packets_left.pop(0))
204
+ i += 1
205
+
206
+ def close(self) -> None:
207
+ self.source_cloud.disconnect()
208
+
209
+ class BaseCloud(AnyCloud[Union[str, int]]):
210
+ """
211
+ Base class for a project's cloud variables. Represents a cloud.
212
+
213
+ When inheriting from this class, the __init__ function of the inherited class:
214
+ - must first call the constructor of the super class: super().__init__()
215
+ - must then set some attributes
216
+
217
+ Attributes that must be specified in the __init__ function of a class inheriting from this one:
218
+ project_id: Project id of the cloud variables
219
+
220
+ cloud_host: URL of the websocket server ("wss://..." or "ws://...")
221
+
222
+ Attributes that can, but don't have to be specified in the __init__ function:
223
+
224
+ _session: Either None or a scratchattach.site.session.Session object. Defaults to None.
225
+
226
+ ws_shortterm_ratelimit: The wait time between cloud variable sets. Defaults to 0.1
227
+
228
+ ws_longterm_ratelimit: The amount of cloud variable set that can be performed long-term without ever getting ratelimited
229
+
230
+ allow_non_numeric: Whether non-numeric cloud variable values are allowed. Defaults to False
231
+
232
+ length_limit: Length limit for cloud variable values. Defaults to 100000
233
+
234
+ username: The username to send during handshake. Defaults to "scratchattach"
235
+
236
+ header: The header to send. Defaults to None
237
+
238
+ cookie: The cookie to send. Defaults to None
239
+
240
+ origin: The origin to send. Defaults to None
241
+
242
+ print_connect_messages: Whether to print a message on every connect to the cloud server. Defaults to False.
243
+ """
244
+
245
+ _PACKET_FAILURE_SLEEPDURATIONS = (0.1, 0.2, 1.5)
246
+
247
+ project_id: Optional[Union[str, int]]
248
+ cloud_host: str
249
+ ws_shortterm_ratelimit: float
250
+ ws_longterm_ratelimit: float
251
+ allow_non_numeric: bool
252
+ length_limit: int
253
+ username: str
254
+ header: Optional[dict]
255
+ cookie: Optional[dict]
256
+ origin: Optional[str]
257
+ print_connect_message: bool
258
+ ws_timeout: Optional[int]
259
+ websocket: websocket.WebSocket
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
267
+
268
+ def __init__(self, *, project_id: Optional[Union[int, str]] = None, _session=None):
269
+
270
+ # Required internal attributes that every object representing a cloud needs to have (no matter what cloud is represented):
271
+ self._session = _session
272
+ self.active_connection = False #whether a connection to a cloud variable server is currently established
273
+
274
+ self.websocket = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE})
275
+ self.recorder = None # A CloudRecorder object that records cloud activity for the values to be retrieved later,
276
+ # which will be saved in this attribute as soon as .get_var is called
277
+ self.first_var_set = 0.0
278
+ self.last_var_set = 0.0
279
+ self.var_sets_since_first = 0
280
+
281
+ # Set default values for attributes that save configurations specific to the represented cloud:
282
+ # (These attributes can be specifically in the constructors of classes inheriting from this base class)
283
+ self.ws_shortterm_ratelimit = 0.06667
284
+ self.ws_longterm_ratelimit = 0.1
285
+ self.ws_timeout = 3 # Timeout for send operations (after the timeout,
286
+ # the connection will be renewed and the operation will be retried 3 times)
287
+ self.allow_non_numeric = False
288
+ self.length_limit = 100000
289
+ self.username = "scratchattach"
290
+ self.header = None
291
+ self.cookie = None
292
+ self.origin = None
293
+ self.print_connect_message = False
294
+
295
+ self.project_id = project_id
296
+
297
+ def _assert_auth(self):
298
+ if self._session is None:
299
+ raise exceptions.Unauthenticated(
300
+ "You need to use session.connect_cloud (NOT get_cloud) in order to perform this operation.")
301
+
302
+ def _send_recursive(self, data : str, *, current_depth: int = 0, max_depth=0):
303
+ try:
304
+ self.websocket.send(data)
305
+ except Exception:
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)
309
+ self.connect()
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):
320
+ packet_string = "".join([json.dumps(packet) + "\n" for packet in packet_list])
321
+ self._send_recursive(packet_string, max_depth=max_retries)
322
+
323
+ def _handshake(self):
324
+ packet = {"method": "handshake", "user": self.username, "project_id": self.project_id}
325
+ self._send_packet(packet)
326
+
327
+ def connect(self):
328
+ self.websocket = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE})
329
+ self.websocket.connect(
330
+ self.cloud_host,
331
+ cookie=self.cookie,
332
+ origin=self.origin,
333
+ enable_multithread=True,
334
+ timeout=self.ws_timeout,
335
+ header=self.header
336
+ )
337
+ self._handshake()
338
+ self.active_connection = True
339
+ if self.print_connect_message:
340
+ print("Connected to cloud server ", self.cloud_host)
341
+
342
+ def disconnect(self):
343
+ self.active_connection = False
344
+ if self.recorder is not None:
345
+ self.recorder.stop()
346
+ self.recorder.disconnect()
347
+ self.recorder = None
348
+ try:
349
+ self.websocket.close()
350
+ except Exception:
351
+ pass
352
+
353
+ def _assert_valid_value(self, value):
354
+ if not (value in [True, False, float('inf'), -float('inf')]):
355
+ value = str(value)
356
+ if len(value) > self.length_limit:
357
+ raise (exceptions.InvalidCloudValue(
358
+ f"Value exceeds length limit: {str(value)}"
359
+ ))
360
+ if not self.allow_non_numeric:
361
+ x = value.replace(".", "")
362
+ x = x.replace("-", "")
363
+ if not (x.isnumeric() or x == ""):
364
+ raise (exceptions.InvalidCloudValue(
365
+ "Value not numeric"
366
+ ))
367
+
368
+ def _enforce_ratelimit(self, *, n):
369
+ # n is the amount of variables being set
370
+ if (time.time() - self.first_var_set) / (
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
373
+ self.first_var_set = time.time()
374
+
375
+ wait_time = self.ws_shortterm_ratelimit * n
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
377
+ wait_time = self.ws_longterm_ratelimit * n
378
+ sleep_time = self.last_var_set + wait_time - time.time()
379
+ if sleep_time > 0:
380
+ time.sleep(sleep_time)
381
+
382
+ def set_var(self, variable, value, *, max_retries: int = 2):
383
+ """
384
+ Sets a cloud variable.
385
+
386
+ Args:
387
+ variable (str): The name of the cloud variable that should be set (provided without the cloud emoji)
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
392
+ """
393
+ self._assert_valid_value(value)
394
+ if not isinstance(variable, str):
395
+ raise ValueError("cloud var name must be a string")
396
+ variable = variable.removeprefix("☁ ")
397
+ if not self.active_connection:
398
+ self.connect()
399
+ self._enforce_ratelimit(n=1)
400
+
401
+ self.var_sets_since_first += 1
402
+
403
+ packet = {
404
+ "method": "set",
405
+ "name": "☁ " + variable,
406
+ "value": value,
407
+ "user": self.username,
408
+ "project_id": self.project_id,
409
+ }
410
+ self._send_packet(packet, max_retries=max_retries)
411
+ self.last_var_set = time.time()
412
+
413
+ def set_vars(self, var_value_dict, *, intelligent_waits=True, max_retries: int = 2):
414
+ """
415
+ Sets multiple cloud variables at once (works for an unlimited amount of variables).
416
+
417
+ Args:
418
+ var_value_dict (dict): variable:value dictionary with the variables / values to set. The dict should like this: {"var1":"value1", "var2":"value2", ...}
419
+
420
+ Kwargs:
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
423
+ """
424
+ if not self.active_connection:
425
+ self.connect()
426
+ if intelligent_waits:
427
+ self._enforce_ratelimit(n=len(var_value_dict))
428
+
429
+ self.var_sets_since_first += len(var_value_dict)
430
+
431
+ packet_list = []
432
+ for variable in var_value_dict:
433
+ value = var_value_dict[variable]
434
+ variable = variable.removeprefix("☁ ")
435
+ self._assert_valid_value(value)
436
+ if not isinstance(variable, str):
437
+ raise ValueError("cloud var name must be a string")
438
+ packet = {
439
+ "method": "set",
440
+ "name": "☁ " + variable,
441
+ "value": value,
442
+ "user": self.username,
443
+ "project_id": self.project_id,
444
+ }
445
+ packet_list.append(packet)
446
+ self._send_packet_list(packet_list, max_retries=max_retries)
447
+ self.last_var_set = time.time()
448
+
449
+ def _assert_recorder_running(self, *, recorder_initial_values: dict[str, Any]) -> cloud_recorder.CloudRecorder:
450
+ if self.recorder is None:
451
+ self.recorder = cloud_recorder.CloudRecorder(self, initial_values=recorder_initial_values)
452
+ self.recorder.start()
453
+ start_time = time.time()
454
+ while not (self.recorder.cloud_values != {} or start_time < time.time() - 5):
455
+ time.sleep(0.01)
456
+ return self.recorder
457
+
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()
467
+
468
+ def create_event_stream(self):
469
+ if self.event_stream:
470
+ raise ValueError("Cloud already has an event stream.")
471
+ self.event_stream = WebSocketEventStream(self)
472
+ return self.event_stream
473
+
474
+ class LogCloudMeta(ABCMeta):
475
+ def __instancecheck__(cls, instance) -> bool:
476
+ if hasattr(instance, "logs"):
477
+ return isinstance(instance, BaseCloud)
478
+ return False
479
+
480
+ class LogCloud(BaseCloud, metaclass=LogCloudMeta):
481
+ @abstractmethod
482
+ def logs(self, *, filter_by_var_named: Optional[str] = None, limit: int = 100, offset: int = 0) -> list[cloud_activity.CloudActivity]:
483
+ pass
@@ -0,0 +1,183 @@
1
+ """v2 ready: ScratchCloud, TwCloud and CustomCloud classes"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from typing import Optional, Any
7
+
8
+ from websocket import WebSocketBadStatusException
9
+
10
+ from ._base import BaseCloud
11
+ from scratchattach.utils.requests import requests
12
+ from scratchattach.utils import exceptions, commons
13
+ from scratchattach.site import cloud_activity
14
+
15
+ class ScratchCloud(BaseCloud):
16
+ def __init__(self, *, project_id, _session=None):
17
+ super().__init__()
18
+
19
+ self.project_id = project_id
20
+
21
+ # Configure this object's attributes specifically for being used with Scratch's cloud:
22
+ self.cloud_host = "wss://clouddata.scratch.mit.edu"
23
+ self.length_limit = 256
24
+ self._session = _session
25
+ if self._session is not None:
26
+ self.username = self._session.username
27
+ self.cookie = "scratchsessionsid=" + self._session.id + ";"
28
+ self.origin = "https://scratch.mit.edu"
29
+
30
+ def connect(self):
31
+ self._assert_auth() # Connecting to Scratch's cloud websocket requires a login to the Scratch website
32
+ try:
33
+ super().connect()
34
+ except WebSocketBadStatusException as e:
35
+ raise exceptions.CloudConnectionError("Error: Scratch's Cloud system may be down. Please try again later.") from e
36
+
37
+ def set_var(self, variable, value, *, max_retries : int = 2):
38
+ self._assert_auth() # Setting a cloud var requires a login to the Scratch website
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
+ self._assert_auth()
43
+ super().set_vars(var_value_dict, intelligent_waits=intelligent_waits, max_retries=max_retries)
44
+
45
+ def logs(self, *, filter_by_var_named=None, limit=100, offset=0) -> list[cloud_activity.CloudActivity]:
46
+ """
47
+ Gets the data from Scratch's clouddata logs.
48
+
49
+ Keyword Arguments:
50
+ filter_by_var_named (str or None): If you only want to get data for one cloud variable, set this argument to its name.
51
+ limit (int): Max. amount of returned activity.
52
+ offset (int): Offset of the first activity in the returned list.
53
+ log_url (str): If you want to get the clouddata from a cloud log API different to Scratch's normal cloud log API, set this argument to the URL of the API. Only set this argument if you know what you are doing. If you want to get the clouddata from the normal API, don't put this argument.
54
+ """
55
+ try:
56
+ data = requests.get(f"https://clouddata.scratch.mit.edu/logs?projectid={self.project_id}&limit={limit}&offset={offset}", timeout=10).json()
57
+ if filter_by_var_named is not None:
58
+ filter_by_var_named = filter_by_var_named.removeprefix("☁ ")
59
+ data = list(filter(lambda k: k["name"] == "☁ "+filter_by_var_named, data))
60
+ for x in data:
61
+ x["cloud"] = self
62
+ return commons.parse_object_list(data, cloud_activity.CloudActivity, self._session, "name")
63
+ except Exception as e:
64
+ raise exceptions.FetchError(str(e))
65
+
66
+ def get_var(self, var, *, recorder_initial_values: Optional[dict[str, Any]] = None, use_logs=False):
67
+ var = var.removeprefix("☁ ")
68
+ if self._session is None or use_logs:
69
+ filtered = self.logs(limit=100, filter_by_var_named="☁ "+var)
70
+ if len(filtered) == 0:
71
+ return None
72
+ return filtered[0].value
73
+ else:
74
+ if self.recorder is None:
75
+ initial_values = self.get_all_vars(use_logs=True)
76
+ return super().get_var("☁ "+var, recorder_initial_values=initial_values)
77
+ else:
78
+ return super().get_var("☁ "+var, recorder_initial_values=recorder_initial_values)
79
+
80
+ def get_all_vars(self, *, recorder_initial_values: Optional[dict[str, Any]] = None, use_logs=False):
81
+ if self._session is None or use_logs:
82
+ logs = self.logs(limit=100)
83
+ logs.reverse()
84
+ clouddata = {}
85
+ for activity in logs:
86
+ clouddata[activity.name] = activity.value
87
+ return clouddata
88
+ else:
89
+ if self.recorder is None:
90
+ initial_values = self.get_all_vars(use_logs=True)
91
+ return super().get_all_vars(recorder_initial_values=initial_values)
92
+ else:
93
+ return super().get_all_vars(recorder_initial_values=recorder_initial_values)
94
+
95
+ def events(self, *, use_logs=False):
96
+ if self._session is None or use_logs:
97
+ from scratchattach.eventhandlers.cloud_events import CloudLogEvents
98
+ return CloudLogEvents(self)
99
+ else:
100
+ return super().events()
101
+
102
+
103
+ class TwCloud(BaseCloud):
104
+ def __init__(self, *, project_id, cloud_host="wss://clouddata.turbowarp.org", purpose="", contact="",
105
+ _session=None):
106
+ super().__init__()
107
+
108
+ self.project_id = project_id
109
+
110
+ # Configure this object's attributes specifically for being used with TurboWarp's cloud:
111
+ self.cloud_host = cloud_host
112
+ self.ws_shortterm_ratelimit = 0 # TurboWarp doesn't enforce a wait time between cloud variable sets
113
+ self.ws_longterm_ratelimit = 0
114
+ self.length_limit = 100000 # TurboWarp doesn't enforce a cloud variable length
115
+ purpose_string = ""
116
+ if purpose != "" or contact != "":
117
+ purpose_string = f" (Purpose:{purpose}; Contact:{contact})"
118
+ self.header = {"User-Agent":f"scratchattach/2.0.0{purpose_string}"}
119
+
120
+ class CustomCloud(BaseCloud):
121
+
122
+ def __init__(self, *, project_id, cloud_host, **kwargs):
123
+ super().__init__()
124
+
125
+ self.project_id = project_id
126
+ self.cloud_host = cloud_host
127
+
128
+ # Configure this object's attributes specifically for the cloud that the developer wants to connect to:
129
+ # -> For this purpose, all additional keyword arguments (kwargs) will be set as attributes of the CustomCloud object
130
+ # This allows the maximum amount of attribute customization
131
+ # See the docstring for the cloud._base.BaseCloud class to find out what attributes can be set / specified as keyword args
132
+ self.__dict__.update(kwargs)
133
+
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
+
136
+
137
+ def get_cloud(project_id, *, CloudClass: type[BaseCloud] = ScratchCloud) -> BaseCloud:
138
+ """
139
+ Connects to a cloud (by default Scratch's cloud) as logged out user.
140
+
141
+ Warning:
142
+ Since this method doesn't connect a login / session to the returned object, setting Scratch cloud variables won't be possible with it.
143
+
144
+ To set Scratch cloud variables, use `scratchattach.site.session.Session.connect_scratch_cloud` instead.
145
+
146
+ Args:
147
+ project_id:
148
+
149
+ Keyword arguments:
150
+ CloudClass: The class that the returned object should be of. By default this class is scratchattach.cloud.ScratchCloud.
151
+
152
+ Returns:
153
+ Type[scratchattach.cloud._base.BaseCloud]: An object representing the cloud of a project. Can be of any class inheriting from BaseCloud.
154
+ """
155
+ warnings.warn(
156
+ "To set Scratch cloud variables, use session.connect_cloud instead of get_cloud",
157
+ exceptions.CloudAuthenticationWarning
158
+ )
159
+ return CloudClass(project_id=project_id)
160
+
161
+ def get_scratch_cloud(project_id):
162
+ """
163
+ Warning:
164
+ Since this method doesn't connect a login / session to the returned object, setting Scratch cloud variables won't be possible with it.
165
+
166
+ To set Scratch cloud variables, use `scratchattach.Session.connect_scratch_cloud` instead.
167
+
168
+
169
+ Returns:
170
+ scratchattach.cloud.ScratchCloud: An object representing the Scratch cloud of a project.
171
+ """
172
+ warnings.warn(
173
+ "To set Scratch cloud variables, use session.connect_scratch_cloud instead of get_scratch_cloud",
174
+ exceptions.CloudAuthenticationWarning
175
+ )
176
+ return ScratchCloud(project_id=project_id)
177
+
178
+ def get_tw_cloud(project_id, *, purpose="", contact="", cloud_host="wss://clouddata.turbowarp.org"):
179
+ """
180
+ Returns:
181
+ scratchattach.cloud.TwCloud: An object representing the TurboWarp cloud of a project.
182
+ """
183
+ return TwCloud(project_id=project_id, purpose=purpose, contact=contact, cloud_host=cloud_host)
@@ -0,0 +1,22 @@
1
+ """
2
+ scratchattach.editor (sbeditor v2) - a library for all things sb3
3
+ """
4
+
5
+ from .asset import Asset, Costume, Sound
6
+ from .blockshape import BlockShapes
7
+ from .project import Project
8
+ from .extension import Extensions, Extension
9
+ from .mutation import Mutation, Argument, ArgumentType, parse_proc_code, construct_proccode, ArgTypes
10
+ from .meta import Meta, set_meta_platform
11
+ from .sprite import Sprite
12
+ from .block import Block
13
+ from .prim import Prim, PrimTypes
14
+ from .backpack_json import load_script as load_script_from_backpack
15
+ from .twconfig import TWConfig, is_valid_twconfig
16
+ from .inputs import Input, ShadowStatuses
17
+ from .field import Field
18
+ from .vlb import Variable, List, Broadcast
19
+ from .comment import Comment
20
+ from .monitor import Monitor
21
+
22
+ from .build_defaulting import add_chain, add_comment, add_block