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