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,609 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import warnings
5
+ from io import BytesIO, TextIOWrapper
6
+ from typing import Optional, Any, BinaryIO
7
+ from zipfile import ZipFile
8
+ from typing import Iterable, TYPE_CHECKING
9
+ from . import base, project, vlb, asset, comment, prim, block, commons, build_defaulting
10
+ if TYPE_CHECKING:
11
+ from . import asset
12
+
13
+ class Sprite(base.ProjectSubcomponent, base.JSONExtractable):
14
+ _local_globals: list[base.NamedIDComponent]
15
+ asset_data: list[asset.AssetFile]
16
+ def __init__(self, is_stage: bool = False, name: str = '', _current_costume: int = 1, _layer_order: Optional[int] = None,
17
+ _volume: int = 100,
18
+ _broadcasts: Optional[list[vlb.Broadcast]] = None,
19
+ _variables: Optional[list[vlb.Variable]] = None, _lists: Optional[list[vlb.List]] = None,
20
+ _costumes: Optional[list[asset.Costume]] = None, _sounds: Optional[list[asset.Sound]] = None,
21
+ _comments: Optional[list[comment.Comment]] = None, _prims: Optional[dict[str, prim.Prim]] = None,
22
+ _blocks: Optional[dict[str, block.Block]] = None,
23
+ # Stage only:
24
+ _tempo: int | float = 60, _video_state: str = "off", _video_transparency: int | float = 50,
25
+ _text_to_speech_language: str = "en", _visible: bool = True,
26
+ # Sprite only:
27
+ _x: int | float = 0, _y: int | float = 0, _size: int | float = 100, _direction: int | float = 90,
28
+ _draggable: bool = False, _rotation_style: str = "all around",
29
+
30
+ *, _project: Optional[project.Project] = None):
31
+ """
32
+ Represents a sprite or the stage (known internally as a Target)
33
+ https://en.scratch-wiki.info/wiki/Scratch_File_Format#Targets
34
+ """
35
+ # Defaulting for list parameters
36
+ if _broadcasts is None:
37
+ _broadcasts = []
38
+ if _variables is None:
39
+ _variables = []
40
+ if _lists is None:
41
+ _lists = []
42
+ if _costumes is None:
43
+ _costumes = []
44
+ if _sounds is None:
45
+ _sounds = []
46
+ if _comments is None:
47
+ _comments = []
48
+ if _prims is None:
49
+ _prims = {}
50
+ if _blocks is None:
51
+ _blocks = {}
52
+
53
+ self.is_stage = is_stage
54
+ self.name = name
55
+ self.current_costume = _current_costume
56
+ self.layer_order = _layer_order
57
+ self.volume = _volume
58
+
59
+ self.broadcasts = _broadcasts
60
+ self.variables = _variables
61
+ self.lists = _lists
62
+ self._local_globals = []
63
+
64
+ self.costumes = _costumes
65
+ self.sounds = _sounds
66
+
67
+ self.comments = _comments
68
+ self.prims = _prims
69
+ self.blocks = _blocks
70
+
71
+ self.tempo = _tempo
72
+ self.video_state = _video_state
73
+ self.video_transparency = _video_transparency
74
+ self.text_to_speech_language = _text_to_speech_language
75
+ self.visible = _visible
76
+ self.x, self.y = _x, _y
77
+ self.size = _size
78
+ self.direction = _direction
79
+ self.draggable = _draggable
80
+ self.rotation_style = _rotation_style
81
+
82
+ self.asset_data = []
83
+
84
+ super().__init__(_project)
85
+
86
+ # Assign sprite
87
+ for iterable in (self.vlbs, self.comments, self.assets, self.prims.values(), self.blocks.values()):
88
+ for sub_component in iterable:
89
+ sub_component.sprite = self
90
+
91
+ def __repr__(self):
92
+ return f"Sprite<{self.name}>"
93
+
94
+ def __enter__(self):
95
+ build_defaulting.stack_add_sprite(self)
96
+ return self
97
+
98
+ def __exit__(self, exc_type, exc_val, exc_tb):
99
+ build_defaulting.pop_sprite(self)
100
+
101
+ def link_subcomponents(self):
102
+ self.link_prims()
103
+ self.link_blocks()
104
+ self.link_comments()
105
+ self.link_vlbs()
106
+
107
+ def link_vlbs(self):
108
+ for _vlb in self.vlbs:
109
+ _vlb.sprite = self
110
+
111
+ def link_prims(self):
112
+ """
113
+ Link primitives to corresponding VLB objects (requires project attribute)
114
+ """
115
+ for _prim in self.prims.values():
116
+ _prim.link_using_sprite()
117
+
118
+ def link_blocks(self):
119
+ """
120
+ Link blocks to sprite/to other blocks
121
+ """
122
+ for _block_id, _block in self.blocks.items():
123
+ _block.link_using_sprite()
124
+
125
+ def link_comments(self):
126
+ for _comment in self.comments:
127
+ _comment.link_using_sprite()
128
+
129
+ def add_local_global(self, _vlb: base.NamedIDComponent):
130
+ """
131
+ Add a global variable/list to this sprite (for when an overarching project/stage is not available)
132
+ """
133
+ self._local_globals.append(_vlb)
134
+ _vlb.sprite = self
135
+
136
+ def add_variable(self, _variable: vlb.Variable):
137
+ self.variables.append(_variable)
138
+ _variable.sprite = self
139
+
140
+ def add_list(self, _list: vlb.List):
141
+ self.lists.append(_list)
142
+ _list.sprite = self
143
+
144
+ def add_broadcast(self, _broadcast: vlb.Broadcast):
145
+ self.broadcasts.append(_broadcast)
146
+ _broadcast.sprite = self
147
+
148
+ def add_vlb(self, _vlb: base.NamedIDComponent):
149
+ if isinstance(_vlb, vlb.Variable):
150
+ self.add_variable(_vlb)
151
+ elif isinstance(_vlb, vlb.List):
152
+ self.add_list(_vlb)
153
+ elif isinstance(_vlb, vlb.Broadcast):
154
+ self.add_broadcast(_vlb)
155
+ else:
156
+ warnings.warn(f"Invalid 'VLB' {_vlb} of type: {type(_vlb)}")
157
+
158
+ def add_block(self, _block: block.Block | prim.Prim) -> block.Block | prim.Prim:
159
+ if _block.sprite is self:
160
+ if _block in self.blocks.values():
161
+ return _block
162
+
163
+ _block.sprite = self
164
+ new_id = self.new_id
165
+
166
+ if isinstance(_block, block.Block):
167
+ self.blocks[new_id] = _block
168
+ _block.id = new_id
169
+ _block.link_using_sprite()
170
+
171
+ elif isinstance(_block, prim.Prim):
172
+ self.prims[new_id] = _block
173
+ _block.link_using_sprite()
174
+
175
+ return _block
176
+
177
+ def add_chain(self, *chain: block.Block | prim.Prim) -> block.Block | prim.Prim:
178
+ """
179
+ Adds a list of blocks to the sprite **AND RETURNS THE FIRST BLOCK**
180
+ :param chain:
181
+ :return:
182
+ """
183
+ chain = tuple(chain)
184
+
185
+ _prev = self.add_block(chain[0])
186
+
187
+ for _block in chain[1:]:
188
+ if not isinstance(_prev, block.Block) or not isinstance(_block, block.Block):
189
+ continue
190
+ _prev = _prev.attach_block(_block)
191
+
192
+ return chain[0]
193
+
194
+ def add_comment(self, _comment: comment.Comment) -> comment.Comment:
195
+ _comment.sprite = self
196
+ if _comment.id is None:
197
+ _comment.id = self.new_id
198
+
199
+ self.comments.append(_comment)
200
+ _comment.link_using_sprite()
201
+
202
+ return _comment
203
+
204
+ def remove_block(self, _block: block.Block):
205
+ for key, val in self.blocks.items():
206
+ if val is _block:
207
+ del self.blocks[key]
208
+ return
209
+
210
+ @property
211
+ def folder(self):
212
+ return commons.get_folder_name(self.name)
213
+
214
+ @property
215
+ def name_nfldr(self):
216
+ return commons.get_name_nofldr(self.name)
217
+
218
+ @property
219
+ def vlbs(self) -> list[base.NamedIDComponent]:
220
+ """
221
+ :return: All vlbs associated with the sprite. No local globals are added
222
+ """
223
+ vlbs: list[base.NamedIDComponent] = []
224
+ vlbs.extend(self.variables)
225
+ vlbs.extend(self.lists)
226
+ vlbs.extend(self.broadcasts)
227
+ return vlbs
228
+
229
+ @property
230
+ def assets(self) -> list[asset.Costume | asset.Sound]:
231
+ return self.costumes + self.sounds
232
+
233
+ @property
234
+ def stage(self) -> Sprite:
235
+ return self.project.stage
236
+
237
+ @property
238
+ def new_id(self):
239
+ return commons.gen_id()
240
+
241
+ @staticmethod
242
+ def from_json(data: dict):
243
+ _is_stage = data["isStage"]
244
+ _name = data["name"]
245
+ _current_costume = data.get("currentCostume", 1)
246
+ _layer_order = data.get("layerOrder", 1)
247
+ _volume = data.get("volume", 100)
248
+
249
+ # Read VLB
250
+ def read_idcomponent(attr_name: str, cls: type[base.IDComponent]):
251
+ _vlbs = []
252
+ for _vlb_id, _vlb_data in data.get(attr_name, {}).items():
253
+ _vlbs.append(cls.from_json((_vlb_id, _vlb_data)))
254
+ return _vlbs
255
+
256
+ _variables = read_idcomponent("variables", vlb.Variable)
257
+ _lists = read_idcomponent("lists", vlb.List)
258
+ _broadcasts = read_idcomponent("broadcasts", vlb.Broadcast)
259
+
260
+ # Read assets
261
+ _costumes = []
262
+ for _costume_data in data.get("costumes", []):
263
+ _costumes.append(asset.Costume.from_json(_costume_data))
264
+ _sounds = []
265
+ for _sound_data in data.get("sounds", []):
266
+ _sounds.append(asset.Sound.from_json(_sound_data))
267
+
268
+ # Read comments
269
+ _comments = read_idcomponent("comments", comment.Comment)
270
+
271
+ # Read blocks/prims
272
+ _prims = {}
273
+ _blocks = {}
274
+ for _block_id, _block_data in data.get("blocks", {}).items():
275
+ if isinstance(_block_data, list):
276
+ # Prim
277
+ _prims[_block_id] = prim.Prim.from_json(_block_data)
278
+ else:
279
+ # Block
280
+ _blocks[_block_id] = block.Block.from_json(_block_data)
281
+
282
+ # Stage/sprite specific vars
283
+ _tempo, _video_state, _video_transparency, _text_to_speech_language = (None,) * 4
284
+ _visible, _x, _y, _size, _direction, _draggable, _rotation_style = (None,) * 7
285
+ if _is_stage:
286
+ _tempo = data.get("tempo", 0)
287
+ _video_state = data.get("videoState", "off")
288
+ _video_transparency = data.get("videoTransparency", 0)
289
+ _text_to_speech_language = data.get("textToSpeechLanguage", "en")
290
+ else:
291
+ _visible = data["visible"]
292
+ _x = data.get("x", 0)
293
+ _y = data.get("y", 0)
294
+ _size = data.get("size", 100)
295
+ _direction = data.get("direction", 90)
296
+ _draggable = data.get("draggable", False)
297
+ _rotation_style = data.get("rotationStyle", "all-around")
298
+
299
+ return Sprite(_is_stage, _name, _current_costume, _layer_order, _volume, _broadcasts, _variables, _lists,
300
+ _costumes,
301
+ _sounds, _comments, _prims, _blocks,
302
+
303
+ _tempo, _video_state, _video_transparency, _text_to_speech_language,
304
+ _visible, _x, _y, _size, _direction, _draggable, _rotation_style
305
+ )
306
+
307
+ def to_json(self) -> dict:
308
+ _json = {
309
+ "isStage": self.is_stage,
310
+ "name": self.name,
311
+ "currentCostume": self.current_costume,
312
+ "volume": self.volume,
313
+ "layerOrder": self.layer_order,
314
+
315
+ "variables": {_variable.id: _variable.to_json() for _variable in self.variables},
316
+ "lists": {_list.id: _list.to_json() for _list in self.lists},
317
+ "broadcasts": {_broadcast.id: _broadcast.to_json() for _broadcast in self.broadcasts},
318
+
319
+ "blocks": {_block_id: _block.to_json() for _block_id, _block in (self.blocks | self.prims).items()},
320
+ "comments": {_comment.id: _comment.to_json() for _comment in self.comments},
321
+
322
+ "costumes": [_costume.to_json() for _costume in self.costumes],
323
+ "sounds": [_sound.to_json() for _sound in self.sounds]
324
+ }
325
+
326
+ if self.is_stage:
327
+ _json.update({
328
+ "tempo": self.tempo,
329
+ "videoTransparency": self.video_transparency,
330
+ "videoState": self.video_state,
331
+ "textToSpeechLanguage": self.text_to_speech_language
332
+ })
333
+ else:
334
+ _json.update({
335
+ "visible": self.visible,
336
+
337
+ "x": self.x, "y": self.y,
338
+ "size": self.size,
339
+ "direction": self.direction,
340
+
341
+ "draggable": self.draggable,
342
+ "rotationStyle": self.rotation_style
343
+ })
344
+
345
+ return _json
346
+
347
+ # Finding/getting from list/dict attributes
348
+ def find_asset(self, value: str, by: str = "name", multiple: bool = False, a_type: Optional[type]=None) -> None | asset.Asset | asset.Sound | asset.Costume | list[asset.Asset | asset.Sound | asset.Costume]:
349
+ if a_type is None:
350
+ a_type = asset.Asset
351
+
352
+ _ret = []
353
+ by = by.lower()
354
+ for _asset in self.assets:
355
+ if not isinstance(_asset, a_type):
356
+ continue
357
+
358
+ # Defaulting
359
+ compare = getattr(_asset, by)
360
+
361
+ if compare == value:
362
+ if multiple:
363
+ _ret.append(_asset)
364
+ else:
365
+ return _asset
366
+
367
+ if multiple:
368
+ return _ret
369
+ return None
370
+
371
+ def find_variable(self, value: str, by: str = "name", multiple: bool = False) -> None | vlb.Variable | list[vlb.Variable]:
372
+ _ret = []
373
+ by = by.lower()
374
+ for _variable in self.variables + self._local_globals:
375
+ if not isinstance(_variable, vlb.Variable):
376
+ continue
377
+
378
+ if by == "id":
379
+ compare = _variable.id
380
+ else:
381
+ # Defaulting
382
+ compare = _variable.name
383
+ if compare == value:
384
+ if multiple:
385
+ _ret.append(_variable)
386
+ else:
387
+ return _variable
388
+ # Search in stage for global variables
389
+ if self.project:
390
+ if not self.is_stage:
391
+ if multiple:
392
+ _ret += self.stage.find_variable(value, by, True)
393
+ else:
394
+ return self.stage.find_variable(value, by)
395
+
396
+ if multiple:
397
+ return _ret
398
+ return None
399
+
400
+ def find_list(self, value: str, by: str = "name", multiple: bool = False) -> None | vlb.List | list[vlb.List]:
401
+ _ret = []
402
+ by = by.lower()
403
+ for _list in self.lists + self._local_globals:
404
+ if not isinstance(_list, vlb.List):
405
+ continue
406
+ if by == "id":
407
+ compare = _list.id
408
+ else:
409
+ # Defaulting
410
+ compare = _list.name
411
+ if compare == value:
412
+ if multiple:
413
+ _ret.append(_list)
414
+ else:
415
+ return _list
416
+ # Search in stage for global lists
417
+ if self.project:
418
+ if not self.is_stage:
419
+ if multiple:
420
+ _ret += self.stage.find_list(value, by, True)
421
+ else:
422
+ return self.stage.find_list(value, by)
423
+
424
+ if multiple:
425
+ return _ret
426
+ return None
427
+
428
+ def find_broadcast(self, value: str, by: str = "name", multiple: bool = False) -> None | vlb.Broadcast | list[
429
+ vlb.Broadcast]:
430
+ _ret = []
431
+ by = by.lower()
432
+ for _broadcast in self.broadcasts + self._local_globals:
433
+ if not isinstance(_broadcast, vlb.Broadcast):
434
+ continue
435
+ if by == "id":
436
+ compare = _broadcast.id
437
+ else:
438
+ # Defaulting
439
+ compare = _broadcast.name
440
+ if compare == value:
441
+ if multiple:
442
+ _ret.append(_broadcast)
443
+ else:
444
+ return _broadcast
445
+ # Search in stage for global broadcasts
446
+ if self.project:
447
+ if not self.is_stage:
448
+ if multiple:
449
+ _ret += self.stage.find_broadcast(value, by, True)
450
+ else:
451
+ return self.stage.find_broadcast(value, by)
452
+
453
+ if multiple:
454
+ return _ret
455
+ return None
456
+
457
+ def find_vlb(self, value: str, by: str = "name",
458
+ multiple: bool = False) -> vlb.Variable | vlb.List | vlb.Broadcast | list[
459
+ vlb.Variable | vlb.List | vlb.Broadcast]:
460
+ if multiple:
461
+ return self.find_variable(value, by, True) + \
462
+ self.find_list(value, by, True) + \
463
+ self.find_broadcast(value, by, True)
464
+ else:
465
+ _ret = self.find_variable(value, by)
466
+ if _ret is not None:
467
+ return _ret
468
+ _ret = self.find_list(value, by)
469
+ if _ret is not None:
470
+ return _ret
471
+ return self.find_broadcast(value, by)
472
+
473
+ def find_block(self, value: str | Any, by: str, multiple: bool = False) -> None | block.Block | prim.Prim | list[
474
+ block.Block | prim.Prim]:
475
+ _ret = []
476
+ by = by.lower()
477
+ for _block_id, _block in (self.blocks | self.prims).items():
478
+ _block: block.Block | prim.Prim
479
+
480
+ is_block = isinstance(_block, block.Block)
481
+ is_prim = isinstance(_block, prim.Prim)
482
+
483
+ compare = None
484
+ if by == "id":
485
+ compare = _block_id
486
+ elif by == "argument ids":
487
+ if is_prim:
488
+ continue
489
+
490
+ if _block.mutation is not None:
491
+ compare = _block.mutation.argument_ids
492
+ elif by == "opcode":
493
+ if is_prim:
494
+ continue
495
+
496
+ # Defaulting
497
+ compare = _block.opcode
498
+ else:
499
+ if is_block:
500
+ compare = _block.opcode
501
+ else:
502
+ compare = _block.value
503
+
504
+ if compare == value:
505
+ if multiple:
506
+ _ret.append(_block)
507
+ else:
508
+ return _block
509
+ # Search in stage for global variables
510
+ if self.project:
511
+ if not self.is_stage:
512
+ if multiple:
513
+ _ret += self.stage.find_block(value, by, True)
514
+ else:
515
+ return self.stage.find_block(value, by)
516
+
517
+ if multiple:
518
+ return _ret
519
+ return None
520
+
521
+ def export(self, fp: Optional[str] = None, *, export_as_zip: bool = True):
522
+ if fp is None:
523
+ fp = commons.sanitize_fn(f"{self.name}.sprite3")
524
+
525
+ data = self.to_json()
526
+
527
+ if export_as_zip:
528
+ with ZipFile(fp, "w") as archive:
529
+ for _asset in self.assets:
530
+ asset_file = _asset.asset_file
531
+ if asset_file.filename not in archive.namelist():
532
+ archive.writestr(asset_file.filename, asset_file.data)
533
+
534
+ archive.writestr("sprite.json", json.dumps(data))
535
+ else:
536
+ with open(fp, "w") as json_file:
537
+ json.dump(data, json_file)
538
+
539
+ @property
540
+ def all_ids(self):
541
+ ret = []
542
+ for _vlb in self.vlbs + self._local_globals:
543
+ ret.append(_vlb.id)
544
+ for iterator in self.blocks.keys(), self.prims.keys():
545
+ ret += list(iterator)
546
+
547
+ return ret
548
+ @staticmethod
549
+ def load_json(data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool = True, _name: Optional[str] = None):
550
+ _dir_for_name = None
551
+
552
+ if _name is None:
553
+ if hasattr(data, "name"):
554
+ _dir_for_name = data.name
555
+
556
+ if not isinstance(_name, str) and _name is not None:
557
+ _name = str(_name)
558
+
559
+ if isinstance(data, bytes):
560
+ data = BytesIO(data)
561
+
562
+ elif isinstance(data, str):
563
+ _dir_for_name = data
564
+ data = open(data, "rb")
565
+
566
+ if _name is None and _dir_for_name is not None:
567
+ # Remove any directory names and the file extension
568
+ _name = _dir_for_name.split('/')[-1]
569
+ _name = '.'.join(_name.split('.')[:-1])
570
+
571
+ asset_data = []
572
+ with data:
573
+ # For if the sprite3 is just JSON (e.g. if it's exported from scratchattach)
574
+ if commons.is_valid_json(data):
575
+ json_str = data
576
+
577
+ else:
578
+ with ZipFile(data) as archive:
579
+ json_str = archive.read("sprite.json")
580
+
581
+ # Also load assets
582
+ if load_assets:
583
+
584
+ for filename in archive.namelist():
585
+ if filename != "sprite.json":
586
+ md5_hash = filename.split('.')[0]
587
+
588
+ asset_data.append(
589
+ asset.AssetFile(filename, archive.read(filename), md5_hash)
590
+ )
591
+ else:
592
+ warnings.warn(
593
+ "Loading sb3 without loading assets. When exporting the project, there may be errors due to assets not being uploaded to the Scratch website")
594
+
595
+ return _name, asset_data, json_str
596
+
597
+ @classmethod
598
+ def from_sprite3(cls, data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool = True, _name: Optional[str] = None):
599
+ """
600
+ Load a project from an .sb3 file/bytes/file path
601
+ """
602
+ _name, asset_data, json_str = cls.load_json(data, load_assets, _name)
603
+ data = json.loads(json_str)
604
+
605
+ sprite = cls.from_json(data)
606
+ # Sprites already have names, so we probably don't want to set it
607
+ # sprite.name = _name
608
+ sprite.asset_data = asset_data
609
+ return sprite