scratchattach 2.1.9__py3-none-any.whl → 2.1.10a1__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 (59) hide show
  1. scratchattach/__init__.py +28 -25
  2. scratchattach/cloud/__init__.py +2 -0
  3. scratchattach/cloud/_base.py +454 -282
  4. scratchattach/cloud/cloud.py +171 -168
  5. scratchattach/editor/__init__.py +21 -0
  6. scratchattach/editor/asset.py +199 -0
  7. scratchattach/editor/backpack_json.py +117 -0
  8. scratchattach/editor/base.py +142 -0
  9. scratchattach/editor/block.py +507 -0
  10. scratchattach/editor/blockshape.py +353 -0
  11. scratchattach/editor/build_defaulting.py +47 -0
  12. scratchattach/editor/comment.py +74 -0
  13. scratchattach/editor/commons.py +243 -0
  14. scratchattach/editor/extension.py +43 -0
  15. scratchattach/editor/field.py +90 -0
  16. scratchattach/editor/inputs.py +132 -0
  17. scratchattach/editor/meta.py +106 -0
  18. scratchattach/editor/monitor.py +175 -0
  19. scratchattach/editor/mutation.py +317 -0
  20. scratchattach/editor/pallete.py +91 -0
  21. scratchattach/editor/prim.py +170 -0
  22. scratchattach/editor/project.py +273 -0
  23. scratchattach/editor/sbuild.py +2837 -0
  24. scratchattach/editor/sprite.py +586 -0
  25. scratchattach/editor/twconfig.py +113 -0
  26. scratchattach/editor/vlb.py +134 -0
  27. scratchattach/eventhandlers/_base.py +99 -92
  28. scratchattach/eventhandlers/cloud_events.py +110 -103
  29. scratchattach/eventhandlers/cloud_recorder.py +26 -21
  30. scratchattach/eventhandlers/cloud_requests.py +460 -452
  31. scratchattach/eventhandlers/cloud_server.py +246 -244
  32. scratchattach/eventhandlers/cloud_storage.py +135 -134
  33. scratchattach/eventhandlers/combine.py +29 -27
  34. scratchattach/eventhandlers/filterbot.py +160 -159
  35. scratchattach/eventhandlers/message_events.py +41 -40
  36. scratchattach/other/other_apis.py +284 -212
  37. scratchattach/other/project_json_capabilities.py +475 -546
  38. scratchattach/site/_base.py +64 -46
  39. scratchattach/site/activity.py +414 -122
  40. scratchattach/site/backpack_asset.py +118 -84
  41. scratchattach/site/classroom.py +430 -142
  42. scratchattach/site/cloud_activity.py +107 -103
  43. scratchattach/site/comment.py +220 -190
  44. scratchattach/site/forum.py +400 -399
  45. scratchattach/site/project.py +806 -787
  46. scratchattach/site/session.py +1134 -867
  47. scratchattach/site/studio.py +611 -609
  48. scratchattach/site/user.py +835 -837
  49. scratchattach/utils/commons.py +243 -148
  50. scratchattach/utils/encoder.py +157 -156
  51. scratchattach/utils/enums.py +197 -190
  52. scratchattach/utils/exceptions.py +233 -206
  53. scratchattach/utils/requests.py +67 -59
  54. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info}/METADATA +155 -146
  55. scratchattach-2.1.10a1.dist-info/RECORD +62 -0
  56. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info}/WHEEL +1 -1
  57. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info/licenses}/LICENSE +21 -21
  58. scratchattach-2.1.9.dist-info/RECORD +0 -40
  59. {scratchattach-2.1.9.dist-info → scratchattach-2.1.10a1.dist-info}/top_level.txt +0 -0
@@ -1,148 +1,243 @@
1
- """v2 ready: Common functions used by various internal modules"""
2
-
3
- from . import exceptions
4
- from threading import Thread
5
- from .requests import Requests as requests
6
-
7
- headers = {
8
- "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
9
- "x-csrftoken": "a",
10
- "x-requested-with": "XMLHttpRequest",
11
- "referer": "https://scratch.mit.edu",
12
- } # headers recommended for accessing API endpoints that don't require verification
13
-
14
- empty_project_json = {
15
- 'targets': [
16
- {
17
- 'isStage': True,
18
- 'name': 'Stage',
19
- 'variables': {
20
- '`jEk@4|i[#Fk?(8x)AV.-my variable': [
21
- 'my variable',
22
- 0,
23
- ],
24
- },
25
- 'lists': {},
26
- 'broadcasts': {},
27
- 'blocks': {},
28
- 'comments': {},
29
- 'currentCostume': 0,
30
- 'costumes': [
31
- {
32
- 'name': '',
33
- 'bitmapResolution': 1,
34
- 'dataFormat': 'svg',
35
- 'assetId': '14e46ec3e2ba471c2adfe8f119052307',
36
- 'md5ext': '14e46ec3e2ba471c2adfe8f119052307.svg',
37
- 'rotationCenterX': 0,
38
- 'rotationCenterY': 0,
39
- },
40
- ],
41
- 'sounds': [],
42
- 'volume': 100,
43
- 'layerOrder': 0,
44
- 'tempo': 60,
45
- 'videoTransparency': 50,
46
- 'videoState': 'on',
47
- 'textToSpeechLanguage': None,
48
- },
49
- ],
50
- 'monitors': [],
51
- 'extensions': [],
52
- 'meta': {
53
- 'semver': '3.0.0',
54
- 'vm': '2.3.0',
55
- 'agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
56
- },
57
- }
58
-
59
-
60
- def api_iterative_data(fetch_func, limit, offset, max_req_limit=40, unpack=True):
61
- """
62
- Iteratively gets data by calling fetch_func with a moving offset and a limit.
63
- Once fetch_func returns None, the retrieval is completed.
64
- """
65
- if limit is None:
66
- limit = max_req_limit
67
- end = offset + limit
68
- api_data = []
69
- for offs in range(offset, end, max_req_limit):
70
- d = fetch_func(
71
- offs, max_req_limit
72
- ) # Mimick actual scratch by only requesting the max amount
73
- if d is None:
74
- break
75
- if unpack:
76
- api_data.extend(d)
77
- else:
78
- api_data.append(d)
79
- if len(d) < max_req_limit:
80
- break
81
- api_data = api_data[:limit]
82
- return api_data
83
-
84
- def api_iterative(
85
- url, *, limit, offset, max_req_limit=40, add_params="", headers=headers, cookies={}
86
- ):
87
- """
88
- Function for getting data from one of Scratch's iterative JSON API endpoints (like /users/<user>/followers, or /users/<user>/projects)
89
- """
90
- if offset < 0:
91
- raise exceptions.BadRequest("offset parameter must be >= 0")
92
- if limit < 0:
93
- raise exceptions.BadRequest("limit parameter must be >= 0")
94
-
95
- def fetch(o, l):
96
- """
97
- Performs a singla API request
98
- """
99
- resp = requests.get(
100
- f"{url}?limit={l}&offset={o}{add_params}", headers=headers, cookies=cookies, timeout=10
101
- ).json()
102
- if not resp:
103
- return None
104
- if resp == {"code": "BadRequest", "message": ""}:
105
- raise exceptions.BadRequest("the passed arguments are invalid")
106
- return resp
107
-
108
- api_data = api_iterative_data(
109
- fetch, limit, offset, max_req_limit=max_req_limit, unpack=True
110
- )
111
- return api_data
112
-
113
- def _get_object(identificator_name, identificator, Class, NotFoundException, session=None):
114
- # Interal function: Generalization of the process ran by get_user, get_studio etc.
115
- # Builds an object of class that is inheriting from BaseSiteComponent
116
- # # Class must inherit from BaseSiteComponent
117
- try:
118
- _object = Class(**{identificator_name:identificator, "_session":session})
119
- r = _object.update()
120
- if r == "429":
121
- raise(exceptions.Response429("Your network is blocked or rate-limited by Scratch.\nIf you're using an online IDE like replit.com, try running the code on your computer."))
122
- if not r:
123
- # Target is unshared. The cases that this can happen in are hardcoded:
124
- from ..site import project
125
- if Class is project.Project: # Case: Target is an unshared project.
126
- return project.PartialProject(**{identificator_name:identificator, "shared":False, "_session":session})
127
- else:
128
- raise NotFoundException
129
- else:
130
- return _object
131
- except KeyError as e:
132
- raise(NotFoundException("Key error at key "+str(e)+" when reading API response"))
133
- except Exception as e:
134
- raise(e)
135
-
136
- def webscrape_count(raw, text_before, text_after):
137
- return int(raw.split(text_before)[1].split(text_after)[0])
138
-
139
- def parse_object_list(raw, Class, session=None, primary_key="id"):
140
- results = []
141
- for raw_dict in raw:
142
- try:
143
- _obj = Class(**{primary_key:raw_dict[primary_key], "_session":session})
144
- _obj._update_from_dict(raw_dict)
145
- results.append(_obj)
146
- except Exception as e:
147
- print("Warning raised by scratchattach: failed to parse ", raw_dict, "error", e)
148
- return results
1
+ """v2 ready: Common functions used by various internal modules"""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional, Final, Any, TypeVar, Callable, TYPE_CHECKING, Union
5
+ from threading import Lock
6
+
7
+ from . import exceptions
8
+ from .requests import Requests as requests
9
+
10
+ from ..site import _base
11
+
12
+ headers: Final = {
13
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
14
+ "(KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
15
+ "x-csrftoken": "a",
16
+ "x-requested-with": "XMLHttpRequest",
17
+ "referer": "https://scratch.mit.edu",
18
+ }
19
+ empty_project_json: Final = {
20
+ 'targets': [
21
+ {
22
+ 'isStage': True,
23
+ 'name': 'Stage',
24
+ 'variables': {
25
+ '`jEk@4|i[#Fk?(8x)AV.-my variable': [
26
+ 'my variable',
27
+ 0,
28
+ ],
29
+ },
30
+ 'lists': {},
31
+ 'broadcasts': {},
32
+ 'blocks': {},
33
+ 'comments': {},
34
+ 'currentCostume': 0,
35
+ 'costumes': [
36
+ {
37
+ 'name': '',
38
+ 'bitmapResolution': 1,
39
+ 'dataFormat': 'svg',
40
+ 'assetId': '14e46ec3e2ba471c2adfe8f119052307',
41
+ 'md5ext': '14e46ec3e2ba471c2adfe8f119052307.svg',
42
+ 'rotationCenterX': 0,
43
+ 'rotationCenterY': 0,
44
+ },
45
+ ],
46
+ 'sounds': [],
47
+ 'volume': 100,
48
+ 'layerOrder': 0,
49
+ 'tempo': 60,
50
+ 'videoTransparency': 50,
51
+ 'videoState': 'on',
52
+ 'textToSpeechLanguage': None,
53
+ },
54
+ ],
55
+ 'monitors': [],
56
+ 'extensions': [],
57
+ 'meta': {
58
+ 'semver': '3.0.0',
59
+ 'vm': '2.3.0',
60
+ 'agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
61
+ 'Chrome/124.0.0.0 Safari/537.36',
62
+ },
63
+ }
64
+
65
+
66
+ def api_iterative_data(fetch_func: Callable[[int, int], list], limit: int, offset: int, max_req_limit: int = 40,
67
+ unpack: bool = True) -> list:
68
+ """
69
+ Iteratively gets data by calling fetch_func with a moving offset and a limit.
70
+ Once fetch_func returns None, the retrieval is completed.
71
+ """
72
+ if limit is None:
73
+ limit = max_req_limit
74
+
75
+ end = offset + limit
76
+ api_data = []
77
+ for offs in range(offset, end, max_req_limit):
78
+ # Mimic actual scratch by only requesting the max amount
79
+ data = fetch_func(offs, max_req_limit)
80
+ if data is None:
81
+ break
82
+
83
+ if unpack:
84
+ api_data.extend(data)
85
+ else:
86
+ api_data.append(data)
87
+
88
+ if len(data) < max_req_limit:
89
+ break
90
+
91
+ api_data = api_data[:limit]
92
+ return api_data
93
+
94
+
95
+ def api_iterative(url: str, *, limit: int, offset: int, max_req_limit: int = 40, add_params: str = "",
96
+ _headers: Optional[dict] = None, cookies: Optional[dict] = None):
97
+ """
98
+ Function for getting data from one of Scratch's iterative JSON API endpoints (like /users/<user>/followers, or /users/<user>/projects)
99
+ """
100
+ if _headers is None:
101
+ _headers = headers.copy()
102
+ if cookies is None:
103
+ cookies = {}
104
+
105
+ if offset < 0:
106
+ raise exceptions.BadRequest("offset parameter must be >= 0")
107
+ if limit < 0:
108
+ raise exceptions.BadRequest("limit parameter must be >= 0")
109
+
110
+ def fetch(off: int, lim: int):
111
+ """
112
+ Performs a single API request
113
+ """
114
+ resp = requests.get(
115
+ f"{url}?limit={lim}&offset={off}{add_params}", headers=_headers, cookies=cookies, timeout=10
116
+ ).json()
117
+
118
+ if not resp:
119
+ return None
120
+ if resp == {"code": "BadRequest", "message": ""}:
121
+ raise exceptions.BadRequest("The passed arguments are invalid")
122
+ return resp
123
+
124
+ api_data = api_iterative_data(
125
+ fetch, limit, offset, max_req_limit=max_req_limit, unpack=True
126
+ )
127
+ return api_data
128
+
129
+ def _get_object(identificator_name, identificator, __class: type[C], NotFoundException, session=None) -> C:
130
+ # Internal function: Generalization of the process ran by get_user, get_studio etc.
131
+ # Builds an object of class that is inheriting from BaseSiteComponent
132
+ # # Class must inherit from BaseSiteComponent
133
+ from ..site import project
134
+ try:
135
+ use_class: type = __class
136
+ if __class is project.PartialProject:
137
+ use_class = project.Project
138
+ assert issubclass(use_class, __class)
139
+ _object = use_class(**{identificator_name: identificator, "_session": session})
140
+ r = _object.update()
141
+ if r == "429":
142
+ raise exceptions.Response429(
143
+ "Your network is blocked or rate-limited by Scratch.\n"
144
+ "If you're using an online IDE like replit.com, try running the code on your computer.")
145
+ if not r:
146
+ # Target is unshared. The cases that this can happen in are hardcoded:
147
+ if __class is project.PartialProject: # Case: Target is an unshared project.
148
+ _object = project.PartialProject(**{identificator_name: identificator,
149
+ "shared": False, "_session": session})
150
+ assert isinstance(_object, __class)
151
+ return _object
152
+ else:
153
+ raise NotFoundException
154
+ else:
155
+ return _object
156
+ except KeyError as e:
157
+ raise NotFoundException(f"Key error at key {e} when reading API response")
158
+ except Exception as e:
159
+ raise e
160
+
161
+
162
+ def webscrape_count(raw, text_before, text_after, cls: type = int) -> int | Any:
163
+ return cls(raw.split(text_before)[1].split(text_after)[0])
164
+
165
+
166
+ if TYPE_CHECKING:
167
+ C = TypeVar("C", bound=_base.BaseSiteComponent)
168
+
169
+ def parse_object_list(raw, __class: type[C], session=None, primary_key="id") -> list[C]:
170
+ results = []
171
+ for raw_dict in raw:
172
+ try:
173
+ _obj = __class(**{primary_key: raw_dict[primary_key], "_session": session})
174
+ _obj._update_from_dict(raw_dict)
175
+ results.append(_obj)
176
+ except Exception as e:
177
+ print("Warning raised by scratchattach: failed to parse ", raw_dict, "error", e)
178
+ return results
179
+
180
+
181
+ class LockEvent:
182
+ """
183
+ Can be waited on and triggered. Not to be confused with threading.Event, which has to be reset.
184
+ """
185
+ locks: list[Lock]
186
+ def __init__(self):
187
+ self.locks = []
188
+ self.use_locks = Lock()
189
+
190
+ def wait(self, blocking: bool = True, timeout: Optional[Union[int, float]] = None) -> bool:
191
+ """
192
+ Wait for the event.
193
+ """
194
+ timeout = -1 if timeout is None else timeout
195
+ if not blocking:
196
+ timeout = 0
197
+ return self.on().acquire(timeout=timeout)
198
+
199
+ def trigger(self):
200
+ """
201
+ Trigger all threads waiting on this event to continue.
202
+ """
203
+ with self.use_locks:
204
+ for lock in self.locks:
205
+ try:
206
+ lock.release() # Unlock the lock once to trigger the event.
207
+ except RuntimeError:
208
+ lock.acquire(timeout=0) # Lock the lock again.
209
+ for lock in self.locks.copy():
210
+ try:
211
+ lock.release() # Unlock the lock once more to make sure it was waited on.
212
+ self.locks.remove(lock)
213
+ except RuntimeError:
214
+ lock.acquire(timeout=0) # Lock the lock again.
215
+
216
+ def on(self) -> Lock:
217
+ """
218
+ Return a lock that will unlock once the event takes place.
219
+ """
220
+ lock = Lock()
221
+ with self.use_locks:
222
+ self.locks.append(lock)
223
+ lock.acquire(timeout=0)
224
+ return lock
225
+
226
+ def get_class_sort_mode(mode: str) -> tuple[str, str]:
227
+ """
228
+ Returns the sort mode for the given mode for classes only
229
+ """
230
+ ascsort = ''
231
+ descsort = ''
232
+
233
+ mode = mode.lower()
234
+ if mode == "last created":
235
+ pass
236
+ elif mode == "students":
237
+ descsort = "student_count"
238
+ elif mode == "a-z":
239
+ ascsort = "title"
240
+ elif mode == "z-a":
241
+ descsort = "title"
242
+
243
+ return ascsort, descsort