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.
Files changed (87) hide show
  1. cli/__about__.py +1 -0
  2. cli/__init__.py +26 -0
  3. cli/cmd/__init__.py +4 -0
  4. cli/cmd/group.py +127 -0
  5. cli/cmd/login.py +60 -0
  6. cli/cmd/profile.py +7 -0
  7. cli/cmd/sessions.py +5 -0
  8. cli/context.py +142 -0
  9. cli/db.py +66 -0
  10. cli/namespace.py +14 -0
  11. {scratchattach/cloud → cloud}/_base.py +112 -87
  12. {scratchattach/cloud → cloud}/cloud.py +16 -16
  13. {scratchattach/editor → editor}/__init__.py +2 -1
  14. {scratchattach/editor → editor}/asset.py +26 -14
  15. {scratchattach/editor → editor}/backpack_json.py +3 -5
  16. {scratchattach/editor → editor}/base.py +2 -4
  17. {scratchattach/editor → editor}/block.py +27 -22
  18. {scratchattach/editor → editor}/blockshape.py +1 -1
  19. {scratchattach/editor → editor}/build_defaulting.py +2 -2
  20. editor/commons.py +145 -0
  21. {scratchattach/editor → editor}/field.py +1 -1
  22. {scratchattach/editor → editor}/inputs.py +6 -3
  23. {scratchattach/editor → editor}/meta.py +10 -7
  24. {scratchattach/editor → editor}/monitor.py +10 -8
  25. {scratchattach/editor → editor}/mutation.py +68 -11
  26. {scratchattach/editor → editor}/pallete.py +1 -3
  27. {scratchattach/editor → editor}/prim.py +4 -0
  28. {scratchattach/editor → editor}/project.py +118 -16
  29. {scratchattach/editor → editor}/sprite.py +25 -15
  30. {scratchattach/editor → editor}/vlb.py +2 -2
  31. {scratchattach/eventhandlers → eventhandlers}/_base.py +1 -0
  32. {scratchattach/eventhandlers → eventhandlers}/cloud_events.py +26 -6
  33. {scratchattach/eventhandlers → eventhandlers}/cloud_recorder.py +4 -4
  34. {scratchattach/eventhandlers → eventhandlers}/cloud_requests.py +139 -54
  35. {scratchattach/eventhandlers → eventhandlers}/cloud_server.py +6 -3
  36. {scratchattach/eventhandlers → eventhandlers}/cloud_storage.py +1 -2
  37. eventhandlers/filterbot.py +163 -0
  38. other/other_apis.py +598 -0
  39. {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/METADATA +7 -11
  40. scratchattach-3.0.0b1.dist-info/RECORD +79 -0
  41. {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/WHEEL +1 -1
  42. scratchattach-3.0.0b1.dist-info/entry_points.txt +2 -0
  43. scratchattach-3.0.0b1.dist-info/top_level.txt +7 -0
  44. {scratchattach/site → site}/_base.py +32 -5
  45. site/activity.py +426 -0
  46. {scratchattach/site → site}/alert.py +4 -5
  47. {scratchattach/site → site}/backpack_asset.py +2 -1
  48. {scratchattach/site → site}/classroom.py +80 -73
  49. {scratchattach/site → site}/cloud_activity.py +43 -29
  50. {scratchattach/site → site}/comment.py +86 -100
  51. {scratchattach/site → site}/forum.py +8 -4
  52. site/placeholder.py +132 -0
  53. {scratchattach/site → site}/project.py +228 -122
  54. {scratchattach/site → site}/session.py +156 -71
  55. {scratchattach/site → site}/studio.py +139 -46
  56. site/typed_dicts.py +151 -0
  57. {scratchattach/site → site}/user.py +511 -215
  58. {scratchattach/utils → utils}/commons.py +12 -4
  59. {scratchattach/utils → utils}/encoder.py +7 -4
  60. {scratchattach/utils → utils}/enums.py +1 -0
  61. {scratchattach/utils → utils}/exceptions.py +36 -2
  62. utils/optional_async.py +154 -0
  63. utils/requests.py +306 -0
  64. scratchattach/__init__.py +0 -29
  65. scratchattach/editor/commons.py +0 -273
  66. scratchattach/eventhandlers/filterbot.py +0 -161
  67. scratchattach/other/other_apis.py +0 -284
  68. scratchattach/site/activity.py +0 -382
  69. scratchattach/utils/requests.py +0 -93
  70. scratchattach-2.1.15b0.dist-info/RECORD +0 -66
  71. scratchattach-2.1.15b0.dist-info/top_level.txt +0 -1
  72. {scratchattach/cloud → cloud}/__init__.py +0 -0
  73. {scratchattach/editor → editor}/code_translation/__init__.py +0 -0
  74. {scratchattach/editor → editor}/code_translation/parse.py +0 -0
  75. {scratchattach/editor → editor}/comment.py +0 -0
  76. {scratchattach/editor → editor}/extension.py +0 -0
  77. {scratchattach/editor → editor}/twconfig.py +0 -0
  78. {scratchattach/eventhandlers → eventhandlers}/__init__.py +0 -0
  79. {scratchattach/eventhandlers → eventhandlers}/combine.py +0 -0
  80. {scratchattach/eventhandlers → eventhandlers}/message_events.py +0 -0
  81. {scratchattach/other → other}/__init__.py +0 -0
  82. {scratchattach/other → other}/project_json_capabilities.py +0 -0
  83. {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
  84. {scratchattach/site → site}/__init__.py +0 -0
  85. {scratchattach/site → site}/browser_cookie3_stub.py +0 -0
  86. {scratchattach/site → site}/browser_cookies.py +0 -0
  87. {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
- var_stets_since_first: int
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={}) -> T:
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={}) -> dict[str, T]:
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] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"],
101
- respond_order="receive", debug: bool = False) -> CloudRequests:
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] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]) -> CloudStorage:
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.var_stets_since_first = 0
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 _send_packet(self, packet):
302
+ def _send_recursive(self, data : str, *, current_depth: int = 0, max_depth=0):
252
303
  try:
253
- self.websocket.send(json.dumps(packet) + "\n")
304
+ self.websocket.send(data)
254
305
  except Exception:
255
- time.sleep(0.1)
256
- self.connect()
257
- time.sleep(0.1)
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(0.2)
264
- try:
265
- self.websocket.send(json.dumps(packet) + "\n")
266
- except Exception:
267
- time.sleep(1.6)
268
- self.connect()
269
- time.sleep(1.4)
270
- try:
271
- self.websocket.send(json.dumps(packet) + "\n")
272
- except Exception:
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
- try:
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.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
352
- self.var_stets_since_first = 0
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
- while self.last_var_set + wait_time >= time.time():
359
- time.sleep(0.001)
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.var_stets_since_first += 1
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(list(var_value_dict.keys())))
427
+ self._enforce_ratelimit(n=len(var_value_dict))
403
428
 
404
- self.var_stets_since_first += len(list(var_value_dict.keys()))
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 get_var(self, var, *, recorder_initial_values={}):
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.get_var(var)
456
+ return self.recorder
433
457
 
434
- def get_all_vars(self, *, recorder_initial_values={}):
435
- if self.recorder is None:
436
- self.recorder = cloud_recorder.CloudRecorder(self, initial_values=recorder_initial_values)
437
- self.recorder.start()
438
- start_time = time.time()
439
- while not (self.recorder.cloud_values != {} or start_time < time.time() - 5):
440
- time.sleep(0.01)
441
- return self.recorder.get_all_vars()
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(f"Error: Scratch's Cloud system may be down. Please try again later.") from e
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:Type[BaseCloud]=ScratchCloud) -> BaseCloud:
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
- "Warning: To set Scratch cloud variables, use session.connect_cloud instead of get_cloud",
157
- exceptions.AnonymousSiteComponentWarning
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.AnonymousSiteComponentWarning
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, default_factory=bytes)
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 hash value of the asset file data
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 an image.
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 turbowarp syntax
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["rotationCenterX"]
198
- rotation_center_y = data["rotationCenterY"]
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 blockchain object containing the script
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 file
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
- retrieved_sprite = build_defaulting.current_sprite()
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