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,507 @@
1
+ from __future__ import annotations
2
+
3
+ import warnings
4
+ from typing import Optional, Iterable, Self
5
+
6
+ from . import base, sprite, mutation, field, inputs, commons, vlb, blockshape, prim, comment, build_defaulting
7
+ from ..utils import exceptions
8
+
9
+
10
+ class Block(base.SpriteSubComponent):
11
+ def __init__(self, _opcode: str, _shadow: bool = False, _top_level: Optional[bool] = None,
12
+ _mutation: Optional[mutation.Mutation] = None, _fields: Optional[dict[str, field.Field]] = None,
13
+ _inputs: Optional[dict[str, inputs.Input]] = None, x: int = 0, y: int = 0, pos: Optional[tuple[int, int]] = None,
14
+
15
+ _next: Optional[Block] = None, _parent: Optional[Block] = None,
16
+ *, _next_id: Optional[str] = None, _parent_id: Optional[str] = None, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT):
17
+ # Defaulting for args
18
+ if _fields is None:
19
+ _fields = {}
20
+ if _inputs is None:
21
+ _inputs = {}
22
+
23
+ if pos is not None:
24
+ x, y = pos
25
+
26
+ self.opcode = _opcode
27
+ self.is_shadow = _shadow
28
+ self.is_top_level = _top_level
29
+
30
+ self.x, self.y = x, y
31
+
32
+ self.mutation = _mutation
33
+ self.fields = _fields
34
+ self.inputs = _inputs
35
+
36
+ self._next_id = _next_id
37
+ """
38
+ Temporarily stores id of next block. Will be used later during project instantiation to find the next block object
39
+ """
40
+ self._parent_id = _parent_id
41
+ """
42
+ Temporarily stores id of parent block. Will be used later during project instantiation to find the parent block object
43
+ """
44
+
45
+ self.next = _next
46
+ self.parent = _parent
47
+
48
+ self.check_toplevel()
49
+
50
+ super().__init__(_sprite)
51
+ self.link_subcomponents()
52
+
53
+ def __repr__(self):
54
+ return f"Block<{self.opcode!r}>"
55
+
56
+ def link_subcomponents(self):
57
+ if self.mutation:
58
+ self.mutation.block = self
59
+
60
+ for iterable in (self.fields.values(), self.inputs.values()):
61
+ for subcomponent in iterable:
62
+ subcomponent.block = self
63
+
64
+ def add_input(self, name: str, _input: inputs.Input) -> Self:
65
+ self.inputs[name] = _input
66
+ for val in (_input.value, _input.obscurer):
67
+ if isinstance(val, Block):
68
+ val.parent = self
69
+ return self
70
+
71
+ def add_field(self, name: str, _field: field.Field) -> Self:
72
+ self.fields[name] = _field
73
+ return self
74
+
75
+ def set_mutation(self, _mutation: mutation.Mutation) -> Self:
76
+ self.mutation = _mutation
77
+ _mutation.block = self
78
+ _mutation.link_arguments()
79
+ return self
80
+
81
+ def set_comment(self, _comment: comment.Comment) -> Self:
82
+ _comment.block = self
83
+ self.sprite.add_comment(_comment)
84
+
85
+ return self
86
+
87
+ def check_toplevel(self):
88
+ self.is_top_level = self.parent is None
89
+
90
+ if not self.is_top_level:
91
+ self.x, self.y = None, None
92
+
93
+ @property
94
+ def target(self):
95
+ """
96
+ Alias for sprite
97
+ """
98
+ return self.sprite
99
+
100
+ @property
101
+ def block_shape(self) -> blockshape.BlockShape:
102
+ """
103
+ Search for the blockshape stored in blockshape.py
104
+ :return: The block's block shape (by opcode)
105
+ """
106
+ _shape = blockshape.BlockShapes.find(self.opcode, "opcode")
107
+ if _shape is None:
108
+ warnings.warn(f"No blockshape {self.opcode!r} exists! Defaulting to {blockshape.BlockShapes.UNDEFINED}")
109
+ return blockshape.BlockShapes.UNDEFINED
110
+ return _shape
111
+
112
+ @property
113
+ def can_next(self):
114
+ """
115
+ :return: Whether the block *can* have a next block (basically checks if it's not a cap block, also considering the behaviour of control_stop)
116
+ """
117
+ _shape = self.block_shape
118
+ if _shape.is_cap is not blockshape.MUTATION_DEPENDENT:
119
+ return _shape.is_attachable
120
+ else:
121
+ if self.mutation is None:
122
+ # If there's no mutation, let's just assume yes
123
+ warnings.warn(f"{self} has no mutation! Assuming we can add block ;-;")
124
+ return True
125
+
126
+ return self.mutation.has_next
127
+
128
+ @property
129
+ def id(self) -> str | None:
130
+ """
131
+ Work out the id of this block by searching through the sprite dictionary
132
+ """
133
+ # warnings.warn(f"Using block IDs can cause consistency issues and is not recommended")
134
+ # This property is used when converting comments to JSON (we don't want random warning when exporting a project)
135
+ for _block_id, _block in self.sprite.blocks.items():
136
+ if _block is self:
137
+ return _block_id
138
+
139
+ # Let's just automatically assign ourselves an id
140
+ self.sprite.add_block(self)
141
+
142
+ @property
143
+ def parent_id(self):
144
+ if self.parent is not None:
145
+ return self.parent.id
146
+ else:
147
+ return None
148
+
149
+ @property
150
+ def next_id(self):
151
+ if self.next is not None:
152
+ return self.next.id
153
+ else:
154
+ return None
155
+
156
+ @property
157
+ def relatives(self) -> list[Block]:
158
+ """
159
+ :return: A list of blocks which are related to this block (e.g. parent, next, inputs)
160
+ """
161
+ _ret = []
162
+
163
+ def yield_block(_block: Block | None):
164
+ if isinstance(_block, Block):
165
+ _ret.append(_block)
166
+
167
+ yield_block(self.next)
168
+ yield_block(self.parent)
169
+
170
+ return _ret
171
+
172
+ @property
173
+ def children(self) -> list[Block | prim.Prim]:
174
+ """
175
+ :return: A list of blocks that are inside of this block, **NOT INCLUDING THE ATTACHED BLOCK**
176
+ """
177
+ _children = []
178
+ for _input in self.inputs.values():
179
+ if isinstance(_input.value, Block) or isinstance(_input.value, prim.Prim):
180
+ _children.append(_input.value)
181
+
182
+ if _input.obscurer is not None:
183
+ _children.append(_input.obscurer)
184
+ return _children
185
+
186
+ @property
187
+ def previous_chain(self):
188
+ if self.parent is None:
189
+ return [self]
190
+
191
+ return [self] + self.parent.previous_chain
192
+
193
+ @property
194
+ def attached_chain(self):
195
+ if self.next is None:
196
+ return [self]
197
+
198
+ return [self] + self.next.attached_chain
199
+
200
+ @property
201
+ def complete_chain(self):
202
+ # Both previous and attached chains start with self
203
+ return self.previous_chain[:1:-1] + self.attached_chain
204
+
205
+ @property
206
+ def top_level_block(self):
207
+ """
208
+ same as the old stack_parent property from sbedtior v1
209
+ """
210
+ return self.previous_chain[-1]
211
+
212
+ @property
213
+ def bottom_level_block(self):
214
+ return self.attached_chain[-1]
215
+
216
+ @property
217
+ def stack_tree(self):
218
+ """
219
+ :return: A tree-like nested list structure representing the stack of blocks, including inputs, starting at this block
220
+ """
221
+ _tree = [self]
222
+ for child in self.children:
223
+ if isinstance(child, prim.Prim):
224
+ _tree.append(child)
225
+ elif isinstance(child, Block):
226
+ _tree.append(child.stack_tree)
227
+
228
+ if self.next:
229
+ _tree += self.next.stack_tree
230
+
231
+ return _tree
232
+
233
+ @property
234
+ def category(self):
235
+ """
236
+ Works out what category of block this is using the opcode. Does not perform validation
237
+ """
238
+ return self.opcode.split('_')[0]
239
+
240
+ @property
241
+ def is_input(self):
242
+ """
243
+ :return: Whether this block is an input obscurer or value
244
+ """
245
+ return self.parent_input is not None
246
+
247
+ @property
248
+ def is_next_block(self):
249
+ """
250
+ :return: Whether this block is attached (as next block) to a previous block and not an input
251
+ """
252
+ return self.parent and not self.is_input
253
+
254
+ @property
255
+ def parent_input(self):
256
+ if not self.parent:
257
+ return None
258
+
259
+ for _input in self.parent.inputs.values():
260
+ if _input.obscurer is self or _input.value is self:
261
+ return _input
262
+ return None
263
+
264
+ @property
265
+ def new_id(self):
266
+ return self.sprite.new_id
267
+
268
+ @property
269
+ def comment(self) -> comment.Comment | None:
270
+ for _comment in self.sprite.comments:
271
+ if _comment.block is self:
272
+ return _comment
273
+ return None
274
+
275
+ @property
276
+ def turbowarp_block_opcode(self):
277
+ """
278
+ :return: The 'opcode' if this is a turbowarp block: e.g.
279
+ - log
280
+ - breakpoint
281
+ - error
282
+ - warn
283
+ - is compiled?
284
+ - is turbowarp?
285
+ - is forkphorus?
286
+ If it's not one, just returns None
287
+ """
288
+ if self.opcode == "procedures_call":
289
+ if self.mutation:
290
+ if self.mutation.proc_code:
291
+ # \u200B is a zero-width space
292
+ if self.mutation.proc_code == "\u200B\u200Bbreakpoint\u200B\u200B":
293
+ return "breakpoint"
294
+ elif self.mutation.proc_code == "\u200B\u200Blog\u200B\u200B %s":
295
+ return "log"
296
+ elif self.mutation.proc_code == "\u200B\u200Berror\u200B\u200B %s":
297
+ return "error"
298
+ elif self.mutation.proc_code == "\u200B\u200Bwarn\u200B\u200B %s":
299
+ return "warn"
300
+
301
+ elif self.opcode == "argument_reporter_boolean":
302
+ arg = self.fields.get("VALUE")
303
+
304
+ if arg is not None:
305
+ arg = arg.value
306
+ if isinstance(arg, str):
307
+ arg = arg.lower()
308
+
309
+ if arg == "is turbowarp?":
310
+ return "is_turbowarp?"
311
+
312
+ elif arg == "is compiled?":
313
+ return "is_compiled?"
314
+
315
+ elif arg == "is forkphorus?":
316
+ return "is_forkphorus?"
317
+
318
+ return None
319
+
320
+ @property
321
+ def is_turbowarp_block(self):
322
+ return self.turbowarp_block_opcode is not None
323
+
324
+ @staticmethod
325
+ def from_json(data: dict) -> Block:
326
+ """
327
+ Load a block from the JSON dictionary.
328
+ :param data: a dictionary (not list)
329
+ :return: The new Block object
330
+ """
331
+ _opcode = data["opcode"]
332
+
333
+ _x, _y = data.get("x"), data.get("y")
334
+
335
+ _next_id = data.get("next")
336
+ _parent_id = data.get("parent")
337
+
338
+ _shadow = data.get("shadow", False)
339
+ _top_level = data.get("topLevel", _parent_id is None)
340
+
341
+ _inputs = {}
342
+ for _input_code, _input_data in data.get("inputs", {}).items():
343
+ _inputs[_input_code] = inputs.Input.from_json(_input_data)
344
+
345
+ _fields = {}
346
+ for _field_code, _field_data in data.get("fields", {}).items():
347
+ _fields[_field_code] = field.Field.from_json(_field_data)
348
+
349
+ if "mutation" in data:
350
+ _mutation = mutation.Mutation.from_json(data["mutation"])
351
+ else:
352
+ _mutation = None
353
+
354
+ return Block(_opcode, _shadow, _top_level, _mutation, _fields, _inputs, _x, _y, _next_id=_next_id,
355
+ _parent_id=_parent_id)
356
+
357
+ def to_json(self) -> dict:
358
+ self.check_toplevel()
359
+
360
+ _json = {
361
+ "opcode": self.opcode,
362
+ "next": self.next_id,
363
+ "parent": self.parent_id,
364
+ "inputs": {_id: _input.to_json() for _id, _input in self.inputs.items()},
365
+ "fields": {_id: _field.to_json() for _id, _field in self.fields.items()},
366
+ "shadow": self.is_shadow,
367
+ "topLevel": self.is_top_level,
368
+ }
369
+ _comment = self.comment
370
+ if _comment:
371
+ commons.noneless_update(_json, {
372
+ "comment": _comment.id
373
+ })
374
+
375
+ if self.is_top_level:
376
+ commons.noneless_update(_json, {
377
+ "x": self.x,
378
+ "y": self.y,
379
+ })
380
+
381
+ if self.mutation is not None:
382
+ commons.noneless_update(_json, {
383
+ "mutation": self.mutation.to_json(),
384
+ })
385
+
386
+ return _json
387
+
388
+ def link_using_sprite(self, link_subs: bool = True):
389
+ if link_subs:
390
+ self.link_subcomponents()
391
+
392
+ if self.mutation:
393
+ self.mutation.link_arguments()
394
+
395
+ if self._parent_id is not None:
396
+ self.parent = self.sprite.find_block(self._parent_id, "id")
397
+ if self.parent is not None:
398
+ self._parent_id = None
399
+
400
+ if self._next_id is not None:
401
+ self.next = self.sprite.find_block(self._next_id, "id")
402
+ if self.next is not None:
403
+ self._next_id = None
404
+
405
+ for _block in self.relatives:
406
+ _block.sprite = self.sprite
407
+
408
+ for _field in self.fields.values():
409
+ if _field.id is not None:
410
+ new_value = self.sprite.find_vlb(_field.id, "id")
411
+ if new_value is None:
412
+ # We probably need to add a local global variable
413
+ _type = _field.type
414
+
415
+ if _type == field.Types.VARIABLE:
416
+ # Create a new variable
417
+ new_value = vlb.Variable(commons.gen_id(),
418
+ _field.value)
419
+ elif _type == field.Types.LIST:
420
+ # Create a list
421
+ new_value = vlb.List(commons.gen_id(),
422
+ _field.value)
423
+ elif _type == field.Types.BROADCAST:
424
+ # Create a broadcast
425
+ new_value = vlb.Broadcast(commons.gen_id(),
426
+ _field.value)
427
+ else:
428
+ # Something probably went wrong
429
+ warnings.warn(
430
+ f"Could not find {_field.id!r} in {self.sprite}. Can't create a new {_type} so we gave a warning")
431
+
432
+ if new_value is not None:
433
+ self.sprite.add_local_global(new_value)
434
+
435
+ # Check again since there may have been a newly created VLB
436
+ if new_value is not None:
437
+ _field.value = new_value
438
+ _field.id = None
439
+
440
+ for _input in self.inputs.values():
441
+ _input.link_using_block()
442
+
443
+ # Adding/removing block
444
+ def attach_block(self, new: Block) -> Block:
445
+ if not self.can_next:
446
+ raise exceptions.BadBlockShape(f"{self.block_shape} cannot be stacked onto")
447
+ elif new.block_shape.is_hat or not new.block_shape.is_stack:
448
+ raise exceptions.BadBlockShape(f"{new.block_shape} is not stackable")
449
+
450
+ new.parent = self
451
+ new.next = self.next
452
+
453
+ self.next = new
454
+
455
+ new.check_toplevel()
456
+ self.sprite.add_block(new)
457
+
458
+ return new
459
+
460
+ def duplicate_single_block(self) -> Block:
461
+ return self.attach_block(self.dcopy())
462
+
463
+ def attach_chain(self, *chain: Iterable[Block]) -> Block:
464
+ attaching_block = self
465
+ for _block in chain:
466
+ attaching_block = attaching_block.attach_block(_block)
467
+
468
+ return attaching_block
469
+
470
+ def duplicate_chain(self) -> Block:
471
+ return self.bottom_level_block.attach_chain(
472
+ *map(Block.dcopy, self.attached_chain)
473
+ )
474
+
475
+ def slot_above(self, new: Block) -> Block:
476
+ if not new.can_next:
477
+ raise exceptions.BadBlockShape(f"{new.block_shape} cannot be stacked onto")
478
+
479
+ elif self.block_shape.is_hat or not self.block_shape.is_stack:
480
+ raise exceptions.BadBlockShape(f"{self.block_shape} is not stackable")
481
+
482
+ new.parent, new.next = self.parent, self
483
+
484
+ self.parent = new
485
+
486
+ if new.parent:
487
+ new.parent.next = new
488
+
489
+ return self.sprite.add_block(new)
490
+
491
+ def delete_single_block(self):
492
+ if self.is_next_block:
493
+ self.parent.next = self.next
494
+
495
+ if self.next:
496
+ self.next.parent = self.parent
497
+
498
+ if self.is_top_level:
499
+ self.next.is_top_level = True
500
+ self.next.x, self.next.y = self.next.x, self.next.y
501
+
502
+ self.sprite.remove_block(self)
503
+
504
+ def delete_chain(self):
505
+ for _block in self.attached_chain:
506
+ _block.delete_single_block()
507
+