scratchattach 2.1.9__py3-none-any.whl → 2.1.10a1__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 (59) hide show
  1. scratchattach/__init__.py +28 -25
  2. scratchattach/cloud/__init__.py +2 -0
  3. scratchattach/cloud/_base.py +454 -282
  4. scratchattach/cloud/cloud.py +171 -168
  5. scratchattach/editor/__init__.py +21 -0
  6. scratchattach/editor/asset.py +199 -0
  7. scratchattach/editor/backpack_json.py +117 -0
  8. scratchattach/editor/base.py +142 -0
  9. scratchattach/editor/block.py +507 -0
  10. scratchattach/editor/blockshape.py +353 -0
  11. scratchattach/editor/build_defaulting.py +47 -0
  12. scratchattach/editor/comment.py +74 -0
  13. scratchattach/editor/commons.py +243 -0
  14. scratchattach/editor/extension.py +43 -0
  15. scratchattach/editor/field.py +90 -0
  16. scratchattach/editor/inputs.py +132 -0
  17. scratchattach/editor/meta.py +106 -0
  18. scratchattach/editor/monitor.py +175 -0
  19. scratchattach/editor/mutation.py +317 -0
  20. scratchattach/editor/pallete.py +91 -0
  21. scratchattach/editor/prim.py +170 -0
  22. scratchattach/editor/project.py +273 -0
  23. scratchattach/editor/sbuild.py +2837 -0
  24. scratchattach/editor/sprite.py +586 -0
  25. scratchattach/editor/twconfig.py +113 -0
  26. scratchattach/editor/vlb.py +134 -0
  27. scratchattach/eventhandlers/_base.py +99 -92
  28. scratchattach/eventhandlers/cloud_events.py +110 -103
  29. scratchattach/eventhandlers/cloud_recorder.py +26 -21
  30. scratchattach/eventhandlers/cloud_requests.py +460 -452
  31. scratchattach/eventhandlers/cloud_server.py +246 -244
  32. scratchattach/eventhandlers/cloud_storage.py +135 -134
  33. scratchattach/eventhandlers/combine.py +29 -27
  34. scratchattach/eventhandlers/filterbot.py +160 -159
  35. scratchattach/eventhandlers/message_events.py +41 -40
  36. scratchattach/other/other_apis.py +284 -212
  37. scratchattach/other/project_json_capabilities.py +475 -546
  38. scratchattach/site/_base.py +64 -46
  39. scratchattach/site/activity.py +414 -122
  40. scratchattach/site/backpack_asset.py +118 -84
  41. scratchattach/site/classroom.py +430 -142
  42. scratchattach/site/cloud_activity.py +107 -103
  43. scratchattach/site/comment.py +220 -190
  44. scratchattach/site/forum.py +400 -399
  45. scratchattach/site/project.py +806 -787
  46. scratchattach/site/session.py +1134 -867
  47. scratchattach/site/studio.py +611 -609
  48. scratchattach/site/user.py +835 -837
  49. scratchattach/utils/commons.py +243 -148
  50. scratchattach/utils/encoder.py +157 -156
  51. scratchattach/utils/enums.py +197 -190
  52. scratchattach/utils/exceptions.py +233 -206
  53. scratchattach/utils/requests.py +67 -59
  54. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info}/METADATA +155 -146
  55. scratchattach-2.1.10a1.dist-info/RECORD +62 -0
  56. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info}/WHEEL +1 -1
  57. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info/licenses}/LICENSE +21 -21
  58. scratchattach-2.1.9.dist-info/RECORD +0 -40
  59. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ import warnings
4
+ from dataclasses import dataclass
5
+ from typing import Optional, Callable, Final
6
+
7
+ from . import base, sprite, vlb, commons, build_defaulting
8
+ from ..utils import enums, exceptions
9
+
10
+
11
+ @dataclass(init=True, repr=True)
12
+ class PrimType(base.JSONSerializable):
13
+ code: int
14
+ name: str
15
+ attrs: list = None
16
+ opcode: str = None
17
+
18
+ def __eq__(self, other):
19
+ if isinstance(other, str):
20
+ return self.name == other
21
+ elif isinstance(other, enums._EnumWrapper):
22
+ other = other.value
23
+ return super().__eq__(other)
24
+
25
+ @staticmethod
26
+ def from_json(data: int):
27
+ return PrimTypes.find(data, "code")
28
+
29
+ def to_json(self) -> int:
30
+ return self.code
31
+
32
+
33
+ BASIC_ATTRS: Final[tuple[str]] = ("value",)
34
+ VLB_ATTRS: Final[tuple[str]] = ("name", "id", "x", "y")
35
+
36
+
37
+ class PrimTypes(enums._EnumWrapper):
38
+ # Yeah, they actually do have opcodes
39
+ NUMBER = PrimType(4, "number", BASIC_ATTRS, "math_number")
40
+ POSITIVE_NUMBER = PrimType(5, "positive number", BASIC_ATTRS, "math_positive_number")
41
+ POSITIVE_INTEGER = PrimType(6, "positive integer", BASIC_ATTRS, "math_whole_number")
42
+ INTEGER = PrimType(7, "integer", BASIC_ATTRS, "math_integer")
43
+ ANGLE = PrimType(8, "angle", BASIC_ATTRS, "math_angle")
44
+ COLOR = PrimType(9, "color", BASIC_ATTRS, "colour_picker")
45
+ STRING = PrimType(10, "string", BASIC_ATTRS, "text")
46
+ BROADCAST = PrimType(11, "broadcast", VLB_ATTRS, "event_broadcast_menu")
47
+ VARIABLE = PrimType(12, "variable", VLB_ATTRS, "data_variable")
48
+ LIST = PrimType(13, "list", VLB_ATTRS, "data_listcontents")
49
+
50
+ @classmethod
51
+ def find(cls, value, by: str, apply_func: Optional[Callable] = None) -> PrimType:
52
+ return super().find(value, by, apply_func=apply_func)
53
+
54
+
55
+ def is_prim_opcode(opcode: str):
56
+ return opcode in PrimTypes.all_of("opcode") and opcode is not None
57
+
58
+
59
+ class Prim(base.SpriteSubComponent):
60
+ def __init__(self, _primtype: PrimType | PrimTypes, _value: Optional[str | vlb.Variable | vlb.List | vlb.Broadcast] = None,
61
+ _name: Optional[str] = None, _id: Optional[str] = None, _x: Optional[int] = None,
62
+ _y: Optional[int] = None, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT):
63
+ """
64
+ Class representing a Scratch string, number, angle, variable etc.
65
+ Technically blocks but behave differently
66
+ https://en.scratch-wiki.info/wiki/Scratch_File_Format#Targets:~:text=A%20few%20blocks,13
67
+ """
68
+ if isinstance(_primtype, PrimTypes):
69
+ _primtype = _primtype.value
70
+
71
+ self.type = _primtype
72
+
73
+ self.value = _value
74
+
75
+ self.name = _name
76
+ """
77
+ Once you get the object associated with this primitive (sprite.link_prims()),
78
+ the name will be removed and the value will be changed from ``None``
79
+ """
80
+ self.value_id = _id
81
+ """
82
+ It's not an object accessed by id, but it may reference an object with an id.
83
+
84
+ ----
85
+
86
+ Once you get the object associated with it (sprite.link_prims()),
87
+ the id will be removed and the value will be changed from ``None``
88
+ """
89
+
90
+ self.x = _x
91
+ self.y = _y
92
+
93
+ super().__init__(_sprite)
94
+
95
+ def __repr__(self):
96
+ if self.is_basic:
97
+ return f"Prim<{self.type.name}: {self.value}>"
98
+ elif self.is_vlb:
99
+ return f"Prim<{self.type.name}: {self.value}>"
100
+ else:
101
+ return f"Prim<{self.type.name}>"
102
+
103
+ @property
104
+ def is_vlb(self):
105
+ return self.type.attrs == VLB_ATTRS
106
+
107
+ @property
108
+ def is_basic(self):
109
+ return self.type.attrs == BASIC_ATTRS
110
+
111
+ @staticmethod
112
+ def from_json(data: list):
113
+ assert isinstance(data, list)
114
+
115
+ _type_idx = data[0]
116
+ _prim_type = PrimTypes.find(_type_idx, "code")
117
+
118
+ _value, _name, _value_id, _x, _y = (None,) * 5
119
+ if _prim_type.attrs == BASIC_ATTRS:
120
+ assert len(data) == 2
121
+ _value = data[1]
122
+
123
+ elif _prim_type.attrs == VLB_ATTRS:
124
+ assert len(data) in (3, 5)
125
+ _name, _value_id = data[1:3]
126
+
127
+ if len(data) == 5:
128
+ _x, _y = data[3:]
129
+
130
+ return Prim(_prim_type, _value, _name, _value_id, _x, _y)
131
+
132
+ def to_json(self) -> list:
133
+ if self.type.attrs == BASIC_ATTRS:
134
+ return [self.type.code, self.value]
135
+ else:
136
+ return commons.trim_final_nones([self.type.code, self.value.name, self.value.id, self.x, self.y])
137
+
138
+ def link_using_sprite(self):
139
+ # Link prim to var/list/broadcast
140
+ if self.is_vlb:
141
+ if self.type.name == "variable":
142
+ self.value = self.sprite.find_variable(self.value_id, "id")
143
+
144
+ elif self.type.name == "list":
145
+ self.value = self.sprite.find_list(self.value_id, "id")
146
+
147
+ elif self.type.name == "broadcast":
148
+ self.value = self.sprite.find_broadcast(self.value_id, "id")
149
+ else:
150
+ # This should never happen
151
+ raise exceptions.BadVLBPrimitiveError(f"{self} claims to be VLB, but is {self.type.name}")
152
+
153
+ if self.value is None:
154
+ if not self.project:
155
+ new_vlb = vlb.construct(self.type.name.lower(), self.value_id, self.name)
156
+ self.sprite.add_local_global(new_vlb)
157
+ self.value = new_vlb
158
+
159
+ else:
160
+ new_vlb = vlb.construct(self.type.name.lower(), self.value_id, self.name)
161
+ self.sprite.stage.add_vlb(new_vlb)
162
+
163
+ warnings.warn(
164
+ f"Prim<name={self.name!r}, id={self.name!r}> has unknown {self.type.name} id; adding as global variable")
165
+ self.name = None
166
+ self.value_id = None
167
+
168
+ @property
169
+ def can_next(self):
170
+ return False
@@ -0,0 +1,273 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import warnings
6
+ from io import BytesIO, TextIOWrapper
7
+ from typing import Optional, Iterable, Generator, BinaryIO
8
+ from zipfile import ZipFile
9
+
10
+ from . import base, meta, extension, monitor, sprite, asset, vlb, twconfig, comment, commons
11
+ from ..site import session
12
+ from ..site.project import get_project
13
+ from ..utils import exceptions
14
+
15
+
16
+ class Project(base.JSONExtractable):
17
+ def __init__(self, _name: Optional[str] = None, _meta: Optional[meta.Meta] = None, _extensions: Iterable[extension.Extension] = (),
18
+ _monitors: Iterable[monitor.Monitor] = (), _sprites: Iterable[sprite.Sprite] = (), *,
19
+ _asset_data: Optional[list[asset.AssetFile]] = None, _session: Optional[session.Session] = None):
20
+ # Defaulting for list parameters
21
+ if _meta is None:
22
+ _meta = meta.Meta()
23
+ if _asset_data is None:
24
+ _asset_data = []
25
+
26
+ self._session = _session
27
+
28
+ self.name = _name
29
+
30
+ self.meta = _meta
31
+ self.extensions = _extensions
32
+ self.monitors = list(_monitors)
33
+ self.sprites = list(_sprites)
34
+
35
+ self.asset_data = _asset_data
36
+
37
+ self._tw_config_comment = None
38
+
39
+ # Link subcomponents
40
+ for iterable in (self.monitors, self.sprites):
41
+ for _subcomponent in iterable:
42
+ _subcomponent.project = self
43
+
44
+ # Link sprites
45
+ _stage_count = 0
46
+
47
+ for _sprite in self.sprites:
48
+ if _sprite.is_stage:
49
+ _stage_count += 1
50
+
51
+ _sprite.link_subcomponents()
52
+
53
+ # Link monitors
54
+ for _monitor in self.monitors:
55
+ _monitor.link_using_project()
56
+
57
+ if _stage_count != 1:
58
+ raise exceptions.InvalidStageCount(f"Project {self}")
59
+
60
+ def __repr__(self):
61
+ _ret = "Project<"
62
+ if self.name is not None:
63
+ _ret += f"name={self.name}, "
64
+ _ret += f"meta={self.meta}"
65
+ _ret += '>'
66
+ return _ret
67
+
68
+ @property
69
+ def stage(self) -> sprite.Sprite:
70
+ for _sprite in self.sprites:
71
+ if _sprite.is_stage:
72
+ return _sprite
73
+ warnings.warn(f"Could not find stage for {self.name}")
74
+
75
+ def to_json(self) -> dict:
76
+ _json = {
77
+ "targets": [_sprite.to_json() for _sprite in self.sprites],
78
+ "monitors": [_monitor.to_json() for _monitor in self.monitors],
79
+ "extensions": [_extension.to_json() for _extension in self.extensions],
80
+ "meta": self.meta.to_json(),
81
+ }
82
+
83
+ return _json
84
+
85
+ @property
86
+ def assets(self) -> Generator[asset.Asset, None, None]:
87
+ for _sprite in self.sprites:
88
+ for _asset in _sprite.assets:
89
+ yield _asset
90
+
91
+ @property
92
+ def tw_config_comment(self) -> comment.Comment | None:
93
+ for _comment in self.stage.comments:
94
+ if twconfig.is_valid_twconfig(_comment.text):
95
+ return _comment
96
+ return None
97
+
98
+ @property
99
+ def tw_config(self) -> twconfig.TWConfig | None:
100
+ _comment = self.tw_config_comment
101
+ if _comment:
102
+ return twconfig.TWConfig.from_str(_comment.text)
103
+ return None
104
+
105
+ @property
106
+ def all_ids(self):
107
+ _ret = []
108
+ for _sprite in self.sprites:
109
+ _ret += _sprite.all_ids
110
+ return _ret
111
+
112
+ @property
113
+ def new_id(self):
114
+ return commons.gen_id()
115
+
116
+ @staticmethod
117
+ def from_json(data: dict):
118
+ assert isinstance(data, dict)
119
+
120
+ # Load metadata
121
+ _meta = meta.Meta.from_json(data.get("meta"))
122
+
123
+ # Load extensions
124
+ _extensions = []
125
+ for _extension_data in data.get("extensions", []):
126
+ _extensions.append(extension.Extension.from_json(_extension_data))
127
+
128
+ # Load monitors
129
+ _monitors = []
130
+ for _monitor_data in data.get("monitors", []):
131
+ _monitors.append(monitor.Monitor.from_json(_monitor_data))
132
+
133
+ # Load sprites (targets)
134
+ _sprites = []
135
+ for _sprite_data in data.get("targets", []):
136
+ _sprites.append(sprite.Sprite.from_json(_sprite_data))
137
+
138
+ return Project(None, _meta, _extensions, _monitors, _sprites)
139
+
140
+ @staticmethod
141
+ def load_json(data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool = True, _name: Optional[str] = None):
142
+ """
143
+ Load project JSON and assets from an .sb3 file/bytes/file path
144
+ :return: Project name, asset data, json string
145
+ """
146
+ _dir_for_name = None
147
+
148
+ if _name is None:
149
+ if hasattr(data, "name"):
150
+ _dir_for_name = data.name
151
+
152
+ if not isinstance(_name, str) and _name is not None:
153
+ _name = str(_name)
154
+
155
+ if isinstance(data, bytes):
156
+ data = BytesIO(data)
157
+
158
+ elif isinstance(data, str):
159
+ _dir_for_name = data
160
+ data = open(data, "rb")
161
+
162
+ if _name is None and _dir_for_name is not None:
163
+ # Remove any directory names and the file extension
164
+ _name = _dir_for_name.split('/')[-1]
165
+ _name = '.'.join(_name.split('.')[:-1])
166
+
167
+ asset_data = []
168
+ with data:
169
+ # For if the sb3 is just JSON (e.g. if it's exported from scratchattach)
170
+ if commons.is_valid_json(data):
171
+ json_str = data
172
+
173
+ else:
174
+ with ZipFile(data) as archive:
175
+ json_str = archive.read("project.json")
176
+
177
+ # Also load assets
178
+ if load_assets:
179
+ for filename in archive.namelist():
180
+ if filename != "project.json":
181
+ md5_hash = filename.split('.')[0]
182
+
183
+ asset_data.append(
184
+ asset.AssetFile(filename, archive.read(filename), md5_hash)
185
+ )
186
+
187
+ else:
188
+ warnings.warn(
189
+ "Loading sb3 without loading assets. When exporting the project, there may be errors due to assets not being uploaded to the Scratch website")
190
+
191
+ return _name, asset_data, json_str
192
+
193
+ @classmethod
194
+ def from_sb3(cls, data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool = True, _name: Optional[str] = None):
195
+ """
196
+ Load a project from an .sb3 file/bytes/file path
197
+ """
198
+ _name, asset_data, json_str = cls.load_json(data, load_assets, _name)
199
+ data = json.loads(json_str)
200
+
201
+ project = cls.from_json(data)
202
+ project.name = _name
203
+ project.asset_data = asset_data
204
+
205
+ return project
206
+
207
+ @staticmethod
208
+ def from_id(project_id: int, _name: Optional[str] = None):
209
+ _proj = get_project(project_id)
210
+ data = json.loads(_proj.get_json())
211
+
212
+ if _name is None:
213
+ _name = _proj.title
214
+ _name = str(_name)
215
+
216
+ _proj = Project.from_json(data)
217
+ _proj.name = _name
218
+ return _proj
219
+
220
+ def find_vlb(self, value: str | None, by: str = "name",
221
+ multiple: bool = False) -> vlb.Variable | vlb.List | vlb.Broadcast | list[
222
+ vlb.Variable | vlb.List | vlb.Broadcast]:
223
+ _ret = []
224
+ for _sprite in self.sprites:
225
+ val = _sprite.find_vlb(value, by, multiple)
226
+ if multiple:
227
+ _ret += val
228
+ else:
229
+ if val is not None:
230
+ return val
231
+ if multiple:
232
+ return _ret
233
+
234
+ def find_sprite(self, value: str | None, by: str = "name",
235
+ multiple: bool = False) -> sprite.Sprite | list[sprite.Sprite]:
236
+ _ret = []
237
+ for _sprite in self.sprites:
238
+ if by == "name":
239
+ _val = _sprite.name
240
+ else:
241
+ _val = getattr(_sprite, by)
242
+
243
+ if _val == value:
244
+ if multiple:
245
+ _ret.append(_sprite)
246
+ else:
247
+ return _sprite
248
+
249
+ if multiple:
250
+ return _ret
251
+
252
+ def export(self, fp: str, *, auto_open: bool = False, export_as_zip: bool = True):
253
+ data = self.to_json()
254
+
255
+ if export_as_zip:
256
+ with ZipFile(fp, "w") as archive:
257
+ for _asset in self.assets:
258
+ asset_file = _asset.asset_file
259
+ if asset_file.filename not in archive.namelist():
260
+ archive.writestr(asset_file.filename, asset_file.data)
261
+
262
+ archive.writestr("project.json", json.dumps(data))
263
+ else:
264
+ with open(fp, "w") as json_file:
265
+ json.dump(data, json_file)
266
+
267
+ if auto_open:
268
+ os.system(f"explorer.exe \"{fp}\"")
269
+
270
+ def add_monitor(self, _monitor: monitor.Monitor) -> monitor.Monitor:
271
+ _monitor.project = self
272
+ _monitor.reporter_id = self.new_id
273
+ self.monitors.append(_monitor)