scratchattach 2.1.13__py3-none-any.whl → 2.1.15b0__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 (55) hide show
  1. scratchattach/cloud/_base.py +12 -8
  2. scratchattach/cloud/cloud.py +19 -7
  3. scratchattach/editor/asset.py +59 -5
  4. scratchattach/editor/base.py +82 -31
  5. scratchattach/editor/block.py +86 -15
  6. scratchattach/editor/blockshape.py +10 -6
  7. scratchattach/editor/build_defaulting.py +6 -2
  8. scratchattach/editor/code_translation/__init__.py +0 -0
  9. scratchattach/editor/code_translation/parse.py +177 -0
  10. scratchattach/editor/comment.py +6 -0
  11. scratchattach/editor/commons.py +49 -19
  12. scratchattach/editor/extension.py +10 -3
  13. scratchattach/editor/field.py +9 -0
  14. scratchattach/editor/inputs.py +4 -1
  15. scratchattach/editor/meta.py +11 -3
  16. scratchattach/editor/monitor.py +46 -38
  17. scratchattach/editor/mutation.py +11 -4
  18. scratchattach/editor/pallete.py +24 -25
  19. scratchattach/editor/prim.py +2 -2
  20. scratchattach/editor/project.py +9 -3
  21. scratchattach/editor/sprite.py +19 -6
  22. scratchattach/editor/twconfig.py +2 -1
  23. scratchattach/editor/vlb.py +1 -1
  24. scratchattach/eventhandlers/_base.py +2 -2
  25. scratchattach/eventhandlers/cloud_events.py +2 -2
  26. scratchattach/eventhandlers/cloud_requests.py +3 -3
  27. scratchattach/eventhandlers/cloud_server.py +3 -3
  28. scratchattach/eventhandlers/message_events.py +1 -1
  29. scratchattach/other/other_apis.py +4 -4
  30. scratchattach/other/project_json_capabilities.py +3 -3
  31. scratchattach/site/_base.py +13 -12
  32. scratchattach/site/activity.py +11 -43
  33. scratchattach/site/alert.py +227 -0
  34. scratchattach/site/backpack_asset.py +2 -2
  35. scratchattach/site/browser_cookie3_stub.py +17 -0
  36. scratchattach/site/browser_cookies.py +27 -21
  37. scratchattach/site/classroom.py +51 -34
  38. scratchattach/site/cloud_activity.py +4 -4
  39. scratchattach/site/comment.py +30 -8
  40. scratchattach/site/forum.py +101 -69
  41. scratchattach/site/project.py +42 -21
  42. scratchattach/site/session.py +170 -80
  43. scratchattach/site/studio.py +4 -4
  44. scratchattach/site/user.py +179 -64
  45. scratchattach/utils/commons.py +35 -23
  46. scratchattach/utils/enums.py +44 -5
  47. scratchattach/utils/exceptions.py +10 -0
  48. scratchattach/utils/requests.py +57 -31
  49. {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/METADATA +8 -3
  50. scratchattach-2.1.15b0.dist-info/RECORD +66 -0
  51. {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/WHEEL +1 -1
  52. scratchattach/editor/sbuild.py +0 -2837
  53. scratchattach-2.1.13.dist-info/RECORD +0 -63
  54. {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/licenses/LICENSE +0 -0
  55. {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/top_level.txt +0 -0
@@ -7,6 +7,7 @@ from typing import Optional, Union, TypeVar, Generic, TYPE_CHECKING, Any
7
7
  from abc import ABC, abstractmethod, ABCMeta
8
8
  from threading import Lock
9
9
  from collections.abc import Iterator
10
+ import warnings
10
11
 
11
12
  if TYPE_CHECKING:
12
13
  from _typeshed import SupportsRead
@@ -24,13 +25,13 @@ class SupportsClose(ABC):
24
25
 
25
26
  import websocket
26
27
 
27
- from ..site import session
28
- from ..eventhandlers import cloud_recorder
29
- from ..utils import exceptions
30
- from ..eventhandlers.cloud_requests import CloudRequests
31
- from ..eventhandlers.cloud_events import CloudEvents
32
- from ..eventhandlers.cloud_storage import CloudStorage
33
- from ..site import cloud_activity
28
+ from scratchattach.site import session
29
+ from scratchattach.eventhandlers import cloud_recorder
30
+ from scratchattach.utils import exceptions
31
+ from scratchattach.eventhandlers.cloud_requests import CloudRequests
32
+ from scratchattach.eventhandlers.cloud_events import CloudEvents
33
+ from scratchattach.eventhandlers.cloud_storage import CloudStorage
34
+ from scratchattach.site import cloud_activity
34
35
 
35
36
  T = TypeVar("T")
36
37
 
@@ -122,7 +123,10 @@ class WebSocketEventStream(EventStream):
122
123
  self.source_cloud.username = cloud.username
123
124
  self.source_cloud.ws_timeout = None # No timeout -> allows continous listening
124
125
  self.reading = Lock()
125
- self.source_cloud.connect()
126
+ try:
127
+ self.source_cloud.connect()
128
+ except exceptions.CloudConnectionError:
129
+ warnings.warn("Initial cloud connection attempt failed, retrying...", exceptions.UnexpectedWebsocketEventWarning)
126
130
  self.packets_left = []
127
131
 
128
132
  def receive_new(self, non_blocking: bool = False):
@@ -1,12 +1,15 @@
1
1
  """v2 ready: ScratchCloud, TwCloud and CustomCloud classes"""
2
2
 
3
3
  from __future__ import annotations
4
+ import warnings
5
+
6
+ from websocket import WebSocketBadStatusException
4
7
 
5
8
  from ._base import BaseCloud
6
9
  from typing import Type
7
- from ..utils.requests import Requests as requests
8
- from ..utils import exceptions, commons
9
- from ..site import cloud_activity
10
+ from scratchattach.utils.requests import requests
11
+ from scratchattach.utils import exceptions, commons
12
+ from scratchattach.site import cloud_activity
10
13
 
11
14
 
12
15
  class ScratchCloud(BaseCloud):
@@ -26,7 +29,10 @@ class ScratchCloud(BaseCloud):
26
29
 
27
30
  def connect(self):
28
31
  self._assert_auth() # Connecting to Scratch's cloud websocket requires a login to the Scratch website
29
- super().connect()
32
+ try:
33
+ super().connect()
34
+ except WebSocketBadStatusException as e:
35
+ raise exceptions.CloudConnectionError(f"Error: Scratch's Cloud system may be down. Please try again later.") from e
30
36
 
31
37
  def set_var(self, variable, value):
32
38
  self._assert_auth() # Setting a cloud var requires a login to the Scratch website
@@ -88,7 +94,7 @@ class ScratchCloud(BaseCloud):
88
94
 
89
95
  def events(self, *, use_logs=False):
90
96
  if self._session is None or use_logs:
91
- from ..eventhandlers.cloud_events import CloudLogEvents
97
+ from scratchattach.eventhandlers.cloud_events import CloudLogEvents
92
98
  return CloudLogEvents(self)
93
99
  else:
94
100
  return super().events()
@@ -146,7 +152,10 @@ def get_cloud(project_id, *, CloudClass:Type[BaseCloud]=ScratchCloud) -> BaseClo
146
152
  Returns:
147
153
  Type[scratchattach.cloud._base.BaseCloud]: An object representing the cloud of a project. Can be of any class inheriting from BaseCloud.
148
154
  """
149
- print("Warning: To set Scratch cloud variables, use session.connect_cloud instead of get_cloud")
155
+ warnings.warn(
156
+ "Warning: To set Scratch cloud variables, use session.connect_cloud instead of get_cloud",
157
+ exceptions.AnonymousSiteComponentWarning
158
+ )
150
159
  return CloudClass(project_id=project_id)
151
160
 
152
161
  def get_scratch_cloud(project_id):
@@ -160,7 +169,10 @@ def get_scratch_cloud(project_id):
160
169
  Returns:
161
170
  scratchattach.cloud.ScratchCloud: An object representing the Scratch cloud of a project.
162
171
  """
163
- print("Warning: To set Scratch cloud variables, use session.connect_scratch_cloud instead of get_scratch_cloud")
172
+ warnings.warn(
173
+ "To set Scratch cloud variables, use session.connect_scratch_cloud instead of get_scratch_cloud",
174
+ exceptions.AnonymousSiteComponentWarning
175
+ )
164
176
  return ScratchCloud(project_id=project_id)
165
177
 
166
178
  def get_tw_cloud(project_id, *, purpose="", contact="", cloud_host="wss://clouddata.turbowarp.org"):
@@ -10,12 +10,19 @@ from typing import Optional
10
10
 
11
11
  @dataclass(init=True, repr=True)
12
12
  class AssetFile:
13
+ """
14
+ Represents the file information for an asset
15
+ - stores the filename, data, and md5 hash
16
+ """
13
17
  filename: str
14
- _data: bytes = field(repr=False, default=None)
15
- _md5: str = field(repr=False, default=None)
18
+ _data: bytes = field(repr=False, default_factory=bytes)
19
+ _md5: str = field(repr=False, default_factory=str)
16
20
 
17
21
  @property
18
22
  def data(self):
23
+ """
24
+ Return the contents of the asset file, as bytes
25
+ """
19
26
  if self._data is None:
20
27
  # Download and cache
21
28
  rq = requests.get(f"https://assets.scratch.mit.edu/internalapi/asset/{self.filename}/get/")
@@ -28,6 +35,9 @@ class AssetFile:
28
35
 
29
36
  @property
30
37
  def md5(self):
38
+ """
39
+ Compute/retrieve the md5 hash value of the asset file data
40
+ """
31
41
  if self._md5 is None:
32
42
  self._md5 = md5(self.data).hexdigest()
33
43
 
@@ -38,7 +48,7 @@ class Asset(base.SpriteSubComponent):
38
48
  def __init__(self,
39
49
  name: str = "costume1",
40
50
  file_name: str = "b7853f557e4426412e64bb3da6531a99.svg",
41
- _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT):
51
+ _sprite: commons.SpriteInput = build_defaulting.SPRITE_DEFAULT):
42
52
  """
43
53
  Represents a generic asset. Can be a sound or an image.
44
54
  https://en.scratch-wiki.info/wiki/Scratch_File_Format#Assets
@@ -60,22 +70,40 @@ class Asset(base.SpriteSubComponent):
60
70
 
61
71
  @property
62
72
  def folder(self):
73
+ """
74
+ Get the folder name of this asset, based on the asset name. Uses the turbowarp syntax
75
+ """
63
76
  return commons.get_folder_name(self.name)
64
77
 
65
78
  @property
66
79
  def name_nfldr(self):
80
+ """
81
+ Get the asset name after removing the folder name
82
+ """
67
83
  return commons.get_name_nofldr(self.name)
68
84
 
69
85
  @property
70
86
  def file_name(self):
87
+ """
88
+ Get the exact file name, as it would be within an sb3 file
89
+ equivalent to the md5ext value using in scratch project JSON
90
+ """
71
91
  return f"{self.id}.{self.data_format}"
72
92
 
73
93
  @property
74
94
  def md5ext(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
+ """
75
99
  return self.file_name
76
100
 
77
101
  @property
78
102
  def parent(self):
103
+ """
104
+ Return the project that this asset is attached to. If there is no attached project,
105
+ try returning the attached sprite
106
+ """
79
107
  if self.project is None:
80
108
  return self.sprite
81
109
  else:
@@ -83,6 +111,9 @@ class Asset(base.SpriteSubComponent):
83
111
 
84
112
  @property
85
113
  def asset_file(self) -> AssetFile:
114
+ """
115
+ Get the associated asset file object for this asset object
116
+ """
86
117
  for asset_file in self.parent.asset_data:
87
118
  if asset_file.filename == self.file_name:
88
119
  return asset_file
@@ -94,17 +125,27 @@ class Asset(base.SpriteSubComponent):
94
125
 
95
126
  @staticmethod
96
127
  def from_json(data: dict):
128
+ """
129
+ Load asset data from project.json
130
+ """
97
131
  _name = data.get("name")
132
+ assert isinstance(_name, str)
98
133
  _file_name = data.get("md5ext")
99
134
  if _file_name is None:
100
135
  if "dataFormat" in data and "assetId" in data:
101
136
  _id = data["assetId"]
102
137
  _data_format = data["dataFormat"]
103
138
  _file_name = f"{_id}.{_data_format}"
139
+ else:
140
+ _file_name = ""
141
+ assert isinstance(_file_name, str)
104
142
 
105
143
  return Asset(_name, _file_name)
106
144
 
107
145
  def to_json(self) -> dict:
146
+ """
147
+ Convert asset data to project.json format
148
+ """
108
149
  return {
109
150
  "name": self.name,
110
151
 
@@ -113,6 +154,7 @@ class Asset(base.SpriteSubComponent):
113
154
  "dataFormat": self.data_format,
114
155
  }
115
156
 
157
+ # todo: implement below:
116
158
  """
117
159
  @staticmethod
118
160
  def from_file(fp: str, name: str = None):
@@ -132,9 +174,9 @@ class Costume(Asset):
132
174
  bitmap_resolution=None,
133
175
  rotation_center_x: int | float = 48,
134
176
  rotation_center_y: int | float = 50,
135
- _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT):
177
+ _sprite: commons.SpriteInput = build_defaulting.SPRITE_DEFAULT):
136
178
  """
137
- A costume. An asset with additional properties
179
+ A costume (image). An asset with additional properties
138
180
  https://en.scratch-wiki.info/wiki/Scratch_File_Format#Costumes
139
181
  """
140
182
  super().__init__(name, file_name, _sprite)
@@ -145,6 +187,9 @@ class Costume(Asset):
145
187
 
146
188
  @staticmethod
147
189
  def from_json(data):
190
+ """
191
+ Load costume data from project.json
192
+ """
148
193
  _asset_load = Asset.from_json(data)
149
194
 
150
195
  bitmap_resolution = data.get("bitmapResolution")
@@ -156,6 +201,9 @@ class Costume(Asset):
156
201
  bitmap_resolution, rotation_center_x, rotation_center_y)
157
202
 
158
203
  def to_json(self) -> dict:
204
+ """
205
+ Convert costume to project.json format
206
+ """
159
207
  _json = super().to_json()
160
208
  _json.update({
161
209
  "bitmapResolution": self.bitmap_resolution,
@@ -184,6 +232,9 @@ class Sound(Asset):
184
232
 
185
233
  @staticmethod
186
234
  def from_json(data):
235
+ """
236
+ Load sound from project.json
237
+ """
187
238
  _asset_load = Asset.from_json(data)
188
239
 
189
240
  rate = data.get("rate")
@@ -191,6 +242,9 @@ class Sound(Asset):
191
242
  return Sound(_asset_load.name, _asset_load.file_name, rate, sample_count)
192
243
 
193
244
  def to_json(self) -> dict:
245
+ """
246
+ Convert Sound to project.json format
247
+ """
194
248
  _json = super().to_json()
195
249
  commons.noneless_update(_json, {
196
250
  "rate": self.rate,
@@ -8,15 +8,21 @@ import copy
8
8
  import json
9
9
  from abc import ABC, abstractmethod
10
10
  from io import TextIOWrapper
11
- from typing import Optional, Any, TYPE_CHECKING, BinaryIO
11
+ from typing import Optional, Any, TYPE_CHECKING, BinaryIO, Union
12
12
 
13
13
  if TYPE_CHECKING:
14
- from . import project, sprite, block, mutation, asset
14
+ from . import project, block, asset
15
+ from . import mutation as module_mutation
16
+ from . import sprite as module_sprite
17
+ from . import commons
15
18
 
16
19
  from . import build_defaulting
17
20
 
18
21
 
19
22
  class Base(ABC):
23
+ """
24
+ Abstract base class for most sa.editor classes. Implements copy functions
25
+ """
20
26
  def dcopy(self):
21
27
  """
22
28
  :return: A **deep** copy of self
@@ -31,22 +37,33 @@ class Base(ABC):
31
37
 
32
38
 
33
39
  class JSONSerializable(Base, ABC):
40
+ """
41
+ 'Interface' for to_json() and from_json() methods
42
+ Also implements save_json() using to_json()
43
+ """
34
44
  @staticmethod
35
45
  @abstractmethod
36
- def from_json(data: dict | list | Any):
46
+ def from_json(data):
37
47
  pass
38
48
 
39
49
  @abstractmethod
40
- def to_json(self) -> dict | list | Any:
50
+ def to_json(self):
41
51
  pass
42
52
 
43
53
  def save_json(self, name: str = ''):
54
+ """
55
+ Save a json file
56
+ """
44
57
  data = self.to_json()
45
58
  with open(f"{self.__class__.__name__.lower()}{name}.json", "w") as f:
46
59
  json.dump(data, f)
47
60
 
48
61
 
49
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
+ """
50
67
  @staticmethod
51
68
  @abstractmethod
52
69
  def load_json(data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool = True, _name: Optional[str] = None) -> tuple[
@@ -58,40 +75,43 @@ class JSONExtractable(JSONSerializable, ABC):
58
75
  :param _name: Any provided name (will automatically find one otherwise)
59
76
  :return: tuple of the name, asset data & json as a string
60
77
  """
61
- ...
62
78
 
63
79
 
64
80
  class ProjectSubcomponent(JSONSerializable, ABC):
81
+ """
82
+ Base class for any class with an associated project
83
+ """
65
84
  def __init__(self, _project: Optional[project.Project] = None):
66
85
  self.project = _project
67
86
 
68
87
 
69
88
  class SpriteSubComponent(JSONSerializable, ABC):
70
- def __init__(self, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT):
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):
71
94
  if _sprite is build_defaulting.SPRITE_DEFAULT:
72
- _sprite = build_defaulting.current_sprite()
73
-
95
+ retrieved_sprite = build_defaulting.current_sprite()
96
+ assert retrieved_sprite is not None, "You don't have any sprites."
97
+ _sprite = retrieved_sprite
74
98
  self.sprite = _sprite
75
99
 
76
- # @property
77
- # def sprite(self):
78
- # if self._sprite is None:
79
- # print("ok, ", build_defaulting.current_sprite())
80
- # return build_defaulting.current_sprite()
81
- # else:
82
- # return self._sprite
83
-
84
- # @sprite.setter
85
- # def sprite(self, value):
86
- # self._sprite = value
87
-
88
100
  @property
89
101
  def project(self) -> project.Project:
90
- return self.sprite.project
102
+ """
103
+ Get associated project by proxy of the associated sprite
104
+ """
105
+ p = self.sprite.project
106
+ assert p is not None
107
+ return p
91
108
 
92
109
 
93
110
  class IDComponent(SpriteSubComponent, ABC):
94
- def __init__(self, _id: str, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT):
111
+ """
112
+ Base class for classes with an id attribute
113
+ """
114
+ def __init__(self, _id: str, _sprite: "commons.SpriteInput" = build_defaulting.SPRITE_DEFAULT):
95
115
  self.id = _id
96
116
  super().__init__(_sprite)
97
117
 
@@ -103,8 +123,7 @@ class NamedIDComponent(IDComponent, ABC):
103
123
  """
104
124
  Base class for Variables, Lists and Broadcasts (Name + ID + sprite)
105
125
  """
106
-
107
- def __init__(self, _id: str, name: str, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT):
126
+ def __init__(self, _id: str, name: str, _sprite: "commons.SpriteInput" = build_defaulting.SPRITE_DEFAULT):
108
127
  self.name = name
109
128
  super().__init__(_id, _sprite)
110
129
 
@@ -113,30 +132,62 @@ class NamedIDComponent(IDComponent, ABC):
113
132
 
114
133
 
115
134
  class BlockSubComponent(JSONSerializable, ABC):
135
+ """
136
+ Base class for classes with associated blocks
137
+ """
116
138
  def __init__(self, _block: Optional[block.Block] = None):
117
139
  self.block = _block
118
140
 
119
141
  @property
120
- def sprite(self) -> sprite.Sprite:
121
- return self.block.sprite
142
+ def sprite(self) -> module_sprite.Sprite:
143
+ """
144
+ Fetch sprite by proxy of the block
145
+ """
146
+ b = self.block
147
+ assert b is not None
148
+ return b.sprite
122
149
 
123
150
  @property
124
151
  def project(self) -> project.Project:
125
- return self.sprite.project
152
+ """
153
+ Fetch project by proxy of the sprite (by proxy of the block)
154
+ """
155
+ p = self.sprite.project
156
+ assert p is not None
157
+ return p
126
158
 
127
159
 
128
160
  class MutationSubComponent(JSONSerializable, ABC):
129
- def __init__(self, _mutation: Optional[mutation.Mutation] = None):
161
+ """
162
+ Base class for classes with associated mutations
163
+ """
164
+ mutation: Optional[module_mutation.Mutation]
165
+ def __init__(self, _mutation: Optional[module_mutation.Mutation] = None):
130
166
  self.mutation = _mutation
131
167
 
132
168
  @property
133
169
  def block(self) -> block.Block:
134
- return self.mutation.block
170
+ """
171
+ Fetch block by proxy of mutation
172
+ """
173
+ m = self.mutation
174
+ assert m is not None
175
+ b = m.block
176
+ assert b is not None
177
+ return b
135
178
 
136
179
  @property
137
- def sprite(self) -> sprite.Sprite:
180
+ def sprite(self) -> module_sprite.Sprite:
181
+ """
182
+ Fetch sprite by proxy of block (by proxy of mutation)
183
+ """
138
184
  return self.block.sprite
139
185
 
140
186
  @property
141
187
  def project(self) -> project.Project:
142
- return self.sprite.project
188
+ """
189
+ Fetch project by proxy of sprite (by proxy of block (by proxy of mutation))
190
+ """
191
+ p = self.sprite.project
192
+ assert p is not None
193
+ return p