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/asset.py ADDED
@@ -0,0 +1,265 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from hashlib import md5, sha256
5
+ import requests
6
+
7
+ from . import base, commons, sprite, build_defaulting
8
+ from typing import Optional
9
+
10
+
11
+ @dataclass(init=True, repr=True)
12
+ class AssetFile:
13
+ """
14
+ Represents the file information for an asset (not the asset metdata)
15
+ - stores the filename, data, and md5 hash
16
+ """
17
+ filename: str
18
+ _data: Optional[bytes] = field(repr=False, default=None)
19
+ _md5: str = field(repr=False, default_factory=str)
20
+
21
+ @property
22
+ def data(self) -> bytes:
23
+ """
24
+ Return the contents of the asset file, as bytes
25
+ """
26
+ if self._data is None:
27
+ # Download and cache
28
+ rq = requests.get(f"https://assets.scratch.mit.edu/internalapi/asset/{self.filename}/get/")
29
+ # print(f"Downloaded {url}")
30
+ if rq.status_code != 200:
31
+ raise ValueError(f"Can't download asset {self.filename}\nIs not uploaded to scratch! Response: {rq.text}")
32
+
33
+ self._data = rq.content
34
+
35
+ return self._data
36
+
37
+ @data.setter
38
+ def data(self, data: bytes):
39
+ self._data = data
40
+
41
+ @property
42
+ def md5(self) -> str:
43
+ """
44
+ Compute/retrieve the md5 hex-digest of the asset file data
45
+ """
46
+ if self._md5 is None:
47
+ self._md5 = md5(self.data).hexdigest()
48
+
49
+ return self._md5
50
+
51
+ @property
52
+ def sha256(self) -> str:
53
+ return sha256(self.data).hexdigest()
54
+
55
+ class Asset(base.SpriteSubComponent):
56
+ def __init__(self,
57
+ name: str = "costume1",
58
+ file_name: str = "b7853f557e4426412e64bb3da6531a99.svg",
59
+ _sprite: commons.SpriteInput = build_defaulting.SPRITE_DEFAULT):
60
+ """
61
+ Represents a generic asset, with metadata. Can be a sound or a costume.
62
+ https://en.scratch-wiki.info/wiki/Scratch_File_Format#Assets
63
+ """
64
+ try:
65
+ asset_id, data_format = file_name.split('.')
66
+ except ValueError:
67
+ raise ValueError(f"Invalid file name: {file_name}, # of '.' in {file_name} ({file_name.count('.')}) != 2; "
68
+ f"(too many/few values to unpack)")
69
+ self.name = name
70
+
71
+ self.id = asset_id
72
+ self.data_format = data_format
73
+
74
+ super().__init__(_sprite)
75
+
76
+ def __repr__(self):
77
+ return f"Asset<{self.name!r}>"
78
+
79
+ @property
80
+ def folder(self):
81
+ """
82
+ Get the folder name of this asset, based on the asset name. Uses the TurboWarp syntax
83
+ """
84
+ return commons.get_folder_name(self.name)
85
+
86
+ @property
87
+ def name_nfldr(self):
88
+ """
89
+ Get the asset name after removing the folder name. Uses the TurboWarp syntax
90
+ """
91
+ return commons.get_name_nofldr(self.name)
92
+
93
+ @property
94
+ def file_name(self):
95
+ """
96
+ Get the exact file name, as it would be within an sb3 file
97
+ equivalent to the md5ext value using in scratch project JSON
98
+ """
99
+ return f"{self.id}.{self.data_format}"
100
+
101
+ @property
102
+ def md5ext(self):
103
+ """
104
+ Get the exact file name, as it would be within an sb3 file
105
+ equivalent to the md5ext value using in scratch project JSON
106
+
107
+ (alias for file_name)
108
+ """
109
+ return self.file_name
110
+
111
+ @property
112
+ def parent(self):
113
+ """
114
+ Return the project (body) that this asset is attached to. If there is no attached project,
115
+ try returning the attached sprite instead.
116
+ """
117
+ if self.project is None:
118
+ return self.sprite
119
+ else:
120
+ return self.project
121
+
122
+ @property
123
+ def asset_file(self) -> AssetFile:
124
+ """
125
+ Get the associated asset file object for this asset object
126
+ """
127
+ for asset_file in self.parent.asset_data:
128
+ if asset_file.filename == self.file_name:
129
+ return asset_file
130
+
131
+ # No pre-existing asset file object; create one and add it to the project
132
+ asset_file = AssetFile(self.file_name)
133
+ self.project.asset_data.append(asset_file)
134
+ return asset_file
135
+
136
+ @staticmethod
137
+ def from_json(data: dict):
138
+ """
139
+ Load asset data from project.json
140
+ """
141
+ _name = data.get("name")
142
+ assert isinstance(_name, str)
143
+ _file_name = data.get("md5ext")
144
+ if _file_name is None:
145
+ if "dataFormat" in data and "assetId" in data:
146
+ _id = data["assetId"]
147
+ _data_format = data["dataFormat"]
148
+ _file_name = f"{_id}.{_data_format}"
149
+ else:
150
+ _file_name = ""
151
+ assert isinstance(_file_name, str)
152
+
153
+ return Asset(_name, _file_name)
154
+
155
+ def to_json(self) -> dict:
156
+ """
157
+ Convert asset data to project.json format
158
+ """
159
+ return {
160
+ "name": self.name,
161
+
162
+ "assetId": self.id,
163
+ "md5ext": self.file_name,
164
+ "dataFormat": self.data_format,
165
+ }
166
+
167
+ # todo: implement below:
168
+ """
169
+ @staticmethod
170
+ def from_file(fp: str, name: str = None):
171
+ image_types = ("png", "jpg", "jpeg", "svg")
172
+ sound_types = ("wav", "mp3")
173
+
174
+ # Should save data as well so it can be uploaded to scratch if required (add to project asset data)
175
+ ...
176
+ """
177
+
178
+
179
+ class Costume(Asset):
180
+ def __init__(self,
181
+ name: str = "Cat",
182
+ file_name: str = "b7853f557e4426412e64bb3da6531a99.svg",
183
+
184
+ bitmap_resolution=None,
185
+ rotation_center_x: int | float = 48,
186
+ rotation_center_y: int | float = 50,
187
+ _sprite: commons.SpriteInput = build_defaulting.SPRITE_DEFAULT):
188
+ """
189
+ A costume (image). An asset with additional properties
190
+ https://en.scratch-wiki.info/wiki/Scratch_File_Format#Costumes
191
+ """
192
+ super().__init__(name, file_name, _sprite)
193
+
194
+ self.bitmap_resolution = bitmap_resolution
195
+ self.rotation_center_x = rotation_center_x
196
+ self.rotation_center_y = rotation_center_y
197
+
198
+ @staticmethod
199
+ def from_json(data):
200
+ """
201
+ Load costume data from project.json
202
+ """
203
+ _asset_load = Asset.from_json(data)
204
+
205
+ bitmap_resolution = data.get("bitmapResolution")
206
+
207
+ rotation_center_x = data.get("rotationCenterX", 0)
208
+ rotation_center_y = data.get("rotationCenterY", 0)
209
+ return Costume(_asset_load.name, _asset_load.file_name,
210
+
211
+ bitmap_resolution, rotation_center_x, rotation_center_y)
212
+
213
+ def to_json(self) -> dict:
214
+ """
215
+ Convert costume to project.json format
216
+ """
217
+ _json = super().to_json()
218
+ _json.update({
219
+ "rotationCenterX": self.rotation_center_x,
220
+ "rotationCenterY": self.rotation_center_y
221
+ })
222
+ if self.bitmap_resolution is not None:
223
+ _json["bitmapResolution"] = self.bitmap_resolution
224
+
225
+ return _json
226
+
227
+
228
+ class Sound(Asset):
229
+ def __init__(self,
230
+ name: str = "pop",
231
+ file_name: str = "83a9787d4cb6f3b7632b4ddfebf74367.wav",
232
+
233
+ rate: Optional[int] = None,
234
+ sample_count: Optional[int] = None,
235
+ _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT):
236
+ """
237
+ A sound. An asset with additional properties
238
+ https://en.scratch-wiki.info/wiki/Scratch_File_Format#Sounds
239
+ """
240
+ super().__init__(name, file_name, _sprite)
241
+
242
+ self.rate = rate
243
+ self.sample_count = sample_count
244
+
245
+ @staticmethod
246
+ def from_json(data):
247
+ """
248
+ Load sound from project.json
249
+ """
250
+ _asset_load = Asset.from_json(data)
251
+
252
+ rate = data.get("rate")
253
+ sample_count = data.get("sampleCount")
254
+ return Sound(_asset_load.name, _asset_load.file_name, rate, sample_count)
255
+
256
+ def to_json(self) -> dict:
257
+ """
258
+ Convert Sound to project.json format
259
+ """
260
+ _json = super().to_json()
261
+ commons.noneless_update(_json, {
262
+ "rate": self.rate,
263
+ "sampleCount": self.sample_count
264
+ })
265
+ return _json
@@ -0,0 +1,115 @@
1
+ """
2
+ Module to deal with the backpack's weird JSON format, by overriding editor classes with new load methods
3
+ """
4
+ from __future__ import annotations
5
+
6
+ from . import block, prim, field, inputs, mutation, sprite
7
+
8
+
9
+ def parse_prim_fields(_fields: dict[str, dict[str, str]]) -> tuple[str | None, str | None, str | None]:
10
+ """
11
+ Function for reading the fields in a backpack **primitive**
12
+ """
13
+ for key, value in _fields.items():
14
+ prim_value, prim_name, prim_id = (None,) * 3
15
+ if key == "NUM":
16
+ prim_value = value.get("value")
17
+ else:
18
+ prim_name = value.get("value")
19
+ prim_id = value.get("id")
20
+
21
+ # There really should only be 1 item, and this function can only return for that item
22
+ return prim_value, prim_name, prim_id
23
+ return (None,) * 3
24
+
25
+
26
+ class BpField(field.Field):
27
+ """
28
+ A normal field but with a different load method
29
+ """
30
+
31
+ @staticmethod
32
+ def from_json(data: dict[str, str]) -> field.Field:
33
+ # We can very simply convert it to the regular format
34
+ data = [data.get("value"), data.get("id")]
35
+ return field.Field.from_json(data)
36
+
37
+
38
+ class BpInput(inputs.Input):
39
+ """
40
+ A normal input but with a different load method
41
+ """
42
+
43
+ @staticmethod
44
+ def from_json(data: dict[str, str]) -> inputs.Input:
45
+ # The actual data is stored in a separate prim block
46
+ _id = data.get("shadow")
47
+ _obscurer_id = data.get("block")
48
+
49
+ if _obscurer_id == _id:
50
+ # If both the shadow and obscurer are the same, then there is no actual obscurer
51
+ _obscurer_id = None
52
+ # We cannot work out the shadow status yet since that is located in the primitive
53
+ return inputs.Input(None, _id=_id, _obscurer_id=_obscurer_id)
54
+
55
+
56
+ class BpBlock(block.Block):
57
+ """
58
+ A normal block but with a different load method
59
+ """
60
+
61
+ @staticmethod
62
+ def from_json(data: dict) -> prim.Prim | block.Block:
63
+ """
64
+ Load a block in the **backpack** JSON format
65
+ :param data: A dictionary (not list)
66
+ :return: A new block/prim object
67
+ """
68
+ _opcode = data["opcode"]
69
+
70
+ _x, _y = data.get("x"), data.get("y")
71
+ if prim.is_prim_opcode(_opcode):
72
+ # This is actually a prim
73
+ prim_value, prim_name, prim_id = parse_prim_fields(data["fields"])
74
+ return prim.Prim(prim.PrimTypes.find(_opcode, "opcode"),
75
+ prim_value, prim_name, prim_id)
76
+
77
+ _next_id = data.get("next")
78
+ _parent_id = data.get("parent")
79
+
80
+ _shadow = data.get("shadow", False)
81
+ _top_level = data.get("topLevel", _parent_id is None)
82
+
83
+ _inputs = {}
84
+ for _input_code, _input_data in data.get("inputs", {}).items():
85
+ _inputs[_input_code] = BpInput.from_json(_input_data)
86
+
87
+ _fields = {}
88
+ for _field_code, _field_data in data.get("fields", {}).items():
89
+ _fields[_field_code] = BpField.from_json(_field_data)
90
+
91
+ if "mutation" in data:
92
+ _mutation = mutation.Mutation.from_json(data["mutation"])
93
+ else:
94
+ _mutation = None
95
+
96
+ return block.Block(_opcode, _shadow, _top_level, _mutation, _fields, _inputs, _x, _y, _next_id=_next_id,
97
+ _parent_id=_parent_id)
98
+
99
+
100
+ def load_script(_script_data: list[dict]) -> sprite.Sprite:
101
+ """
102
+ Loads a script into a sprite from the backpack JSON format
103
+ :param _script_data: Backpack script JSON data
104
+ :return: a Sprite object containing the script
105
+ """
106
+ # Using a sprite since it simplifies things, e.g. local global loading
107
+ _blockchain = sprite.Sprite()
108
+
109
+ for _block_data in _script_data:
110
+ _block = BpBlock.from_json(_block_data)
111
+ _block.sprite = _blockchain
112
+ _blockchain.blocks[_block_data["id"]] = _block
113
+
114
+ _blockchain.link_subcomponents()
115
+ return _blockchain
editor/base.py ADDED
@@ -0,0 +1,191 @@
1
+ """
2
+ Editor base classes
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import copy
8
+ import json
9
+ from abc import ABC, abstractmethod
10
+ from io import TextIOWrapper
11
+ from typing import Optional, Any, TYPE_CHECKING, BinaryIO, Union
12
+
13
+ if TYPE_CHECKING:
14
+ from . import project, block, asset
15
+ from . import mutation as module_mutation
16
+ from . import sprite as module_sprite
17
+ from . import commons
18
+
19
+ from . import build_defaulting
20
+
21
+
22
+ class Base(ABC):
23
+ """
24
+ Abstract base class for most sa.editor classes. Implements copy functions
25
+ """
26
+ def dcopy(self):
27
+ """
28
+ :return: A **deep** copy of self
29
+ """
30
+ return copy.deepcopy(self)
31
+
32
+ def copy(self):
33
+ """
34
+ :return: A **shallow** copy of self
35
+ """
36
+ return copy.copy(self)
37
+
38
+
39
+ class JSONSerializable(Base, ABC):
40
+ """
41
+ 'Interface' for to_json() and from_json() methods
42
+ Also implements save_json() using to_json()
43
+ """
44
+ @staticmethod
45
+ @abstractmethod
46
+ def from_json(data):
47
+ pass
48
+
49
+ @abstractmethod
50
+ def to_json(self):
51
+ pass
52
+
53
+ def save_json(self, name: str = ''):
54
+ """
55
+ Save json to a file. Adds '.json' for you.
56
+ """
57
+ data = self.to_json()
58
+ with open(f"{self.__class__.__name__.lower()}{name}.json", "w") as f:
59
+ json.dump(data, f)
60
+
61
+
62
+ class JSONExtractable(JSONSerializable, ABC):
63
+ """
64
+ Interface for objects that can be loaded from zip archives containing json files (sprite/project)
65
+ Only has one method - load_json
66
+ """
67
+ @staticmethod
68
+ @abstractmethod
69
+ def load_json(data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool = True, _name: Optional[str] = None) -> tuple[
70
+ str, list[asset.AssetFile], str]:
71
+ """
72
+ Automatically extracts the JSON data as a string, as well as providing auto naming
73
+ :param data: Either a string of JSON, sb3 file as bytes or as a file object
74
+ :param load_assets: Whether to extract assets as well (if applicable)
75
+ :param _name: Any provided name (will automatically find one otherwise)
76
+ :return: tuple of the name, asset data & json as a string
77
+ """
78
+
79
+
80
+ class ProjectSubcomponent(JSONSerializable, ABC):
81
+ """
82
+ Base class for any class with an associated project
83
+ """
84
+ def __init__(self, _project: Optional[project.Project] = None):
85
+ self.project = _project
86
+
87
+
88
+ class SpriteSubComponent(JSONSerializable, ABC):
89
+ """
90
+ Base class for any class with an associated sprite
91
+ """
92
+ sprite: module_sprite.Sprite
93
+ def __init__(self, _sprite: "commons.SpriteInput" = build_defaulting.SPRITE_DEFAULT):
94
+ if _sprite is build_defaulting.SPRITE_DEFAULT:
95
+ _sprite = build_defaulting.current_sprite()
96
+ self.sprite = _sprite
97
+
98
+ @property
99
+ def project(self) -> project.Project:
100
+ """
101
+ Get associated project by proxy of the associated sprite
102
+ """
103
+ p = self.sprite.project
104
+ assert p is not None
105
+ return p
106
+
107
+
108
+ class IDComponent(SpriteSubComponent, ABC):
109
+ """
110
+ Base class for classes with an id attribute
111
+ """
112
+ def __init__(self, _id: str, _sprite: "commons.SpriteInput" = build_defaulting.SPRITE_DEFAULT):
113
+ self.id = _id
114
+ super().__init__(_sprite)
115
+
116
+ def __repr__(self):
117
+ return f"<{self.__class__.__name__}: {self.id}>"
118
+
119
+
120
+ class NamedIDComponent(IDComponent, ABC):
121
+ """
122
+ Base class for Variables, Lists and Broadcasts (Name + ID + sprite)
123
+ """
124
+ def __init__(self, _id: str, name: str, _sprite: "commons.SpriteInput" = build_defaulting.SPRITE_DEFAULT):
125
+ self.name = name
126
+ super().__init__(_id, _sprite)
127
+
128
+ def __repr__(self):
129
+ return f"<{self.__class__.__name__} '{self.name}'>"
130
+
131
+
132
+ class BlockSubComponent(JSONSerializable, ABC):
133
+ """
134
+ Base class for classes with associated blocks
135
+ """
136
+ def __init__(self, _block: Optional[block.Block] = None):
137
+ self.block = _block
138
+
139
+ @property
140
+ def sprite(self) -> module_sprite.Sprite:
141
+ """
142
+ Fetch sprite by proxy of the block
143
+ """
144
+ b = self.block
145
+ assert b is not None
146
+ return b.sprite
147
+
148
+ @property
149
+ def project(self) -> project.Project:
150
+ """
151
+ Fetch project by proxy of the sprite (by proxy of the block)
152
+ """
153
+ p = self.sprite.project
154
+ assert p is not None
155
+ return p
156
+
157
+
158
+ class MutationSubComponent(JSONSerializable, ABC):
159
+ """
160
+ Base class for classes with associated mutations
161
+ """
162
+ mutation: Optional[module_mutation.Mutation]
163
+ def __init__(self, _mutation: Optional[module_mutation.Mutation] = None):
164
+ self.mutation = _mutation
165
+
166
+ @property
167
+ def block(self) -> block.Block:
168
+ """
169
+ Fetch block by proxy of mutation
170
+ """
171
+ m = self.mutation
172
+ assert m is not None
173
+ b = m.block
174
+ assert b is not None
175
+ return b
176
+
177
+ @property
178
+ def sprite(self) -> module_sprite.Sprite:
179
+ """
180
+ Fetch sprite by proxy of block (by proxy of mutation)
181
+ """
182
+ return self.block.sprite
183
+
184
+ @property
185
+ def project(self) -> project.Project:
186
+ """
187
+ Fetch project by proxy of sprite (by proxy of block (by proxy of mutation))
188
+ """
189
+ p = self.sprite.project
190
+ assert p is not None
191
+ return p