scratchattach 3.0.0b0__py3-none-any.whl → 3.0.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. cli/__about__.py +1 -0
  2. cli/__init__.py +26 -0
  3. cli/cmd/__init__.py +4 -0
  4. cli/cmd/group.py +127 -0
  5. cli/cmd/login.py +60 -0
  6. cli/cmd/profile.py +7 -0
  7. cli/cmd/sessions.py +5 -0
  8. cli/context.py +142 -0
  9. cli/db.py +66 -0
  10. cli/namespace.py +14 -0
  11. cloud/__init__.py +2 -0
  12. cloud/_base.py +483 -0
  13. cloud/cloud.py +183 -0
  14. editor/__init__.py +22 -0
  15. editor/asset.py +265 -0
  16. editor/backpack_json.py +115 -0
  17. editor/base.py +191 -0
  18. editor/block.py +584 -0
  19. editor/blockshape.py +357 -0
  20. editor/build_defaulting.py +51 -0
  21. editor/code_translation/__init__.py +0 -0
  22. editor/code_translation/parse.py +177 -0
  23. editor/comment.py +80 -0
  24. editor/commons.py +145 -0
  25. editor/extension.py +50 -0
  26. editor/field.py +99 -0
  27. editor/inputs.py +138 -0
  28. editor/meta.py +117 -0
  29. editor/monitor.py +185 -0
  30. editor/mutation.py +381 -0
  31. editor/pallete.py +88 -0
  32. editor/prim.py +174 -0
  33. editor/project.py +381 -0
  34. editor/sprite.py +609 -0
  35. editor/twconfig.py +114 -0
  36. editor/vlb.py +134 -0
  37. eventhandlers/__init__.py +0 -0
  38. eventhandlers/_base.py +101 -0
  39. eventhandlers/cloud_events.py +130 -0
  40. eventhandlers/cloud_recorder.py +26 -0
  41. eventhandlers/cloud_requests.py +544 -0
  42. eventhandlers/cloud_server.py +249 -0
  43. eventhandlers/cloud_storage.py +135 -0
  44. eventhandlers/combine.py +30 -0
  45. eventhandlers/filterbot.py +163 -0
  46. eventhandlers/message_events.py +42 -0
  47. other/__init__.py +0 -0
  48. other/other_apis.py +598 -0
  49. other/project_json_capabilities.py +475 -0
  50. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/METADATA +1 -1
  51. scratchattach-3.0.0b1.dist-info/RECORD +79 -0
  52. scratchattach-3.0.0b1.dist-info/top_level.txt +7 -0
  53. site/__init__.py +0 -0
  54. site/_base.py +93 -0
  55. site/activity.py +426 -0
  56. site/alert.py +226 -0
  57. site/backpack_asset.py +119 -0
  58. site/browser_cookie3_stub.py +17 -0
  59. site/browser_cookies.py +61 -0
  60. site/classroom.py +454 -0
  61. site/cloud_activity.py +121 -0
  62. site/comment.py +228 -0
  63. site/forum.py +436 -0
  64. site/placeholder.py +132 -0
  65. site/project.py +932 -0
  66. site/session.py +1323 -0
  67. site/studio.py +704 -0
  68. site/typed_dicts.py +151 -0
  69. site/user.py +1252 -0
  70. utils/__init__.py +0 -0
  71. utils/commons.py +263 -0
  72. utils/encoder.py +161 -0
  73. utils/enums.py +237 -0
  74. utils/exceptions.py +277 -0
  75. utils/optional_async.py +154 -0
  76. utils/requests.py +306 -0
  77. scratchattach/__init__.py +0 -37
  78. scratchattach/__main__.py +0 -93
  79. scratchattach-3.0.0b0.dist-info/RECORD +0 -8
  80. scratchattach-3.0.0b0.dist-info/top_level.txt +0 -1
  81. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/WHEEL +0 -0
  82. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/entry_points.txt +0 -0
  83. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
editor/commons.py ADDED
@@ -0,0 +1,145 @@
1
+ """
2
+ Shared functions used by the editor module
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import random
8
+ import string
9
+ from typing_extensions import Optional, Final, Any, TYPE_CHECKING, Union, Self, TypeVar
10
+ from enum import EnumMeta, Enum
11
+
12
+ if TYPE_CHECKING:
13
+ from . import sprite, build_defaulting
14
+
15
+ SpriteInput = Union[sprite.Sprite, build_defaulting._SetSprite]
16
+ else:
17
+ SpriteInput = Any
18
+
19
+ T = TypeVar('T')
20
+ from scratchattach.utils import exceptions
21
+
22
+ DIGITS: Final[tuple[str, ...]] = tuple("0123456789")
23
+
24
+ # Strangely enough, it seems like something in string.punctuation causes issues. Not sure why
25
+ ID_CHARS: Final[str] = string.ascii_letters + string.digits # + string.punctuation
26
+
27
+
28
+ def is_valid_json(_str: Any) -> bool:
29
+ """
30
+ Try to load a json string, if it fails, return False, else return true.
31
+ """
32
+ try:
33
+ json.loads(_str)
34
+ return True
35
+ except (ValueError, TypeError):
36
+ return False
37
+
38
+
39
+ def noneless_update(obj: dict, update: dict) -> None:
40
+ """
41
+ equivalent to dict.update, except and values of None are not assigned
42
+ """
43
+ for key, value in update.items():
44
+ if value is not None:
45
+ obj[key] = value
46
+
47
+
48
+ def remove_nones(obj: dict) -> None:
49
+ """
50
+ Removes all None values from a dict.
51
+ :param obj: Dictionary to remove all None values.
52
+ """
53
+ nones = []
54
+ for key, value in obj.items():
55
+ if value is None:
56
+ nones.append(key)
57
+ for key in nones:
58
+ del obj[key]
59
+
60
+
61
+ def safe_get(lst: list | tuple, _i: int, default: Optional[Any] = None) -> Any:
62
+ """
63
+ Like dict.get() but for lists
64
+ """
65
+ if len(lst) <= _i:
66
+ return default
67
+ else:
68
+ return lst[_i]
69
+
70
+
71
+ def trim_final_nones(lst: list[T]) -> list[T]:
72
+ """
73
+ Removes the last None values from a list until a non-None value is hit.
74
+ :param lst: list which will **not** be modified.
75
+ """
76
+ i = len(lst)
77
+ for item in lst[::-1]:
78
+ if item is not None:
79
+ break
80
+ i -= 1
81
+ return lst[:i]
82
+
83
+
84
+ def dumps_ifnn(obj: Any) -> Optional[str]:
85
+ """
86
+ Return json.dumps(obj) if the object is not None
87
+ """
88
+ if obj is None:
89
+ return None
90
+ else:
91
+ return json.dumps(obj)
92
+
93
+
94
+ def gen_id() -> str:
95
+ """
96
+ Generate an id for scratch blocks/variables/lists/broadcasts
97
+
98
+ The old 'naïve' method but that chances of a repeat are so miniscule
99
+ Have to check if whitespace chars break it
100
+ May later add checking within sprites so that we don't need such long ids (we can save space this way)
101
+ If this is implemented, we would need to be careful when merging sprites/adding blocks etc
102
+ """
103
+ return ''.join(random.choices(ID_CHARS, k=20))
104
+
105
+
106
+ def sanitize_fn(filename: str):
107
+ """
108
+ Removes illegal chars from a filename
109
+ :return: Sanitized filename
110
+ """
111
+ # Maybe could import a slugify module, but it's a bit overkill
112
+ ret = ''
113
+ for char in filename:
114
+ if char in string.ascii_letters + string.digits + "-_":
115
+ ret += char
116
+ else:
117
+ ret += '_'
118
+ return ret
119
+
120
+
121
+ def get_folder_name(name: str) -> str | None:
122
+ """
123
+ Get the name of the folder if this is a turbowarp-style costume name
124
+ """
125
+ if name.startswith('//'):
126
+ return None
127
+
128
+ if '//' in name:
129
+ return name.split('//')[0]
130
+ else:
131
+ return None
132
+
133
+
134
+ def get_name_nofldr(name: str) -> str:
135
+ """
136
+ Get the sprite/asset name without the folder name
137
+ """
138
+ fldr = get_folder_name(name)
139
+ if fldr is None:
140
+ return name
141
+ else:
142
+ return name[len(fldr) + 2:]
143
+
144
+ Singleton = Enum
145
+
editor/extension.py ADDED
@@ -0,0 +1,50 @@
1
+ """
2
+ Enum & dataclass representing extension categories
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+
8
+ from dataclasses import dataclass
9
+
10
+ from . import base
11
+ from scratchattach.utils import enums
12
+
13
+
14
+ @dataclass
15
+ class Extension(base.JSONSerializable):
16
+ """
17
+ Represents an extension in the Scratch block pallete - e.g. video sensing
18
+ """
19
+ code: str
20
+ name: str = None
21
+
22
+ def __eq__(self, other):
23
+ return self.code == other.code
24
+
25
+ @staticmethod
26
+ def from_json(data: str):
27
+ assert isinstance(data, str)
28
+ _extension = Extensions.find(data, "code")
29
+ if _extension is None:
30
+ _extension = Extension(data)
31
+
32
+ return _extension
33
+
34
+ def to_json(self) -> str:
35
+ return self.code
36
+
37
+
38
+ class Extensions(enums._EnumWrapper):
39
+ BOOST = Extension("boost", "LEGO BOOST Extension")
40
+ EV3 = Extension("ev3", "LEGO MINDSTORMS EV3 Extension")
41
+ GDXFOR = Extension("gdxfor", "Go Direct Force & Acceleration Extension")
42
+ MAKEYMAKEY = Extension("makeymakey", "Makey Makey Extension")
43
+ MICROBIT = Extension("microbit", "micro:bit Extension")
44
+ MUSIC = Extension("music", "Music Extension")
45
+ PEN = Extension("pen", "Pen Extension")
46
+ TEXT2SPEECH = Extension("text2speech", "Text to Speech Extension")
47
+ TRANSLATE = Extension("translate", "Translate Extension")
48
+ VIDEOSENSING = Extension("videoSensing", "Video Sensing Extension")
49
+ WEDO2 = Extension("wedo2", "LEGO Education WeDo 2.0 Extension")
50
+ COREEXAMPLE = Extension("coreExample", "CoreEx Extension") # hidden extension!
editor/field.py ADDED
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, TYPE_CHECKING, Final
4
+
5
+
6
+ if TYPE_CHECKING:
7
+ from . import block, vlb
8
+
9
+ from . import base, commons
10
+
11
+
12
+ class Types:
13
+ VARIABLE: Final[str] = "variable"
14
+ LIST: Final[str] = "list"
15
+ BROADCAST: Final[str] = "broadcast"
16
+ DEFAULT: Final[str] = "default"
17
+
18
+
19
+ class Field(base.BlockSubComponent):
20
+ def __init__(self, _value: str | vlb.Variable | vlb.List | vlb.Broadcast, _id: Optional[str] = None, *, _block: Optional[block.Block] = None):
21
+ """
22
+ A field for a scratch block
23
+ https://en.scratch-wiki.info/wiki/Scratch_File_Format#Blocks:~:text=it.%5B9%5D-,fields,element%2C%20which%20is%20the%20ID%20of%20the%20field%27s%20value.%5B10%5D,-shadow
24
+ """
25
+ self.value = _value
26
+ self.id = _id
27
+ """
28
+ ID of associated VLB. Will be used to get VLB object during sprite initialisation, where it will be replaced with 'None'
29
+ """
30
+ super().__init__(_block)
31
+
32
+ def __repr__(self):
33
+ if self.id is not None:
34
+ # This shouldn't occur after sprite initialisation
35
+ return f"<Field {self.value!r} : {self.id!r}>"
36
+ else:
37
+ return f"<Field {self.value!r}>"
38
+
39
+ @property
40
+ def value_id(self):
41
+ """
42
+ Get the id of the value associated with this field (if applicable) - when value is var/list/broadcast
43
+ """
44
+ if self.id is not None:
45
+ return self.id
46
+ else:
47
+ if hasattr(self.value, "id"):
48
+ return self.value.id
49
+ else:
50
+ return None
51
+
52
+ @property
53
+ def value_str(self):
54
+ """
55
+ Convert the associated value to a string - if this is a VLB, return the VLB name
56
+ """
57
+ if not isinstance(self.value, base.NamedIDComponent):
58
+ return self.value
59
+ else:
60
+ return self.value.name
61
+
62
+ @property
63
+ def name(self) -> str:
64
+ """
65
+ Fetch the name of this field using the associated block
66
+ """
67
+ for _name, _field in self.block.fields.items():
68
+ if _field is self:
69
+ return _name
70
+
71
+ @property
72
+ def type(self):
73
+ """
74
+ Infer the type of value that this field holds
75
+ :return: A string (from field.Types) as a name of the type
76
+ """
77
+ if "variable" in self.name.lower():
78
+ return Types.VARIABLE
79
+ elif "list" in self.name.lower():
80
+ return Types.LIST
81
+ elif "broadcast" in self.name.lower():
82
+ return Types.BROADCAST
83
+ else:
84
+ return Types.DEFAULT
85
+
86
+ @staticmethod
87
+ def from_json(data: list[str, str | None]):
88
+ # Sometimes you may have a stray field with no id. Not sure why
89
+ while len(data) < 2:
90
+ data.append(None)
91
+ data = data[:2]
92
+
93
+ _value, _id = data
94
+ return Field(_value, _id)
95
+
96
+ def to_json(self):
97
+ return commons.trim_final_nones([
98
+ self.value_str, self.value_id
99
+ ])
editor/inputs.py ADDED
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ import warnings
4
+ from typing import Optional, Final
5
+
6
+ from . import block
7
+ from . import base, commons, prim
8
+ from dataclasses import dataclass
9
+
10
+
11
+ @dataclass
12
+ class ShadowStatus:
13
+ """
14
+ Dataclass representing a possible shadow value for a block and giving it a name
15
+ """
16
+ idx: int
17
+ name: str
18
+
19
+ def __repr__(self):
20
+ return f"<ShadowStatus {self.name!r} ({self.idx})>"
21
+
22
+
23
+ class ShadowStatuses:
24
+ # Not an enum so you don't need to do .value
25
+ # Uh why?
26
+ HAS_SHADOW: Final[ShadowStatus] = ShadowStatus(1, "has shadow")
27
+ NO_SHADOW: Final[ShadowStatus] = ShadowStatus(2, "no shadow")
28
+ OBSCURED: Final[ShadowStatus] = ShadowStatus(3, "obscured")
29
+
30
+ @classmethod
31
+ def find(cls, idx: int) -> ShadowStatus:
32
+ for status in (cls.HAS_SHADOW, cls.NO_SHADOW, cls.OBSCURED):
33
+ if status.idx == idx:
34
+ return status
35
+
36
+ raise ValueError(f"Invalid ShadowStatus idx={idx}")
37
+
38
+
39
+ class Input(base.BlockSubComponent):
40
+ def __init__(self, _shadow: ShadowStatus | None = ShadowStatuses.HAS_SHADOW, _value: Optional[prim.Prim | block.Block | str] = None, _id: Optional[str] = None,
41
+ _obscurer: Optional[prim.Prim | block.Block | str] = None, *, _obscurer_id: Optional[str] = None, _block: Optional[block.Block] = None):
42
+ """
43
+ An input for a scratch block
44
+ https://en.scratch-wiki.info/wiki/Scratch_File_Format#Blocks:~:text=inputs,it.%5B9%5D
45
+ """
46
+ super().__init__(_block)
47
+
48
+ # If the shadow is None, we'll have to work it out later
49
+ self.shadow = _shadow
50
+
51
+ self.value: prim.Prim | block.Block = _value
52
+ self.obscurer: prim.Prim | block.Block = _obscurer
53
+
54
+ self._id = _id
55
+ """
56
+ ID referring to the input value. Upon project initialisation, this will be set to None and the value attribute will be set to the relevant object
57
+ """
58
+ self._obscurer_id = _obscurer_id
59
+ """
60
+ ID referring to the obscurer. Upon project initialisation, this will be set to None and the obscurer attribute will be set to the relevant block
61
+ """
62
+
63
+ def __repr__(self):
64
+ if self._id is not None:
65
+ return f"<Input<id={self._id!r}>"
66
+ else:
67
+ return f"<Input {self.value!r}>"
68
+
69
+ @staticmethod
70
+ def from_json(data: list):
71
+ _shadow = ShadowStatuses.find(data[0])
72
+
73
+ _value, _id = None, None
74
+ if isinstance(data[1], list):
75
+ _value = prim.Prim.from_json(data[1])
76
+ else:
77
+ _id = data[1]
78
+
79
+ _obscurer_data = commons.safe_get(data, 2)
80
+
81
+ _obscurer, _obscurer_id = None, None
82
+ if isinstance(_obscurer_data, list):
83
+ _obscurer = prim.Prim.from_json(_obscurer_data)
84
+ else:
85
+ _obscurer_id = _obscurer_data
86
+ return Input(_shadow, _value, _id, _obscurer, _obscurer_id=_obscurer_id)
87
+
88
+ def to_json(self) -> list:
89
+ data = [self.shadow.idx]
90
+
91
+ def add_pblock(pblock: prim.Prim | block.Block | None):
92
+ """
93
+ Adds a primitive or a block to the data in the right format
94
+ """
95
+ if pblock is None:
96
+ return
97
+
98
+ if isinstance(pblock, prim.Prim):
99
+ data.append(pblock.to_json())
100
+
101
+ elif isinstance(pblock, block.Block):
102
+ data.append(pblock.id)
103
+
104
+ else:
105
+ warnings.warn(f"Bad prim/block {pblock!r} of type {type(pblock)}")
106
+
107
+ add_pblock(self.value)
108
+ add_pblock(self.obscurer)
109
+
110
+ return data
111
+
112
+ def link_using_block(self):
113
+ """
114
+ Link the Input object to any menu blocks, obscurer blocks, sprites, and links any of its subcomponents
115
+ """
116
+
117
+ # Link to value
118
+ if self._id is not None:
119
+ new_value = self.sprite.find_block(self._id, "id")
120
+ if new_value is not None:
121
+ self.value = new_value
122
+ self._id = None
123
+
124
+ # Link to obscurer
125
+ if self._obscurer_id is not None:
126
+ new_block = self.sprite.find_block(self._obscurer_id, "id")
127
+ if new_block is not None:
128
+ self.obscurer = new_block
129
+ self._obscurer_id = None
130
+
131
+ # Link value to sprite
132
+ if isinstance(self.value, prim.Prim):
133
+ self.value.sprite = self.sprite
134
+ self.value.link_using_sprite()
135
+
136
+ # Link obscurer to sprite
137
+ if self.obscurer is not None:
138
+ self.obscurer.sprite = self.sprite
editor/meta.py ADDED
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass, field
5
+
6
+ from . import base, commons
7
+ from typing import Optional
8
+
9
+
10
+ @dataclass
11
+ class PlatformMeta(base.JSONSerializable):
12
+ """
13
+ Represents a TurboWarp platform meta object
14
+ """
15
+ name: str = None
16
+ url: str = field(repr=True, default=None)
17
+
18
+ def __bool__(self):
19
+ return self.name is not None or self.url is not None
20
+
21
+ def to_json(self):
22
+ _json = {"name": self.name, "url": self.url}
23
+ commons.remove_nones(_json)
24
+ return _json
25
+
26
+ @staticmethod
27
+ def from_json(data: dict | None):
28
+ if data is None:
29
+ return PlatformMeta()
30
+ else:
31
+ return PlatformMeta(data.get("name"), data.get("url"))
32
+
33
+
34
+ DEFAULT_VM = "0.1.0"
35
+ DEFAULT_AGENT = "scratchattach.editor by https://scratch.mit.edu/users/timmccool/"
36
+ DEFAULT_PLATFORM = PlatformMeta("scratchattach", "https://github.com/timMcCool/scratchattach/")
37
+
38
+ EDIT_META = False
39
+ META_SET_PLATFORM = False
40
+
41
+
42
+ def set_meta_platform(true_false: bool = None):
43
+ """
44
+ toggle whether to set the meta platform by default (or specify a value)
45
+ """
46
+ global META_SET_PLATFORM
47
+ if true_false is None:
48
+ true_false = bool(1 - META_SET_PLATFORM)
49
+ META_SET_PLATFORM = true_false
50
+
51
+
52
+ class Meta(base.JSONSerializable):
53
+ def __init__(self, semver: str = "3.0.0", vm: str = DEFAULT_VM, agent: str = DEFAULT_AGENT,
54
+ platform: Optional[PlatformMeta] = None):
55
+ """
56
+ Represents metadata of the project
57
+ https://en.scratch-wiki.info/wiki/Scratch_File_Format#Metadata
58
+ """
59
+ if platform is None and META_SET_PLATFORM:
60
+ platform = DEFAULT_PLATFORM.dcopy()
61
+
62
+ self.semver = semver
63
+ self.vm = vm
64
+ self.agent = agent
65
+ self.platform = platform
66
+
67
+ if not self.vm_is_valid:
68
+ raise ValueError(
69
+ f"{vm!r} does not match pattern '^([0-9]+\\.[0-9]+\\.[0-9]+)($|-)' - maybe try '0.0.0'?")
70
+
71
+ def __repr__(self):
72
+ data = f"{self.semver} : {self.vm} : {self.agent}"
73
+ if self.platform:
74
+ data += f": {self.platform}"
75
+
76
+ return f"Meta<{data}>"
77
+
78
+ @property
79
+ def vm_is_valid(self):
80
+ """
81
+ Check whether the vm value is valid using a regex
82
+ regex pattern from TurboWarp ↓↓↓↓
83
+ """
84
+ return re.match("^([0-9]+\\.[0-9]+\\.[0-9]+)($|-)", self.vm) is not None
85
+
86
+ def to_json(self):
87
+ _json: dict[str, str | dict[str, str]] = {
88
+ "semver": self.semver,
89
+ "vm": self.vm,
90
+ "agent": self.agent
91
+ }
92
+
93
+ if self.platform:
94
+ _json["platform"] = self.platform.to_json()
95
+ return _json
96
+
97
+ @staticmethod
98
+ def from_json(data: dict[str, str | dict[str, str]] | None):
99
+ if data is None:
100
+ data = {"semver": "3.0.0"}
101
+
102
+ semver = data["semver"]
103
+ vm = data.get("vm")
104
+ agent = data.get("agent")
105
+ platform = PlatformMeta.from_json(data.get("platform"))
106
+
107
+ if EDIT_META or vm is None:
108
+ vm = DEFAULT_VM
109
+
110
+ if EDIT_META or agent is None:
111
+ agent = DEFAULT_AGENT
112
+
113
+ if EDIT_META:
114
+ if META_SET_PLATFORM and not platform:
115
+ platform = DEFAULT_PLATFORM.dcopy()
116
+
117
+ return Meta(semver, vm, agent, platform)