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,175 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from . import project
7
+
8
+ from . import base
9
+
10
+
11
+ class Monitor(base.ProjectSubcomponent):
12
+ def __init__(self, reporter: Optional[base.NamedIDComponent] = None,
13
+ mode: str = "default",
14
+ opcode: str = "data_variable",
15
+ params: Optional[dict] = None,
16
+ sprite_name: Optional[str] = None,
17
+ value=0,
18
+ width: int | float = 0,
19
+ height: int | float = 0,
20
+ x: int | float = 5,
21
+ y: int | float = 5,
22
+ visible: bool = False,
23
+ slider_min: int | float = 0,
24
+ slider_max: int | float = 100,
25
+ is_discrete: bool = True, *, reporter_id: Optional[str] = None, _project: Optional[project.Project] = None):
26
+ """
27
+ Represents a variable/list monitor
28
+ https://en.scratch-wiki.info/wiki/Scratch_File_Format#Monitors
29
+ """
30
+ assert isinstance(reporter, base.SpriteSubComponent) or reporter is None
31
+
32
+ self.reporter_id = reporter_id
33
+ """
34
+ ID referencing the VLB being referenced. Replaced with None during project instantiation, where the reporter attribute is updated
35
+ """
36
+
37
+ self.reporter = reporter
38
+ if params is None:
39
+ params = {}
40
+
41
+ self.mode = mode
42
+
43
+ self.opcode = opcode
44
+ self.params = params
45
+
46
+ self.sprite_name = sprite_name
47
+
48
+ self.value = value
49
+
50
+ self.width, self.height = width, height
51
+ self.x, self.y = x, y
52
+
53
+ self.visible = visible
54
+
55
+ self.slider_min, self.slider_max = slider_min, slider_max
56
+ self.is_discrete = is_discrete
57
+
58
+ super().__init__(_project)
59
+
60
+ def __repr__(self):
61
+ return f"Monitor<{self.opcode}>"
62
+
63
+ @property
64
+ def id(self):
65
+ if self.reporter is not None:
66
+ return self.reporter.id
67
+ # if isinstance(self.reporter, str):
68
+ # return self.reporter
69
+ # else:
70
+ # return self.reporter.id
71
+ else:
72
+ return self.reporter_id
73
+
74
+ @staticmethod
75
+ def from_json(data: dict):
76
+ _id = data["id"]
77
+ # ^^ NEED TO FIND REPORTER OBJECT
78
+
79
+ mode = data["mode"]
80
+
81
+ opcode = data["opcode"]
82
+ params: dict = data["params"]
83
+
84
+ sprite_name = data["spriteName"]
85
+
86
+ value = data["value"]
87
+
88
+ width, height = data["width"], data["height"]
89
+ x, y = data["x"], data["y"]
90
+
91
+ visible = data["visible"]
92
+
93
+ if "isDiscrete" in data.keys():
94
+ slider_min, slider_max = data["sliderMin"], data["sliderMax"]
95
+ is_discrete = data["isDiscrete"]
96
+ else:
97
+ slider_min, slider_max, is_discrete = None, None, None
98
+
99
+ return Monitor(None, mode, opcode, params, sprite_name, value, width, height, x, y, visible, slider_min,
100
+ slider_max, is_discrete, reporter_id=_id)
101
+
102
+ def to_json(self):
103
+ _json = {
104
+ "id": self.id,
105
+ "mode": self.mode,
106
+
107
+ "opcode": self.opcode,
108
+ "params": self.params,
109
+
110
+ "spriteName": self.sprite_name,
111
+
112
+ "value": self.value,
113
+
114
+ "width": self.width,
115
+ "height": self.height,
116
+
117
+ "x": self.x,
118
+ "y": self.y,
119
+
120
+ "visible": self.visible
121
+ }
122
+ if self.is_discrete is not None:
123
+ _json["sliderMin"] = self.slider_min
124
+ _json["sliderMax"] = self.slider_max
125
+ _json["isDiscrete"] = self.is_discrete
126
+
127
+ return _json
128
+
129
+ def link_using_project(self):
130
+ assert self.project is not None
131
+
132
+ if self.opcode in ("data_variable", "data_listcontents", "event_broadcast_menu"):
133
+ new_vlb = self.project.find_vlb(self.reporter_id, "id")
134
+ if new_vlb is not None:
135
+ self.reporter = new_vlb
136
+ self.reporter_id = None
137
+
138
+ # @staticmethod
139
+ # def from_reporter(reporter: Block, _id: str = None, mode: str = "default",
140
+ # opcode: str = None, sprite_name: str = None, value=0, width: int | float = 0,
141
+ # height: int | float = 0,
142
+ # x: int | float = 5, y: int | float = 5, visible: bool = False, slider_min: int | float = 0,
143
+ # slider_max: int | float = 100, is_discrete: bool = True, params: dict = None):
144
+ # if "reporter" not in reporter.stack_type:
145
+ # warnings.warn(f"{reporter} is not a reporter block; the monitor will return '0'")
146
+ # elif "(menu)" in reporter.stack_type:
147
+ # warnings.warn(f"{reporter} is a menu block; the monitor will return '0'")
148
+ # # Maybe add note that length of list doesn't work fsr?? idk
149
+ # if _id is None:
150
+ # _id = reporter.opcode
151
+ # if opcode is None:
152
+ # opcode = reporter.opcode # .replace('_', ' ')
153
+
154
+ # if params is None:
155
+ # params = {}
156
+ # for field in reporter.fields:
157
+ # if field.value_id is None:
158
+ # params[field.id] = field.value
159
+ # else:
160
+ # params[field.id] = field.value, field.value_id
161
+
162
+ # return Monitor(
163
+ # _id,
164
+ # mode,
165
+ # opcode,
166
+
167
+ # params,
168
+ # sprite_name,
169
+ # value,
170
+
171
+ # width, height,
172
+ # x, y,
173
+ # visible,
174
+ # slider_min, slider_max, is_discrete
175
+ # )
@@ -0,0 +1,317 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import warnings
5
+ from dataclasses import dataclass
6
+ from typing import Optional, TYPE_CHECKING, Iterable, Any
7
+
8
+ from . import base, commons
9
+ from ..utils import enums
10
+
11
+ if TYPE_CHECKING:
12
+ from . import block
13
+
14
+
15
+ @dataclass(init=True)
16
+ class ArgumentType(base.Base):
17
+ type: str
18
+ proc_str: str
19
+
20
+ def __eq__(self, other):
21
+ if isinstance(other, enums._EnumWrapper):
22
+ other = other.value
23
+
24
+ assert isinstance(other, ArgumentType)
25
+
26
+ return self.type == other.type
27
+
28
+ def __repr__(self):
29
+ return f"<ArgType {self.type!r}>"
30
+
31
+ @property
32
+ def default(self) -> str | None:
33
+ if self.proc_str == "%b":
34
+ return "false"
35
+ elif self.proc_str == "%s":
36
+ return ''
37
+ else:
38
+ return None
39
+
40
+
41
+ @dataclass(init=True, repr=True)
42
+ class ArgSettings(base.Base):
43
+ ids: bool
44
+ names: bool
45
+ defaults: bool
46
+
47
+ def __int__(self):
48
+ return (int(self.ids) +
49
+ int(self.names) +
50
+ int(self.defaults))
51
+
52
+ def __eq__(self, other):
53
+ return (self.ids == other.ids and
54
+ self.names == other.names and
55
+ self.defaults == other.defaults)
56
+
57
+ def __gt__(self, other):
58
+ return int(self) > int(other)
59
+
60
+ def __lt__(self, other):
61
+ return int(self) > int(other)
62
+
63
+
64
+ @dataclass(init=True, repr=True)
65
+ class Argument(base.MutationSubComponent):
66
+ name: str
67
+ default: str = ''
68
+
69
+ _id: str = None
70
+ """
71
+ Argument ID: Will be used to replace other parameters during block instantiation.
72
+ """
73
+
74
+ @property
75
+ def index(self):
76
+ return self.mutation.arguments.index(self)
77
+
78
+ @property
79
+ def type(self) -> None | ArgumentType:
80
+ i = 0
81
+ goal = self.index
82
+ for token in parse_proc_code(self.mutation.proc_code):
83
+ if isinstance(token, ArgumentType):
84
+ if i == goal:
85
+ return token
86
+ i += 1
87
+
88
+ @staticmethod
89
+ def from_json(data: dict | list | Any):
90
+ warnings.warn("No from_json method defined for Arguments (yet?)")
91
+
92
+ def to_json(self) -> dict | list | Any:
93
+ warnings.warn("No to_json method defined for Arguments (yet?)")
94
+
95
+ def link_using_mutation(self):
96
+ if self._id is None:
97
+ self._id = self.block.new_id
98
+
99
+
100
+ class ArgTypes(enums._EnumWrapper):
101
+ BOOLEAN = ArgumentType("boolean", "%b")
102
+ NUMBER_OR_TEXT = ArgumentType("number or text", "%s")
103
+
104
+
105
+ def parse_proc_code(_proc_code: str) -> list[str, ArgumentType] | None:
106
+ if _proc_code is None:
107
+ return None
108
+ token = ''
109
+ tokens = []
110
+
111
+ last_char = ''
112
+ for char in _proc_code:
113
+ if last_char == '%':
114
+ if char in "sb":
115
+ # If we've hit an %s or %b
116
+ token = token[:-1]
117
+ # Clip the % sign off the token
118
+
119
+ if token != '':
120
+ # Make sure not to append an empty token
121
+ tokens.append(token)
122
+
123
+ # Add the parameter token
124
+ token = f"%{char}"
125
+ if token == "%b":
126
+ tokens.append(ArgTypes.BOOLEAN.value.dcopy())
127
+ elif token == "%s":
128
+ tokens.append(ArgTypes.NUMBER_OR_TEXT.value.dcopy())
129
+
130
+ token = ''
131
+ continue
132
+
133
+ token += char
134
+ last_char = char
135
+
136
+ if token != '':
137
+ tokens.append(token)
138
+
139
+ return tokens
140
+
141
+
142
+ class Mutation(base.BlockSubComponent):
143
+ def __init__(self, _tag_name: str = "mutation", _children: Optional[list] = None, _proc_code: Optional[str] = None,
144
+ _is_warp: Optional[bool] = None, _arguments: Optional[list[Argument]] = None, _has_next: Optional[bool] = None,
145
+ _argument_settings: Optional[ArgSettings] = None, *,
146
+ _block: Optional[block.Block] = None):
147
+ """
148
+ Mutation for Control:stop block and procedures
149
+ https://en.scratch-wiki.info/wiki/Scratch_File_Format#Mutations
150
+ """
151
+ # Defaulting for args
152
+ if _children is None:
153
+ _children = []
154
+
155
+ if _argument_settings is None:
156
+ if _arguments:
157
+ _argument_settings = ArgSettings(
158
+ _arguments[0]._id is None,
159
+ _arguments[0].name is None,
160
+ _arguments[0].default is None
161
+ )
162
+ else:
163
+ _argument_settings = ArgSettings(False, False, False)
164
+
165
+ self.tag_name = _tag_name
166
+ self.children = _children
167
+
168
+ self.proc_code = _proc_code
169
+ self.is_warp = _is_warp
170
+ self.arguments = _arguments
171
+ self.og_argument_settings = _argument_settings
172
+
173
+ self.has_next = _has_next
174
+
175
+ super().__init__(_block)
176
+
177
+ def __repr__(self):
178
+ if self.arguments is not None:
179
+ return f"Mutation<args={self.arguments}>"
180
+ else:
181
+ return f"Mutation<hasnext={self.has_next}>"
182
+
183
+ @property
184
+ def argument_ids(self):
185
+ if self.arguments is not None:
186
+ return [_arg._id for _arg in self.arguments]
187
+ else:
188
+ return None
189
+
190
+ @property
191
+ def argument_names(self):
192
+ if self.arguments is not None:
193
+ return [_arg.name for _arg in self.arguments]
194
+ else:
195
+ return None
196
+
197
+ @property
198
+ def argument_defaults(self):
199
+ if self.arguments is not None:
200
+ return [_arg.default for _arg in self.arguments]
201
+ else:
202
+ return None
203
+
204
+ @property
205
+ def argument_settings(self) -> ArgSettings:
206
+ return ArgSettings(bool(commons.safe_get(self.argument_ids, 0)),
207
+ bool(commons.safe_get(self.argument_names, 0)),
208
+ bool(commons.safe_get(self.argument_defaults, 0)))
209
+
210
+ @property
211
+ def parsed_proc_code(self) -> list[str, ArgumentType] | None:
212
+ return parse_proc_code(self.proc_code)
213
+
214
+ @staticmethod
215
+ def from_json(data: dict) -> Mutation:
216
+ assert isinstance(data, dict)
217
+
218
+ _tag_name = data.get("tagName", "mutation")
219
+ _children = data.get("children", [])
220
+
221
+ # procedures_prototype & procedures_call attrs
222
+ _proc_code = data.get("proccode")
223
+ _is_warp = data.get("warp")
224
+ if isinstance(_is_warp, str):
225
+ _is_warp = json.loads(_is_warp)
226
+
227
+ _argument_ids = data.get("argumentids")
228
+ # For some reason these are stored as JSON strings
229
+ if _argument_ids is not None:
230
+ _argument_ids = json.loads(_argument_ids)
231
+
232
+ # procedures_prototype attrs
233
+ _argument_names = data.get("argumentnames")
234
+ _argument_defaults = data.get("argumentdefaults")
235
+ # For some reason these are stored as JSON strings
236
+ if _argument_names is not None:
237
+ assert isinstance(_argument_names, str)
238
+ _argument_names = json.loads(_argument_names)
239
+ if _argument_defaults is not None:
240
+ assert isinstance(_argument_defaults, str)
241
+ _argument_defaults = json.loads(_argument_defaults)
242
+ _argument_settings = ArgSettings(_argument_ids is not None,
243
+ _argument_names is not None,
244
+ _argument_defaults is not None)
245
+
246
+ # control_stop attrs
247
+ _has_next = data.get("hasnext")
248
+ if isinstance(_has_next, str):
249
+ _has_next = json.loads(_has_next)
250
+
251
+ def get(_lst: list | tuple | None, _idx: int):
252
+ if _lst is None:
253
+ return None
254
+
255
+ if len(_lst) <= _idx:
256
+ return None
257
+ else:
258
+ return _lst[_idx]
259
+
260
+ if _argument_ids is None:
261
+ _arguments = None
262
+ else:
263
+ _arguments = []
264
+ for i, _arg_id in enumerate(_argument_ids):
265
+ _arg_name = get(_argument_names, i)
266
+ _arg_default = get(_argument_defaults, i)
267
+
268
+ _arguments.append(Argument(_arg_name, _arg_default, _arg_id))
269
+
270
+ return Mutation(_tag_name, _children, _proc_code, _is_warp, _arguments, _has_next, _argument_settings)
271
+
272
+ def to_json(self) -> dict | None:
273
+ _json = {
274
+ "tagName": self.tag_name,
275
+ "children": self.children,
276
+ }
277
+ commons.noneless_update(_json, {
278
+ "proccode": self.proc_code,
279
+ "warp": commons.dumps_ifnn(self.is_warp),
280
+ "argumentids": commons.dumps_ifnn(self.argument_ids),
281
+ "argumentnames": commons.dumps_ifnn(self.argument_names),
282
+ "argumentdefaults": commons.dumps_ifnn(self.argument_defaults),
283
+
284
+ "hasNext": commons.dumps_ifnn(self.has_next)
285
+ })
286
+
287
+ return _json
288
+
289
+ def link_arguments(self):
290
+ if self.arguments is None:
291
+ return
292
+
293
+ # You only need to fetch argument data if you actually have arguments
294
+ if len(self.arguments) > 0:
295
+ if self.arguments[0].name is None:
296
+ # This requires linking
297
+ _proc_uses = self.sprite.find_block(self.argument_ids, "argument ids", True)
298
+ # Note: Sometimes there may not be any argument ids provided. There will be no way to find out the names
299
+ # Technically, defaults can be found by using the proc code
300
+ for _use in _proc_uses:
301
+ if _use.mutation.argument_settings > self.argument_settings:
302
+ self.arguments = _use.mutation.arguments
303
+ if int(self.argument_settings) == 3:
304
+ # If all of our argument data is filled, we can stop early
305
+ return
306
+
307
+ # We can still work out argument defaults from parsing the proc code
308
+ if self.arguments[0].default is None:
309
+ _parsed = self.parsed_proc_code
310
+ _arg_phs: Iterable[ArgumentType] = filter(lambda tkn: isinstance(tkn, ArgumentType),
311
+ _parsed)
312
+ for i, _arg_ph in enumerate(_arg_phs):
313
+ self.arguments[i].default = _arg_ph.default
314
+
315
+ for _argument in self.arguments:
316
+ _argument.mutation = self
317
+ _argument.link_using_mutation()
@@ -0,0 +1,91 @@
1
+ """
2
+ Collection of block information, stating input/field names and opcodes
3
+ New version of sbuild.py
4
+
5
+ May want to completely change this later
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+
11
+ from . import prim
12
+ from ..utils.enums import _EnumWrapper
13
+
14
+
15
+ @dataclass(init=True, repr=True)
16
+ class FieldUsage:
17
+ name: str
18
+ value_type: prim.PrimTypes = None
19
+
20
+
21
+ @dataclass(init=True, repr=True)
22
+ class SpecialFieldUsage(FieldUsage):
23
+ name: str
24
+ attrs: list[str] = None
25
+ if attrs is None:
26
+ attrs = []
27
+
28
+ value_type: None = None
29
+
30
+
31
+ @dataclass(init=True, repr=True)
32
+ class InputUsage:
33
+ name: str
34
+ value_type: prim.PrimTypes = None
35
+ default_obscurer: BlockUsage = None
36
+
37
+
38
+ @dataclass(init=True, repr=True)
39
+ class BlockUsage:
40
+ opcode: str
41
+ fields: list[FieldUsage] = None
42
+ if fields is None:
43
+ fields = []
44
+
45
+ inputs: list[InputUsage] = None
46
+ if inputs is None:
47
+ inputs = []
48
+
49
+
50
+ class BlockUsages(_EnumWrapper):
51
+ # Special Enum blocks
52
+ MATH_NUMBER = BlockUsage(
53
+ "math_number",
54
+ [SpecialFieldUsage("NUM", ["name", "value"])]
55
+ )
56
+ MATH_POSITIVE_NUMBER = BlockUsage(
57
+ "math_positive_number",
58
+ [SpecialFieldUsage("NUM", ["name", "value"])]
59
+ )
60
+ MATH_WHOLE_NUMBER = BlockUsage(
61
+ "math_whole_number",
62
+ [SpecialFieldUsage("NUM", ["name", "value"])]
63
+ )
64
+ MATH_INTEGER = BlockUsage(
65
+ "math_integer",
66
+ [SpecialFieldUsage("NUM", ["name", "value"])]
67
+ )
68
+ MATH_ANGLE = BlockUsage(
69
+ "math_angle",
70
+ [SpecialFieldUsage("NUM", ["name", "value"])]
71
+ )
72
+ COLOUR_PICKER = BlockUsage(
73
+ "colour_picker",
74
+ [SpecialFieldUsage("COLOUR", ["name", "value"])]
75
+ )
76
+ TEXT = BlockUsage(
77
+ "text",
78
+ [SpecialFieldUsage("TEXT", ["name", "value"])]
79
+ )
80
+ EVENT_BROADCAST_MENU = BlockUsage(
81
+ "event_broadcast_menu",
82
+ [SpecialFieldUsage("BROADCAST_OPTION", ["name", "id", "value", "variableType"])]
83
+ )
84
+ DATA_VARIABLE = BlockUsage(
85
+ "data_variable",
86
+ [SpecialFieldUsage("VARIABLE", ["name", "id", "value", "variableType"])]
87
+ )
88
+ DATA_LISTCONTENTS = BlockUsage(
89
+ "data_listcontents",
90
+ [SpecialFieldUsage("LIST", ["name", "id", "value", "variableType"])]
91
+ )