scratchattach 3.0.0b0__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
- cloud/__init__.py +2 -0
- cloud/_base.py +483 -0
- cloud/cloud.py +183 -0
- editor/__init__.py +22 -0
- editor/asset.py +265 -0
- editor/backpack_json.py +115 -0
- editor/base.py +191 -0
- editor/block.py +584 -0
- editor/blockshape.py +357 -0
- editor/build_defaulting.py +51 -0
- editor/code_translation/__init__.py +0 -0
- editor/code_translation/parse.py +177 -0
- editor/comment.py +80 -0
- editor/commons.py +145 -0
- editor/extension.py +50 -0
- editor/field.py +99 -0
- editor/inputs.py +138 -0
- editor/meta.py +117 -0
- editor/monitor.py +185 -0
- editor/mutation.py +381 -0
- editor/pallete.py +88 -0
- editor/prim.py +174 -0
- editor/project.py +381 -0
- editor/sprite.py +609 -0
- editor/twconfig.py +114 -0
- editor/vlb.py +134 -0
- eventhandlers/__init__.py +0 -0
- eventhandlers/_base.py +101 -0
- eventhandlers/cloud_events.py +130 -0
- eventhandlers/cloud_recorder.py +26 -0
- eventhandlers/cloud_requests.py +544 -0
- eventhandlers/cloud_server.py +249 -0
- eventhandlers/cloud_storage.py +135 -0
- eventhandlers/combine.py +30 -0
- eventhandlers/filterbot.py +163 -0
- eventhandlers/message_events.py +42 -0
- other/__init__.py +0 -0
- other/other_apis.py +598 -0
- other/project_json_capabilities.py +475 -0
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/METADATA +1 -1
- scratchattach-3.0.0b1.dist-info/RECORD +79 -0
- scratchattach-3.0.0b1.dist-info/top_level.txt +7 -0
- site/__init__.py +0 -0
- site/_base.py +93 -0
- site/activity.py +426 -0
- site/alert.py +226 -0
- site/backpack_asset.py +119 -0
- site/browser_cookie3_stub.py +17 -0
- site/browser_cookies.py +61 -0
- site/classroom.py +454 -0
- site/cloud_activity.py +121 -0
- site/comment.py +228 -0
- site/forum.py +436 -0
- site/placeholder.py +132 -0
- site/project.py +932 -0
- site/session.py +1323 -0
- site/studio.py +704 -0
- site/typed_dicts.py +151 -0
- site/user.py +1252 -0
- utils/__init__.py +0 -0
- utils/commons.py +263 -0
- utils/encoder.py +161 -0
- utils/enums.py +237 -0
- utils/exceptions.py +277 -0
- utils/optional_async.py +154 -0
- utils/requests.py +306 -0
- scratchattach/__init__.py +0 -37
- scratchattach/__main__.py +0 -93
- scratchattach-3.0.0b0.dist-info/RECORD +0 -8
- scratchattach-3.0.0b0.dist-info/top_level.txt +0 -1
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/WHEEL +0 -0
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
editor/asset.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from hashlib import md5, sha256
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from . import base, commons, sprite, build_defaulting
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(init=True, repr=True)
|
|
12
|
+
class AssetFile:
|
|
13
|
+
"""
|
|
14
|
+
Represents the file information for an asset (not the asset metdata)
|
|
15
|
+
- stores the filename, data, and md5 hash
|
|
16
|
+
"""
|
|
17
|
+
filename: str
|
|
18
|
+
_data: Optional[bytes] = field(repr=False, default=None)
|
|
19
|
+
_md5: str = field(repr=False, default_factory=str)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def data(self) -> bytes:
|
|
23
|
+
"""
|
|
24
|
+
Return the contents of the asset file, as bytes
|
|
25
|
+
"""
|
|
26
|
+
if self._data is None:
|
|
27
|
+
# Download and cache
|
|
28
|
+
rq = requests.get(f"https://assets.scratch.mit.edu/internalapi/asset/{self.filename}/get/")
|
|
29
|
+
# print(f"Downloaded {url}")
|
|
30
|
+
if rq.status_code != 200:
|
|
31
|
+
raise ValueError(f"Can't download asset {self.filename}\nIs not uploaded to scratch! Response: {rq.text}")
|
|
32
|
+
|
|
33
|
+
self._data = rq.content
|
|
34
|
+
|
|
35
|
+
return self._data
|
|
36
|
+
|
|
37
|
+
@data.setter
|
|
38
|
+
def data(self, data: bytes):
|
|
39
|
+
self._data = data
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def md5(self) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Compute/retrieve the md5 hex-digest of the asset file data
|
|
45
|
+
"""
|
|
46
|
+
if self._md5 is None:
|
|
47
|
+
self._md5 = md5(self.data).hexdigest()
|
|
48
|
+
|
|
49
|
+
return self._md5
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def sha256(self) -> str:
|
|
53
|
+
return sha256(self.data).hexdigest()
|
|
54
|
+
|
|
55
|
+
class Asset(base.SpriteSubComponent):
|
|
56
|
+
def __init__(self,
|
|
57
|
+
name: str = "costume1",
|
|
58
|
+
file_name: str = "b7853f557e4426412e64bb3da6531a99.svg",
|
|
59
|
+
_sprite: commons.SpriteInput = build_defaulting.SPRITE_DEFAULT):
|
|
60
|
+
"""
|
|
61
|
+
Represents a generic asset, with metadata. Can be a sound or a costume.
|
|
62
|
+
https://en.scratch-wiki.info/wiki/Scratch_File_Format#Assets
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
asset_id, data_format = file_name.split('.')
|
|
66
|
+
except ValueError:
|
|
67
|
+
raise ValueError(f"Invalid file name: {file_name}, # of '.' in {file_name} ({file_name.count('.')}) != 2; "
|
|
68
|
+
f"(too many/few values to unpack)")
|
|
69
|
+
self.name = name
|
|
70
|
+
|
|
71
|
+
self.id = asset_id
|
|
72
|
+
self.data_format = data_format
|
|
73
|
+
|
|
74
|
+
super().__init__(_sprite)
|
|
75
|
+
|
|
76
|
+
def __repr__(self):
|
|
77
|
+
return f"Asset<{self.name!r}>"
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def folder(self):
|
|
81
|
+
"""
|
|
82
|
+
Get the folder name of this asset, based on the asset name. Uses the TurboWarp syntax
|
|
83
|
+
"""
|
|
84
|
+
return commons.get_folder_name(self.name)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def name_nfldr(self):
|
|
88
|
+
"""
|
|
89
|
+
Get the asset name after removing the folder name. Uses the TurboWarp syntax
|
|
90
|
+
"""
|
|
91
|
+
return commons.get_name_nofldr(self.name)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def file_name(self):
|
|
95
|
+
"""
|
|
96
|
+
Get the exact file name, as it would be within an sb3 file
|
|
97
|
+
equivalent to the md5ext value using in scratch project JSON
|
|
98
|
+
"""
|
|
99
|
+
return f"{self.id}.{self.data_format}"
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def md5ext(self):
|
|
103
|
+
"""
|
|
104
|
+
Get the exact file name, as it would be within an sb3 file
|
|
105
|
+
equivalent to the md5ext value using in scratch project JSON
|
|
106
|
+
|
|
107
|
+
(alias for file_name)
|
|
108
|
+
"""
|
|
109
|
+
return self.file_name
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def parent(self):
|
|
113
|
+
"""
|
|
114
|
+
Return the project (body) that this asset is attached to. If there is no attached project,
|
|
115
|
+
try returning the attached sprite instead.
|
|
116
|
+
"""
|
|
117
|
+
if self.project is None:
|
|
118
|
+
return self.sprite
|
|
119
|
+
else:
|
|
120
|
+
return self.project
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def asset_file(self) -> AssetFile:
|
|
124
|
+
"""
|
|
125
|
+
Get the associated asset file object for this asset object
|
|
126
|
+
"""
|
|
127
|
+
for asset_file in self.parent.asset_data:
|
|
128
|
+
if asset_file.filename == self.file_name:
|
|
129
|
+
return asset_file
|
|
130
|
+
|
|
131
|
+
# No pre-existing asset file object; create one and add it to the project
|
|
132
|
+
asset_file = AssetFile(self.file_name)
|
|
133
|
+
self.project.asset_data.append(asset_file)
|
|
134
|
+
return asset_file
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def from_json(data: dict):
|
|
138
|
+
"""
|
|
139
|
+
Load asset data from project.json
|
|
140
|
+
"""
|
|
141
|
+
_name = data.get("name")
|
|
142
|
+
assert isinstance(_name, str)
|
|
143
|
+
_file_name = data.get("md5ext")
|
|
144
|
+
if _file_name is None:
|
|
145
|
+
if "dataFormat" in data and "assetId" in data:
|
|
146
|
+
_id = data["assetId"]
|
|
147
|
+
_data_format = data["dataFormat"]
|
|
148
|
+
_file_name = f"{_id}.{_data_format}"
|
|
149
|
+
else:
|
|
150
|
+
_file_name = ""
|
|
151
|
+
assert isinstance(_file_name, str)
|
|
152
|
+
|
|
153
|
+
return Asset(_name, _file_name)
|
|
154
|
+
|
|
155
|
+
def to_json(self) -> dict:
|
|
156
|
+
"""
|
|
157
|
+
Convert asset data to project.json format
|
|
158
|
+
"""
|
|
159
|
+
return {
|
|
160
|
+
"name": self.name,
|
|
161
|
+
|
|
162
|
+
"assetId": self.id,
|
|
163
|
+
"md5ext": self.file_name,
|
|
164
|
+
"dataFormat": self.data_format,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# todo: implement below:
|
|
168
|
+
"""
|
|
169
|
+
@staticmethod
|
|
170
|
+
def from_file(fp: str, name: str = None):
|
|
171
|
+
image_types = ("png", "jpg", "jpeg", "svg")
|
|
172
|
+
sound_types = ("wav", "mp3")
|
|
173
|
+
|
|
174
|
+
# Should save data as well so it can be uploaded to scratch if required (add to project asset data)
|
|
175
|
+
...
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class Costume(Asset):
|
|
180
|
+
def __init__(self,
|
|
181
|
+
name: str = "Cat",
|
|
182
|
+
file_name: str = "b7853f557e4426412e64bb3da6531a99.svg",
|
|
183
|
+
|
|
184
|
+
bitmap_resolution=None,
|
|
185
|
+
rotation_center_x: int | float = 48,
|
|
186
|
+
rotation_center_y: int | float = 50,
|
|
187
|
+
_sprite: commons.SpriteInput = build_defaulting.SPRITE_DEFAULT):
|
|
188
|
+
"""
|
|
189
|
+
A costume (image). An asset with additional properties
|
|
190
|
+
https://en.scratch-wiki.info/wiki/Scratch_File_Format#Costumes
|
|
191
|
+
"""
|
|
192
|
+
super().__init__(name, file_name, _sprite)
|
|
193
|
+
|
|
194
|
+
self.bitmap_resolution = bitmap_resolution
|
|
195
|
+
self.rotation_center_x = rotation_center_x
|
|
196
|
+
self.rotation_center_y = rotation_center_y
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def from_json(data):
|
|
200
|
+
"""
|
|
201
|
+
Load costume data from project.json
|
|
202
|
+
"""
|
|
203
|
+
_asset_load = Asset.from_json(data)
|
|
204
|
+
|
|
205
|
+
bitmap_resolution = data.get("bitmapResolution")
|
|
206
|
+
|
|
207
|
+
rotation_center_x = data.get("rotationCenterX", 0)
|
|
208
|
+
rotation_center_y = data.get("rotationCenterY", 0)
|
|
209
|
+
return Costume(_asset_load.name, _asset_load.file_name,
|
|
210
|
+
|
|
211
|
+
bitmap_resolution, rotation_center_x, rotation_center_y)
|
|
212
|
+
|
|
213
|
+
def to_json(self) -> dict:
|
|
214
|
+
"""
|
|
215
|
+
Convert costume to project.json format
|
|
216
|
+
"""
|
|
217
|
+
_json = super().to_json()
|
|
218
|
+
_json.update({
|
|
219
|
+
"rotationCenterX": self.rotation_center_x,
|
|
220
|
+
"rotationCenterY": self.rotation_center_y
|
|
221
|
+
})
|
|
222
|
+
if self.bitmap_resolution is not None:
|
|
223
|
+
_json["bitmapResolution"] = self.bitmap_resolution
|
|
224
|
+
|
|
225
|
+
return _json
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class Sound(Asset):
|
|
229
|
+
def __init__(self,
|
|
230
|
+
name: str = "pop",
|
|
231
|
+
file_name: str = "83a9787d4cb6f3b7632b4ddfebf74367.wav",
|
|
232
|
+
|
|
233
|
+
rate: Optional[int] = None,
|
|
234
|
+
sample_count: Optional[int] = None,
|
|
235
|
+
_sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT):
|
|
236
|
+
"""
|
|
237
|
+
A sound. An asset with additional properties
|
|
238
|
+
https://en.scratch-wiki.info/wiki/Scratch_File_Format#Sounds
|
|
239
|
+
"""
|
|
240
|
+
super().__init__(name, file_name, _sprite)
|
|
241
|
+
|
|
242
|
+
self.rate = rate
|
|
243
|
+
self.sample_count = sample_count
|
|
244
|
+
|
|
245
|
+
@staticmethod
|
|
246
|
+
def from_json(data):
|
|
247
|
+
"""
|
|
248
|
+
Load sound from project.json
|
|
249
|
+
"""
|
|
250
|
+
_asset_load = Asset.from_json(data)
|
|
251
|
+
|
|
252
|
+
rate = data.get("rate")
|
|
253
|
+
sample_count = data.get("sampleCount")
|
|
254
|
+
return Sound(_asset_load.name, _asset_load.file_name, rate, sample_count)
|
|
255
|
+
|
|
256
|
+
def to_json(self) -> dict:
|
|
257
|
+
"""
|
|
258
|
+
Convert Sound to project.json format
|
|
259
|
+
"""
|
|
260
|
+
_json = super().to_json()
|
|
261
|
+
commons.noneless_update(_json, {
|
|
262
|
+
"rate": self.rate,
|
|
263
|
+
"sampleCount": self.sample_count
|
|
264
|
+
})
|
|
265
|
+
return _json
|
editor/backpack_json.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module to deal with the backpack's weird JSON format, by overriding editor classes with new load methods
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from . import block, prim, field, inputs, mutation, sprite
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_prim_fields(_fields: dict[str, dict[str, str]]) -> tuple[str | None, str | None, str | None]:
|
|
10
|
+
"""
|
|
11
|
+
Function for reading the fields in a backpack **primitive**
|
|
12
|
+
"""
|
|
13
|
+
for key, value in _fields.items():
|
|
14
|
+
prim_value, prim_name, prim_id = (None,) * 3
|
|
15
|
+
if key == "NUM":
|
|
16
|
+
prim_value = value.get("value")
|
|
17
|
+
else:
|
|
18
|
+
prim_name = value.get("value")
|
|
19
|
+
prim_id = value.get("id")
|
|
20
|
+
|
|
21
|
+
# There really should only be 1 item, and this function can only return for that item
|
|
22
|
+
return prim_value, prim_name, prim_id
|
|
23
|
+
return (None,) * 3
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BpField(field.Field):
|
|
27
|
+
"""
|
|
28
|
+
A normal field but with a different load method
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def from_json(data: dict[str, str]) -> field.Field:
|
|
33
|
+
# We can very simply convert it to the regular format
|
|
34
|
+
data = [data.get("value"), data.get("id")]
|
|
35
|
+
return field.Field.from_json(data)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BpInput(inputs.Input):
|
|
39
|
+
"""
|
|
40
|
+
A normal input but with a different load method
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def from_json(data: dict[str, str]) -> inputs.Input:
|
|
45
|
+
# The actual data is stored in a separate prim block
|
|
46
|
+
_id = data.get("shadow")
|
|
47
|
+
_obscurer_id = data.get("block")
|
|
48
|
+
|
|
49
|
+
if _obscurer_id == _id:
|
|
50
|
+
# If both the shadow and obscurer are the same, then there is no actual obscurer
|
|
51
|
+
_obscurer_id = None
|
|
52
|
+
# We cannot work out the shadow status yet since that is located in the primitive
|
|
53
|
+
return inputs.Input(None, _id=_id, _obscurer_id=_obscurer_id)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BpBlock(block.Block):
|
|
57
|
+
"""
|
|
58
|
+
A normal block but with a different load method
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def from_json(data: dict) -> prim.Prim | block.Block:
|
|
63
|
+
"""
|
|
64
|
+
Load a block in the **backpack** JSON format
|
|
65
|
+
:param data: A dictionary (not list)
|
|
66
|
+
:return: A new block/prim object
|
|
67
|
+
"""
|
|
68
|
+
_opcode = data["opcode"]
|
|
69
|
+
|
|
70
|
+
_x, _y = data.get("x"), data.get("y")
|
|
71
|
+
if prim.is_prim_opcode(_opcode):
|
|
72
|
+
# This is actually a prim
|
|
73
|
+
prim_value, prim_name, prim_id = parse_prim_fields(data["fields"])
|
|
74
|
+
return prim.Prim(prim.PrimTypes.find(_opcode, "opcode"),
|
|
75
|
+
prim_value, prim_name, prim_id)
|
|
76
|
+
|
|
77
|
+
_next_id = data.get("next")
|
|
78
|
+
_parent_id = data.get("parent")
|
|
79
|
+
|
|
80
|
+
_shadow = data.get("shadow", False)
|
|
81
|
+
_top_level = data.get("topLevel", _parent_id is None)
|
|
82
|
+
|
|
83
|
+
_inputs = {}
|
|
84
|
+
for _input_code, _input_data in data.get("inputs", {}).items():
|
|
85
|
+
_inputs[_input_code] = BpInput.from_json(_input_data)
|
|
86
|
+
|
|
87
|
+
_fields = {}
|
|
88
|
+
for _field_code, _field_data in data.get("fields", {}).items():
|
|
89
|
+
_fields[_field_code] = BpField.from_json(_field_data)
|
|
90
|
+
|
|
91
|
+
if "mutation" in data:
|
|
92
|
+
_mutation = mutation.Mutation.from_json(data["mutation"])
|
|
93
|
+
else:
|
|
94
|
+
_mutation = None
|
|
95
|
+
|
|
96
|
+
return block.Block(_opcode, _shadow, _top_level, _mutation, _fields, _inputs, _x, _y, _next_id=_next_id,
|
|
97
|
+
_parent_id=_parent_id)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def load_script(_script_data: list[dict]) -> sprite.Sprite:
|
|
101
|
+
"""
|
|
102
|
+
Loads a script into a sprite from the backpack JSON format
|
|
103
|
+
:param _script_data: Backpack script JSON data
|
|
104
|
+
:return: a Sprite object containing the script
|
|
105
|
+
"""
|
|
106
|
+
# Using a sprite since it simplifies things, e.g. local global loading
|
|
107
|
+
_blockchain = sprite.Sprite()
|
|
108
|
+
|
|
109
|
+
for _block_data in _script_data:
|
|
110
|
+
_block = BpBlock.from_json(_block_data)
|
|
111
|
+
_block.sprite = _blockchain
|
|
112
|
+
_blockchain.blocks[_block_data["id"]] = _block
|
|
113
|
+
|
|
114
|
+
_blockchain.link_subcomponents()
|
|
115
|
+
return _blockchain
|
editor/base.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Editor base classes
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import copy
|
|
8
|
+
import json
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from io import TextIOWrapper
|
|
11
|
+
from typing import Optional, Any, TYPE_CHECKING, BinaryIO, Union
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from . import project, block, asset
|
|
15
|
+
from . import mutation as module_mutation
|
|
16
|
+
from . import sprite as module_sprite
|
|
17
|
+
from . import commons
|
|
18
|
+
|
|
19
|
+
from . import build_defaulting
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Base(ABC):
|
|
23
|
+
"""
|
|
24
|
+
Abstract base class for most sa.editor classes. Implements copy functions
|
|
25
|
+
"""
|
|
26
|
+
def dcopy(self):
|
|
27
|
+
"""
|
|
28
|
+
:return: A **deep** copy of self
|
|
29
|
+
"""
|
|
30
|
+
return copy.deepcopy(self)
|
|
31
|
+
|
|
32
|
+
def copy(self):
|
|
33
|
+
"""
|
|
34
|
+
:return: A **shallow** copy of self
|
|
35
|
+
"""
|
|
36
|
+
return copy.copy(self)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class JSONSerializable(Base, ABC):
|
|
40
|
+
"""
|
|
41
|
+
'Interface' for to_json() and from_json() methods
|
|
42
|
+
Also implements save_json() using to_json()
|
|
43
|
+
"""
|
|
44
|
+
@staticmethod
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def from_json(data):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def to_json(self):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def save_json(self, name: str = ''):
|
|
54
|
+
"""
|
|
55
|
+
Save json to a file. Adds '.json' for you.
|
|
56
|
+
"""
|
|
57
|
+
data = self.to_json()
|
|
58
|
+
with open(f"{self.__class__.__name__.lower()}{name}.json", "w") as f:
|
|
59
|
+
json.dump(data, f)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class JSONExtractable(JSONSerializable, ABC):
|
|
63
|
+
"""
|
|
64
|
+
Interface for objects that can be loaded from zip archives containing json files (sprite/project)
|
|
65
|
+
Only has one method - load_json
|
|
66
|
+
"""
|
|
67
|
+
@staticmethod
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def load_json(data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool = True, _name: Optional[str] = None) -> tuple[
|
|
70
|
+
str, list[asset.AssetFile], str]:
|
|
71
|
+
"""
|
|
72
|
+
Automatically extracts the JSON data as a string, as well as providing auto naming
|
|
73
|
+
:param data: Either a string of JSON, sb3 file as bytes or as a file object
|
|
74
|
+
:param load_assets: Whether to extract assets as well (if applicable)
|
|
75
|
+
:param _name: Any provided name (will automatically find one otherwise)
|
|
76
|
+
:return: tuple of the name, asset data & json as a string
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ProjectSubcomponent(JSONSerializable, ABC):
|
|
81
|
+
"""
|
|
82
|
+
Base class for any class with an associated project
|
|
83
|
+
"""
|
|
84
|
+
def __init__(self, _project: Optional[project.Project] = None):
|
|
85
|
+
self.project = _project
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class SpriteSubComponent(JSONSerializable, ABC):
|
|
89
|
+
"""
|
|
90
|
+
Base class for any class with an associated sprite
|
|
91
|
+
"""
|
|
92
|
+
sprite: module_sprite.Sprite
|
|
93
|
+
def __init__(self, _sprite: "commons.SpriteInput" = build_defaulting.SPRITE_DEFAULT):
|
|
94
|
+
if _sprite is build_defaulting.SPRITE_DEFAULT:
|
|
95
|
+
_sprite = build_defaulting.current_sprite()
|
|
96
|
+
self.sprite = _sprite
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def project(self) -> project.Project:
|
|
100
|
+
"""
|
|
101
|
+
Get associated project by proxy of the associated sprite
|
|
102
|
+
"""
|
|
103
|
+
p = self.sprite.project
|
|
104
|
+
assert p is not None
|
|
105
|
+
return p
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class IDComponent(SpriteSubComponent, ABC):
|
|
109
|
+
"""
|
|
110
|
+
Base class for classes with an id attribute
|
|
111
|
+
"""
|
|
112
|
+
def __init__(self, _id: str, _sprite: "commons.SpriteInput" = build_defaulting.SPRITE_DEFAULT):
|
|
113
|
+
self.id = _id
|
|
114
|
+
super().__init__(_sprite)
|
|
115
|
+
|
|
116
|
+
def __repr__(self):
|
|
117
|
+
return f"<{self.__class__.__name__}: {self.id}>"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class NamedIDComponent(IDComponent, ABC):
|
|
121
|
+
"""
|
|
122
|
+
Base class for Variables, Lists and Broadcasts (Name + ID + sprite)
|
|
123
|
+
"""
|
|
124
|
+
def __init__(self, _id: str, name: str, _sprite: "commons.SpriteInput" = build_defaulting.SPRITE_DEFAULT):
|
|
125
|
+
self.name = name
|
|
126
|
+
super().__init__(_id, _sprite)
|
|
127
|
+
|
|
128
|
+
def __repr__(self):
|
|
129
|
+
return f"<{self.__class__.__name__} '{self.name}'>"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class BlockSubComponent(JSONSerializable, ABC):
|
|
133
|
+
"""
|
|
134
|
+
Base class for classes with associated blocks
|
|
135
|
+
"""
|
|
136
|
+
def __init__(self, _block: Optional[block.Block] = None):
|
|
137
|
+
self.block = _block
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def sprite(self) -> module_sprite.Sprite:
|
|
141
|
+
"""
|
|
142
|
+
Fetch sprite by proxy of the block
|
|
143
|
+
"""
|
|
144
|
+
b = self.block
|
|
145
|
+
assert b is not None
|
|
146
|
+
return b.sprite
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def project(self) -> project.Project:
|
|
150
|
+
"""
|
|
151
|
+
Fetch project by proxy of the sprite (by proxy of the block)
|
|
152
|
+
"""
|
|
153
|
+
p = self.sprite.project
|
|
154
|
+
assert p is not None
|
|
155
|
+
return p
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class MutationSubComponent(JSONSerializable, ABC):
|
|
159
|
+
"""
|
|
160
|
+
Base class for classes with associated mutations
|
|
161
|
+
"""
|
|
162
|
+
mutation: Optional[module_mutation.Mutation]
|
|
163
|
+
def __init__(self, _mutation: Optional[module_mutation.Mutation] = None):
|
|
164
|
+
self.mutation = _mutation
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def block(self) -> block.Block:
|
|
168
|
+
"""
|
|
169
|
+
Fetch block by proxy of mutation
|
|
170
|
+
"""
|
|
171
|
+
m = self.mutation
|
|
172
|
+
assert m is not None
|
|
173
|
+
b = m.block
|
|
174
|
+
assert b is not None
|
|
175
|
+
return b
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def sprite(self) -> module_sprite.Sprite:
|
|
179
|
+
"""
|
|
180
|
+
Fetch sprite by proxy of block (by proxy of mutation)
|
|
181
|
+
"""
|
|
182
|
+
return self.block.sprite
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def project(self) -> project.Project:
|
|
186
|
+
"""
|
|
187
|
+
Fetch project by proxy of sprite (by proxy of block (by proxy of mutation))
|
|
188
|
+
"""
|
|
189
|
+
p = self.sprite.project
|
|
190
|
+
assert p is not None
|
|
191
|
+
return p
|