diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 617577ee..7b45b4ab 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -20,18 +20,12 @@ body: label: What happened? validations: required: true - - type: dropdown - id: cloudvariables-location + - type: textarea + id: code attributes: - label: Cloud variables - description: If your bug is based in some form on cloud variables, which cloud variable server did you use? If your bug doesn't have anything to do with cloud variables, submit "No cloud vars" - options: - - Scratch - - TurboWarp - - Custom Cloud Server - - Other - - No cloud vars - default: 0 + label: Your code. + description: Put your code here. Be careful not to reveal your login data. + render: python validations: required: true - type: textarea diff --git a/.gitignore b/.gitignore index d5adc59f..f4afdc51 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ scratchattach.code-workspace **/.DS_Store setup.py setup.py -.env \ No newline at end of file +.env +tests/manual_tests/** diff --git a/assets/CloudRequests_Template.sb3 b/assets/CloudRequests_Template.sb3 index 2c6362ec..862eda26 100644 Binary files a/assets/CloudRequests_Template.sb3 and b/assets/CloudRequests_Template.sb3 differ diff --git a/requirements.txt b/requirements.txt index 53d1e870..9125bd3b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,5 @@ websocket-client requests bs4 SimpleWebSocketServer +typing-extensions +browser_cookie3 diff --git a/scratchattach/cloud/_base.py b/scratchattach/cloud/_base.py index 99993f3e..4f8b3660 100644 --- a/scratchattach/cloud/_base.py +++ b/scratchattach/cloud/_base.py @@ -24,13 +24,13 @@ def close(self) -> None: import websocket -from ..site import session -from ..eventhandlers import cloud_recorder -from ..utils import exceptions -from ..eventhandlers.cloud_requests import CloudRequests -from ..eventhandlers.cloud_events import CloudEvents -from ..eventhandlers.cloud_storage import CloudStorage -from ..site import cloud_activity +from scratchattach.site import session +from scratchattach.eventhandlers import cloud_recorder +from scratchattach.utils import exceptions +from scratchattach.eventhandlers.cloud_requests import CloudRequests +from scratchattach.eventhandlers.cloud_events import CloudEvents +from scratchattach.eventhandlers.cloud_storage import CloudStorage +from scratchattach.site import cloud_activity T = TypeVar("T") diff --git a/scratchattach/cloud/cloud.py b/scratchattach/cloud/cloud.py index 84799ec1..05d04169 100644 --- a/scratchattach/cloud/cloud.py +++ b/scratchattach/cloud/cloud.py @@ -4,9 +4,9 @@ from ._base import BaseCloud from typing import Type -from ..utils.requests import Requests as requests -from ..utils import exceptions, commons -from ..site import cloud_activity +from scratchattach.utils.requests import requests +from scratchattach.utils import exceptions, commons +from scratchattach.site import cloud_activity class ScratchCloud(BaseCloud): @@ -88,7 +88,7 @@ def get_all_vars(self, *, use_logs=False): def events(self, *, use_logs=False): if self._session is None or use_logs: - from ..eventhandlers.cloud_events import CloudLogEvents + from scratchattach.eventhandlers.cloud_events import CloudLogEvents return CloudLogEvents(self) else: return super().events() diff --git a/scratchattach/editor/asset.py b/scratchattach/editor/asset.py index 4c2988c4..2a244586 100644 --- a/scratchattach/editor/asset.py +++ b/scratchattach/editor/asset.py @@ -10,12 +10,19 @@ @dataclass(init=True, repr=True) class AssetFile: + """ + Represents the file information for an asset + - stores the filename, data, and md5 hash + """ filename: str - _data: bytes = field(repr=False, default=None) - _md5: str = field(repr=False, default=None) + _data: bytes = field(repr=False, default_factory=bytes) + _md5: str = field(repr=False, default_factory=str) @property def data(self): + """ + Return the contents of the asset file, as bytes + """ if self._data is None: # Download and cache rq = requests.get(f"https://assets.scratch.mit.edu/internalapi/asset/{self.filename}/get/") @@ -28,6 +35,9 @@ def data(self): @property def md5(self): + """ + Compute/retrieve the md5 hash value of the asset file data + """ if self._md5 is None: self._md5 = md5(self.data).hexdigest() @@ -38,7 +48,7 @@ class Asset(base.SpriteSubComponent): def __init__(self, name: str = "costume1", file_name: str = "b7853f557e4426412e64bb3da6531a99.svg", - _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): + _sprite: commons.SpriteInput = build_defaulting.SPRITE_DEFAULT): """ Represents a generic asset. Can be a sound or an image. https://en.scratch-wiki.info/wiki/Scratch_File_Format#Assets @@ -60,22 +70,40 @@ def __repr__(self): @property def folder(self): + """ + Get the folder name of this asset, based on the asset name. Uses the turbowarp syntax + """ return commons.get_folder_name(self.name) @property def name_nfldr(self): + """ + Get the asset name after removing the folder name + """ return commons.get_name_nofldr(self.name) @property def file_name(self): + """ + Get the exact file name, as it would be within an sb3 file + equivalent to the md5ext value using in scratch project JSON + """ return f"{self.id}.{self.data_format}" @property def md5ext(self): + """ + Get the exact file name, as it would be within an sb3 file + equivalent to the md5ext value using in scratch project JSON + """ return self.file_name @property def parent(self): + """ + Return the project that this asset is attached to. If there is no attached project, + try returning the attached sprite + """ if self.project is None: return self.sprite else: @@ -83,6 +111,9 @@ def parent(self): @property def asset_file(self) -> AssetFile: + """ + Get the associated asset file object for this asset object + """ for asset_file in self.parent.asset_data: if asset_file.filename == self.file_name: return asset_file @@ -94,17 +125,27 @@ def asset_file(self) -> AssetFile: @staticmethod def from_json(data: dict): + """ + Load asset data from project.json + """ _name = data.get("name") + assert isinstance(_name, str) _file_name = data.get("md5ext") if _file_name is None: if "dataFormat" in data and "assetId" in data: _id = data["assetId"] _data_format = data["dataFormat"] _file_name = f"{_id}.{_data_format}" + else: + _file_name = "" + assert isinstance(_file_name, str) return Asset(_name, _file_name) def to_json(self) -> dict: + """ + Convert asset data to project.json format + """ return { "name": self.name, @@ -113,6 +154,7 @@ def to_json(self) -> dict: "dataFormat": self.data_format, } + # todo: implement below: """ @staticmethod def from_file(fp: str, name: str = None): @@ -132,9 +174,9 @@ def __init__(self, bitmap_resolution=None, rotation_center_x: int | float = 48, rotation_center_y: int | float = 50, - _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): + _sprite: commons.SpriteInput = build_defaulting.SPRITE_DEFAULT): """ - A costume. An asset with additional properties + A costume (image). An asset with additional properties https://en.scratch-wiki.info/wiki/Scratch_File_Format#Costumes """ super().__init__(name, file_name, _sprite) @@ -145,6 +187,9 @@ def __init__(self, @staticmethod def from_json(data): + """ + Load costume data from project.json + """ _asset_load = Asset.from_json(data) bitmap_resolution = data.get("bitmapResolution") @@ -156,6 +201,9 @@ def from_json(data): bitmap_resolution, rotation_center_x, rotation_center_y) def to_json(self) -> dict: + """ + Convert costume to project.json format + """ _json = super().to_json() _json.update({ "bitmapResolution": self.bitmap_resolution, @@ -184,6 +232,9 @@ def __init__(self, @staticmethod def from_json(data): + """ + Load sound from project.json + """ _asset_load = Asset.from_json(data) rate = data.get("rate") @@ -191,6 +242,9 @@ def from_json(data): return Sound(_asset_load.name, _asset_load.file_name, rate, sample_count) def to_json(self) -> dict: + """ + Convert Sound to project.json format + """ _json = super().to_json() commons.noneless_update(_json, { "rate": self.rate, diff --git a/scratchattach/editor/base.py b/scratchattach/editor/base.py index aebbbca8..d60fc10c 100644 --- a/scratchattach/editor/base.py +++ b/scratchattach/editor/base.py @@ -8,15 +8,21 @@ import json from abc import ABC, abstractmethod from io import TextIOWrapper -from typing import Optional, Any, TYPE_CHECKING, BinaryIO +from typing import Optional, Any, TYPE_CHECKING, BinaryIO, Union if TYPE_CHECKING: - from . import project, sprite, block, mutation, asset + from . import project, block, asset + from . import mutation as module_mutation + from . import sprite as module_sprite + from . import commons from . import build_defaulting class Base(ABC): + """ + Abstract base class for most sa.editor classes. Implements copy functions + """ def dcopy(self): """ :return: A **deep** copy of self @@ -31,22 +37,33 @@ def copy(self): class JSONSerializable(Base, ABC): + """ + 'Interface' for to_json() and from_json() methods + Also implements save_json() using to_json() + """ @staticmethod @abstractmethod - def from_json(data: dict | list | Any): + def from_json(data): pass @abstractmethod - def to_json(self) -> dict | list | Any: + def to_json(self): pass def save_json(self, name: str = ''): + """ + Save a json file + """ data = self.to_json() with open(f"{self.__class__.__name__.lower()}{name}.json", "w") as f: json.dump(data, f) class JSONExtractable(JSONSerializable, ABC): + """ + Interface for objects that can be loaded from zip archives containing json files (sprite/project) + Only has one method - load_json + """ @staticmethod @abstractmethod def load_json(data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool = True, _name: Optional[str] = None) -> tuple[ @@ -58,40 +75,43 @@ def load_json(data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool = :param _name: Any provided name (will automatically find one otherwise) :return: tuple of the name, asset data & json as a string """ - ... class ProjectSubcomponent(JSONSerializable, ABC): + """ + Base class for any class with an associated project + """ def __init__(self, _project: Optional[project.Project] = None): self.project = _project class SpriteSubComponent(JSONSerializable, ABC): - def __init__(self, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): + """ + Base class for any class with an associated sprite + """ + sprite: module_sprite.Sprite + def __init__(self, _sprite: "commons.SpriteInput" = build_defaulting.SPRITE_DEFAULT): if _sprite is build_defaulting.SPRITE_DEFAULT: - _sprite = build_defaulting.current_sprite() - + retrieved_sprite = build_defaulting.current_sprite() + assert retrieved_sprite is not None, "You don't have any sprites." + _sprite = retrieved_sprite self.sprite = _sprite - # @property - # def sprite(self): - # if self._sprite is None: - # print("ok, ", build_defaulting.current_sprite()) - # return build_defaulting.current_sprite() - # else: - # return self._sprite - - # @sprite.setter - # def sprite(self, value): - # self._sprite = value - @property def project(self) -> project.Project: - return self.sprite.project + """ + Get associated project by proxy of the associated sprite + """ + p = self.sprite.project + assert p is not None + return p class IDComponent(SpriteSubComponent, ABC): - def __init__(self, _id: str, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): + """ + Base class for classes with an id attribute + """ + def __init__(self, _id: str, _sprite: "commons.SpriteInput" = build_defaulting.SPRITE_DEFAULT): self.id = _id super().__init__(_sprite) @@ -103,8 +123,7 @@ class NamedIDComponent(IDComponent, ABC): """ Base class for Variables, Lists and Broadcasts (Name + ID + sprite) """ - - def __init__(self, _id: str, name: str, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): + def __init__(self, _id: str, name: str, _sprite: "commons.SpriteInput" = build_defaulting.SPRITE_DEFAULT): self.name = name super().__init__(_id, _sprite) @@ -113,30 +132,62 @@ def __repr__(self): class BlockSubComponent(JSONSerializable, ABC): + """ + Base class for classes with associated blocks + """ def __init__(self, _block: Optional[block.Block] = None): self.block = _block @property - def sprite(self) -> sprite.Sprite: - return self.block.sprite + def sprite(self) -> module_sprite.Sprite: + """ + Fetch sprite by proxy of the block + """ + b = self.block + assert b is not None + return b.sprite @property def project(self) -> project.Project: - return self.sprite.project + """ + Fetch project by proxy of the sprite (by proxy of the block) + """ + p = self.sprite.project + assert p is not None + return p class MutationSubComponent(JSONSerializable, ABC): - def __init__(self, _mutation: Optional[mutation.Mutation] = None): + """ + Base class for classes with associated mutations + """ + mutation: Optional[module_mutation.Mutation] + def __init__(self, _mutation: Optional[module_mutation.Mutation] = None): self.mutation = _mutation @property def block(self) -> block.Block: - return self.mutation.block + """ + Fetch block by proxy of mutation + """ + m = self.mutation + assert m is not None + b = m.block + assert b is not None + return b @property - def sprite(self) -> sprite.Sprite: + def sprite(self) -> module_sprite.Sprite: + """ + Fetch sprite by proxy of block (by proxy of mutation) + """ return self.block.sprite @property def project(self) -> project.Project: - return self.sprite.project + """ + Fetch project by proxy of sprite (by proxy of block (by proxy of mutation)) + """ + p = self.sprite.project + assert p is not None + return p diff --git a/scratchattach/editor/block.py b/scratchattach/editor/block.py index b812080c..4ed47650 100644 --- a/scratchattach/editor/block.py +++ b/scratchattach/editor/block.py @@ -1,20 +1,24 @@ from __future__ import annotations import warnings -from typing import Optional, Iterable +from typing import Optional, Iterable, Union from typing_extensions import Self from . import base, sprite, mutation, field, inputs, commons, vlb, blockshape, prim, comment, build_defaulting -from ..utils import exceptions +from scratchattach.utils import exceptions class Block(base.SpriteSubComponent): + """ + Represents a block in the scratch editor, as a subcomponent of a sprite. + """ + _id: Optional[str] = None def __init__(self, _opcode: str, _shadow: bool = False, _top_level: Optional[bool] = None, _mutation: Optional[mutation.Mutation] = None, _fields: Optional[dict[str, field.Field]] = None, _inputs: Optional[dict[str, inputs.Input]] = None, x: int = 0, y: int = 0, pos: Optional[tuple[int, int]] = None, _next: Optional[Block] = None, _parent: Optional[Block] = None, - *, _next_id: Optional[str] = None, _parent_id: Optional[str] = None, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT): + *, _next_id: Optional[str] = None, _parent_id: Optional[str] = None, _sprite: commons.SpriteInput = build_defaulting.SPRITE_DEFAULT): # Defaulting for args if _fields is None: _fields = {} @@ -34,14 +38,11 @@ def __init__(self, _opcode: str, _shadow: bool = False, _top_level: Optional[boo self.fields = _fields self.inputs = _inputs + # Temporarily stores id of next block. Will be used later during project instantiation to find the next block object self._next_id = _next_id - """ - Temporarily stores id of next block. Will be used later during project instantiation to find the next block object - """ + + # Temporarily stores id of parent block. Will be used later during project instantiation to find the parent block object self._parent_id = _parent_id - """ - Temporarily stores id of parent block. Will be used later during project instantiation to find the parent block object - """ self.next = _next self.parent = _parent @@ -55,6 +56,9 @@ def __repr__(self): return f"Block<{self.opcode!r}>" def link_subcomponents(self): + """ + Iterate through subcomponents and assign the 'block' attribute + """ if self.mutation: self.mutation.block = self @@ -63,6 +67,9 @@ def link_subcomponents(self): subcomponent.block = self def add_input(self, name: str, _input: inputs.Input) -> Self: + """ + Add an input to the block. + """ # not sure what else to say self.inputs[name] = _input for val in (_input.value, _input.obscurer): if isinstance(val, Block): @@ -70,22 +77,34 @@ def add_input(self, name: str, _input: inputs.Input) -> Self: return self def add_field(self, name: str, _field: field.Field) -> Self: + """ + Add a field to the block. + """ # not sure what else to sa self.fields[name] = _field return self def set_mutation(self, _mutation: mutation.Mutation) -> Self: + """ + Attach a mutation object and call mutation.link_arguments() + """ # this comment explains *what* this does, not *why* self.mutation = _mutation _mutation.block = self _mutation.link_arguments() return self def set_comment(self, _comment: comment.Comment) -> Self: + """ + Attach a comment and add it to the sprite. + """ _comment.block = self self.sprite.add_comment(_comment) return self def check_toplevel(self): + """ + Edit the toplevel, x, and y attributes based on whether the parent attribute is None + """ self.is_top_level = self.parent is None if not self.is_top_level: @@ -95,7 +114,7 @@ def check_toplevel(self): def target(self): """ Alias for sprite - """ + """ # remove this? return self.sprite @property @@ -107,7 +126,8 @@ def block_shape(self) -> blockshape.BlockShape: _shape = blockshape.BlockShapes.find(self.opcode, "opcode") if _shape is None: warnings.warn(f"No blockshape {self.opcode!r} exists! Defaulting to {blockshape.BlockShapes.UNDEFINED}") - return blockshape.BlockShapes.UNDEFINED + _shape = blockshape.BlockShapes.UNDEFINED + assert isinstance(_shape, blockshape.BlockShape) return _shape @property @@ -127,21 +147,32 @@ def can_next(self): return self.mutation.has_next @property - def id(self) -> str | None: + def id(self) -> str: """ Work out the id of this block by searching through the sprite dictionary """ + if self._id: + return self.id # warnings.warn(f"Using block IDs can cause consistency issues and is not recommended") # This property is used when converting comments to JSON (we don't want random warning when exporting a project) for _block_id, _block in self.sprite.blocks.items(): if _block is self: - return _block_id + self._id = _block_id + return self.id # Let's just automatically assign ourselves an id self.sprite.add_block(self) + return self.id + + @id.setter + def id(self, value: str) -> None: + self._id = value @property def parent_id(self): + """ + Get the id of the parent block, if applicable + """ if self.parent is not None: return self.parent.id else: @@ -149,6 +180,9 @@ def parent_id(self): @property def next_id(self): + """ + Get the id of the next block, if applicable + """ if self.next is not None: return self.next.id else: @@ -186,13 +220,19 @@ def children(self) -> list[Block | prim.Prim]: @property def previous_chain(self): - if self.parent is None: + """ + Recursive getter method to get all previous blocks in the blockchain (until hitting a top-level block) + """ + if self.parent is None: # todo: use is_top_level? return [self] return [self] + self.parent.previous_chain @property def attached_chain(self): + """ + Recursive getter method to get all next blocks in the blockchain (until hitting a bottom-levell block) + """ if self.next is None: return [self] @@ -200,23 +240,30 @@ def attached_chain(self): @property def complete_chain(self): - # Both previous and attached chains start with self + """ + Attach previous and attached chains from this block + """ return self.previous_chain[:1:-1] + self.attached_chain @property def top_level_block(self): """ + Get the first block in the block stack that this block is part of same as the old stack_parent property from sbedtior v1 """ return self.previous_chain[-1] @property def bottom_level_block(self): + """ + Get the last block in the block stack that this block is part of + """ return self.attached_chain[-1] @property def stack_tree(self): """ + Useful for showing a block stack in the console, using pprint :return: A tree-like nested list structure representing the stack of blocks, including inputs, starting at this block """ _tree = [self] @@ -254,6 +301,9 @@ def is_next_block(self): @property def parent_input(self): + """ + Fetch an input that this block is placed inside of (if applicable) + """ if not self.parent: return None @@ -268,6 +318,9 @@ def new_id(self): @property def comment(self) -> comment.Comment | None: + """ + Fetch an associated comment (if applicable) by searching the associated sprite + """ for _comment in self.sprite.comments: if _comment.block is self: return _comment @@ -320,6 +373,9 @@ def turbowarp_block_opcode(self): @property def is_turbowarp_block(self): + """ + Return whether this block is actually a turbowarp debugger/boolean block, based on mutation + """ return self.turbowarp_block_opcode is not None @staticmethod @@ -356,6 +412,9 @@ def from_json(data: dict) -> Block: _parent_id=_parent_id) def to_json(self) -> dict: + """ + Convert a block to the project.json format + """ self.check_toplevel() _json = { @@ -387,6 +446,9 @@ def to_json(self) -> dict: return _json def link_using_sprite(self, link_subs: bool = True): + """ + Link this block to various other blocks once the sprite has been assigned + """ if link_subs: self.link_subcomponents() @@ -443,6 +505,9 @@ def link_using_sprite(self, link_subs: bool = True): # Adding/removing block def attach_block(self, new: Block) -> Block: + """ + Connect another block onto the boottom of this block (not necessarily bottom of chain) + """ if not self.can_next: raise exceptions.BadBlockShape(f"{self.block_shape} cannot be stacked onto") elif new.block_shape.is_hat or not new.block_shape.is_stack: @@ -474,6 +539,9 @@ def duplicate_chain(self) -> Block: ) def slot_above(self, new: Block) -> Block: + """ + Place a single block directly above this block + """ if not new.can_next: raise exceptions.BadBlockShape(f"{new.block_shape} cannot be stacked onto") @@ -503,6 +571,9 @@ def delete_single_block(self): self.sprite.remove_block(self) def delete_chain(self): + """ + Delete all blocks in the attached blockchain (and self) + """ for _block in self.attached_chain: _block.delete_single_block() diff --git a/scratchattach/editor/blockshape.py b/scratchattach/editor/blockshape.py index ebfa6614..a60f1731 100644 --- a/scratchattach/editor/blockshape.py +++ b/scratchattach/editor/blockshape.py @@ -3,15 +3,18 @@ """ from __future__ import annotations -# Perhaps this should be merged with pallet.py +# Perhaps this should be merged with pallete.py from dataclasses import dataclass from typing import Final from . import commons -from ..utils.enums import _EnumWrapper +from scratchattach.utils.enums import _EnumWrapper class _MutationDependent(commons.Singleton): + """ + Singleton value that represents the uncertainty of a vablue because it depends on block mutation data. + """ def __bool__(self): raise TypeError("Need mutation data to work out attribute value.") @@ -23,7 +26,7 @@ def __bool__(self): @dataclass(init=True, repr=True) class BlockShape: """ - A class that describes the shape of a block; e.g. is it a stack, c-mouth, cap, hat reporter, boolean or menu block? + The shape of a block; e.g. is it a stack, c-mouth, cap, hat reporter, boolean or menu block? """ is_stack: bool | _MutationDependent = False # Most blocks - e.g. move [10] steps is_c_mouth: bool | _MutationDependent = False # Has substack - e.g. repeat @@ -262,7 +265,7 @@ class BlockShapes(_EnumWrapper): MAKEYMAKEY_MENU_KEY = BlockShape(is_reporter=True, is_menu=True, opcode="makeymakey_menu_KEY") MAKEYMAKEY_MENU_SEQUENCE = BlockShape(is_reporter=True, is_menu=True, opcode="makeymakey_menu_SEQUENCE") - MICROBIT_WHENBUTTONPRESSED = BlockShape(opcode="microbit_whenButtonPressed") + MICROBIT_WHENBUTTONPRESSED = BlockShape(opcode="microbit_whenButtonPressed") # todo: finish this MICROBIT_ISBUTTONPRESSED = BlockShape(opcode="microbit_isButtonPressed") MICROBIT_WHENGESTURE = BlockShape(opcode="microbit_whenGesture") MICROBIT_DISPLAYSYMBOL = BlockShape(opcode="microbit_displaySymbol") diff --git a/scratchattach/editor/build_defaulting.py b/scratchattach/editor/build_defaulting.py index b7d22f7d..ef946c11 100644 --- a/scratchattach/editor/build_defaulting.py +++ b/scratchattach/editor/build_defaulting.py @@ -3,7 +3,7 @@ """ from __future__ import annotations -from typing import Iterable, TYPE_CHECKING, Final +from typing import Iterable, TYPE_CHECKING, Final, Literal if TYPE_CHECKING: from . import sprite, block, prim, comment @@ -11,11 +11,12 @@ class _SetSprite(commons.Singleton): + INSTANCE = 0 def __repr__(self): return f'' -SPRITE_DEFAULT: Final[_SetSprite] = _SetSprite() +SPRITE_DEFAULT: Final[Literal[_SetSprite.INSTANCE]] = _SetSprite.INSTANCE _sprite_stack: list[sprite.Sprite] = [] @@ -25,6 +26,9 @@ def stack_add_sprite(_sprite: sprite.Sprite): def current_sprite() -> sprite.Sprite | None: + """ + Retrieve the default sprite from the top of the sprite stack + """ if len(_sprite_stack) == 0: return None return _sprite_stack[-1] diff --git a/scratchattach/editor/code_translation/__init__.py b/scratchattach/editor/code_translation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scratchattach/editor/code_translation/example.txt b/scratchattach/editor/code_translation/example.txt new file mode 100644 index 00000000..1de0e5a8 --- /dev/null +++ b/scratchattach/editor/code_translation/example.txt @@ -0,0 +1,15 @@ +## For testing. The file extension is temporary and might change in the future. ## + +when (green_flag_clicked) { + until { + say() + say() + } + sa() +} + +custom_block hello_%s_ (, awd) { + until(1) { + say((8 + 4) * (3 - 1) / (2 + 6 / 3) + (15 - (4 * 2)) * (9 + 1) / 5 - 18 / (3 + 2 * 1)) + } ## Hi! ## ## Hi1 ## ## ## +} \ No newline at end of file diff --git a/scratchattach/editor/code_translation/language.lark b/scratchattach/editor/code_translation/language.lark new file mode 100644 index 00000000..f0b8c520 --- /dev/null +++ b/scratchattach/editor/code_translation/language.lark @@ -0,0 +1,58 @@ +start: top_level_block* + +top_level_block: hat "{" block* "}" [comments] + | PREPROC_INSTR + | COMMMENT + +PREPROC_INSTR: "%%" _PREPROC_INSTR_CONTENT "%%" + +_PREPROC_INSTR_CONTENT: /[\s\S]+?/ + +comments: COMMMENT+ + +COMMMENT: "##" _COMMMENT_CONTENT "##" + +_COMMMENT_CONTENT: /[\s\S]+?/ + +hat: [event_hat | block_hat] + +event_hat: "when"i "(" EVENT ")" +block_hat: "custom_block"i BLOCK_NAME "(" [ param ("," param)* ] ")" + +param: value_param + | bool_param +value_param: PARAM_NAME +bool_param: "<" PARAM_NAME ">" + +EVENT: "green_flag_clicked"i + +block: (CONTROL_BLOCK_NAME ["(" block_params ")"] "{" block_content "}" + | BLOCK_NAME ["(" block_params ")"] [";" | "\n" | " "]) [comments] + +block_params: expr* +block_content: block* + +expr: (addition | subtraction | multiplication | division | LITERAL_NUMBER | "(" expr ")") [comments] + +low_expr1: (("(" expr ")") | LITERAL_NUMBER) [comments] +low_expr2: multiplication | division | low_expr1 + +addition: expr "+" expr +subtraction: expr "-" low_expr2 +multiplication: low_expr2 "*" low_expr2 +division: low_expr2 "/" low_expr1 + +CONTROL_BLOCK_NAME: "repeat"i + | "until"i + | "forever"i + +PARAM_NAME: ("a".."z" | "A".."Z" | "_" | "-" | "%" | "+")+ +BLOCK_NAME: [MODULE_NAME "."] ("a".."z" | "A".."Z" | "_" | "-" | "%" | "+")+ + +MODULE_NAME: "params"i + | "vars"i + | "lists"i + +WS: /\s+/ +%ignore WS +%import common.SIGNED_NUMBER -> LITERAL_NUMBER \ No newline at end of file diff --git a/scratchattach/editor/code_translation/parse.py b/scratchattach/editor/code_translation/parse.py new file mode 100644 index 00000000..2ca7a516 --- /dev/null +++ b/scratchattach/editor/code_translation/parse.py @@ -0,0 +1,177 @@ +from __future__ import annotations +from pathlib import Path +from typing import Union, Generic, TypeVar +from abc import ABC, abstractmethod +from collections.abc import Sequence + +from lark import Lark, Transformer, Tree, Token, v_args +from lark.reconstruct import Reconstructor + +R = TypeVar("R") +class SupportsRead(ABC, Generic[R]): + @abstractmethod + def read(self, size: int | None = -1) -> R: + pass + +LANG_PATH = Path(__file__).parent / "language.lark" + +lang = Lark(LANG_PATH.read_text(), maybe_placeholders=False) +reconstructor = Reconstructor(lang) + +def parse(script: Union[str, bytes, SupportsRead[str], Path]) -> Tree: + if isinstance(script, Path): + script = script.read_text() + if isinstance(script, SupportsRead): + read_data = script.read() + assert isinstance(read_data, str) + script = read_data + if isinstance(script, bytes): + script = script.decode("utf-8") + return lang.parse(script) + +def unparse(tree: Tree) -> str: + return reconstructor.reconstruct(tree) + +class PrettyUnparser(Transformer): + INDENT_STRING = " " + + @classmethod + def _indent(cls, text): + if not text: + return "" + return "\n".join(cls.INDENT_STRING + line for line in text.splitlines()) + + def PARAM_NAME(self, token): + return token.value + + def BLOCK_NAME(self, token): + return token.value + + def EVENT(self, token): + return token.value + + def CONTROL_BLOCK_NAME(self, token): + return token.value + + def _PREPROC_INSTR_CONTENT(self, token): + return token.value + + def _COMMMENT_CONTENT(self, token): + return token.value + + @v_args(inline=True) + def hat(self, child): + return child + + @v_args(inline=True) + def param(self, child): + return child + + @v_args(inline=True) + def value_param(self, name): + return name + + @v_args(inline=True) + def bool_param(self, name): + return f"<{name}>" + + @v_args(inline=True) + def event_hat(self, event_name): + return f"when ({event_name})" + + def block_hat(self, items): + name, *params = items + params_str = ", ".join(params) + return f"custom_block {name} ({params_str})" + + @v_args(inline=True) + def PREPROC_INSTR(self, content): + return f"{content}" + + @v_args(inline=True) + def COMMMENT(self, content): + return f"{content}" + + def block(self, items): + params = [] + inner_blocks = [] + comments = [] + for i in items[1:]: + if not isinstance(i, Tree): + continue + if str(i.data) == "block_content": + inner_blocks.extend(i.children) + if str(i.data) == "block_params": + params.extend(i.children) + if str(i.data) == "comments": + comments.extend(i.children) + block_name = items[0] + block_text = f"{block_name}({', '.join(params)})" if params or not inner_blocks else f"{block_name}" + if inner_blocks: + blocks_content = "\n".join(inner_blocks) + indented_content = self._indent(blocks_content) + block_text += f" {{\n{indented_content}\n}}" + if comments: + block_text += f" {' '.join(comments)}" + return block_text + + def LITERAL_NUMBER(self, number: str): + return number + + def expr(self, items): + text = items[0] + if len(items) > 1: + text += f" {' '.join(items[1].children)}" + return text + + def low_expr1(self, items): + text = f"({items[0]})" if " " in items[0] else items[0] + if len(items) > 1: + text += f" {' '.join(items[1].children)}" + return text + + @v_args(inline=True) + def low_expr2(self, item): + return item + + def addition(self, items): + return items[0] + " + " + items[1] + + def subtraction(self, items): + return items[0] + " - " + items[1] + + def multiplication(self, items): + return items[0] + " * " + items[1] + + def division(self, items): + return items[0] + " / " + items[1] + + def top_level_block(self, items): + first_item = items[0] + if first_item.startswith("%%") or first_item.startswith("##"): + return first_item + + hat, *blocks = items + blocks_content = "\n".join(blocks) + indented_content = self._indent(blocks_content) + return f"{hat} {{\n{indented_content}\n}}" + + def start(self, items): + return "\n\n".join(items) + +def pretty_unparse(tree: Tree): + return PrettyUnparser().transform(tree) + +if __name__ == "__main__": + EXAMPLE_FILE = Path(__file__).parent / "example.txt" + tree = parse(EXAMPLE_FILE) + print(tree.pretty()) + print() + print() + print(tree) + print() + print() + print(unparse(tree)) + print() + print() + print(pretty_unparse(tree)) \ No newline at end of file diff --git a/scratchattach/editor/comment.py b/scratchattach/editor/comment.py index 82babd80..932c1075 100644 --- a/scratchattach/editor/comment.py +++ b/scratchattach/editor/comment.py @@ -5,6 +5,9 @@ class Comment(base.IDComponent): + """ + Represents a comment in the scratch editor. + """ def __init__(self, _id: Optional[str] = None, _block: Optional[block.Block] = None, x: int = 0, y: int = 0, width: int = 200, height: int = 200, minimized: bool = False, text: str = '', *, _block_id: Optional[str] = None, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT, pos: Optional[tuple[int, int]] = None): @@ -32,6 +35,9 @@ def __repr__(self): @property def block_id(self): + """ + Retrieve the id of the associateed block (if applicable) + """ if self.block is not None: return self.block.id elif self._block_id is not None: diff --git a/scratchattach/editor/commons.py b/scratchattach/editor/commons.py index 110c9c9e..76e785e8 100644 --- a/scratchattach/editor/commons.py +++ b/scratchattach/editor/commons.py @@ -6,11 +6,19 @@ import json import random import string -from typing import Optional, Final, Any +from typing import Optional, Final, Any, TYPE_CHECKING, Union +from enum import Enum -from ..utils import exceptions +if TYPE_CHECKING: + from . import sprite, build_defaulting -DIGITS: Final[tuple[str]] = tuple("0123456789") + SpriteInput = Union[sprite.Sprite, build_defaulting._SetSprite] +else: + SpriteInput = Any + +from scratchattach.utils import exceptions + +DIGITS: Final[tuple[str, ...]] = tuple("0123456789") ID_CHARS: Final[str] = string.ascii_letters + string.digits # + string.punctuation @@ -72,7 +80,8 @@ def read_exponent(sub: str): return json.loads(ret) - +# todo: consider if this should be moved to util.commons instead of editor.commons +# note: this is currently unused code def consume_json(_str: str, i: int = 0) -> str | float | int | dict | list | bool | None: """ *'gobble up some JSON until we hit something not quite so tasty'* @@ -134,16 +143,20 @@ def is_partial_json(_str: str, i: int = 0) -> bool: def is_valid_json(_str: Any) -> bool: + """ + Try to load a json string, if it fails, return False, else return true. + """ try: json.loads(_str) return True - except ValueError: - return False - except TypeError: + except (ValueError, TypeError): return False def noneless_update(obj: dict, update: dict) -> None: + """ + equivalent to dict.update, except and values of None are not assigned + """ for key, value in update.items(): if value is not None: obj[key] = value @@ -163,6 +176,9 @@ def remove_nones(obj: dict) -> None: def safe_get(lst: list | tuple, _i: int, default: Optional[Any] = None) -> Any: + """ + Like dict.get() but for lists + """ if len(lst) <= _i: return default else: @@ -182,7 +198,10 @@ def trim_final_nones(lst: list) -> list: return lst[:i] -def dumps_ifnn(obj: Any) -> str: +def dumps_ifnn(obj: Any) -> Optional[str]: + """ + Return json.dumps(obj) if the object is not None + """ if obj is None: return None else: @@ -190,9 +209,13 @@ def dumps_ifnn(obj: Any) -> str: def gen_id() -> str: - # The old 'naïve' method but that chances of a repeat are so miniscule - # Have to check if whitespace chars break it - # May later add checking within sprites so that we don't need such long ids (we can save space this way) + """ + Generate an id for scratch blocks/variables/lists/broadcasts + + The old 'naïve' method but that chances of a repeat are so miniscule + Have to check if whitespace chars break it + May later add checking within sprites so that we don't need such long ids (we can save space this way) + """ return ''.join(random.choices(ID_CHARS, k=20)) @@ -212,6 +235,9 @@ def sanitize_fn(filename: str): def get_folder_name(name: str) -> str | None: + """ + Get the name of the folder if this is a turbowarp-style costume name + """ if name.startswith('//'): return None @@ -231,13 +257,8 @@ def get_name_nofldr(name: str) -> str: else: return name[len(fldr) + 2:] - -class Singleton(object): - _instance: Singleton +# Parent enum class +class Singleton(Enum): def __new__(cls, *args, **kwargs): - if hasattr(cls, "_instance"): - return cls._instance - else: - cls._instance = super(Singleton, cls).__new__(cls) - return cls._instance + return super().__new__(cls, 0) diff --git a/scratchattach/editor/extension.py b/scratchattach/editor/extension.py index 81bfed06..866c9596 100644 --- a/scratchattach/editor/extension.py +++ b/scratchattach/editor/extension.py @@ -1,14 +1,21 @@ +""" +Enum & dataclass representing extension categories +""" + from __future__ import annotations from dataclasses import dataclass from . import base -from ..utils import enums +from scratchattach.utils import enums -@dataclass(init=True, repr=True) +@dataclass class Extension(base.JSONSerializable): + """ + Represents an extension in the Scratch block pallete - e.g. video sensing + """ code: str name: str = None @@ -40,4 +47,4 @@ class Extensions(enums._EnumWrapper): TRANSLATE = Extension("translate", "Translate Extension") VIDEOSENSING = Extension("videoSensing", "Video Sensing Extension") WEDO2 = Extension("wedo2", "LEGO Education WeDo 2.0 Extension") - COREEXAMPLE = Extension("coreExample", "CoreEx Extension") + COREEXAMPLE = Extension("coreExample", "CoreEx Extension") # hidden extension! diff --git a/scratchattach/editor/field.py b/scratchattach/editor/field.py index 2a0dc088..b8e4a898 100644 --- a/scratchattach/editor/field.py +++ b/scratchattach/editor/field.py @@ -38,6 +38,9 @@ def __repr__(self): @property def value_id(self): + """ + Get the id of the value associated with this field (if applicable) - when value is var/list/broadcast + """ if self.id is not None: return self.id else: @@ -48,6 +51,9 @@ def value_id(self): @property def value_str(self): + """ + Convert the associated value to a string - if this is a VLB, return the VLB name + """ if not isinstance(self.value, base.NamedIDComponent): return self.value else: @@ -55,6 +61,9 @@ def value_str(self): @property def name(self) -> str: + """ + Fetch the name of this field using the associated block + """ for _name, _field in self.block.fields.items(): if _field is self: return _name diff --git a/scratchattach/editor/inputs.py b/scratchattach/editor/inputs.py index c43050b2..3a8dba8d 100644 --- a/scratchattach/editor/inputs.py +++ b/scratchattach/editor/inputs.py @@ -8,8 +8,11 @@ from dataclasses import dataclass -@dataclass(init=True) +@dataclass class ShadowStatus: + """ + Dataclass representing a possible shadow value and giving it a name + """ idx: int name: str diff --git a/scratchattach/editor/meta.py b/scratchattach/editor/meta.py index 88b9a56f..9eef4f87 100644 --- a/scratchattach/editor/meta.py +++ b/scratchattach/editor/meta.py @@ -7,7 +7,7 @@ from typing import Optional -@dataclass(init=True, repr=True) +@dataclass class PlatformMeta(base.JSONSerializable): name: str = None url: str = field(repr=True, default=None) @@ -36,8 +36,13 @@ def from_json(data: dict | None): META_SET_PLATFORM = False -def set_meta_platform(true_false: bool = False): +def set_meta_platform(true_false: bool = None): + """ + toggle whether to set the meta platform by default (or specify a value) + """ global META_SET_PLATFORM + if true_false is None: + true_false = bool(1 - true_false) META_SET_PLATFORM = true_false @@ -69,7 +74,10 @@ def __repr__(self): @property def vm_is_valid(self): - # Thanks to TurboWarp for this pattern ↓↓↓↓, I just copied it + """ + Check whether the vm value is valid using a regex + Thanks to TurboWarp for this pattern ↓↓↓↓, I just copied it + """ return re.match("^([0-9]+\\.[0-9]+\\.[0-9]+)($|-)", self.vm) is not None def to_json(self): diff --git a/scratchattach/editor/monitor.py b/scratchattach/editor/monitor.py index 75a778c0..2f78dba0 100644 --- a/scratchattach/editor/monitor.py +++ b/scratchattach/editor/monitor.py @@ -2,6 +2,8 @@ from typing import Optional, TYPE_CHECKING +from typing_extensions import deprecated + if TYPE_CHECKING: from . import project @@ -26,6 +28,8 @@ def __init__(self, reporter: Optional[base.NamedIDComponent] = None, """ Represents a variable/list monitor https://en.scratch-wiki.info/wiki/Scratch_File_Format#Monitors + + Instantiating these yourself and attaching these to projects can lead to interesting results! """ assert isinstance(reporter, base.SpriteSubComponent) or reporter is None @@ -135,41 +139,43 @@ def link_using_project(self): self.reporter = new_vlb self.reporter_id = None - # @staticmethod - # def from_reporter(reporter: Block, _id: str = None, mode: str = "default", - # opcode: str = None, sprite_name: str = None, value=0, width: int | float = 0, - # height: int | float = 0, - # x: int | float = 5, y: int | float = 5, visible: bool = False, slider_min: int | float = 0, - # slider_max: int | float = 100, is_discrete: bool = True, params: dict = None): - # if "reporter" not in reporter.stack_type: - # warnings.warn(f"{reporter} is not a reporter block; the monitor will return '0'") - # elif "(menu)" in reporter.stack_type: - # warnings.warn(f"{reporter} is a menu block; the monitor will return '0'") - # # Maybe add note that length of list doesn't work fsr?? idk - # if _id is None: - # _id = reporter.opcode - # if opcode is None: - # opcode = reporter.opcode # .replace('_', ' ') - - # if params is None: - # params = {} - # for field in reporter.fields: - # if field.value_id is None: - # params[field.id] = field.value - # else: - # params[field.id] = field.value, field.value_id - - # return Monitor( - # _id, - # mode, - # opcode, - - # params, - # sprite_name, - # value, - - # width, height, - # x, y, - # visible, - # slider_min, slider_max, is_discrete - # ) + # todo: consider reimplementing this + @deprecated("This method does not work correctly (This may be fixed in the future)") + @staticmethod + def from_reporter(reporter: Block, _id: str = None, mode: str = "default", + opcode: str = None, sprite_name: str = None, value=0, width: int | float = 0, + height: int | float = 0, + x: int | float = 5, y: int | float = 5, visible: bool = False, slider_min: int | float = 0, + slider_max: int | float = 100, is_discrete: bool = True, params: dict = None): + if "reporter" not in reporter.stack_type: + warnings.warn(f"{reporter} is not a reporter block; the monitor will return '0'") + elif "(menu)" in reporter.stack_type: + warnings.warn(f"{reporter} is a menu block; the monitor will return '0'") + # Maybe add note that length of list doesn't work fsr?? idk + if _id is None: + _id = reporter.opcode + if opcode is None: + opcode = reporter.opcode # .replace('_', ' ') + + if params is None: + params = {} + for field in reporter.fields: + if field.value_id is None: + params[field.id] = field.value + else: + params[field.id] = field.value, field.value_id + + return Monitor( + _id, + mode, + opcode, + + params, + sprite_name, + value, + + width, height, + x, y, + visible, + slider_min, slider_max, is_discrete + ) diff --git a/scratchattach/editor/mutation.py b/scratchattach/editor/mutation.py index e12566a1..a416fb14 100644 --- a/scratchattach/editor/mutation.py +++ b/scratchattach/editor/mutation.py @@ -6,13 +6,13 @@ from typing import Optional, TYPE_CHECKING, Iterable, Any from . import base, commons -from ..utils import enums +from scratchattach.utils import enums if TYPE_CHECKING: from . import block -@dataclass(init=True) +@dataclass class ArgumentType(base.Base): type: str proc_str: str @@ -38,7 +38,7 @@ def default(self) -> str | None: return None -@dataclass(init=True, repr=True) +@dataclass class ArgSettings(base.Base): ids: bool names: bool @@ -61,7 +61,7 @@ def __lt__(self, other): return int(self) > int(other) -@dataclass(init=True, repr=True) +@dataclass class Argument(base.MutationSubComponent): name: str default: str = '' @@ -103,6 +103,10 @@ class ArgTypes(enums._EnumWrapper): def parse_proc_code(_proc_code: str) -> list[str, ArgumentType] | None: + """ + Parse a proccode (part of a mutation) into argument types and strings + """ + if _proc_code is None: return None token = '' @@ -209,6 +213,9 @@ def argument_settings(self) -> ArgSettings: @property def parsed_proc_code(self) -> list[str, ArgumentType] | None: + """ + Parse the proc code into arguments & strings + """ return parse_proc_code(self.proc_code) @staticmethod diff --git a/scratchattach/editor/pallete.py b/scratchattach/editor/pallete.py index 587fb485..4f157544 100644 --- a/scratchattach/editor/pallete.py +++ b/scratchattach/editor/pallete.py @@ -6,43 +6,42 @@ """ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field +from typing import Optional from . import prim -from ..utils.enums import _EnumWrapper +from scratchattach.utils.enums import _EnumWrapper -@dataclass(init=True, repr=True) +@dataclass class FieldUsage: name: str - value_type: prim.PrimTypes = None + value_type: Optional[prim.PrimTypes] = None -@dataclass(init=True, repr=True) +@dataclass class SpecialFieldUsage(FieldUsage): name: str - attrs: list[str] = None - if attrs is None: - attrs = [] + value_type: None = None # Order cannot be changed + attrs: list[str] = field(default_factory=list) - value_type: None = None -@dataclass(init=True, repr=True) +@dataclass class InputUsage: name: str - value_type: prim.PrimTypes = None - default_obscurer: BlockUsage = None + value_type: Optional[prim.PrimTypes] = None + default_obscurer: Optional[BlockUsage] = None -@dataclass(init=True, repr=True) +@dataclass class BlockUsage: opcode: str - fields: list[FieldUsage] = None + fields: Optional[list[FieldUsage]] = None if fields is None: fields = [] - inputs: list[InputUsage] = None + inputs: Optional[list[InputUsage]] = None if inputs is None: inputs = [] @@ -51,41 +50,41 @@ class BlockUsages(_EnumWrapper): # Special Enum blocks MATH_NUMBER = BlockUsage( "math_number", - [SpecialFieldUsage("NUM", ["name", "value"])] + [SpecialFieldUsage("NUM", attrs=["name", "value"])] ) MATH_POSITIVE_NUMBER = BlockUsage( "math_positive_number", - [SpecialFieldUsage("NUM", ["name", "value"])] + [SpecialFieldUsage("NUM", attrs=["name", "value"])] ) MATH_WHOLE_NUMBER = BlockUsage( "math_whole_number", - [SpecialFieldUsage("NUM", ["name", "value"])] + [SpecialFieldUsage("NUM", attrs=["name", "value"])] ) MATH_INTEGER = BlockUsage( "math_integer", - [SpecialFieldUsage("NUM", ["name", "value"])] + [SpecialFieldUsage("NUM", attrs=["name", "value"])] ) MATH_ANGLE = BlockUsage( "math_angle", - [SpecialFieldUsage("NUM", ["name", "value"])] + [SpecialFieldUsage("NUM", attrs=["name", "value"])] ) COLOUR_PICKER = BlockUsage( "colour_picker", - [SpecialFieldUsage("COLOUR", ["name", "value"])] + [SpecialFieldUsage("COLOUR", attrs=["name", "value"])] ) TEXT = BlockUsage( "text", - [SpecialFieldUsage("TEXT", ["name", "value"])] + [SpecialFieldUsage("TEXT", attrs=["name", "value"])] ) EVENT_BROADCAST_MENU = BlockUsage( "event_broadcast_menu", - [SpecialFieldUsage("BROADCAST_OPTION", ["name", "id", "value", "variableType"])] + [SpecialFieldUsage("BROADCAST_OPTION", attrs=["name", "id", "value", "variableType"])] ) DATA_VARIABLE = BlockUsage( "data_variable", - [SpecialFieldUsage("VARIABLE", ["name", "id", "value", "variableType"])] + [SpecialFieldUsage("VARIABLE", attrs=["name", "id", "value", "variableType"])] ) DATA_LISTCONTENTS = BlockUsage( "data_listcontents", - [SpecialFieldUsage("LIST", ["name", "id", "value", "variableType"])] + [SpecialFieldUsage("LIST", attrs=["name", "id", "value", "variableType"])] ) diff --git a/scratchattach/editor/prim.py b/scratchattach/editor/prim.py index 98fd0f94..969b37da 100644 --- a/scratchattach/editor/prim.py +++ b/scratchattach/editor/prim.py @@ -5,10 +5,10 @@ from typing import Optional, Callable, Final from . import base, sprite, vlb, commons, build_defaulting -from ..utils import enums, exceptions +from scratchattach.utils import enums, exceptions -@dataclass(init=True, repr=True) +@dataclass class PrimType(base.JSONSerializable): code: int name: str diff --git a/scratchattach/editor/project.py b/scratchattach/editor/project.py index 1793d433..4a1d9e2c 100644 --- a/scratchattach/editor/project.py +++ b/scratchattach/editor/project.py @@ -8,12 +8,15 @@ from zipfile import ZipFile from . import base, meta, extension, monitor, sprite, asset, vlb, twconfig, comment, commons -from ..site import session -from ..site.project import get_project -from ..utils import exceptions +from scratchattach.site import session +from scratchattach.site.project import get_project +from scratchattach.utils import exceptions class Project(base.JSONExtractable): + """ + sa.editor's equivalent of the ProjectBody. Represents the editor contents of a scratch project + """ def __init__(self, _name: Optional[str] = None, _meta: Optional[meta.Meta] = None, _extensions: Iterable[extension.Extension] = (), _monitors: Iterable[monitor.Monitor] = (), _sprites: Iterable[sprite.Sprite] = (), *, _asset_data: Optional[list[asset.AssetFile]] = None, _session: Optional[session.Session] = None): @@ -268,6 +271,9 @@ def export(self, fp: str, *, auto_open: bool = False, export_as_zip: bool = True os.system(f"explorer.exe \"{fp}\"") def add_monitor(self, _monitor: monitor.Monitor) -> monitor.Monitor: + """ + Bind a monitor to this project. Doing these manually can lead to interesting results. + """ _monitor.project = self _monitor.reporter_id = self.new_id self.monitors.append(_monitor) diff --git a/scratchattach/editor/sbuild.py b/scratchattach/editor/sbuild.py deleted file mode 100644 index ac807796..00000000 --- a/scratchattach/editor/sbuild.py +++ /dev/null @@ -1,2837 +0,0 @@ -from __future__ import annotations - -from .. import editor -from typing import Optional - -# Copied from sbuild so we have to make a few wrappers ;-; -# May need to recreate this from scratch. In which case, it is to be done in palette.py -class Block(editor.Block): - ... - -class Input(editor.Input): - ... -class Field(editor.Field): - ... -class Variable(editor.Variable): - ... -class List(editor.List): - ... -class Broadcast(editor.Broadcast): - ... -class Mutation(editor.Mutation): - ... - - -class Motion: - class MoveSteps(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_movesteps", _shadow=shadow, pos=pos) - - def set_steps(self, value, input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("STEPS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class TurnRight(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_turnright", _shadow=shadow, pos=pos) - - def set_degrees(self, value, input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("DEGREES", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class TurnLeft(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_turnleft", _shadow=shadow, pos=pos) - - def set_degrees(self, value, input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("DEGREES", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class GoTo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_goto", _shadow=shadow, pos=pos) - - def set_to(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input(Input("TO", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class GoToMenu(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_goto_menu", _shadow=shadow, pos=pos) - - def set_to(self, value: str = "_random_", value_id: Optional[str] = None): - return self.add_field(Field("TO", value, value_id)) - - class GoToXY(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_gotoxy", _shadow=shadow, pos=pos) - - def set_x(self, value, input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input(Input("X", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_y(self, value, input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input(Input("Y", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class GlideTo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_glideto", _shadow=shadow, pos=pos) - - def set_secs(self, value, input_type: str | int = "positive number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input(Input("SECS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_to(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input(Input("TO", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class GlideToMenu(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_glideto_menu", _shadow=shadow, pos=pos) - - def set_to(self, value: str = "_random_", value_id: Optional[str] = None): - return self.add_field(Field("TO", value, value_id)) - - class GlideSecsToXY(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_glidesecstoxy", _shadow=shadow, pos=pos) - - def set_x(self, value, input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input(Input("X", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_y(self, value, input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input(Input("Y", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_secs(self, value, input_type: str | int = "positive number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input(Input("SECS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class PointInDirection(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_pointindirection", _shadow=shadow, pos=pos) - - def set_direction(self, value, input_type: str | int = "angle", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("DIRECTION", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class PointTowards(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_pointtowards", _shadow=shadow, pos=pos) - - def set_towards(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("TOWARDS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class PointTowardsMenu(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_pointtowards_menu", _shadow=shadow, pos=pos) - - def set_towards(self, value: str = "_mouse_", value_id: Optional[str] = None): - return self.add_field(Field("TOWARDS", value, value_id)) - - class ChangeXBy(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_changexby", _shadow=shadow, pos=pos) - - def set_dx(self, value, input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input(Input("DX", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class ChangeYBy(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_changeyby", _shadow=shadow, pos=pos) - - def set_dy(self, value, input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input(Input("DY", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class SetX(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_setx", _shadow=shadow, pos=pos) - - def set_x(self, value, input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input(Input("X", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class SetY(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_sety", _shadow=shadow, pos=pos) - - def set_y(self, value, input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input(Input("Y", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class IfOnEdgeBounce(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_ifonedgebounce", _shadow=shadow, pos=pos) - - class SetRotationStyle(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_setrotationstyle", _shadow=shadow, pos=pos) - - def set_style(self, value: str = "all around", value_id: Optional[str] = None): - return self.add_field(Field("STYLE", value, value_id)) - - class XPosition(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_xposition", _shadow=shadow, pos=pos) - - class YPosition(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_yposition", _shadow=shadow, pos=pos) - - class Direction(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_direction", _shadow=shadow, pos=pos) - - class ScrollRight(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_scroll_right", _shadow=shadow, pos=pos) - - def set_distance(self, value, input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("DISTANCE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class ScrollUp(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_scroll_up", _shadow=shadow, pos=pos) - - def set_distance(self, value, input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("DISTANCE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class AlignScene(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_align_scene", _shadow=shadow, pos=pos) - - def set_alignment(self, value: str = "bottom-left", value_id: Optional[str] = None): - return self.add_field(Field("ALIGNMENT", value, value_id)) - - class XScroll(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_xscroll", _shadow=shadow, pos=pos) - - class YScroll(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "motion_yscroll", _shadow=shadow, pos=pos) - - -class Looks: - class SayForSecs(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_sayforsecs", _shadow=shadow, pos=pos) - - def set_message(self, value="Hello!", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("MESSAGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) - ) - - def set_secs(self, value=2, input_type: str | int = "positive integer", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("SECS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) - ) - - class Say(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_say", _shadow=shadow, pos=pos) - - def set_message(self, value="Hello!", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("MESSAGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) - ) - - class ThinkForSecs(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_thinkforsecs", _shadow=shadow, pos=pos) - - def set_message(self, value="Hmm...", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("MESSAGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) - ) - - def set_secs(self, value=2, input_type: str | int = "positive integer", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("SECS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) - ) - - class Think(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_think", _shadow=shadow, pos=pos) - - def set_message(self, value="Hmm...", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("MESSAGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) - ) - - class SwitchCostumeTo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_switchcostumeto", _shadow=shadow, pos=pos) - - def set_costume(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("COSTUME", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Costume(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_costume", _shadow=shadow, pos=pos) - - def set_costume(self, value: str = "costume1", value_id: Optional[str] = None): - return self.add_field(Field("COSTUME", value, value_id)) - - class NextCostume(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_nextcostume", _shadow=shadow, pos=pos) - - class SwitchBackdropTo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_switchbackdropto", _shadow=shadow, pos=pos) - - def set_backdrop(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("BACKDROP", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Backdrops(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_backdrops", _shadow=shadow, pos=pos) - - def set_backdrop(self, value: str = "costume1", value_id: Optional[str] = None): - return self.add_field(Field("BACKDROP", value, value_id)) - - class SwitchBackdropToAndWait(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_switchbackdroptoandwait", _shadow=shadow, pos=pos) - - def set_backdrop(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("BACKDROP", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class NextBackdrop(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_nextbackdrop", _shadow=shadow, pos=pos) - - class ChangeSizeBy(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_changesizeby", _shadow=shadow, pos=pos) - - def set_change(self, value="10", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("CHANGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class SetSizeTo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_setsizeto", _shadow=shadow, pos=pos) - - def set_size(self, value="100", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("SIZE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class ChangeEffectBy(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_changeeffectby", _shadow=shadow, pos=pos) - - def set_change(self, value="100", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("CHANGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_effect(self, value: str = "COLOR", value_id: Optional[str] = None): - return self.add_field(Field("EFFECT", value, value_id)) - - class SetEffectTo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_seteffectto", _shadow=shadow, pos=pos) - - def set_value(self, value="0", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("VALUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_effect(self, value: str = "COLOR", value_id: Optional[str] = None): - return self.add_field(Field("EFFECT", value, value_id)) - - class ClearGraphicEffects(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_cleargraphiceffects", _shadow=shadow, pos=pos) - - class Hide(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_hide", _shadow=shadow, pos=pos) - - class Show(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_show", _shadow=shadow, pos=pos) - - class GoToFrontBack(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_gotofrontback", _shadow=shadow, pos=pos) - - def set_front_back(self, value: str = "front", value_id: Optional[str] = None): - return self.add_field(Field("FRONT_BACK", value, value_id)) - - class GoForwardBackwardLayers(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_goforwardbackwardlayers", _shadow=shadow, pos=pos) - - def set_num(self, value="1", input_type: str | int = "positive integer", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("NUM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_fowrward_backward(self, value: str = "forward", value_id: Optional[str] = None): - return self.add_field(Field("FORWARD_BACKWARD", value, value_id)) - - class CostumeNumberName(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_costumenumbername", _shadow=shadow, pos=pos) - - def set_number_name(self, value: str = "string", value_id: Optional[str] = None): - return self.add_field(Field("NUMBER_NAME", value, value_id)) - - class BackdropNumberName(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_backdropnumbername", _shadow=shadow, pos=pos) - - def set_number_name(self, value: str = "number", value_id: Optional[str] = None): - return self.add_field(Field("NUMBER_NAME", value, value_id)) - - class Size(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_size", _shadow=shadow, pos=pos) - - class HideAllSprites(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_hideallsprites", _shadow=shadow, pos=pos) - - class SetStretchTo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_setstretchto", _shadow=shadow, pos=pos) - - def set_stretch(self, value="100", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("STRETCH", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class ChangeStretchBy(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "looks_changestretchby", _shadow=shadow, pos=pos) - - def set_change(self, value="10", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("CHANGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - -class Sounds: - class Play(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sound_play", _shadow=shadow, pos=pos) - - def set_sound_menu(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("SOUND_MENU", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class SoundsMenu(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sound_sounds_menu", _shadow=shadow, pos=pos) - - def set_sound_menu(self, value: str = "pop", value_id: Optional[str] = None): - return self.add_field(Field("SOUND_MENU", value, value_id)) - - class PlayUntilDone(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sound_playuntildone", _shadow=shadow, pos=pos) - - def set_sound_menu(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("SOUND_MENU", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class StopAllSounds(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sound_stopallsounds", _shadow=shadow, pos=pos) - - class ChangeEffectBy(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sound_changeeffectby", _shadow=shadow, pos=pos) - - def set_value(self, value="10", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("VALUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_effect(self, value: str = "PITCH", value_id: Optional[str] = None): - return self.add_field(Field("EFFECT", value, value_id)) - - class SetEffectTo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sound_seteffectto", _shadow=shadow, pos=pos) - - def set_value(self, value="100", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("VALUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_effect(self, value: str = "PITCH", value_id: Optional[str] = None): - return self.add_field(Field("EFFECT", value, value_id)) - - class ClearEffects(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sound_cleareffects", _shadow=shadow, pos=pos) - - class ChangeVolumeBy(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sound_changevolumeby", _shadow=shadow, pos=pos) - - def set_volume(self, value="-10", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("VOLUME", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class SetVolumeTo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sound_setvolumeto", _shadow=shadow, pos=pos) - - def set_volume(self, value="100", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("VOLUME", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Volume(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sound_volume", _shadow=shadow, pos=pos) - - -class Events: - class WhenFlagClicked(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "event_whenflagclicked", _shadow=shadow, pos=pos) - - class WhenKeyPressed(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "event_whenkeypressed", _shadow=shadow, pos=pos) - - def set_key_option(self, value: str = "space", value_id: Optional[str] = None): - return self.add_field(Field("KEY_OPTION", value, value_id)) - - class WhenThisSpriteClicked(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "event_whenthisspriteclicked", _shadow=shadow, pos=pos) - - class WhenStageClicked(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "event_whenstageclicked", _shadow=shadow, pos=pos) - - class WhenBackdropSwitchesTo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "event_whenbackdropswitchesto", _shadow=shadow, pos=pos) - - def set_backdrop(self, value: str = "backdrop1", value_id: Optional[str] = None): - return self.add_field(Field("BACKDROP", value, value_id)) - - class WhenGreaterThan(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "event_whengreaterthan", _shadow=shadow, pos=pos) - - def set_value(self, value="10", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("VALUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_when_greater_than_menu(self, value: str = "LOUDNESS", value_id: Optional[str] = None): - return self.add_field(Field("WHENGREATERTHANMENU", value, value_id)) - - class WhenBroadcastReceived(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "event_whenbroadcastreceived", _shadow=shadow, pos=pos) - - def set_broadcast_option(self, value="message1", value_id: str = "I didn't get an id..."): - return self.add_field(Field("BROADCAST_OPTION", value, value_id)) - - class Broadcast(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "event_broadcast", _shadow=shadow, pos=pos) - - def set_broadcast_input(self, value="message1", input_type: str | int = "broadcast", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("BROADCAST_INPUT", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class BroadcastAndWait(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "event_broadcastandwait", _shadow=shadow, pos=pos) - - def set_broadcast_input(self, value="message1", input_type: str | int = "broadcast", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("BROADCAST_INPUT", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class WhenTouchingObject(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "event_whentouchingobject", _shadow=shadow, pos=pos) - - def set_touching_object_menu(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("TOUCHINGOBJECTMENU", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class TouchingObjectMenu(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "event_touchingobjectmenu", _shadow=shadow, pos=pos) - - def set_touching_object_menu(self, value: str = "_mouse_", value_id: Optional[str] = None): - return self.add_field(Field("TOUCHINGOBJECTMENU", value, value_id)) - - -class Control: - class Wait(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "control_wait", _shadow=shadow, pos=pos) - - def set_duration(self, value="1", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("DURATION", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Forever(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "control_forever", _shadow=shadow, pos=pos, can_next=False) - - def set_substack(self, value, input_type: str | int = "block", shadow_status: int = 2, *, - input_id: Optional[str] = None): - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - inp = Input("SUBSTACK", value, input_type, shadow_status, input_id=input_id) - return self.add_input(inp) - - class If(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "control_if", _shadow=shadow, pos=pos) - - def set_substack(self, value, input_type: str | int = "block", shadow_status: int = 2, *, - input_id: Optional[str] = None): - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - inp = Input("SUBSTACK", value, input_type, shadow_status, input_id=input_id) - return self.add_input(inp) - - def set_condition(self, value, input_type: str | int = "block", shadow_status: int = 2, *, - input_id: Optional[str] = None): - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - inp = Input("CONDITION", value, input_type, shadow_status, input_id=input_id) - return self.add_input(inp) - - class IfElse(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "control_if_else", _shadow=shadow, pos=pos) - - def set_substack1(self, value, input_type: str | int = "block", shadow_status: int = 2, *, - input_id: Optional[str] = None): - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - inp = Input("SUBSTACK", value, input_type, shadow_status, input_id=input_id) - return self.add_input(inp) - - def set_substack2(self, value, input_type: str | int = "block", shadow_status: int = 2, *, - input_id: Optional[str] = None): - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - inp = Input("SUBSTACK2", value, input_type, shadow_status, input_id=input_id) - return self.add_input(inp) - - def set_condition(self, value, input_type: str | int = "block", shadow_status: int = 2, *, - input_id: Optional[str] = None): - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - inp = Input("CONDITION", value, input_type, shadow_status, input_id=input_id) - return self.add_input(inp) - - class WaitUntil(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "control_wait_until", _shadow=shadow, pos=pos) - - def set_condition(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("CONDITION", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class RepeatUntil(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "control_repeat_until", _shadow=shadow, pos=pos) - - def set_substack(self, value, input_type: str | int = "block", shadow_status: int = 2, *, - input_id: Optional[str] = None): - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - inp = Input("SUBSTACK", value, input_type, shadow_status, input_id=input_id) - return self.add_input(inp) - - def set_condition(self, value, input_type: str | int = "block", shadow_status: int = 2, *, - input_id: Optional[str] = None): - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - inp = Input("CONDITION", value, input_type, shadow_status, input_id=input_id) - return self.add_input(inp) - - class While(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "control_while", _shadow=shadow, pos=pos) - - def set_substack(self, value, input_type: str | int = "block", shadow_status: int = 2, *, - input_id: Optional[str] = None): - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - inp = Input("SUBSTACK", value, input_type, shadow_status, input_id=input_id) - return self.add_input(inp) - - def set_condition(self, value, input_type: str | int = "block", shadow_status: int = 2, *, - input_id: Optional[str] = None): - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - inp = Input("CONDITION", value, input_type, shadow_status, input_id=input_id) - return self.add_input(inp) - - class Stop(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "control_stop", _shadow=shadow, pos=pos, mutation=Mutation()) - - def set_stop_option(self, value: str = "all", value_id: Optional[str] = None): - return self.add_field(Field("STOP_OPTION", value, value_id)) - - def set_hasnext(self, has_next: bool = True): - self.mutation.has_next = has_next - return self - - class StartAsClone(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "control_start_as_clone", _shadow=shadow, pos=pos) - - class CreateCloneOf(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "control_create_clone_of", _shadow=shadow, pos=pos) - - def set_clone_option(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("CLONE_OPTION", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class CreateCloneOfMenu(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "control_create_clone_of_menu", _shadow=shadow, pos=pos) - - def set_clone_option(self, value: str = "_myself_", value_id: Optional[str] = None): - return self.add_field(Field("CLONE_OPTION", value, value_id)) - - class DeleteThisClone(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "control_delete_this_clone", _shadow=shadow, pos=pos, can_next=False) - - class ForEach(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "control_for_each", _shadow=shadow, pos=pos) - - def set_substack(self, value, input_type: str | int = "block", shadow_status: int = 2, *, - input_id: Optional[str] = None): - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - inp = Input("SUBSTACK", value, input_type, shadow_status, input_id=input_id) - return self.add_input(inp) - - def set_value(self, value="5", input_type: str | int = "positive integer", shadow_status: int = 1, *, - input_id: Optional[str] = None): - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - inp = Input("VALUE", value, input_type, shadow_status, input_id=input_id) - return self.add_input(inp) - - def set_variable(self, value: str = "i", value_id: Optional[str] = None): - return self.add_field(Field("VARIABLE", value, value_id)) - - class GetCounter(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "control_get_counter", _shadow=shadow, pos=pos) - - class IncrCounter(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "control_incr_counter", _shadow=shadow, pos=pos) - - class ClearCounter(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "control_clear_counter", _shadow=shadow, pos=pos) - - class AllAtOnce(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "control_all_at_once", _shadow=shadow, pos=pos) - - def set_substack(self, value, input_type: str | int = "block", shadow_status: int = 2, *, - input_id: Optional[str] = None): - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - inp = Input("SUBSTACK", value, input_type, shadow_status, input_id=input_id) - return self.add_input(inp) - - -class Sensing: - class TouchingObject(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_touchingobject", _shadow=shadow, pos=pos) - - def set_touching_object_menu(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("TOUCHINGOBJECTMENU", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class TouchingObjectMenu(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_touchingobjectmenu", _shadow=shadow, pos=pos) - - def set_touching_object_menu(self, value: str = "_mouse_", value_id: Optional[str] = None): - return self.add_field(Field("TOUCHINGOBJECTMENU", value, value_id)) - - class TouchingColor(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_touchingcolor", _shadow=shadow, pos=pos) - - def set_color(self, value="#0000FF", input_type: str | int = "color", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("COLOR", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class ColorIsTouchingColor(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_coloristouchingcolor", _shadow=shadow, pos=pos) - - def set_color1(self, value="#0000FF", input_type: str | int = "color", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("COLOR", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_color2(self, value="#00FF00", input_type: str | int = "color", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("COLOR2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class DistanceTo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_distanceto", _shadow=shadow, pos=pos) - - def set_distance_to_menu(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("DISTANCETOMENU", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class DistanceToMenu(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_distancetomenu", _shadow=shadow, pos=pos) - - def set_distance_to_menu(self, value: str = "_mouse_", value_id: Optional[str] = None): - return self.add_field(Field("DISTANCETOMENU", value, value_id)) - - class Loud(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_loud", _shadow=shadow, pos=pos) - - class AskAndWait(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_askandwait", _shadow=shadow, pos=pos) - - def set_question(self, value="What's your name?", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("QUESTION", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) - ) - - class Answer(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_answer", _shadow=shadow, pos=pos) - - class KeyPressed(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_keypressed", _shadow=shadow, pos=pos) - - def set_key_option(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("KEY_OPTION", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class KeyOptions(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_keyoptions", _shadow=shadow, pos=pos) - - def set_key_option(self, value: str = "space", value_id: Optional[str] = None): - return self.add_field(Field("KEY_OPTION", value, value_id)) - - class MouseDown(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_mousedown", _shadow=shadow, pos=pos) - - class MouseX(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_mousex", _shadow=shadow, pos=pos) - - class MouseY(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_mousey", _shadow=shadow, pos=pos) - - class SetDragMode(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_setdragmode", _shadow=shadow, pos=pos) - - def set_drag_mode(self, value: str = "draggable", value_id: Optional[str] = None): - return self.add_field(Field("DRAG_MODE", value, value_id)) - - class Loudness(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_loudness", _shadow=shadow, pos=pos) - - class Timer(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_timer", _shadow=shadow, pos=pos) - - class ResetTimer(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_resettimer", _shadow=shadow, pos=pos) - - class Of(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_of", _shadow=shadow, pos=pos) - - def set_object(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("OBJECT", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_property(self, value: str = "backdrop #", value_id: Optional[str] = None): - return self.add_field(Field("PROPERTY", value, value_id)) - - class OfObjectMenu(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_of_object_menu", _shadow=shadow, pos=pos) - - def set_object(self, value: str = "_stage_", value_id: Optional[str] = None): - return self.add_field(Field("OBJECT", value, value_id)) - - class Current(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_current", _shadow=shadow, pos=pos) - - def set_current_menu(self, value: str = "YEAR", value_id: Optional[str] = None): - return self.add_field(Field("CURRENTMENU", value, value_id)) - - class DaysSince2000(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_dayssince2000", _shadow=shadow, pos=pos) - - class Username(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_username", _shadow=shadow, pos=pos) - - class UserID(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "sensing_userid", _shadow=shadow, pos=pos) - - -class Operators: - class Add(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_add", _shadow=shadow, pos=pos) - - def set_num1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("NUM1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_num2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("NUM2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Subtract(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_subtract", _shadow=shadow, pos=pos) - - def set_num1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("NUM1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_num2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("NUM2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Multiply(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_multiply", _shadow=shadow, pos=pos) - - def set_num1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("NUM1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_num2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("NUM2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Divide(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_divide", _shadow=shadow, pos=pos) - - def set_num1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("NUM1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_num2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("NUM2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Random(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_random", _shadow=shadow, pos=pos) - - def set_from(self, value="1", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("FROM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_to(self, value="10", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("TO", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class GT(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_gt", _shadow=shadow, pos=pos) - - def set_operand1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("OPERAND1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_operand2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("OPERAND2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class LT(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_lt", _shadow=shadow, pos=pos) - - def set_operand1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("OPERAND1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_operand2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("OPERAND2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Equals(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_equals", _shadow=shadow, pos=pos) - - def set_operand1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("OPERAND1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_operand2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("OPERAND2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class And(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_and", _shadow=shadow, pos=pos) - - def set_operand1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("OPERAND1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_operand2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("OPERAND2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Or(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_or", _shadow=shadow, pos=pos) - - def set_operand1(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("OPERAND1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_operand2(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("OPERAND2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Not(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_not", _shadow=shadow, pos=pos) - - def set_operand(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("OPERAND", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Join(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_join", _shadow=shadow, pos=pos) - - def set_string1(self, value="apple ", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("STRING1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_string2(self, value="banana", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("STRING2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class LetterOf(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_letter_of", _shadow=shadow, pos=pos) - - def set_letter(self, value="1", input_type: str | int = "positive integer", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("LETTER", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_string(self, value="apple", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("STRING", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Length(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_length", _shadow=shadow, pos=pos) - - def set_string(self, value="apple", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("STRING", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Contains(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_contains", _shadow=shadow, pos=pos) - - def set_string1(self, value="apple", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("STRING1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_string2(self, value="a", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("STRING2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Mod(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_mod", _shadow=shadow, pos=pos) - - def set_num1(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("NUM1", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_num2(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("NUM2", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Round(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_round", _shadow=shadow, pos=pos) - - def set_num(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("NUM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class MathOp(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "operator_mathop", _shadow=shadow, pos=pos) - - def set_num(self, value='', input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("NUM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_operator(self, value: str = "abs", value_id: Optional[str] = None): - return self.add_field(Field("OPERATOR", value, value_id)) - - -class Data: - class VariableArr(Block): - def __init__(self, value, input_type: str | int = "variable", shadow_status: Optional[int] = None, *, - pos: tuple[int | float, int | float] = (0, 0)): - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - inp = Input(None, value, input_type, shadow_status) - if inp.type_str == "block": - arr = inp.json[0] - else: - arr = inp.json[1][-1] - - super().__init__(array=arr, pos=pos) - - class Variable(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_variable", _shadow=shadow, pos=pos) - - def set_variable(self, value: str | Variable = "variable", value_id: Optional[str] = None): - return self.add_field(Field("VARIABLE", value, value_id)) - - class SetVariableTo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_setvariableto", _shadow=shadow, pos=pos) - - def set_value(self, value="0", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("VALUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_variable(self, value: str | Variable = "variable", value_id: Optional[str] = None): - return self.add_field(Field("VARIABLE", value, value_id)) - - class ChangeVariableBy(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_changevariableby", _shadow=shadow, pos=pos) - - def set_value(self, value="1", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("VALUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_variable(self, value: str | Variable = "variable", value_id: Optional[str] = None): - return self.add_field(Field("VARIABLE", value, value_id)) - - class ShowVariable(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_showvariable", _shadow=shadow, pos=pos) - - def set_variable(self, value: str | Variable = "variable", value_id: Optional[str] = None): - return self.add_field(Field("VARIABLE", value, value_id)) - - class HideVariable(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_hidevariable", _shadow=shadow, pos=pos) - - def set_variable(self, value: str | Variable = "variable", value_id: Optional[str] = None): - return self.add_field(Field("VARIABLE", value, value_id)) - - class ListArr(Block): - def __init__(self, value, input_type: str | int = "list", shadow_status: Optional[int] = None, *, - pos: tuple[int | float, int | float] = (0, 0)): - inp = Input(None, value, input_type, shadow_status) - if inp.type_str == "block": - arr = inp.json[0] - else: - arr = inp.json[1][-1] - - super().__init__(array=arr, pos=pos) - - class ListContents(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_listcontents", _shadow=shadow, pos=pos) - - def set_list(self, value: str | List = "my list", value_id: Optional[str] = None): - return self.add_field(Field("LIST", value, value_id)) - - class AddToList(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_addtolist", _shadow=shadow, pos=pos) - - def set_item(self, value="thing", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("ITEM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_list(self, value: str | List = "list", value_id: Optional[str] = None): - return self.add_field(Field("LIST", value, value_id)) - - class DeleteOfList(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_deleteoflist", _shadow=shadow, pos=pos) - - def set_index(self, value="random", input_type: str | int = "positive integer", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("INDEX", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_list(self, value: str | List = "list", value_id: Optional[str] = None): - return self.add_field(Field("LIST", value, value_id)) - - class InsertAtList(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_insertatlist", _shadow=shadow, pos=pos) - - def set_item(self, value="thing", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("ITEM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_index(self, value="random", input_type: str | int = "positive integer", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("INDEX", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_list(self, value: str | List = "list", value_id: Optional[str] = None): - return self.add_field(Field("LIST", value, value_id)) - - class DeleteAllOfList(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_deletealloflist", _shadow=shadow, pos=pos) - - def set_list(self, value: str | List = "list", value_id: Optional[str] = None): - return self.add_field(Field("LIST", value, value_id)) - - class ReplaceItemOfList(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_replaceitemoflist", _shadow=shadow, pos=pos) - - def set_item(self, value="thing", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("ITEM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_index(self, value="random", input_type: str | int = "positive integer", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("INDEX", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_list(self, value: str | List = "list", value_id: Optional[str] = None): - return self.add_field(Field("LIST", value, value_id)) - - class ItemOfList(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_itemoflist", _shadow=shadow, pos=pos) - - def set_index(self, value="random", input_type: str | int = "positive integer", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("INDEX", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_list(self, value: str | List = "list", value_id: Optional[str] = None): - return self.add_field(Field("LIST", value, value_id)) - - class ItemNumOfList(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_itemnumoflist", _shadow=shadow, pos=pos) - - def set_item(self, value="thing", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("ITEM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_list(self, value: str | List = "list", value_id: Optional[str] = None): - return self.add_field(Field("LIST", value, value_id)) - - class LengthOfList(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_lengthoflist", _shadow=shadow, pos=pos) - - def set_list(self, value: str | List = "list", value_id: Optional[str] = None): - return self.add_field(Field("LIST", value, value_id)) - - class ListContainsItem(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_listcontainsitem", _shadow=shadow, pos=pos) - - def set_item(self, value="thing", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("ITEM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_list(self, value: str | List = "list", value_id: Optional[str] = None): - return self.add_field(Field("LIST", value, value_id)) - - class ShowList(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_showlist", _shadow=shadow, pos=pos) - - def set_list(self, value: str | List = "list", value_id: Optional[str] = None): - return self.add_field(Field("LIST", value, value_id)) - - class HideList(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_hidelist", _shadow=shadow, pos=pos) - - def set_list(self, value: str | List = "list", value_id: Optional[str] = None): - return self.add_field(Field("LIST", value, value_id)) - - class ListIndexAll(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_listindexall", _shadow=shadow, pos=pos) - - class ListIndexRandom(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "data_listindexrandom", _shadow=shadow, pos=pos) - - -class Proc: - class Definition(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "procedures_definition", _shadow=shadow, pos=pos) - - def set_custom_block(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("custom_block", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Call(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "procedures_call", _shadow=shadow, pos=pos, mutation=Mutation()) - - def set_proc_code(self, proc_code: str = ''): - self.mutation.proc_code = proc_code - return self - - def set_argument_ids(self, *argument_ids: list[str]): - self.mutation.argument_ids = argument_ids - return self - - def set_warp(self, warp: bool = True): - self.mutation.warp = warp - return self - - def set_arg(self, arg, value='', input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input(arg, value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class Declaration(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "procedures_declaration", _shadow=shadow, pos=pos, mutation=Mutation()) - - def set_proc_code(self, proc_code: str = ''): - self.mutation.proc_code = proc_code - return self - - class Prototype(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "procedures_prototype", _shadow=shadow, pos=pos, mutation=Mutation()) - - def set_proc_code(self, proc_code: str = ''): - self.mutation.proc_code = proc_code - return self - - def set_argument_ids(self, *argument_ids: list[str]): - self.mutation.argument_ids = argument_ids - return self - - def set_argument_names(self, *argument_names: list[str]): - self.mutation.argument_names = list(argument_names) - return self - - def set_argument_defaults(self, *argument_defaults: list[str]): - self.mutation.argument_defaults = argument_defaults - return self - - def set_warp(self, warp: bool = True): - self.mutation.warp = warp - return self - - def set_arg(self, arg, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input(arg, value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - -class Args: - class EditorBoolean(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "argument_editor_boolean", _shadow=shadow, pos=pos, mutation=Mutation()) - - def set_text(self, value: str = "foo", value_id: Optional[str] = None): - return self.add_field(Field("TEXT", value, value_id)) - - class EditorStringNumber(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "argument_editor_string_number", _shadow=shadow, pos=pos, mutation=Mutation()) - - def set_text(self, value: str = "foo", value_id: Optional[str] = None): - return self.add_field(Field("TEXT", value, value_id)) - - class ReporterBoolean(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "argument_reporter_boolean", _shadow=shadow, pos=pos, mutation=Mutation()) - - def set_value(self, value: str = "boolean", value_id: Optional[str] = None): - return self.add_field(Field("VALUE", value, value_id)) - - class ReporterStringNumber(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "argument_reporter_string_number", _shadow=shadow, pos=pos, mutation=Mutation()) - - def set_value(self, value: str = "boolean", value_id: Optional[str] = None): - return self.add_field(Field("VALUE", value, value_id)) - - -class Addons: - class IsTurbowarp(Args.ReporterBoolean): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(_shadow=shadow, pos=pos) - self.set_value("is turbowarp?") - - class IsCompiled(Args.ReporterBoolean): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(_shadow=shadow, pos=pos) - self.set_value("is compiled?") - - class IsForkphorus(Args.ReporterBoolean): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(_shadow=shadow, pos=pos) - self.set_value("is forkphorus?") - - class Breakpoint(Proc.Call): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(_shadow=shadow, pos=pos) - self.set_proc_code("​​breakpoint​​") - - class Log(Proc.Call): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(_shadow=shadow, pos=pos) - self.set_proc_code("​​log​​ %s") - self.set_argument_ids("arg0") - - def set_message(self, value='', input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - return self.set_arg("arg0", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) - - class Warn(Proc.Call): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(_shadow=shadow, pos=pos) - self.set_proc_code("​​warn​​ %s") - self.set_argument_ids("arg0") - - def set_message(self, value='', input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - return self.set_arg("arg0", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) - - class Error(Proc.Call): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(_shadow=shadow, pos=pos) - self.set_proc_code("​​error​​ %s") - self.set_argument_ids("arg0") - - def set_message(self, value='', input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - return self.set_arg("arg0", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer) - - -class Pen: - class Clear(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "pen_clear", _shadow=shadow, pos=pos) - - class Stamp(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "pen_stamp", _shadow=shadow, pos=pos) - - class PenDown(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "pen_penDown", _shadow=shadow, pos=pos) - - class PenUp(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "pen_penUp", _shadow=shadow, pos=pos) - - class SetPenColorToColor(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "pen_setPenColorToColor", _shadow=shadow, pos=pos) - - def set_color(self, value="#FF0000", input_type: str | int = "color", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("COLOR", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class ChangePenParamBy(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "pen_changePenColorParamBy", _shadow=shadow, pos=pos) - - def set_param(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("COLOR_PARAM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_value(self, value="10", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("VALUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class SetPenParamTo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "pen_setPenColorParamTo", _shadow=shadow, pos=pos) - - def set_param(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("COLOR_PARAM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_value(self, value="10", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("VALUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class ChangePenSizeBy(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "pen_changePenSizeBy", _shadow=shadow, pos=pos) - - def set_size(self, value="1", input_type: str | int = "positive number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("SIZE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class SetPenSizeTo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "pen_setPenSizeTo", _shadow=shadow, pos=pos) - - def set_size(self, value="1", input_type: str | int = "positive number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("SIZE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class SetPenHueTo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "pen_setPenHueToNumber", _shadow=shadow, pos=pos) - - def set_hue(self, value="1", input_type: str | int = "positive number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("HUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class ChangePenHueBy(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "pen_changePenHueBy", _shadow=shadow, pos=pos) - - def set_hue(self, value="1", input_type: str | int = "positive number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("HUE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class SetPenShadeTo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "pen_setPenShadeToNumber", _shadow=shadow, pos=pos) - - def set_shade(self, value="1", input_type: str | int = "positive number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("SHADE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class ChangePenShadeBy(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "pen_changePenShadeBy", _shadow=shadow, pos=pos) - - def set_shade(self, value="1", input_type: str | int = "positive number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("SHADE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class ColorParamMenu(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "pen_menu_colorParam", _shadow=shadow, pos=pos) - - def set_color_param(self, value: str = "color", value_id: Optional[str] = None): - return self.add_field(Field("colorParam", value, value_id)) - - -class Music: - class PlayDrumForBeats(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "music_playDrumForBeats", _shadow=shadow, pos=pos) - - def set_drum(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("DRUM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_beats(self, value="0.25", input_type: str | int = "positive number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("BEATS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class PlayNoteForBeats(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "music_playDrumForBeats", _shadow=shadow, pos=pos) - - def set_note(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("NOTE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_beats(self, value="0.25", input_type: str | int = "positive number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("BEATS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class RestForBeats(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "music_restForBeats", _shadow=shadow, pos=pos) - - def set_beats(self, value="0.25", input_type: str | int = "positive number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("BEATS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class SetTempo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "music_setTempo", _shadow=shadow, pos=pos) - - def set_beats(self, value="60", input_type: str | int = "positive number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("TEMPO", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class ChangeTempo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "music_changeTempo", _shadow=shadow, pos=pos) - - def set_beats(self, value="60", input_type: str | int = "positive number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("TEMPO", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class GetTempo(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "music_getTempo", _shadow=shadow, pos=pos) - - class SetInstrument(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "music_setInstrument", _shadow=shadow, pos=pos) - - def set_instrument(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("INSTRUMENT", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class MidiPlayDrumForBeats(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "music_midiPlayDrumForBeats", _shadow=shadow, pos=pos) - - def set_drum(self, value="123", input_type: str | int = "positive integer", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("DRUM", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_beats(self, value="1", input_type: str | int = "positive number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("BEATS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class MidiSetInstrument(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "music_midiSetInstrument", _shadow=shadow, pos=pos) - - def set_instrument(self, value="6", input_type: str | int = "positive integer", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("INSTRUMENT", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class MenuDrum(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "music_menu_DRUM", _shadow=shadow, pos=pos) - - def set_drum(self, value: str = "1", value_id: Optional[str] = None): - return self.add_field(Field("DRUM", value, value_id)) - - class MenuInstrument(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "music_menu_INSTRUMENT", _shadow=shadow, pos=pos) - - def set_instrument(self, value: str = "1", value_id: Optional[str] = None): - return self.add_field(Field("INSTRUMENT", value, value_id)) - - -class VideoSensing: - class WhenMotionGreaterThan(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "videoSensing_whenMotionGreaterThan", _shadow=shadow, pos=pos) - - def set_reference(self, value="10", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("REFERENCE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class VideoOn(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "videoSensing_videoOn", _shadow=shadow, pos=pos) - - def set_attribute(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("ATTRIBUTE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_subject(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("SUBJECT", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class MenuAttribute(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "videoSensing_menu_ATTRIBUTE", _shadow=shadow, pos=pos) - - def set_attribute(self, value: str = "motion", value_id: Optional[str] = None): - return self.add_field(Field("ATTRIBUTE", value, value_id)) - - class MenuSubject(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "videoSensing_menu_SUBJECT", _shadow=shadow, pos=pos) - - def set_subject(self, value: str = "this sprite", value_id: Optional[str] = None): - return self.add_field(Field("SUBJECT", value, value_id)) - - class VideoToggle(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "videoSensing_videoToggle", _shadow=shadow, pos=pos) - - def set_video_state(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("VIDEO_STATE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class MenuVideoState(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "videoSensing_menu_VIDEO_STATE", _shadow=shadow, pos=pos) - - def set_video_state(self, value: str = "on", value_id: Optional[str] = None): - return self.add_field(Field("VIDEO_STATE", value, value_id)) - - class SetVideoTransparency(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "videoSensing_setVideoTransparency", _shadow=shadow, pos=pos) - - def set_transparency(self, value: str = "50", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("TRANSPARENCY", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - -class Text2Speech: - class SpeakAndWait(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "text2speech_speakAndWait", _shadow=shadow, pos=pos) - - def set_words(self, value: str = "50", input_type: str | int = "number", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("WORDS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class SetVoice(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "text2speech_setVoice", _shadow=shadow, pos=pos) - - def set_voice(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("VOICE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class MenuVoices(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "text2speech_menu_voices", _shadow=shadow, pos=pos) - - def set_voices(self, value: str = "ALTO", value_id: Optional[str] = None): - return self.add_field(Field("voices", value, value_id)) - - class SetLanguage(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "text2speech_setLanguage", _shadow=shadow, pos=pos) - - def set_language(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("LANGUAGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class MenuLanguages(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "text2speech_menu_languages", _shadow=shadow, pos=pos) - - def set_languages(self, value: str = "en", value_id: Optional[str] = None): - return self.add_field(Field("languages", value, value_id)) - - -class Translate: - class GetTranslate(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "translate_getTranslate", _shadow=shadow, pos=pos) - - def set_words(self, value="hello!", input_type: str | int = "string", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("WORDS", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - def set_language(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("LANGUAGE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class MenuLanguages(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "translate_menu_languages", _shadow=shadow, pos=pos) - - def set_languages(self, value: str = "sv", value_id: Optional[str] = None): - return self.add_field(Field("languages", value, value_id)) - - class GetViewerLanguage(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "translate_getViewerLanguage", _shadow=shadow, pos=pos) - - -class MakeyMakey: - class WhenMakeyKeyPressed(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "makeymakey_whenMakeyKeyPressed", _shadow=shadow, pos=pos) - - def set_key(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("KEY", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class MenuKey(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "makeymakey_menu_KEY", _shadow=shadow, pos=pos) - - def set_key(self, value: str = "SPACE", value_id: Optional[str] = None): - return self.add_field(Field("KEY", value, value_id)) - - class WhenCodePressed(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "makeymakey_whenCodePressed", _shadow=shadow, pos=pos) - - def set_sequence(self, value, input_type: str | int = "block", shadow_status: int = 1, *, - input_id: Optional[str] = None, obscurer: Optional[str | Block] = None): - - if isinstance(value, Block): - value = self.target.add_block(value) - elif isinstance(value, list) or isinstance(value, tuple): - if isinstance(value[0], Block): - value = self.target.link_chain(value) - return self.add_input( - Input("SEQUENCE", value, input_type, shadow_status, input_id=input_id, obscurer=obscurer)) - - class MenuSequence(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "makeymakey_menu_SEQUENCE", _shadow=shadow, pos=pos) - - def set_key(self, value: str = "LEFT UP RIGHT", value_id: Optional[str] = None): - return self.add_field(Field("SEQUENCE", value, value_id)) - - -class CoreExample: - class ExampleOpcode(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "coreExample_exampleOpcode", _shadow=shadow, pos=pos) - - class ExampleWithInlineImage(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "coreExample_exampleWithInlineImage", _shadow=shadow, pos=pos) - - -class OtherBlocks: - class Note(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "note", _shadow=shadow, pos=pos) - - def set_note(self, value: str = "60", value_id: Optional[str] = None): - return self.add_field(Field("NOTE", value, value_id)) - - class Matrix(Block): - def __init__(self, *, shadow: bool = True, pos: tuple[int | float, int | float] = (0, 0)): - super().__init__(None, "matrix", _shadow=shadow, pos=pos) - - def set_note(self, value: str = "0101010101100010101000100", value_id: Optional[str] = None): - return self.add_field(Field("MATRIX", value, value_id)) - - class RedHatBlock(Block): - def __init__(self, *, shadow: bool = False, pos: tuple[int | float, int | float] = (0, 0)): - # Note: There is no single opcode for the red hat block as the block is simply the result of an error - # The opcode here has been set to 'redhatblock' to make it obvious what is going on - - # (It's not called red_hat_block because then TurboWarp thinks that it's supposed to find an extension - # called red) - - # Appendix: You **CAN** actually add comments to this block, however it will make the block misbehave in the - # editor. The link between the comment and the block will not be visible, but will be visible with the - # corresponding TurboWarp addon - super().__init__(None, "redhatblock", _shadow=shadow, pos=pos, can_next=False) diff --git a/scratchattach/editor/sprite.py b/scratchattach/editor/sprite.py index 528ca953..6b30f1f9 100644 --- a/scratchattach/editor/sprite.py +++ b/scratchattach/editor/sprite.py @@ -11,6 +11,8 @@ from . import asset class Sprite(base.ProjectSubcomponent, base.JSONExtractable): + _local_globals: list[base.NamedIDComponent] + asset_data: list[asset.AssetFile] def __init__(self, is_stage: bool = False, name: str = '', _current_costume: int = 1, _layer_order: Optional[int] = None, _volume: int = 100, _broadcasts: Optional[list[vlb.Broadcast]] = None, @@ -120,6 +122,9 @@ def link_comments(self): _comment.link_using_sprite() def add_local_global(self, _vlb: base.NamedIDComponent): + """ + Add a global variable/list to this sprite (for when an overarching project/stage is not available) + """ self._local_globals.append(_vlb) _vlb.sprite = self @@ -128,11 +133,11 @@ def add_variable(self, _variable: vlb.Variable): _variable.sprite = self def add_list(self, _list: vlb.List): - self.variables.append(_list) + self.lists.append(_list) _list.sprite = self def add_broadcast(self, _broadcast: vlb.Broadcast): - self.variables.append(_broadcast) + self.broadcasts.append(_broadcast) _broadcast.sprite = self def add_vlb(self, _vlb: base.NamedIDComponent): @@ -151,18 +156,20 @@ def add_block(self, _block: block.Block | prim.Prim) -> block.Block | prim.Prim: return _block _block.sprite = self + new_id = self.new_id if isinstance(_block, block.Block): - self.blocks[self.new_id] = _block + self.blocks[new_id] = _block + _block.id = new_id _block.link_using_sprite() elif isinstance(_block, prim.Prim): - self.prims[self.new_id] = _block + self.prims[new_id] = _block _block.link_using_sprite() return _block - def add_chain(self, *chain: Iterable[block.Block | prim.Prim]) -> block.Block | prim.Prim: + def add_chain(self, *chain: block.Block | prim.Prim) -> block.Block | prim.Prim: """ Adds a list of blocks to the sprite **AND RETURNS THE FIRST BLOCK** :param chain: @@ -173,6 +180,8 @@ def add_chain(self, *chain: Iterable[block.Block | prim.Prim]) -> block.Block | _prev = self.add_block(chain[0]) for _block in chain[1:]: + if not isinstance(_prev, block.Block) or not isinstance(_block, block.Block): + continue _prev = _prev.attach_block(_block) return chain[0] @@ -206,7 +215,11 @@ def vlbs(self) -> list[base.NamedIDComponent]: """ :return: All vlbs associated with the sprite. No local globals are added """ - return self.variables + self.lists + self.broadcasts + vlbs: list[base.NamedIDComponent] = [] + vlbs.extend(self.variables) + vlbs.extend(self.lists) + vlbs.extend(self.broadcasts) + return vlbs @property def assets(self) -> list[asset.Costume | asset.Sound]: diff --git a/scratchattach/editor/todo.md b/scratchattach/editor/todo.md deleted file mode 100644 index 203a81e6..00000000 --- a/scratchattach/editor/todo.md +++ /dev/null @@ -1,105 +0,0 @@ -# Things to add to scratchattach.editor (sbeditor v2) - -## All - -- [ ] Docstrings -- [x] Dealing with stuff from the backpack (it's in a weird format): This may require a whole separate module -- [ ] Getter functions (`@property`) instead of directly editing attrs (make them protected attrs) -- [ ] Check if whitespace chars break IDs -- [ ] Maybe blockchain should be renamed to 'script' -- [ ] Perhaps use sprites as blockchain wrappers due to their existing utility (loading of local globals etc) -- [ ] bs4 styled search function -- [ ] ScratchJR project parser (lol) -- [ ] Error checking (for when you need to specify sprite etc) -- [x] Split json unpacking and the use of .from_json method so that it is easy to just extract json data (but not parse - it) - -## Project - -- [x] Asset list -- [ ] Obfuscation -- [x] Detection for twconfig -- [x] Edit twconfig -- [ ] Find targets - -## Block - -### Finding blocks/attrs - -- [x] Top level block (stack parent) -- [x] Previous chain -- [x] Attached chain -- [x] Complete chain -- [x] Block categories -- [x] Block shape attr aka stack type (Stack/hat/c-mouth/end/reporter/boolean detection) -- [x] `can_next` property -- [x] `is_input` property: Check if block is an input obscurer -- [x] `parent_input` property: Get input that this block obscures -- [x] `stack_tree` old 'subtree' property: Get the 'ast' of this blockchain (a 'tree' structure - well actually a list - of lists) -- [x] `children` property - list of all blocks with this block as a parent except next block (any input obscurers) -- [x] Detection for turbowarp debug blocks - (proc codes: - `"​​log​​ %s", - "​​breakpoint​​", - "​​error​​ %s", - "​​warn​​ %s"` - note: they all have ZWSPs) -- [x] Detection for `` and `` and `` booleans - -### Adding/removing blocks - -- [x] Add block to sprite -- [x] Duplicating (single) block -- [x] Attach block -- [x] Duplicating blockchain -- [x] Slot above (if possible - raise error if not) -- [x] Attach blockchain -- [x] Delete block -- [x] Delete blockchain -- [x] Add/edit inputs -- [x] Add/edit fields -- [x] Add mutation -- [x] Add comment -- [x] Get comment - -## Mutation - -- [ ] Proc code builder -- [x] get type of argument (bool/str) inside argument class -- [ ] to/from json for args? - -## Sprite - -### Finding ID components - -- [x] Find var/list/broadcast -- [x] Find block/prim -- [ ] Add costume/sound -- [ ] Add var/list/broadcast -- [ ] Add arbitrary block/blockchain -- [ ] Asset count -- [ ] Obfuscation -- [ ] Var/list/broadcast/block/comment/whole id list (like `_EnumWrapper.all_of`) -- [ ] Get custom blocks list -- [ ] With statements for sprite to allow for choosing default sprite - -## Vars/lists/broadcasts - -- [ ] idk - -## Monitors - -- [ ] Get relevant var/list if applicable -- [ ] Generate from block - -## Assets - -- [x] Download assets -- [ ] Upload asset -- [ ] Load from file (auto-detect type) - -## Pallet - -- [ ] Add all block defaults (like sbuild.py) -- [ ] Actions (objects that behave like blocks but add multiple blocks - e.g. a 'superjoin' block that you can use to - join more than 2 strings with one block (by actually building multiple join blocks)) \ No newline at end of file diff --git a/scratchattach/editor/twconfig.py b/scratchattach/editor/twconfig.py index 858bd005..07a50a3e 100644 --- a/scratchattach/editor/twconfig.py +++ b/scratchattach/editor/twconfig.py @@ -17,7 +17,7 @@ _END = " // _twconfig_" -@dataclass(init=True, repr=True) +@dataclass class TWConfig(base.JSONSerializable): framerate: int = None, interpolation: bool = False, @@ -100,6 +100,7 @@ def get_twconfig_data(string: str) -> dict | None: return None +# todo: move this to commons.py? def none_if_eq(data, compare) -> Any | None: """ Returns None if data and compare are the same diff --git a/scratchattach/editor/vlb.py b/scratchattach/editor/vlb.py index 2527e599..6570f7f2 100644 --- a/scratchattach/editor/vlb.py +++ b/scratchattach/editor/vlb.py @@ -9,7 +9,7 @@ from typing import Optional, Literal from . import base, sprite, build_defaulting -from ..utils import exceptions +from scratchattach.utils import exceptions class Variable(base.NamedIDComponent): diff --git a/scratchattach/eventhandlers/_base.py b/scratchattach/eventhandlers/_base.py index a70ff53f..7f24da92 100644 --- a/scratchattach/eventhandlers/_base.py +++ b/scratchattach/eventhandlers/_base.py @@ -5,8 +5,8 @@ from threading import Thread from collections.abc import Callable import traceback -from ..utils.requests import Requests as requests -from ..utils import exceptions +from scratchattach.utils.requests import requests +from scratchattach.utils import exceptions class BaseEventHandler(ABC): _events: defaultdict[str, list[Callable]] diff --git a/scratchattach/eventhandlers/cloud_events.py b/scratchattach/eventhandlers/cloud_events.py index 43f5e59f..a8ae8480 100644 --- a/scratchattach/eventhandlers/cloud_events.py +++ b/scratchattach/eventhandlers/cloud_events.py @@ -1,9 +1,9 @@ """CloudEvents class""" from __future__ import annotations -from ..cloud import _base +from scratchattach.cloud import _base from ._base import BaseEventHandler -from ..site import cloud_activity +from scratchattach.site import cloud_activity import time import json from collections.abc import Iterator diff --git a/scratchattach/eventhandlers/cloud_requests.py b/scratchattach/eventhandlers/cloud_requests.py index 8d2d120d..f6c745b3 100644 --- a/scratchattach/eventhandlers/cloud_requests.py +++ b/scratchattach/eventhandlers/cloud_requests.py @@ -2,13 +2,13 @@ from __future__ import annotations from .cloud_events import CloudEvents -from ..site import project +from scratchattach.site import project from threading import Thread, Event, current_thread import time import random import traceback -from ..utils.encoder import Encoding -from ..utils import exceptions +from scratchattach.utils.encoder import Encoding +from scratchattach.utils import exceptions class Request: diff --git a/scratchattach/eventhandlers/cloud_server.py b/scratchattach/eventhandlers/cloud_server.py index cf0af26c..bd9b1736 100644 --- a/scratchattach/eventhandlers/cloud_server.py +++ b/scratchattach/eventhandlers/cloud_server.py @@ -2,11 +2,11 @@ from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket from threading import Thread -from ..utils import exceptions +from scratchattach.utils import exceptions import json import time -from ..site import cloud_activity -from ..site.user import User +from scratchattach.site import cloud_activity +from scratchattach.site.user import User from ._base import BaseEventHandler class TwCloudSocket(WebSocket): diff --git a/scratchattach/eventhandlers/message_events.py b/scratchattach/eventhandlers/message_events.py index 574f2360..1ffc794e 100644 --- a/scratchattach/eventhandlers/message_events.py +++ b/scratchattach/eventhandlers/message_events.py @@ -1,7 +1,7 @@ """MessageEvents class""" from __future__ import annotations -from ..site import user +from scratchattach.site import user from ._base import BaseEventHandler import time diff --git a/scratchattach/other/other_apis.py b/scratchattach/other/other_apis.py index ea6ef707..ff6cecf5 100644 --- a/scratchattach/other/other_apis.py +++ b/scratchattach/other/other_apis.py @@ -4,10 +4,10 @@ import json from dataclasses import dataclass, field -from ..utils import commons -from ..utils.enums import Languages, Language, TTSVoices, TTSVoice -from ..utils.exceptions import BadRequest, InvalidLanguage, InvalidTTSGender -from ..utils.requests import Requests as requests +from scratchattach.utils import commons +from scratchattach.utils.enums import Languages, Language, TTSVoices, TTSVoice +from scratchattach.utils.exceptions import BadRequest, InvalidLanguage, InvalidTTSGender +from scratchattach.utils.requests import requests from typing import Optional diff --git a/scratchattach/other/project_json_capabilities.py b/scratchattach/other/project_json_capabilities.py index 4e114591..cfbcebfc 100644 --- a/scratchattach/other/project_json_capabilities.py +++ b/scratchattach/other/project_json_capabilities.py @@ -10,9 +10,9 @@ import string import zipfile from abc import ABC, abstractmethod -from ..utils import exceptions -from ..utils.commons import empty_project_json -from ..utils.requests import Requests as requests +from scratchattach.utils import exceptions +from scratchattach.utils.commons import empty_project_json +from scratchattach.utils.requests import requests # noinspection PyPep8Naming def load_components(json_data: list, ComponentClass: type, target_list: list): for element in json_data: diff --git a/scratchattach/site/_base.py b/scratchattach/site/_base.py index 738a0d29..41214cb5 100644 --- a/scratchattach/site/_base.py +++ b/scratchattach/site/_base.py @@ -1,27 +1,29 @@ from __future__ import annotations from abc import ABC, abstractmethod +from typing import TypeVar, Optional import requests -from ..utils import exceptions, commons -from typing import TypeVar -from types import FunctionType +from scratchattach.utils import exceptions, commons +from . import session C = TypeVar("C", bound="BaseSiteComponent") class BaseSiteComponent(ABC): - @abstractmethod - def __init__(self): - self._session = None - self._cookies = None - self._headers = None - self.update_API = None + _session: Optional[session.Session] + update_api: str + _headers: dict[str, str] + _cookies: dict[str, str] + + # @abstractmethod + # def __init__(self): # dataclasses do not implement __init__ directly + # pass def update(self): """ Updates the attributes of the object by performing an API response. Returns True if the update was successful. """ response = self.update_function( - self.update_API, + self.update_api, headers=self._headers, cookies=self._cookies, timeout=10 ) @@ -45,7 +47,6 @@ def _update_from_dict(self, data) -> bool: """ Parses the API response that is fetched in the update-method. Class specific, must be overridden in classes inheriting from this one. """ - pass def _assert_auth(self): if self._session is None: @@ -59,7 +60,7 @@ def _make_linked_object(self, identificator_id, identificator, Class: type[C], N """ return commons._get_object(identificator_id, identificator, Class, NotFoundException, self._session) - update_function: FunctionType = requests.get + update_function = requests.get """ Internal function run on update. Function is a method of the 'requests' module/class """ diff --git a/scratchattach/site/activity.py b/scratchattach/site/activity.py index 30870692..15555b8f 100644 --- a/scratchattach/site/activity.py +++ b/scratchattach/site/activity.py @@ -5,7 +5,7 @@ from . import user, project, studio from ._base import BaseSiteComponent -from ..utils import exceptions +from scratchattach.utils import exceptions class Activity(BaseSiteComponent): @@ -20,7 +20,6 @@ def __str__(self): return str(self.raw) def __init__(self, **entries): - # Set attributes every Activity object needs to have: self._session = None self.raw = None @@ -81,7 +80,8 @@ def _update_from_json(self, data: dict): recipient_username = None default_case = False - """Whether this is 'blank'; it will default to 'user performed an action'""" + # Even if `activity_type` is an invalid value; it will default to 'user performed an action' + if activity_type == 0: # follow followed_username = data["followed_username"] @@ -150,13 +150,7 @@ def _update_from_json(self, data: dict): self.project_id = project_id self.recipient_username = recipient_username - elif activity_type == 8: - default_case = True - - elif activity_type == 9: - default_case = True - - elif activity_type == 10: + elif activity_type in (8, 9, 10): # Share/Reshare project project_id = data["project"] is_reshare = data["is_reshare"] @@ -187,9 +181,8 @@ def _update_from_json(self, data: dict): self.project_id = parent_id self.recipient_username = recipient_username - elif activity_type == 12: - default_case = True - + # type 12 does not exist in the HTML. That's why it was removed, not merged with type 13. + elif activity_type == 13: # Create ('add') studio studio_id = data["gallery"] @@ -216,16 +209,7 @@ def _update_from_json(self, data: dict): self.username = username self.gallery_id = studio_id - elif activity_type == 16: - default_case = True - - elif activity_type == 17: - default_case = True - - elif activity_type == 18: - default_case = True - - elif activity_type == 19: + elif activity_type in (16, 17, 18, 19): # Remove project from studio project_id = data["project"] @@ -240,13 +224,7 @@ def _update_from_json(self, data: dict): self.username = username self.project_id = project_id - elif activity_type == 20: - default_case = True - - elif activity_type == 21: - default_case = True - - elif activity_type == 22: + elif activity_type in (20, 21, 22): # Was promoted to manager for studio studio_id = data["gallery"] @@ -260,13 +238,7 @@ def _update_from_json(self, data: dict): self.recipient_username = recipient_username self.gallery_id = studio_id - elif activity_type == 23: - default_case = True - - elif activity_type == 24: - default_case = True - - elif activity_type == 25: + elif activity_type in (23, 24, 25): # Update profile raw = f"{username} made a profile update" @@ -276,10 +248,7 @@ def _update_from_json(self, data: dict): self.username = username - elif activity_type == 26: - default_case = True - - elif activity_type == 27: + elif activity_type in (26, 27): # Comment (quite complicated) comment_type: int = data["comment_type"] fragment = data["comment_fragment"] @@ -313,13 +282,12 @@ def _update_from_json(self, data: dict): self.comment_obj_id = comment_obj_id self.comment_obj_title = comment_obj_title self.comment_id = comment_id - else: default_case = True if default_case: # This is coded in the scratch HTML, haven't found an example of it though - raw = f"{username} performed an action" + raw = f"{username} performed an action." self.raw = raw self.datetime_created = _time diff --git a/scratchattach/site/alert.py b/scratchattach/site/alert.py new file mode 100644 index 00000000..3e37f840 --- /dev/null +++ b/scratchattach/site/alert.py @@ -0,0 +1,227 @@ +# classroom alerts (& normal alerts in the future) + +from __future__ import annotations + +import json +import pprint +import warnings +from dataclasses import dataclass, field, KW_ONLY +from datetime import datetime +from typing import TYPE_CHECKING, Any, Optional, Union +from typing_extensions import Self + +from . import user, project, studio, comment, session +from scratchattach.utils import enums + +if TYPE_CHECKING: + ... + + +# todo: implement regular alerts +# If you implement regular alerts, it may be applicable to make EducatorAlert a subclass. + + +@dataclass +class EducatorAlert: + """ + Represents an alert for student activity, viewable at https://scratch.mit.edu/site-api/classrooms/alerts/ + + Attributes: + model: The type of alert (presumably); should always equal "educators.educatoralert" in this class + type: An integer that identifies the type of alert, differentiating e.g. against bans or autoban or censored comments etc + raw: The raw JSON data from the API + id: The ID of the alert (internally called 'pk' by scratch, not sure what this is for) + time_read: The time the alert was read + time_created: The time the alert was created + target: The user that the alert is about (the student) + actor: The user that created the alert (the admin) + target_object: The object that the alert is about (e.g. a project, studio, or comment) + notification_type: not sure what this is for, but inferred from the scratch HTML reference + """ + _: KW_ONLY + # required attrs + target: user.User + actor: user.User + target_object: Optional[Union[project.Project, studio.Studio, comment.Comment, studio.Studio]] + notification_type: str + _session: Optional[session.Session] + + # defaulted attrs + model: str = "educators.educatoralert" + type: int = -1 + raw: dict = field(repr=False, default_factory=dict) + id: int = -1 + time_read: datetime = datetime.fromtimestamp(0.0) + time_created: datetime = datetime.fromtimestamp(0.0) + + + @classmethod + def from_json(cls, data: dict[str, Any], _session: Optional[session.Session] = None) -> Self: + """ + Load an EducatorAlert from a JSON object. + + Arguments: + data (dict): The JSON object + _session (session.Session): The session object used to load this data, to 'connect' to the alerts rather than just 'get' them + + Returns: + EducatorAlert: The loaded EducatorAlert object + """ + model = data.get("model") # With this class, should be equal to educators.educatoralert + assert isinstance(model, str) + alert_id = data.get("pk") # not sure what kind of pk/id this is. Doesn't seem to be a user or class id. + assert isinstance(alert_id, int) + + fields = data.get("fields") + assert isinstance(fields, dict) + + time_read_raw = fields.get("educator_datetime_read") + assert isinstance(time_read_raw, str) + time_read: datetime = datetime.fromisoformat(time_read_raw) + + admin_action = fields.get("admin_action") + assert isinstance(admin_action, dict) + + time_created_raw = admin_action.get("datetime_created") + assert isinstance(time_created_raw, str) + time_created: datetime = datetime.fromisoformat(time_created_raw) + + alert_type = admin_action.get("type") + assert isinstance(alert_type, int) + + target_data = admin_action.get("target_user") + assert isinstance(target_data, dict) + target = user.User(username=target_data.get("username"), + id=target_data.get("pk"), + icon_url=target_data.get("thumbnail_url"), + admin=target_data.get("admin", False), + _session=_session) + + actor_data = admin_action.get("actor") + assert isinstance(actor_data, dict) + actor = user.User(username=actor_data.get("username"), + id=actor_data.get("pk"), + icon_url=actor_data.get("thumbnail_url"), + admin=actor_data.get("admin", False), + _session=_session) + + object_id = admin_action.get("object_id") # this could be a comment id, a project id, etc. + assert isinstance(object_id, int) + target_object: project.Project | studio.Studio | comment.Comment | None = None + + extra_data: dict[str, Any] = json.loads(admin_action.get("extra_data", "{}")) + # todo: if possible, properly implement the incomplete parts of this parser (look for warning.warn()) + notification_type: str = "" + + if "project_title" in extra_data: + # project + target_object = project.Project(id=object_id, + title=extra_data["project_title"], + _session=_session) + elif "comment_content" in extra_data: + # comment + comment_data: dict[str, Any] = extra_data["comment_content"] + content: str | None = comment_data.get("content") + + comment_obj_id: int | None = comment_data.get("comment_obj_id") + + comment_type: int | None = comment_data.get("comment_type") + + if comment_type == 0: + # project + comment_source_type = comment.CommentSource.PROJECT + elif comment_type == 1: + # profile + comment_source_type = comment.CommentSource.USER_PROFILE + else: + # probably a studio + comment_source_type = comment.CommentSource.STUDIO + warnings.warn( + f"The parser was not able to recognise the \"comment_type\" of {comment_type} in the alert JSON response.\n" + f"Full response: \n{pprint.pformat(data)}.\n\n" + f"Please draft an issue on github: https://github.com/TimMcCool/scratchattach/issues, providing this " + f"whole error message. This will allow us to implement an incomplete part of this parser") + + # the comment_obj's corresponding attribute of comment.Comment is the place() method. As it has no cache, the title data is wasted. + # if the comment_obj is deleted, this is still a valid way of working out the title/username + + target_object = comment.Comment( + id=object_id, + content=content, + source=comment_source_type, + source_id=comment_obj_id, + _session=_session + ) + + elif "gallery_title" in extra_data: + # studio + # possible implemented incorrectly + target_object = studio.Studio( + id=object_id, + title=extra_data["gallery_title"], + _session=_session + ) + elif "notification_type" in extra_data: + # possible implemented incorrectly + notification_type = extra_data["notification_type"] + else: + warnings.warn( + f"The parser was not able to recognise the \"extra_data\" in the alert JSON response.\n" + f"Full response: \n{pprint.pformat(data)}.\n\n" + f"Please draft an issue on github: https://github.com/TimMcCool/scratchattach/issues, providing this " + f"whole error message. This will allow us to implement an incomplete part of this parser") + + return cls( + id=alert_id, + model=model, + type=alert_type, + raw=data, + time_read=time_read, + time_created=time_created, + target=target, + actor=actor, + target_object=target_object, + notification_type=notification_type, + _session=_session + ) + + def __str__(self): + return f"EducatorAlert: {self.message}" + + @property + def alert_type(self) -> enums.AlertType: + """ + Get an associated AlertType object for this alert (based on the type index) + """ + alert_type = enums.AlertTypes.find(self.type) + if not alert_type: + alert_type = enums.AlertTypes.default.value + + return alert_type + + @property + def message(self): + """ + Format the alert message using the alert type's message template, as it would be on the website. + """ + raw_message = self.alert_type.message + comment_content = "" + if isinstance(self.target_object, comment.Comment): + comment_content = self.target_object.content + + return raw_message.format(username=self.target.username, + project=self.target_object_title, + studio=self.target_object_title, + notification_type=self.notification_type, + comment=comment_content) + + @property + def target_object_title(self): + """ + Get the title of the target object (if applicable) + """ + if isinstance(self.target_object, project.Project): + return self.target_object.title + if isinstance(self.target_object, studio.Studio): + return self.target_object.title + return None # explicit diff --git a/scratchattach/site/backpack_asset.py b/scratchattach/site/backpack_asset.py index 48b65aac..fe58ad65 100644 --- a/scratchattach/site/backpack_asset.py +++ b/scratchattach/site/backpack_asset.py @@ -5,8 +5,8 @@ import logging from ._base import BaseSiteComponent -from ..utils import exceptions -from ..utils.requests import Requests as requests +from scratchattach.utils import exceptions +from scratchattach.utils.requests import requests diff --git a/scratchattach/site/browser_cookie3_stub.py b/scratchattach/site/browser_cookie3_stub.py new file mode 100644 index 00000000..9d63d7df --- /dev/null +++ b/scratchattach/site/browser_cookie3_stub.py @@ -0,0 +1,17 @@ +# browser_cookie3.pyi + +import http.cookiejar +from typing import Optional + +def chrome(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented +def chromium(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented +def firefox(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented +def opera(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented +def edge(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented +def brave(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented +def vivaldi(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented +def safari(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented +def lynx(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented +def w3m(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented + +def load() -> http.cookiejar.CookieJar: return NotImplemented diff --git a/scratchattach/site/browser_cookies.py b/scratchattach/site/browser_cookies.py index 19d2ff7f..ba784a66 100644 --- a/scratchattach/site/browser_cookies.py +++ b/scratchattach/site/browser_cookies.py @@ -1,9 +1,13 @@ -from typing import Optional +from typing import Optional, TYPE_CHECKING +from typing_extensions import assert_never from http.cookiejar import CookieJar from enum import Enum, auto browsercookie_err = None try: - import browsercookie + if TYPE_CHECKING: + from . import browser_cookie3_stub as browser_cookie3 + else: + import browser_cookie3 except Exception as e: browsercookie = None browsercookie_err = e @@ -15,8 +19,8 @@ class Browser(Enum): EDGE = auto() SAFARI = auto() CHROMIUM = auto() - EDGE_DEV = auto() VIVALDI = auto() + EDGE_DEV = auto() FIREFOX = Browser.FIREFOX @@ -24,32 +28,34 @@ class Browser(Enum): EDGE = Browser.EDGE SAFARI = Browser.SAFARI CHROMIUM = Browser.CHROMIUM -EDGE_DEV = Browser.EDGE_DEV VIVALDI = Browser.VIVALDI ANY = Browser.ANY +EDGE_DEV = Browser.EDGE_DEV def cookies_from_browser(browser : Browser = ANY) -> dict[str, str]: """ Import cookies from browser to login """ - if not browsercookie: + if not browser_cookie3: raise browsercookie_err or ModuleNotFoundError() cookies : Optional[CookieJar] = None - if browser == ANY: - cookies = browsercookie.load() - elif browser == FIREFOX: - cookies = browsercookie.firefox() - elif browser == CHROME: - cookies = browsercookie.chrome() - elif browser == EDGE: - cookies = browsercookie.edge() - elif browser == SAFARI: - cookies = browsercookie.safari() - elif browser == CHROMIUM: - cookies = browsercookie.chromium() - elif browser == EDGE_DEV: - cookies = browsercookie.edge_dev() - elif browser == VIVALDI: - cookies = browsercookie.vivaldi() + if browser is Browser.ANY: + cookies = browser_cookie3.load() + elif browser is Browser.FIREFOX: + cookies = browser_cookie3.firefox() + elif browser is Browser.CHROME: + cookies = browser_cookie3.chrome() + elif browser is Browser.EDGE: + cookies = browser_cookie3.edge() + elif browser is Browser.SAFARI: + cookies = browser_cookie3.safari() + elif browser is Browser.CHROMIUM: + cookies = browser_cookie3.chromium() + elif browser is Browser.VIVALDI: + cookies = browser_cookie3.vivaldi() + elif browser is Browser.EDGE_DEV: + raise ValueError("EDGE_DEV is not supported anymore.") + else: + assert_never(browser) assert isinstance(cookies, CookieJar) return {cookie.name: cookie.value for cookie in cookies if "scratch.mit.edu" in cookie.domain and cookie.value} \ No newline at end of file diff --git a/scratchattach/site/classroom.py b/scratchattach/site/classroom.py index 9a2663ab..3d8149ef 100644 --- a/scratchattach/site/classroom.py +++ b/scratchattach/site/classroom.py @@ -2,57 +2,65 @@ import datetime import warnings -from typing import Optional, TYPE_CHECKING, Any +from dataclasses import dataclass, field +from datetime import datetime +from typing import Optional, TYPE_CHECKING, Any, Callable import bs4 +from bs4 import BeautifulSoup if TYPE_CHECKING: - from ..site.session import Session + from scratchattach.site.session import Session -from ..utils.commons import requests +from scratchattach.utils.commons import requests from . import user, activity from ._base import BaseSiteComponent -from ..utils import exceptions, commons -from ..utils.commons import headers - -from bs4 import BeautifulSoup +from scratchattach.utils import exceptions, commons +from scratchattach.utils.commons import headers +@dataclass class Classroom(BaseSiteComponent): - def __init__(self, **entries): + title: str = None + id: int = None + classtoken: str = None + + author: user.User = None + about_class: str = None + working_on: str = None + + is_closed: bool = False + datetime: datetime = None + + + update_function: Callable = field(repr=False, default=requests.get) + _session: Optional[Session] = field(repr=False, default=None) + + def __post_init__(self): # Info on how the .update method has to fetch the data: # NOTE: THIS DOESN'T WORK WITH CLOSED CLASSES! - self.update_function = requests.get - if "id" in entries: - self.update_API = f"https://api.scratch.mit.edu/classrooms/{entries['id']}" - elif "classtoken" in entries: - self.update_API = f"https://api.scratch.mit.edu/classtoken/{entries['classtoken']}" + if self.id: + self.update_api = f"https://api.scratch.mit.edu/classrooms/{self.id}" + elif self.classtoken: + self.update_api = f"https://api.scratch.mit.edu/classtoken/{self.classtoken}" else: - raise KeyError(f"No class id or token provided! Entries: {entries}") - - # Set attributes every Project object needs to have: - self._session: Session = None - self.id = None - self.classtoken = None - - self.__dict__.update(entries) + raise KeyError(f"No class id or token provided! {self.__dict__ = }") # Headers and cookies: if self._session is None: - self._headers = headers + self._headers = commons.headers self._cookies = {} else: self._headers = self._session._headers self._cookies = self._session._cookies # Headers for operations that require accept and Content-Type fields: - self._json_headers = dict(self._headers) - self._json_headers["accept"] = "application/json" - self._json_headers["Content-Type"] = "application/json" - self.is_closed = False + self._json_headers = {**self._headers, + "accept": "application/json", + "Content-Type": "application/json"} - def __repr__(self) -> str: - return f"classroom called {self.title!r}" + def __str__(self) -> str: + return f"" def update(self): try: @@ -305,7 +313,8 @@ def close(self) -> None: warnings.warn(f"{self._session} may not be authenticated to edit {self}") raise e - def register_student(self, username: str, password: str = '', birth_month: Optional[int] = None, birth_year: Optional[int] = None, + def register_student(self, username: str, password: str = '', birth_month: Optional[int] = None, + birth_year: Optional[int] = None, gender: Optional[str] = None, country: Optional[str] = None, is_robot: bool = False) -> None: return register_by_token(self.id, self.classtoken, username, password, birth_month, birth_year, gender, country, is_robot) @@ -346,7 +355,8 @@ def public_activity(self, *, limit=20): return activities - def activity(self, student: str = "all", mode: str = "Last created", page: Optional[int] = None) -> list[dict[str, Any]]: + def activity(self, student: str = "all", mode: str = "Last created", page: Optional[int] = None) -> list[ + dict[str, Any]]: """ Get a list of private activity, only available to the class owner. Returns: @@ -421,7 +431,7 @@ def register_by_token(class_id: int, class_token: str, username: str, password: "is_robot": is_robot} response = requests.post("https://scratch.mit.edu/classes/register_new_student/", - data=data, headers=headers, cookies={"scratchcsrftoken": 'a'}) + data=data, headers=commons.headers, cookies={"scratchcsrftoken": 'a'}) ret = response.json()[0] if "username" in ret: diff --git a/scratchattach/site/cloud_activity.py b/scratchattach/site/cloud_activity.py index 63b64e0f..f0a40710 100644 --- a/scratchattach/site/cloud_activity.py +++ b/scratchattach/site/cloud_activity.py @@ -91,8 +91,8 @@ def actor(self): """ if self.username is None: return None - from ..site import user - from ..utils import exceptions + from scratchattach.site import user + from scratchattach.utils import exceptions return self._make_linked_object("username", self.username, user.User, exceptions.UserNotFound) def project(self): @@ -101,7 +101,7 @@ def project(self): """ if self.cloud is None: return None - from ..site import project - from ..utils import exceptions + from scratchattach.site import project + from scratchattach.utils import exceptions return self._make_linked_object("id", self.cloud.project_id, project.Project, exceptions.ProjectNotFound) diff --git a/scratchattach/site/comment.py b/scratchattach/site/comment.py index 3c66ece8..c42d6258 100644 --- a/scratchattach/site/comment.py +++ b/scratchattach/site/comment.py @@ -1,17 +1,33 @@ """Comment class""" from __future__ import annotations +from typing import Union, Optional, Any +from typing_extensions import assert_never # importing from typing caused me errors +from enum import Enum, auto + from . import user, project, studio from ._base import BaseSiteComponent -from ..utils import exceptions +from scratchattach.utils import exceptions +class CommentSource(Enum): + PROJECT = auto() + USER_PROFILE = auto() + STUDIO = auto() class Comment(BaseSiteComponent): """ Represents a Scratch comment (on a profile, studio or project) """ - - def str(self): + id: Union[int, str] + source: CommentSource + source_id: Union[int, str] + cached_replies: Optional[list[Comment]] + parent_id: Optional[Union[int, str]] + cached_parent_comment: Optional[Comment] + commentee_id: Optional[int] + content: Any + + def __str__(self): return str(self.content) def __init__(self, **entries): @@ -93,12 +109,14 @@ def place(self) -> user.User | studio.Studio | project.Project: If the place can't be traced back, None is returned. """ - if self.source == "profile": + if self.source == CommentSource.USER_PROFILE: return self._make_linked_object("username", self.source_id, user.User, exceptions.UserNotFound) - if self.source == "studio": + elif self.source == CommentSource.STUDIO: return self._make_linked_object("id", self.source_id, studio.Studio, exceptions.UserNotFound) - if self.source == "project": + elif self.source == CommentSource.PROJECT: return self._make_linked_object("id", self.source_id, project.Project, exceptions.UserNotFound) + else: + assert_never(self.source) def parent_comment(self) -> Comment | None: if self.parent_id is None: diff --git a/scratchattach/site/forum.py b/scratchattach/site/forum.py index 5605b92e..7db7e8d1 100644 --- a/scratchattach/site/forum.py +++ b/scratchattach/site/forum.py @@ -1,16 +1,21 @@ """ForumTopic and ForumPost classes""" from __future__ import annotations -from . import user -from ..utils.commons import headers -from ..utils import exceptions, commons -from ._base import BaseSiteComponent -import xml.etree.ElementTree as ET -from bs4 import BeautifulSoup +from dataclasses import dataclass, field +from typing import Optional, Any from urllib.parse import urlparse, parse_qs +import xml.etree.ElementTree as ET -from ..utils.requests import Requests as requests +from bs4 import BeautifulSoup, Tag + +from . import user +from . import session as module_session +from scratchattach.utils.commons import headers +from scratchattach.utils import exceptions, commons +from ._base import BaseSiteComponent +from scratchattach.utils.requests import requests +@dataclass class ForumTopic(BaseSiteComponent): ''' Represents a Scratch forum topic. @@ -33,28 +38,26 @@ class ForumTopic(BaseSiteComponent): :.update(): Updates the attributes ''' - - def __init__(self, **entries): + id: int + title: str + category_name: str + last_updated: str + _session: Optional[module_session.Session] = field(default=None) + reply_count: Optional[int] = field(default=None) + view_count: Optional[int] = field(default=None) + + def __post_init__(self): # Info on how the .update method has to fetch the data: self.update_function = requests.get - self.update_API = f"https://scratch.mit.edu/discuss/feeds/topic/{entries['id']}/" - - # Set attributes every Project object needs to have: - self._session = None - self.id = 0 - self.reply_count = None - self.view_count = None - - # Update attributes from entries dict: - self.__dict__.update(entries) + self.update_api = f"https://scratch.mit.edu/discuss/feeds/topic/{self.id}/" # Headers and cookies: if self._session is None: self._headers = headers self._cookies = {} else: - self._headers = self._session._headers - self._cookies = self._session._cookies + self._headers = self._session.get_headers() + self._cookies = self._session.get_cookies() # Headers for operations that require accept and Content-Type fields: self._json_headers = dict(self._headers) @@ -65,7 +68,7 @@ def update(self): # As there is no JSON API for getting forum topics anymore, # the data has to be retrieved from the XML feed. response = self.update_function( - self.update_API, + self.update_api, headers = self._headers, cookies = self._cookies, timeout=20 # fetching forums can take very long ) @@ -87,17 +90,22 @@ def update(self): raise exceptions.ScrapeError(str(e)) else: raise exceptions.ForumContentNotFound - - return self._update_from_dict(dict( - title = title, category_name = category_name, last_updated = last_updated - )) - - - def _update_from_dict(self, data): - self.__dict__.update(data) + self.title = title + self.category_name = category_name + self.last_updated = last_updated return True + + @classmethod + def from_id(cls, __id: int, session: module_session.Session, update: bool = False): + new = cls(id=__id, _session=session, title="", last_updated="", category_name="") + if update: + new.update() + return new + + def _update_from_dict(self, data: dict[str, Any]): + self.__dict__.update(data) - def posts(self, *, page=1, order="oldest"): + def posts(self, *, page=1, order="oldest") -> list[ForumPost]: """ Args: page (int): The page of the forum topic that should be returned. First page is at index 1. @@ -117,10 +125,11 @@ def posts(self, *, page=1, order="oldest"): raise exceptions.FetchError(str(e)) try: soup = BeautifulSoup(response.content, 'html.parser') - soup = soup.find("div", class_="djangobb") - + soup_elm = soup.find("div", class_="djangobb") + assert isinstance(soup_elm, Tag) try: - pagination_div = soup.find('div', class_='pagination') + pagination_div = soup_elm.find('div', class_='pagination') + assert isinstance(pagination_div, Tag) num_pages = int(pagination_div.find_all('a', class_='page')[-1].text) except Exception: num_pages = 1 @@ -128,8 +137,9 @@ def posts(self, *, page=1, order="oldest"): try: # get topic category: topic_category = "" - breadcrumb_ul = soup.find_all('ul')[1] # Find the second ul element + breadcrumb_ul = soup_elm.find_all('ul')[1] # Find the second ul element if breadcrumb_ul: + assert isinstance(breadcrumb_ul, Tag) link = breadcrumb_ul.find_all('a')[1] # Get the right anchor tag topic_category = link.text.strip() # Extract and strip text content except Exception as e: @@ -139,12 +149,14 @@ def posts(self, *, page=1, order="oldest"): # get corresponding posts: post_htmls = soup.find_all('div', class_='blockpost') for raw_post in post_htmls: - post = ForumPost(id=int(raw_post['id'].replace("p", "")), topic_id=self.id, _session=self._session, topic_category=topic_category, topic_num_pages=num_pages) - post._update_from_html(raw_post) + if not isinstance(raw_post, Tag): + continue + post = ForumPost(id=int(str(raw_post['id']).replace("p", "")), topic_id=self.id, _session=self._session, topic_category=topic_category, topic_num_pages=num_pages) + post.update_from_html(raw_post) posts.append(post) except Exception as e: - raise exceptions.ScrapeError(str(e)) + raise exceptions.ScrapeError() from e return posts @@ -157,7 +169,7 @@ def first_post(self): if len(posts) > 0: return posts[0] - +@dataclass class ForumPost(BaseSiteComponent): ''' Represents a Scratch forum post. @@ -190,34 +202,39 @@ class ForumPost(BaseSiteComponent): :.update(): Updates the attributes ''' - - def __init__(self, **entries): + id: int = field(default=0) + topic_id: int = field(default=0) + topic_name: str = field(default="") + topic_category: str = field(default="") + topic_num_pages: int = field(default=0) + author_name: str = field(default="") + author_avatar_url: str = field(default="") + posted: str = field(default="") + deleted: bool = field(default=False) + html_content: str = field(default="") + content: str = field(default="") + post_index: int = field(default=0) + _session: Optional[module_session.Session] = field(default=None) + def __post_init__(self): # A forum post can't be updated the usual way as there is no API anymore - self.update_function = None - self.update_API = None - - # Set attributes every Project object needs to have: - self._session = None - self.id = 0 - self.topic_id = 0 - self.deleted = False - - # Update attributes from entries dict: - self.__dict__.update(entries) + self.update_api = "" # Headers and cookies: if self._session is None: self._headers = headers self._cookies = {} else: - self._headers = self._session._headers - self._cookies = self._session._cookies + self._headers = self._session.get_headers() + self._cookies = self._session.get_cookies() # Headers for operations that require accept and Content-Type fields: self._json_headers = dict(self._headers) self._json_headers["accept"] = "application/json" self._json_headers["Content-Type"] = "application/json" + + def update_function(self, *args, **kwargs): + raise TypeError("Forum posts cannot be updated like this") def update(self): """ @@ -225,32 +242,47 @@ def update(self): As there is no API for retrieving a single post anymore, this requires reloading the forum page. """ page = 1 - posts = ForumTopic(id=self.topic_id, _session=self._session).posts(page=1) + posts = ForumTopic.from_id(self.topic_id, session=self._session).posts(page=1) while posts != []: matching = list(filter(lambda x : int(x.id) == int(self.id), posts)) if len(matching) > 0: this = matching[0] break page += 1 - posts = ForumTopic(id=self.topic_id, _session=self._session).posts(page=page) + posts = ForumTopic.from_id(self.topic_id, session=self._session).posts(page=page) else: return False - - return self._update_from_dict(this.__dict__) + self._update_from_dict(vars(this)) - def _update_from_dict(self, data): + def _update_from_dict(self, data: dict[str, Any]): self.__dict__.update(data) return True - - def _update_from_html(self, soup_html): - self.post_index = int(soup_html.find('span', class_='conr').text.strip('#')) - self.id = int(soup_html['id'].replace("p", "")) - self.posted = soup_html.find('a', href=True).text.strip() - self.content = soup_html.find('div', class_='post_body_html').text.strip() + + def update_from_html(self, soup_html: Tag): + return self._update_from_html(soup_html) + + def _update_from_html(self, soup_html: Tag): + post_index_elm = soup_html.find('span', class_='conr') + assert isinstance(post_index_elm, Tag) + id_attr = soup_html['id'] + assert isinstance(id_attr, str) + posted_elm = soup_html.find('a', href=True) + assert isinstance(posted_elm, Tag) + content_elm = soup_html.find('div', class_='post_body_html') + assert isinstance(content_elm, Tag) + author_name_elm = soup_html.select_one('dl dt a') + assert isinstance(author_name_elm, Tag) + topic_name_elm = soup_html.find('h3') + assert isinstance(topic_name_elm, Tag) + + self.post_index = int(post_index_elm.text.strip('#')) + self.id = int(id_attr.replace("p", "")) + self.posted = posted_elm.text.strip() + self.content = content_elm.text.strip() self.html_content = str(soup_html.find('div', class_='post_body_html')) - self.author_name = soup_html.find('dl').find('dt').find('a').text.strip() - self.author_avatar_url = soup_html.find('dl').find('dt').find('a')['href'] - self.topic_name = soup_html.find('h3').text.strip() + self.author_name = author_name_elm.text.strip() + self.author_avatar_url = str(author_name_elm['href']) + self.topic_name = topic_name_elm.text.strip() return True def topic(self): @@ -270,7 +302,7 @@ def author(self): """ return self._make_linked_object("username", self.author_name, user.User, exceptions.UserNotFound) - def edit(self, new_content): + def edit(self, new_content: str): """ Changes the content of the forum post. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_post` or through another method that requires authentication. You must own the forum post. diff --git a/scratchattach/site/project.py b/scratchattach/site/project.py index 5146d2e5..097859eb 100644 --- a/scratchattach/site/project.py +++ b/scratchattach/site/project.py @@ -5,15 +5,19 @@ import random import base64 import time +import zipfile +from io import BytesIO + +from typing import Any from . import user, comment, studio -from ..utils import exceptions -from ..utils import commons -from ..utils.commons import empty_project_json, headers +from scratchattach.utils import exceptions +from scratchattach.utils import commons +from scratchattach.utils.commons import empty_project_json, headers from ._base import BaseSiteComponent -from ..other.project_json_capabilities import ProjectBody -from ..utils.requests import Requests as requests +from scratchattach.other.project_json_capabilities import ProjectBody +from scratchattach.utils.requests import requests -CREATE_PROJECT_USES = [] +CREATE_PROJECT_USES: list[float] = [] class PartialProject(BaseSiteComponent): """ @@ -27,7 +31,7 @@ def __init__(self, **entries): # Info on how the .update method has to fetch the data: self.update_function = requests.get - self.update_API = f"https://api.scratch.mit.edu/projects/{entries['id']}" + self.update_api = f"https://api.scratch.mit.edu/projects/{entries['id']}" # Set attributes every Project object needs to have: self._session = None @@ -126,6 +130,9 @@ def is_shared(self): p = get_project(self.id) return isinstance(p, Project) + def raw_json_or_empty(self) -> dict[str, Any]: + return empty_project_json + def create_remix(self, *, title=None, project_json=None): # not working """ Creates a project on the Scratch website. @@ -142,10 +149,7 @@ def create_remix(self, *, title=None, project_json=None): # not working else: title = " remix" if project_json is None: - if "title" in self.__dict__: - project_json = self.raw_json() - else: - project_json = empty_project_json + project_json = self.raw_json_or_empty() if len(CREATE_PROJECT_USES) < 5: CREATE_PROJECT_USES.insert(0, time.time()) @@ -306,16 +310,32 @@ def raw_json(self): """ try: self.update() - return requests.get( - f"https://projects.scratch.mit.edu/{self.id}?token={self.project_token}", - timeout=10, - ).json() - except Exception: + + except Exception as e: raise ( exceptions.FetchError( - "Either the project was created with an old Scratch version, or you're not authorized for accessing it" + f"You're not authorized for accessing {self}.\nException: {e}" ) ) + + with requests.no_error_handling(): + resp = requests.get( + f"https://projects.scratch.mit.edu/{self.id}?token={self.project_token}", + timeout=10, + ) + + try: + return resp.json() + except json.JSONDecodeError: + # I am not aware of any cases where this will not be a zip file + # in the future, cache a projectbody object here and just return the json + # that is fetched from there to not waste existing asset data from this zip file + + with zipfile.ZipFile(BytesIO(resp.content)) as zipf: + return json.load(zipf.open("project.json")) + + def raw_json_or_empty(self): + return self.raw_json() def creator_agent(self): """ @@ -382,7 +402,7 @@ def comment_by_id(self, comment_id): ).json() if r is None: raise exceptions.CommentNotFound() - _comment = comment.Comment(id=r["id"], _session=self._session, source="project", source_id=self.id) + _comment = comment.Comment(id=r["id"], _session=self._session, source=comment.CommentSource.PROJECT, source_id=self.id) _comment._update_from_dict(r) return _comment @@ -611,7 +631,7 @@ def post_comment(self, content, *, parent_id="", commentee_id=""): ) if "id" not in r: raise exceptions.CommentPostFailure(r) - _comment = comment.Comment(id=r["id"], _session=self._session, source="project", source_id=self.id) + _comment = comment.Comment(id=r["id"], _session=self._session, source=comment.CommentSource.PROJECT, source_id=self.id) _comment._update_from_dict(r) return _comment diff --git a/scratchattach/site/session.py b/scratchattach/site/session.py index 4128bbc0..42fcf6eb 100644 --- a/scratchattach/site/session.py +++ b/scratchattach/site/session.py @@ -10,37 +10,34 @@ import re import time import warnings -from typing import Optional, TypeVar, TYPE_CHECKING, overload +import zlib + +from dataclasses import dataclass, field +from typing import Optional, TypeVar, TYPE_CHECKING, overload, Any, Union from contextlib import contextmanager from threading import local -# import secrets -# import zipfile -# from typing import Type Type = type -try: - from warnings import deprecated -except ImportError: - deprecated = lambda x: (lambda y: y) + if TYPE_CHECKING: from _typeshed import FileDescriptorOrPath, SupportsRead - from ..cloud._base import BaseCloud + from scratchattach.cloud._base import BaseCloud T = TypeVar("T", bound=BaseCloud) else: T = TypeVar("T") -from bs4 import BeautifulSoup +from bs4 import BeautifulSoup, Tag +from typing_extensions import deprecated -from . import activity, classroom, forum, studio, user, project, backpack_asset +from . import activity, classroom, forum, studio, user, project, backpack_asset, alert # noinspection PyProtectedMember from ._base import BaseSiteComponent -from ..cloud import cloud, _base -from ..eventhandlers import message_events, filterbot -from ..other import project_json_capabilities -from ..utils import commons -from ..utils import exceptions -from ..utils.commons import headers, empty_project_json, webscrape_count, get_class_sort_mode -from ..utils.requests import Requests as requests +from scratchattach.cloud import cloud, _base +from scratchattach.eventhandlers import message_events, filterbot +from scratchattach.other import project_json_capabilities +from scratchattach.utils import commons, exceptions +from scratchattach.utils.commons import headers, empty_project_json, webscrape_count, get_class_sort_mode +from scratchattach.utils.requests import requests from .browser_cookies import Browser, ANY, cookies_from_browser ratelimit_cache: dict[str, list[float]] = {} @@ -63,6 +60,7 @@ def enforce_ratelimit(__type: str, name: str, amount: int = 5, duration: int = 6 C = TypeVar("C", bound=BaseSiteComponent) +@dataclass class Session(BaseSiteComponent): """ Represents a Scratch log in / session. Stores authentication data (session id and xtoken). @@ -76,27 +74,28 @@ class Session(BaseSiteComponent): mute_status: Information about commenting restrictions of the associated account banned: Returns True if the associated account is banned """ - session_string: str | None = None + username: str = None + _user: user.User = field(repr=False, default=None) - def __str__(self) -> str: - return f"Login for account {self.username!r}" + id: str = None + session_string: str | None = field(repr=False, default=None) + xtoken: str = field(repr=False, default=None) + email: str = field(repr=False, default=None) - def __init__(self, **entries): - # Info on how the .update method has to fetch the data: - self.update_function = requests.post - self.update_API = "https://scratch.mit.edu/session" + new_scratcher: bool = field(repr=False, default=None) + mute_status: Any = field(repr=False, default=None) + banned: bool = field(repr=False, default=None) - # Set attributes every Session object needs to have: - self.id = None - self.username = None - self.xtoken = None - self.new_scratcher = None + time_created: datetime.datetime = None + language: str = field(repr=False, default="en") - # Set attributes that Session object may get - self._user: user.User = None + def __str__(self) -> str: + return f"" - # Update attributes from entries dict: - self.__dict__.update(entries) + def __post_init__(self): + # Info on how the .update method has to fetch the data: + self.update_function = requests.post + self.update_api = "https://scratch.mit.edu/session" # Set alternative attributes: self._username = self.username # backwards compatibility with v1 @@ -111,6 +110,9 @@ def __init__(self, **entries): "Content-Type": "application/json", } + if self.id: + self._process_session_id() + def _update_from_dict(self, data: dict): # Note: there are a lot more things you can get from this data dict. # Maybe it would be a good idea to also store the dict itself? @@ -133,13 +135,34 @@ def _update_from_dict(self, data: dict): self.banned = data["user"]["banned"] if self.banned: - warnings.warn(f"Warning: The account {self._username} you logged in to is BANNED. " + warnings.warn(f"Warning: The account {self.username} you logged in to is BANNED. " f"Some features may not work properly.") if self.has_outstanding_email_confirmation: - warnings.warn(f"Warning: The account {self._username} you logged is not email confirmed. " + warnings.warn(f"Warning: The account {self.username} you logged is not email confirmed. " f"Some features may not work properly.") return True + def _process_session_id(self): + assert self.id + + data, self.time_created = decode_session_id(self.id) + + self.username = data["username"] + self._username = self.username + if self._user: + self._user.username = self.username + else: + self._user = user.User(_session=self, username=self.username) + + self._user.id = data["_auth_user_id"] + self.xtoken = data["token"] + self._headers["X-Token"] = self.xtoken + + # not saving the login ip because it is a security issue, and is not very helpful + + self.language = data["_language"] + # self._cookies["scratchlanguage"] = self.language + def connect_linked_user(self) -> user.User: """ Gets the user associated with the login / session. @@ -158,7 +181,7 @@ def connect_linked_user(self) -> user.User: self._user = self.connect_user(self._username) return self._user - def get_linked_user(self) -> 'user.User': + def get_linked_user(self) -> user.User: # backwards compatibility with v1 # To avoid inconsistencies with "connect" and "get", this function was renamed @@ -183,16 +206,27 @@ def resend_email(self, password: str): password (str): Password associated with the session (not stored) """ requests.post("https://scratch.mit.edu/accounts/email_change/", - data={"email_address": self.new_email_address, + data={"email_address": self.get_new_email_address(), "password": password}, headers=self._headers, cookies=self._cookies) @property + @deprecated("Use get_new_email_address instead.") def new_email_address(self) -> str: """ Gets the (unconfirmed) email address that this session has requested to transfer to, if any, otherwise the current address. + Returns: + str: The email that this session wants to switch to + """ + return self.get_new_email_address() + + def get_new_email_address(self) -> str: + """ + Gets the (unconfirmed) email address that this session has requested to transfer to, if any, + otherwise the current address. + Returns: str: The email that this session wants to switch to """ @@ -203,6 +237,10 @@ def new_email_address(self) -> str: email = None for label_span in soup.find_all("span", {"class": "label"}): + if not isinstance(label_span, Tag): + continue + if not isinstance(label_span.parent, Tag): + continue if label_span.contents[0] == "New Email Address": return label_span.parent.contents[-1].text.strip("\n ") @@ -252,6 +290,13 @@ def admin_messages(self, *, limit=40, offset=0) -> list[dict]: def classroom_alerts(self, _classroom: Optional[classroom.Classroom | int] = None, mode: str = "Last created", page: Optional[int] = None): + """ + Load and parse admin alerts, optionally for a specific class, using https://scratch.mit.edu/site-api/classrooms/alerts/ + + Returns: + list[alert.EducatorAlert]: A list of parsed EducatorAlert objects + """ + if isinstance(_classroom, classroom.Classroom): _classroom = _classroom.id @@ -266,7 +311,9 @@ def classroom_alerts(self, _classroom: Optional[classroom.Classroom | int] = Non params={"page": page, "ascsort": ascsort, "descsort": descsort}, headers=self._headers, cookies=self._cookies).json() - return data + alerts = [alert.EducatorAlert.from_json(alert_data, self) for alert_data in data] + + return alerts def clear_messages(self): """ @@ -618,9 +665,10 @@ def mystuff_studios(self, filter_arg: str = "all", *, page: int = 1, sort_by: st ascsort = sort_by descsort = "" try: + params: dict[str, Union[str, int]] = {"page": page, "ascsort": ascsort, "descsort": descsort} targets = requests.get( f"https://scratch.mit.edu/site-api/galleries/{filter_arg}/", - params={"page": page, "ascsort": ascsort, "descsort": descsort}, + params=params, headers=headers, cookies=self._cookies, timeout=10 @@ -646,6 +694,9 @@ def mystuff_studios(self, filter_arg: str = "all", *, page: int = 1, sort_by: st raise exceptions.FetchError() def mystuff_classes(self, mode: str = "Last created", page: Optional[int] = None) -> list[classroom.Classroom]: + if self.is_teacher is None: + self.update() + if not self.is_teacher: raise exceptions.Unauthorized(f"{self.username} is not a teacher; can't have classes") ascsort, descsort = get_class_sort_mode(mode) @@ -849,6 +900,7 @@ def connect_user_by_id(self, user_id: int) -> user.User: Returns: scratchattach.user.User: An object that represents the requested user and allows you to perform actions on the user (like user.follow) """ + # noinspection PyDeprecation return self._make_linked_object("username", self.find_username_from_id(user_id), user.User, exceptions.UserNotFound) @@ -981,11 +1033,50 @@ def connect_filterbot(self, *, log_deletions=True) -> filterbot.Filterbot: def get_session_string(self) -> str: assert self.session_string return self.session_string + + def get_headers(self) -> dict[str, str]: + return self._headers + + def get_cookies(self) -> dict[str, str]: + return self._cookies + + +# ------ # + +def decode_session_id(session_id: str) -> tuple[dict[str, str], datetime.datetime]: + """ + Extract the JSON data from the main part of a session ID string + Session id is in the format: + :: + + p1 contains a base64-zlib compressed JSON string + p2 is a base 62 encoded timestamp + p3 might be a `synchronous signature` for the first 2 parts (might be useless for us) + + The dict has these attributes: + - username + - _auth_user_id + - testcookie + - _auth_user_backend + - token + - login-ip + - _language + - django_timezone + - _auth_user_hash + """ + p1, p2, p3 = session_id.split(':') + + return ( + json.loads(zlib.decompress(base64.urlsafe_b64decode(p1 + "=="))), + datetime.datetime.fromtimestamp(commons.b62_decode(p2)) + ) + # ------ # suppressed_login_warning = local() + @contextmanager def suppress_login_warning(): """ @@ -998,6 +1089,7 @@ def suppress_login_warning(): finally: suppressed_login_warning.suppressed -= 1 + def issue_login_warning() -> None: """ Issue a login data warning. @@ -1012,6 +1104,7 @@ def issue_login_warning() -> None: exceptions.LoginDataWarning ) + def login_by_id(session_id: str, *, username: Optional[str] = None, password: Optional[str] = None, xtoken=None) -> Session: """ Creates a session / log in to the Scratch website with the specified session id. @@ -1028,39 +1121,20 @@ def login_by_id(session_id: str, *, username: Optional[str] = None, password: Op Returns: scratchattach.session.Session: An object that represents the created login / session """ - # Removed this from docstring since it doesn't exist: - # timeout (int): Optional, but recommended. - # Specify this when the Python environment's IP address is blocked by Scratch's API, - # but you still want to use cloud variables. - # Generate session_string (a scratchattach-specific authentication method) + # should this be changed to a @property? issue_login_warning() if password is not None: - session_data = dict(session_id=session_id, username=username, password=password) + session_data = dict(id=session_id, username=username, password=password) session_string = base64.b64encode(json.dumps(session_data).encode()).decode() else: session_string = None - _session = Session(id=session_id, username=username, session_string=session_string, xtoken=xtoken) - try: - status = _session.update() - except Exception as e: - status = False - warnings.warn(f"Key error at key {e} when reading scratch.mit.edu/session API response") - - if status is not True: - if _session.xtoken is None: - if _session.username is None: - warnings.warn("Warning: Logged in by id, but couldn't fetch XToken. " - "Make sure the provided session id is valid. " - "Setting cloud variables can still work if you provide a " - "`username='username'` keyword argument to the sa.login_by_id function") - else: - warnings.warn("Warning: Logged in by id, but couldn't fetch XToken. " - "Make sure the provided session id is valid.") - else: - warnings.warn("Warning: Logged in by id, but couldn't fetch session info. " - "This won't affect any other features.") + _session = Session(id=session_id, username=username, session_string=session_string) + if xtoken is not None: + # xtoken is retrievable from session id, so the most we can do is assert equality + assert xtoken == _session.xtoken + return _session @@ -1087,14 +1161,16 @@ def login(username, password, *, timeout=10) -> Session: # Post request to login API: _headers = headers.copy() _headers["Cookie"] = "scratchcsrftoken=a;scratchlanguage=en;" - request = requests.post( - "https://scratch.mit.edu/login/", json={"username": username, "password": password}, headers=_headers, - - timeout=timeout, errorhandling = False - ) + with requests.no_error_handling(): + request = requests.post( + "https://scratch.mit.edu/login/", json={"username": username, "password": password}, headers=_headers, + timeout=timeout + ) try: - session_id = str(re.search('"(.*)"', request.headers["Set-Cookie"]).group()) - except (AttributeError, Exception): + result = re.search('"(.*)"', request.headers["Set-Cookie"]) + assert result is not None + session_id = str(result.group()) + except Exception: raise exceptions.LoginFailure( "Either the provided authentication data is wrong or your network is banned from Scratch.\n\nIf you're using an online IDE (like replit.com) Scratch possibly banned its IP address. In this case, try logging in with your session id: https://github.com/TimMcCool/scratchattach/wiki#logging-in") @@ -1102,6 +1178,7 @@ def login(username, password, *, timeout=10) -> Session: with suppress_login_warning(): return login_by_id(session_id, username=username, password=password) + def login_by_session_string(session_string: str) -> Session: """ Login using a session string. @@ -1109,6 +1186,13 @@ def login_by_session_string(session_string: str) -> Session: issue_login_warning() session_string = base64.b64decode(session_string).decode() # unobfuscate session_data = json.loads(session_string) + try: + assert session_data.get("id") + with suppress_login_warning(): + return login_by_id(session_data["id"], username=session_data.get("username"), + password=session_data.get("password")) + except Exception: + pass try: assert session_data.get("session_id") with suppress_login_warning(): @@ -1124,6 +1208,7 @@ def login_by_session_string(session_string: str) -> Session: pass raise ValueError("Couldn't log in.") + def login_by_io(file: SupportsRead[str]) -> Session: """ Login using a file object. @@ -1131,6 +1216,7 @@ def login_by_io(file: SupportsRead[str]) -> Session: with suppress_login_warning(): return login_by_session_string(file.read()) + def login_by_file(file: FileDescriptorOrPath) -> Session: """ Login using a path to a file. @@ -1138,6 +1224,7 @@ def login_by_file(file: FileDescriptorOrPath) -> Session: with suppress_login_warning(), open(file, encoding="utf-8") as f: return login_by_io(f) + def login_from_browser(browser: Browser = ANY): """ Login from a browser diff --git a/scratchattach/site/studio.py b/scratchattach/site/studio.py index b412dbe6..beea0ed0 100644 --- a/scratchattach/site/studio.py +++ b/scratchattach/site/studio.py @@ -4,11 +4,11 @@ import json import random from . import user, comment, project, activity -from ..utils import exceptions, commons -from ..utils.commons import api_iterative, headers +from scratchattach.utils import exceptions, commons +from scratchattach.utils.commons import api_iterative, headers from ._base import BaseSiteComponent -from ..utils.requests import Requests as requests +from scratchattach.utils.requests import requests class Studio(BaseSiteComponent): @@ -49,7 +49,7 @@ def __init__(self, **entries): # Info on how the .update method has to fetch the data: self.update_function = requests.get - self.update_API = f"https://api.scratch.mit.edu/studios/{entries['id']}" + self.update_api = f"https://api.scratch.mit.edu/studios/{entries['id']}" # Set attributes every Project object needs to have: self._session = None @@ -156,7 +156,7 @@ def comment_by_id(self, comment_id): ).json() if r is None: raise exceptions.CommentNotFound() - _comment = comment.Comment(id=r["id"], _session=self._session, source="studio", source_id=self.id) + _comment = comment.Comment(id=r["id"], _session=self._session, source=comment.CommentSource.STUDIO, source_id=self.id) _comment._update_from_dict(r) return _comment @@ -191,7 +191,7 @@ def post_comment(self, content, *, parent_id="", commentee_id=""): ).json() if "id" not in r: raise exceptions.CommentPostFailure(r) - _comment = comment.Comment(id=r["id"], _session=self._session, source="studio", source_id=self.id) + _comment = comment.Comment(id=r["id"], _session=self._session, source=comment.CommentSource.STUDIO, source_id=self.id) _comment._update_from_dict(r) return _comment diff --git a/scratchattach/site/user.py b/scratchattach/site/user.py index 08af5d66..5b3ccbb1 100644 --- a/scratchattach/site/user.py +++ b/scratchattach/site/user.py @@ -3,22 +3,36 @@ import json import random +import re import string -from datetime import datetime +from datetime import datetime, timezone +from enum import Enum + +from typing_extensions import deprecated +from bs4 import BeautifulSoup, Tag + +from ._base import BaseSiteComponent +from scratchattach.eventhandlers import message_events + +from scratchattach.utils import commons +from scratchattach.utils import exceptions +from scratchattach.utils.commons import headers +from scratchattach.utils.requests import requests -from ..eventhandlers import message_events from . import project -from ..utils import exceptions from . import studio from . import forum -from bs4 import BeautifulSoup -from ._base import BaseSiteComponent -from ..utils.commons import headers -from ..utils import commons from . import comment from . import activity +from . import classroom -from ..utils.requests import Requests as requests +class Rank(Enum): + """ + Possible ranks in scratch + """ + NEW_SCRATCHER = 0 + SCRATCHER = 1 + SCRATCH_TEAM = 2 class Verificator: @@ -62,7 +76,7 @@ def __init__(self, **entries): # Info on how the .update method has to fetch the data: self.update_function = requests.get - self.update_API = f"https://api.scratch.mit.edu/users/{entries['username']}" + self.update_api = f"https://api.scratch.mit.edu/users/{entries['username']}" # Set attributes every User object needs to have: self._session = None @@ -70,6 +84,10 @@ def __init__(self, **entries): self.username = None self.name = None + # cache value for classroom getter method (using @property) + # first value is whether the cache has actually been set (because it can be None), second is the value itself + self._classroom: tuple[bool, classroom.Classroom | None] = False, None + # Update attributes from entries dict: entries.setdefault("name", entries.get("username")) self.__dict__.update(entries) @@ -120,6 +138,45 @@ def _assert_permission(self): raise exceptions.Unauthorized( "You need to be authenticated as the profile owner to do this.") + @property + def classroom(self) -> classroom.Classroom | None: + """ + Get a user's associated classroom, and return it as a `scratchattach.classroom.Classroom` object. + If there is no associated classroom, returns `None` + """ + if not self._classroom[0]: + resp = requests.get(f"https://scratch.mit.edu/users/{self.username}/") + soup = BeautifulSoup(resp.text, "html.parser") + + details = soup.find("p", {"class": "profile-details"}) + assert isinstance(details, Tag) + + class_name, class_id, is_closed = None, None, None + for a in details.find_all("a"): + if not isinstance(a, Tag): + continue + href = str(a.get("href")) + if re.match(r"/classes/\d*/", href): + class_name = a.text.strip()[len("Student of: "):] + is_closed = class_name.endswith("\n (ended)") # as this has a \n, we can be sure + if is_closed: + class_name = class_name[:-7].strip() + + class_id = href.split('/')[2] + break + + if class_name: + self._classroom = True, classroom.Classroom( + _session=self, + id=class_id, + title=class_name, + is_closed=is_closed + ) + else: + self._classroom = True, None + + return self._classroom[1] + def does_exist(self): """ Returns: @@ -131,6 +188,8 @@ def does_exist(self): elif status_code == 404: return False + # Will maybe be deprecated later, but for now still has its own purpose. + #@deprecated("This function is partially deprecated. Use user.rank() instead.") def is_new_scratcher(self): """ Returns: @@ -350,7 +409,7 @@ def loves(self, *, limit=40, offset=0, get_full_project: bool = False) -> list[p # A thumbnail link (no need to webscrape this) # A title # An Author (called an owner for some reason) - + assert isinstance(project_element, Tag) project_anchors = project_element.find_all("a") # Each list item has three tags, the first two linking the project # 1st contains tag @@ -359,10 +418,16 @@ def loves(self, *, limit=40, offset=0, get_full_project: bool = False) -> list[p # This function is pretty handy! # I'll use it for an id from a string like: /projects/1070616180/ - project_id = commons.webscrape_count(project_anchors[0].attrs["href"], + first_anchor = project_anchors[0] + second_anchor = project_anchors[1] + third_anchor = project_anchors[2] + assert isinstance(first_anchor, Tag) + assert isinstance(second_anchor, Tag) + assert isinstance(third_anchor, Tag) + project_id = commons.webscrape_count(first_anchor.attrs["href"], "/projects/", "/") - title = project_anchors[1].contents[0] - author = project_anchors[2].contents[0] + title = second_anchor.contents[0] + author = third_anchor.contents[0] # Instantiating a project with the properties that we know # This may cause issues (see below) @@ -545,11 +610,11 @@ def post_comment(self, content, *, parent_id="", commentee_id=""): data = { 'id': text.split('
')[1].split('"
')[0], + 'content': text.split('
')[1].split('
')[0].strip(), 'reply_count': 0, 'cached_replies': [] } - _comment = comment.Comment(source="profile", parent_id=None if parent_id=="" else parent_id, commentee_id=commentee_id, source_id=self.username, id=data["id"], _session = self._session) + _comment = comment.Comment(source=comment.CommentSource.USER_PROFILE, parent_id=None if parent_id=="" else parent_id, commentee_id=commentee_id, source_id=self.username, id=data["id"], _session = self._session, datetime = datetime.now()) _comment._update_from_dict(data) return _comment except Exception: @@ -560,7 +625,7 @@ def post_comment(self, content, *, parent_id="", commentee_id=""): raw_error_data = text.split('')[0] error_data = json.loads(raw_error_data) expires = error_data['mute_status']['muteExpiresAt'] - expires = datetime.utcfromtimestamp(expires) + expires = datetime.fromtimestamp(expires, timezone.utc) raise(exceptions.CommentPostFailure(f"You have been muted. Mute expires on {expires}")) else: raise(exceptions.FetchError(f"Couldn't parse API response: {r.text!r}")) @@ -696,7 +761,7 @@ def comments(self, *, page=1, limit=None): 'content': content, 'datetime_created': time, } - _comment = comment.Comment(source="profile", source_id=self.username, _session = self._session) + _comment = comment.Comment(source=comment.CommentSource.USER_PROFILE, source_id=self.username, _session = self._session) _comment._update_from_dict(main_comment) ALL_REPLIES = [] @@ -719,7 +784,7 @@ def comments(self, *, page=1, limit=None): "parent_id" : comment_id, "cached_parent_comment" : _comment, } - _r_comment = comment.Comment(source="profile", source_id=self.username, _session = self._session, cached_parent_comment=_comment) + _r_comment = comment.Comment(source=comment.CommentSource.USER_PROFILE, source_id=self.username, _session = self._session, cached_parent_comment=_comment) _r_comment._update_from_dict(reply_data) ALL_REPLIES.append(_r_comment) @@ -820,6 +885,24 @@ def verify_identity(self, *, verification_project_id=395330233): v = Verificator(self, verification_project_id) return v + def rank(self): + """ + Finds the rank of the user. + May replace user.scratchteam and user.is_new_scratcher in the future. + """ + + if self.is_new_scratcher(): + return Rank.NEW_SCRATCHER + # Is New Scratcher + + if not self.scratchteam: + return Rank.SCRATCHER + # Is Scratcher + + return Rank.SCRATCH_TEAM + # Is Scratch Team member + + # ------ # def get_user(username) -> User: diff --git a/scratchattach/utils/commons.py b/scratchattach/utils/commons.py index 4d977497..637661b5 100644 --- a/scratchattach/utils/commons.py +++ b/scratchattach/utils/commons.py @@ -1,13 +1,17 @@ """v2 ready: Common functions used by various internal modules""" from __future__ import annotations +import string + from typing import Optional, Final, Any, TypeVar, Callable, TYPE_CHECKING, Union +from threading import Event as ManualResetEvent from threading import Lock from . import exceptions -from .requests import Requests as requests +from .requests import requests + +from scratchattach.site import _base -from ..site import _base headers: Final = { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " @@ -130,7 +134,7 @@ def _get_object(identificator_name, identificator, __class: type[C], NotFoundExc # Internal function: Generalization of the process ran by get_user, get_studio etc. # Builds an object of class that is inheriting from BaseSiteComponent # # Class must inherit from BaseSiteComponent - from ..site import project + from scratchattach.site import project try: use_class: type = __class if __class is project.PartialProject: @@ -182,44 +186,41 @@ class LockEvent: """ Can be waited on and triggered. Not to be confused with threading.Event, which has to be reset. """ - locks: list[Lock] + _event: ManualResetEvent + _locks: list[Lock] + _access_locks: Lock def __init__(self): - self.locks = [] - self.use_locks = Lock() + self._event = ManualResetEvent() + self._locks = [] + self._access_locks = Lock() def wait(self, blocking: bool = True, timeout: Optional[Union[int, float]] = None) -> bool: """ Wait for the event. """ - timeout = -1 if timeout is None else timeout - if not blocking: - timeout = 0 - return self.on().acquire(timeout=timeout) + return self._event.wait(timeout if blocking else 0) def trigger(self): """ Trigger all threads waiting on this event to continue. """ - with self.use_locks: - for lock in self.locks: + with self._access_locks: + for lock in self._locks: try: - lock.release() # Unlock the lock once to trigger the event. + lock.release() except RuntimeError: - lock.acquire(timeout=0) # Lock the lock again. - for lock in self.locks.copy(): - try: - lock.release() # Unlock the lock once more to make sure it was waited on. - self.locks.remove(lock) - except RuntimeError: - lock.acquire(timeout=0) # Lock the lock again. + pass + self._locks.clear() + self._event.set() + self._event = ManualResetEvent() def on(self) -> Lock: """ - Return a lock that will unlock once the event takes place. + Return a lock that will unlock once the event takes place. Return value has to be waited on to wait for the event. """ lock = Lock() - with self.use_locks: - self.locks.append(lock) + with self._access_locks: + self._locks.append(lock) lock.acquire(timeout=0) return lock @@ -241,3 +242,14 @@ def get_class_sort_mode(mode: str) -> tuple[str, str]: descsort = "title" return ascsort, descsort + + +def b62_decode(s: str): + chars = string.digits + string.ascii_uppercase + string.ascii_lowercase + + ret = 0 + for char in s: + ret = ret * 62 + chars.index(char) + + return ret + diff --git a/scratchattach/utils/enums.py b/scratchattach/utils/enums.py index 9d4ee76b..f5dc27fb 100644 --- a/scratchattach/utils/enums.py +++ b/scratchattach/utils/enums.py @@ -4,13 +4,12 @@ """ from __future__ import annotations -from enum import Enum from dataclasses import dataclass - +from enum import Enum from typing import Optional, Callable, Iterable -@dataclass(init=True, repr=True) +@dataclass class Language: name: str = None code: str = None @@ -44,7 +43,7 @@ def apply_func(x): if apply_func(_val) == value: return item_obj - + except TypeError: pass @@ -167,7 +166,7 @@ def all_of(cls, attr_name: str = "name", apply_func: Optional[Callable] = None) return super().all_of(attr_name, apply_func) -@dataclass(init=True, repr=True) +@dataclass class TTSVoice: name: str gender: str @@ -195,3 +194,43 @@ def find(cls, value, by: str = "name", apply_func: Optional[Callable] = None) -> def all_of(cls, attr_name: str = "name", apply_func: Optional[Callable] = None) -> Iterable: return super().all_of(attr_name, apply_func) + +@dataclass +class AlertType: + id: int + message: str + + +class AlertTypes(_EnumWrapper): + """ + Enum for associating alert type indecies with their messages, for use with the str.format() method. + """ + # Reference: https://github.com/TimMcCool/scratchattach/issues/304#issuecomment-2800110811 + # NOTE: THE TEXT WITHIN THE BRACES HERE MATTERS! IF YOU WANT TO CHANGE IT, MAKE SURE TO EDIT `site.alert.EducatorAlert`! + ban = AlertType(0, "{username} was banned.") + unban = AlertType(1, "{username} was unbanned.") + excluded_from_homepage = AlertType(2, "{username} was excluded from homepage") + excluded_from_homepage2 = AlertType(3, "{username} was excluded from homepage") # for some reason there are duplicates + notified = AlertType(4, "{username} was notified by a Scratch Administrator. Notification type: {notification_type}") # not sure what notification type is + autoban = AlertType(5, "{username} was banned automatically") + autoremoved = AlertType(6, "{project} by {username} was removed automatically") + project_censored2 = AlertType(7, "{project} by {username} was censored.") # + project_censored = AlertType(20, "{project} by {username} was censored.") + project_uncensored = AlertType(8, "{project} by {username} was uncensored.") + project_reviewed2 = AlertType(9, "{project} by {username} was reviewed by a Scratch Administrator.") # + project_reviewed = AlertType(10, "{project} by {username} was reviewed by a Scratch Administrator.") + project_deleted = AlertType(11, "{project} by {username} was deleted by a Scratch Administrator.") + user_deleted2 = AlertType(12, "{username} was deleted by a Scratch Administrator") # + user_deleted = AlertType(17, "{username} was deleted by a Scratch Administrator") + studio_reviewed2 = AlertType(13, "{studio} was reviewed by a Scratch Administrator.") # + studio_reviewed = AlertType(14, "{studio} was reviewed by a Scratch Administrator.") + studio_deleted = AlertType(15, "{studio} was deleted by a Scratch Administrator.") + email_confirm2 = AlertType(16, "The email address of {username} was confirmed by a Scratch Administrator") # + email_confirm = AlertType(18, "The email address of {username} was confirmed by a Scratch Administrator") # no '.' in HTML + email_unconfirm = AlertType(19, "The email address of {username} was set as not confirmed by a Scratch Administrator") + automute = AlertType(22, "{username} was automatically muted by our comment filters. The comment they tried to post was: {comment}") + default = AlertType(-1, "{username} had an admin action performed.") # default case + + @classmethod + def find(cls, value, by: str = "id", apply_func: Optional[Callable] = None) -> AlertType: + return super().find(value, by, apply_func) diff --git a/scratchattach/utils/requests.py b/scratchattach/utils/requests.py index c015cfe6..075ab779 100644 --- a/scratchattach/utils/requests.py +++ b/scratchattach/utils/requests.py @@ -1,67 +1,93 @@ from __future__ import annotations -import requests -from . import exceptions +from collections.abc import MutableMapping, Iterator +from typing import Optional +from contextlib import contextmanager + +from typing_extensions import override +from requests import Session as HTTPSession +from requests import Response -proxies = None +from . import exceptions +proxies: Optional[MutableMapping[str, str]] = None -class Requests: +class Requests(HTTPSession): """ Centralized HTTP request handler (for better error handling and proxies) """ + error_handling: bool = True - @staticmethod - def check_response(r: requests.Response): + def check_response(self, r: Response): if r.status_code == 403 or r.status_code == 401: - raise exceptions.Unauthorized(f"Request content: {r.content}") + raise exceptions.Unauthorized(f"Request content: {r.content!r}") if r.status_code == 500: raise exceptions.APIError("Internal Scratch server error") if r.status_code == 429: raise exceptions.Response429("You are being rate-limited (or blocked) by Scratch") - if r.text == '{"code":"BadRequest","message":""}': - raise exceptions.BadRequest("Make sure all provided arguments are valid") - if r.text == '{"code":"BadRequest","message":""}': + if r.json() == {"code":"BadRequest","message":""}: raise exceptions.BadRequest("Make sure all provided arguments are valid") - @staticmethod - def get(url, *, data=None, json=None, headers=None, cookies=None, timeout=None, params=None): + @override + def get(self, *args, **kwargs): + kwargs.setdefault("proxies", proxies) try: - r = requests.get(url, data=data, json=json, headers=headers, cookies=cookies, params=params, - timeout=timeout, proxies=proxies) + r = super().get(*args, **kwargs) except Exception as e: raise exceptions.FetchError(e) - Requests.check_response(r) + if self.error_handling: + self.check_response(r) return r - @staticmethod - def post(url, *, data=None, json=None, headers=None, cookies=None, timeout=None, params=None, files=None, errorhandling=True, ): + @override + def post(self, *args, **kwargs): + kwargs.setdefault("proxies", proxies) try: - r = requests.post(url, data=data, json=json, headers=headers, cookies=cookies, params=params, - timeout=timeout, proxies=proxies, files=files) + r = super().post(*args, **kwargs) except Exception as e: raise exceptions.FetchError(e) - if errorhandling: - Requests.check_response(r) + if self.error_handling: + self.check_response(r) return r - @staticmethod - def delete(url, *, data=None, json=None, headers=None, cookies=None, timeout=None, params=None): + @override + def delete(self, *args, **kwargs): + kwargs.setdefault("proxies", proxies) try: - r = requests.delete(url, data=data, json=json, headers=headers, cookies=cookies, params=params, - timeout=timeout, proxies=proxies) + r = super().delete(*args, **kwargs) except Exception as e: raise exceptions.FetchError(e) - Requests.check_response(r) + if self.error_handling: + self.check_response(r) return r - @staticmethod - def put(url, *, data=None, json=None, headers=None, cookies=None, timeout=None, params=None): + @override + def put(self, *args, **kwargs): + kwargs.setdefault("proxies", proxies) try: - r = requests.put(url, data=data, json=json, headers=headers, cookies=cookies, params=params, - timeout=timeout, proxies=proxies) + r = super().put(*args, **kwargs) except Exception as e: raise exceptions.FetchError(e) - Requests.check_response(r) + if self.error_handling: + self.check_response(r) return r + + @contextmanager + def no_error_handling(self) -> Iterator[None]: + val_before = self.error_handling + self.error_handling = False + try: + yield + finally: + self.error_handling = val_before + + @contextmanager + def yes_error_handling(self) -> Iterator[None]: + val_before = self.error_handling + self.error_handling = True + try: + yield + finally: + self.error_handling = val_before +requests = Requests() diff --git a/setup.py b/setup.py index e8c4a4b5..e0b2cd4f 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,11 @@ VERSION = '2.1.13' DESCRIPTION = 'A Scratch API Wrapper' -LONG_DESCRIPTION = DESCRIPTION +with open('README.md', encoding='utf-8') as f: + LONG_DESCRIPTION = f.read() + +with open('requirements.txt', encoding='utf-8') as f: + requirements = f.read().strip().splitlines() # Setting up setup( @@ -14,11 +18,13 @@ author_email="", description=DESCRIPTION, long_description_content_type="text/markdown", - long_description=open('README.md', encoding='utf-8').read(), + long_description=LONG_DESCRIPTION, packages=find_packages(), - install_requires=["websocket-client","requests","bs4","SimpleWebSocketServer", "typing-extensions"], + install_requires=requirements, + extras_require={ + "lark": ["lark"] + }, keywords=['scratch api', 'scratchattach', 'scratch api python', 'scratch python', 'scratch for python', 'scratch', 'scratch cloud', 'scratch cloud variables', 'scratch bot'], - url='https://scratchattach.tim1de.net', classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -26,5 +32,9 @@ "Operating System :: Unix", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", - ] + ], + project_urls={ + "Source": "https://github.com/timmccool/scratchattach", + "Homepage": 'https://scratchattach.tim1de.net' + } ) diff --git a/tests/test1.py b/tests/test1.py deleted file mode 100644 index 005fb529..00000000 --- a/tests/test1.py +++ /dev/null @@ -1,20 +0,0 @@ -exit() -import dotenv -dotenv.load_dotenv() -import scratchattach -import os - -session_string = os.getenv("SCRATCH_SESSION_STRING") -assert session_string - -session = scratchattach.login_by_session_string(session_string) - -cloud = session.connect_cloud(693122627) -client = cloud.requests() - -@client.request(name="get") -def get(user1, user2): - print(user1, user2, client.get_requester()) - return "hi" - -client.start() \ No newline at end of file