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
@@ -3,22 +3,37 @@ from __future__ import annotations
3
3
 
4
4
  import json
5
5
  import random
6
+ import re
6
7
  import string
7
- from datetime import datetime
8
+ from datetime import datetime, timezone
9
+ from enum import Enum
10
+ import warnings
11
+
12
+ from typing_extensions import deprecated
13
+ from bs4 import BeautifulSoup, Tag
14
+
15
+ from ._base import BaseSiteComponent
16
+ from scratchattach.eventhandlers import message_events
17
+
18
+ from scratchattach.utils import commons
19
+ from scratchattach.utils import exceptions
20
+ from scratchattach.utils.commons import headers
21
+ from scratchattach.utils.requests import requests
8
22
 
9
- from ..eventhandlers import message_events
10
23
  from . import project
11
- from ..utils import exceptions
12
24
  from . import studio
13
25
  from . import forum
14
- from bs4 import BeautifulSoup
15
- from ._base import BaseSiteComponent
16
- from ..utils.commons import headers
17
- from ..utils import commons
18
26
  from . import comment
19
27
  from . import activity
28
+ from . import classroom
20
29
 
21
- from ..utils.requests import Requests as requests
30
+ class Rank(Enum):
31
+ """
32
+ Possible ranks in scratch
33
+ """
34
+ NEW_SCRATCHER = 0
35
+ SCRATCHER = 1
36
+ SCRATCH_TEAM = 2
22
37
 
23
38
  class Verificator:
24
39
 
@@ -62,7 +77,7 @@ class User(BaseSiteComponent):
62
77
 
63
78
  # Info on how the .update method has to fetch the data:
64
79
  self.update_function = requests.get
65
- self.update_API = f"https://api.scratch.mit.edu/users/{entries['username']}"
80
+ self.update_api = f"https://api.scratch.mit.edu/users/{entries['username']}"
66
81
 
67
82
  # Set attributes every User object needs to have:
68
83
  self._session = None
@@ -70,6 +85,10 @@ class User(BaseSiteComponent):
70
85
  self.username = None
71
86
  self.name = None
72
87
 
88
+ # cache value for classroom getter method (using @property)
89
+ # first value is whether the cache has actually been set (because it can be None), second is the value itself
90
+ self._classroom: tuple[bool, classroom.Classroom | None] = False, None
91
+
73
92
  # Update attributes from entries dict:
74
93
  entries.setdefault("name", entries.get("username"))
75
94
  self.__dict__.update(entries)
@@ -120,27 +139,73 @@ class User(BaseSiteComponent):
120
139
  raise exceptions.Unauthorized(
121
140
  "You need to be authenticated as the profile owner to do this.")
122
141
 
142
+ @property
143
+ def classroom(self) -> classroom.Classroom | None:
144
+ """
145
+ Get a user's associated classroom, and return it as a `scratchattach.classroom.Classroom` object.
146
+ If there is no associated classroom, returns `None`
147
+ """
148
+ if not self._classroom[0]:
149
+ with requests.no_error_handling():
150
+ resp = requests.get(f"https://scratch.mit.edu/users/{self.username}/")
151
+ soup = BeautifulSoup(resp.text, "html.parser")
152
+
153
+ details = soup.find("p", {"class": "profile-details"})
154
+ assert isinstance(details, Tag)
155
+
156
+ class_name, class_id, is_closed = None, 0, False
157
+ for a in details.find_all("a"):
158
+ if not isinstance(a, Tag):
159
+ continue
160
+ href = str(a.get("href"))
161
+ if re.match(r"/classes/\d*/", href):
162
+ class_name = a.text.strip()[len("Student of: "):]
163
+ is_closed = class_name.endswith("\n (ended)") # as this has a \n, we can be sure
164
+ if is_closed:
165
+ class_name = class_name[:-7].strip()
166
+
167
+ class_id = int(href.split('/')[2])
168
+ break
169
+
170
+ if class_name:
171
+ self._classroom = True, classroom.Classroom(
172
+ _session=self._session,
173
+ id=class_id,
174
+ title=class_name,
175
+ is_closed=is_closed
176
+ )
177
+ else:
178
+ self._classroom = True, None
179
+
180
+ return self._classroom[1]
181
+
123
182
  def does_exist(self):
124
183
  """
125
184
  Returns:
126
185
  boolean : True if the user exists, False if the user is deleted, None if an error occured
127
186
  """
128
- status_code = requests.get(f"https://scratch.mit.edu/users/{self.username}/").status_code
187
+ with requests.no_error_handling():
188
+ status_code = requests.get(f"https://scratch.mit.edu/users/{self.username}/").status_code
129
189
  if status_code == 200:
130
190
  return True
131
191
  elif status_code == 404:
132
192
  return False
133
193
 
194
+ # Will maybe be deprecated later, but for now still has its own purpose.
195
+ #@deprecated("This function is partially deprecated. Use user.rank() instead.")
134
196
  def is_new_scratcher(self):
135
197
  """
136
198
  Returns:
137
199
  boolean : True if the user has the New Scratcher status, else False
138
200
  """
139
201
  try:
140
- res = requests.get(f"https://scratch.mit.edu/users/{self.username}/").text
141
- group=res[res.rindex('<span class="group">'):][:70]
142
- return "new scratcher" in group.lower()
143
- except Exception:
202
+ with requests.no_error_handling():
203
+ res = requests.get(f"https://scratch.mit.edu/users/{self.username}/").text
204
+ group = res[res.rindex('<span class="group">'):][:70]
205
+ return "new scratcher" in group.lower()
206
+
207
+ except Exception as e:
208
+ warnings.warn(f"Caught exception {e=}")
144
209
  return None
145
210
 
146
211
  def message_count(self):
@@ -169,19 +234,21 @@ class User(BaseSiteComponent):
169
234
 
170
235
  def follower_count(self):
171
236
  # follower count
172
- text = requests.get(
173
- f"https://scratch.mit.edu/users/{self.username}/followers/",
174
- headers = self._headers
175
- ).text
176
- return commons.webscrape_count(text, "Followers (", ")")
237
+ with requests.no_error_handling():
238
+ text = requests.get(
239
+ f"https://scratch.mit.edu/users/{self.username}/followers/",
240
+ headers = self._headers
241
+ ).text
242
+ return commons.webscrape_count(text, "Followers (", ")")
177
243
 
178
244
  def following_count(self):
179
245
  # following count
180
- text = requests.get(
181
- f"https://scratch.mit.edu/users/{self.username}/following/",
182
- headers = self._headers
183
- ).text
184
- return commons.webscrape_count(text, "Following (", ")")
246
+ with requests.no_error_handling():
247
+ text = requests.get(
248
+ f"https://scratch.mit.edu/users/{self.username}/following/",
249
+ headers = self._headers
250
+ ).text
251
+ return commons.webscrape_count(text, "Following (", ")")
185
252
 
186
253
  def followers(self, *, limit=40, offset=0):
187
254
  """
@@ -215,7 +282,7 @@ class User(BaseSiteComponent):
215
282
  """
216
283
  return [i.name for i in self.following(limit=limit, offset=offset)]
217
284
 
218
- def is_following(self, user):
285
+ def is_following(self, user: str):
219
286
  """
220
287
  Returns:
221
288
  boolean: Whether the user is following the user provided as argument
@@ -229,11 +296,11 @@ class User(BaseSiteComponent):
229
296
  if user in following_names:
230
297
  following = True
231
298
  break
232
- if following_names == []:
299
+ if not following_names:
233
300
  break
234
301
  offset += 20
235
- except Exception:
236
- print("Warning: API error when performing following check")
302
+ except Exception as e:
303
+ print(f"Warning: API error when performing following check: {e=}")
237
304
  return following
238
305
  return following
239
306
 
@@ -242,28 +309,46 @@ class User(BaseSiteComponent):
242
309
  Returns:
243
310
  boolean: Whether the user is followed by the user provided as argument
244
311
  """
245
- return User(username=user).is_following(self.username)
312
+ offset = 0
313
+ followed = False
314
+
315
+ while True:
316
+ try:
317
+ followed_names = self.follower_names(limit=20, offset=offset)
318
+ if user in followed_names:
319
+ followed = True
320
+ break
321
+ if not followed_names:
322
+ break
323
+ offset += 20
324
+ except Exception as e:
325
+ print(f"Warning: API error when performing following check: {e=}")
326
+ return followed
327
+ return followed
246
328
 
247
329
  def project_count(self):
248
- text = requests.get(
249
- f"https://scratch.mit.edu/users/{self.username}/projects/",
250
- headers = self._headers
251
- ).text
252
- return commons.webscrape_count(text, "Shared Projects (", ")")
330
+ with requests.no_error_handling():
331
+ text = requests.get(
332
+ f"https://scratch.mit.edu/users/{self.username}/projects/",
333
+ headers = self._headers
334
+ ).text
335
+ return commons.webscrape_count(text, "Shared Projects (", ")")
253
336
 
254
337
  def studio_count(self):
255
- text = requests.get(
256
- f"https://scratch.mit.edu/users/{self.username}/studios/",
257
- headers = self._headers
258
- ).text
259
- return commons.webscrape_count(text, "Studios I Curate (", ")")
338
+ with requests.no_error_handling():
339
+ text = requests.get(
340
+ f"https://scratch.mit.edu/users/{self.username}/studios/",
341
+ headers = self._headers
342
+ ).text
343
+ return commons.webscrape_count(text, "Studios I Curate (", ")")
260
344
 
261
345
  def studios_following_count(self):
262
- text = requests.get(
263
- f"https://scratch.mit.edu/users/{self.username}/studios_following/",
264
- headers = self._headers
265
- ).text
266
- return commons.webscrape_count(text, "Studios I Follow (", ")")
346
+ with requests.no_error_handling():
347
+ text = requests.get(
348
+ f"https://scratch.mit.edu/users/{self.username}/studios_following/",
349
+ headers = self._headers
350
+ ).text
351
+ return commons.webscrape_count(text, "Studios I Follow (", ")")
267
352
 
268
353
  def studios(self, *, limit=40, offset=0):
269
354
  _studios = commons.api_iterative(
@@ -314,8 +399,9 @@ class User(BaseSiteComponent):
314
399
  # The index of the first project on page #n is just (n-1) * 40
315
400
  first_idx = (page - 1) * 40
316
401
 
317
- page_content = requests.get(f"https://scratch.mit.edu/projects/all/{self.username}/loves/"
318
- f"?page={page}", headers=self._headers).content
402
+ with requests.no_error_handling():
403
+ page_content = requests.get(f"https://scratch.mit.edu/projects/all/{self.username}/loves/"
404
+ f"?page={page}", headers=self._headers).content
319
405
 
320
406
  soup = BeautifulSoup(
321
407
  page_content,
@@ -350,7 +436,7 @@ class User(BaseSiteComponent):
350
436
  # A thumbnail link (no need to webscrape this)
351
437
  # A title
352
438
  # An Author (called an owner for some reason)
353
-
439
+ assert isinstance(project_element, Tag)
354
440
  project_anchors = project_element.find_all("a")
355
441
  # Each list item has three <a> tags, the first two linking the project
356
442
  # 1st contains <img> tag
@@ -359,10 +445,16 @@ class User(BaseSiteComponent):
359
445
 
360
446
  # This function is pretty handy!
361
447
  # I'll use it for an id from a string like: /projects/1070616180/
362
- project_id = commons.webscrape_count(project_anchors[0].attrs["href"],
448
+ first_anchor = project_anchors[0]
449
+ second_anchor = project_anchors[1]
450
+ third_anchor = project_anchors[2]
451
+ assert isinstance(first_anchor, Tag)
452
+ assert isinstance(second_anchor, Tag)
453
+ assert isinstance(third_anchor, Tag)
454
+ project_id = commons.webscrape_count(first_anchor.attrs["href"],
363
455
  "/projects/", "/")
364
- title = project_anchors[1].contents[0]
365
- author = project_anchors[2].contents[0]
456
+ title = second_anchor.contents[0]
457
+ author = third_anchor.contents[0]
366
458
 
367
459
  # Instantiating a project with the properties that we know
368
460
  # This may cause issues (see below)
@@ -383,10 +475,11 @@ class User(BaseSiteComponent):
383
475
  return _projects
384
476
 
385
477
  def loves_count(self):
386
- text = requests.get(
387
- f"https://scratch.mit.edu/projects/all/{self.username}/loves/",
388
- headers=self._headers
389
- ).text
478
+ with requests.no_error_handling():
479
+ text = requests.get(
480
+ f"https://scratch.mit.edu/projects/all/{self.username}/loves/",
481
+ headers=self._headers
482
+ ).text
390
483
 
391
484
  # If there are no loved projects, then Scratch doesn't actually display the number - so we have to catch this
392
485
  soup = BeautifulSoup(text, "html.parser")
@@ -407,10 +500,11 @@ class User(BaseSiteComponent):
407
500
  return commons.parse_object_list(_projects, project.Project, self._session)
408
501
 
409
502
  def favorites_count(self):
410
- text = requests.get(
411
- f"https://scratch.mit.edu/users/{self.username}/favorites/",
412
- headers = self._headers
413
- ).text
503
+ with requests.no_error_handling():
504
+ text = requests.get(
505
+ f"https://scratch.mit.edu/users/{self.username}/favorites/",
506
+ headers=self._headers
507
+ ).text
414
508
  return commons.webscrape_count(text, "Favorites (", ")")
415
509
 
416
510
  def toggle_commenting(self):
@@ -545,11 +639,11 @@ class User(BaseSiteComponent):
545
639
  data = {
546
640
  'id': text.split('<div id="comments-')[1].split('" class="comment')[0],
547
641
  'author': {"username": text.split('" data-comment-user="')[1].split('"><img class')[0]},
548
- 'content': text.split('<div class="content">')[1].split('"</div>')[0],
642
+ 'content': text.split('<div class="content">')[1].split('</div>')[0].strip(),
549
643
  'reply_count': 0,
550
644
  'cached_replies': []
551
645
  }
552
- _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)
646
+ _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, datetime = datetime.now())
553
647
  _comment._update_from_dict(data)
554
648
  return _comment
555
649
  except Exception:
@@ -560,7 +654,7 @@ class User(BaseSiteComponent):
560
654
  raw_error_data = text.split('<script id="error-data" type="application/json">')[1].split('</script>')[0]
561
655
  error_data = json.loads(raw_error_data)
562
656
  expires = error_data['mute_status']['muteExpiresAt']
563
- expires = datetime.utcfromtimestamp(expires)
657
+ expires = datetime.fromtimestamp(expires, timezone.utc)
564
658
  raise(exceptions.CommentPostFailure(f"You have been muted. Mute expires on {expires}"))
565
659
  else:
566
660
  raise(exceptions.FetchError(f"Couldn't parse API response: {r.text!r}"))
@@ -588,7 +682,8 @@ class User(BaseSiteComponent):
588
682
  Returns:
589
683
  list<scratchattach.Activity>: The user's activity data as parsed list of scratchattach.activity.Activity objects
590
684
  """
591
- soup = BeautifulSoup(requests.get(f"https://scratch.mit.edu/messages/ajax/user-activity/?user={self.username}&max={limit}").text, 'html.parser')
685
+ with requests.no_error_handling():
686
+ soup = BeautifulSoup(requests.get(f"https://scratch.mit.edu/messages/ajax/user-activity/?user={self.username}&max={limit}").text, 'html.parser')
592
687
 
593
688
  activities = []
594
689
  source = soup.find_all("li")
@@ -606,7 +701,8 @@ class User(BaseSiteComponent):
606
701
  Returns:
607
702
  str: The raw user activity HTML data
608
703
  """
609
- return requests.get(f"https://scratch.mit.edu/messages/ajax/user-activity/?user={self.username}&max={limit}").text
704
+ with requests.no_error_handling():
705
+ return requests.get(f"https://scratch.mit.edu/messages/ajax/user-activity/?user={self.username}&max={limit}").text
610
706
 
611
707
 
612
708
  def follow(self):
@@ -675,7 +771,8 @@ class User(BaseSiteComponent):
675
771
  URL = f"https://scratch.mit.edu/site-api/comments/user/{self.username}/?page={page}"
676
772
  DATA = []
677
773
 
678
- page_contents = requests.get(URL).content
774
+ with requests.no_error_handling():
775
+ page_contents = requests.get(URL).content
679
776
 
680
777
  soup = BeautifulSoup(page_contents, "html.parser")
681
778
 
@@ -820,6 +917,24 @@ class User(BaseSiteComponent):
820
917
  v = Verificator(self, verification_project_id)
821
918
  return v
822
919
 
920
+ def rank(self):
921
+ """
922
+ Finds the rank of the user.
923
+ May replace user.scratchteam and user.is_new_scratcher in the future.
924
+ """
925
+
926
+ if self.is_new_scratcher():
927
+ return Rank.NEW_SCRATCHER
928
+ # Is New Scratcher
929
+
930
+ if not self.scratchteam:
931
+ return Rank.SCRATCHER
932
+ # Is Scratcher
933
+
934
+ return Rank.SCRATCH_TEAM
935
+ # Is Scratch Team member
936
+
937
+
823
938
  # ------ #
824
939
 
825
940
  def get_user(username) -> User:
@@ -1,13 +1,17 @@
1
1
  """v2 ready: Common functions used by various internal modules"""
2
2
  from __future__ import annotations
3
3
 
4
+ import string
5
+
4
6
  from typing import Optional, Final, Any, TypeVar, Callable, TYPE_CHECKING, Union
7
+ from threading import Event as ManualResetEvent
5
8
  from threading import Lock
6
9
 
7
10
  from . import exceptions
8
- from .requests import Requests as requests
11
+ from .requests import requests
12
+
13
+ from scratchattach.site import _base
9
14
 
10
- from ..site import _base
11
15
 
12
16
  headers: Final = {
13
17
  "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
130
134
  # Internal function: Generalization of the process ran by get_user, get_studio etc.
131
135
  # Builds an object of class that is inheriting from BaseSiteComponent
132
136
  # # Class must inherit from BaseSiteComponent
133
- from ..site import project
137
+ from scratchattach.site import project
134
138
  try:
135
139
  use_class: type = __class
136
140
  if __class is project.PartialProject:
@@ -182,44 +186,41 @@ class LockEvent:
182
186
  """
183
187
  Can be waited on and triggered. Not to be confused with threading.Event, which has to be reset.
184
188
  """
185
- locks: list[Lock]
189
+ _event: ManualResetEvent
190
+ _locks: list[Lock]
191
+ _access_locks: Lock
186
192
  def __init__(self):
187
- self.locks = []
188
- self.use_locks = Lock()
193
+ self._event = ManualResetEvent()
194
+ self._locks = []
195
+ self._access_locks = Lock()
189
196
 
190
197
  def wait(self, blocking: bool = True, timeout: Optional[Union[int, float]] = None) -> bool:
191
198
  """
192
199
  Wait for the event.
193
200
  """
194
- timeout = -1 if timeout is None else timeout
195
- if not blocking:
196
- timeout = 0
197
- return self.on().acquire(timeout=timeout)
201
+ return self._event.wait(timeout if blocking else 0)
198
202
 
199
203
  def trigger(self):
200
204
  """
201
205
  Trigger all threads waiting on this event to continue.
202
206
  """
203
- with self.use_locks:
204
- for lock in self.locks:
207
+ with self._access_locks:
208
+ for lock in self._locks:
205
209
  try:
206
- lock.release() # Unlock the lock once to trigger the event.
210
+ lock.release()
207
211
  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.
212
+ pass
213
+ self._locks.clear()
214
+ self._event.set()
215
+ self._event = ManualResetEvent()
215
216
 
216
217
  def on(self) -> Lock:
217
218
  """
218
- Return a lock that will unlock once the event takes place.
219
+ Return a lock that will unlock once the event takes place. Return value has to be waited on to wait for the event.
219
220
  """
220
221
  lock = Lock()
221
- with self.use_locks:
222
- self.locks.append(lock)
222
+ with self._access_locks:
223
+ self._locks.append(lock)
223
224
  lock.acquire(timeout=0)
224
225
  return lock
225
226
 
@@ -241,3 +242,14 @@ def get_class_sort_mode(mode: str) -> tuple[str, str]:
241
242
  descsort = "title"
242
243
 
243
244
  return ascsort, descsort
245
+
246
+
247
+ def b62_decode(s: str):
248
+ chars = string.digits + string.ascii_uppercase + string.ascii_lowercase
249
+
250
+ ret = 0
251
+ for char in s:
252
+ ret = ret * 62 + chars.index(char)
253
+
254
+ return ret
255
+
@@ -4,13 +4,12 @@ Adapted from https://translate-service.scratch.mit.edu/supported?language=en
4
4
  """
5
5
  from __future__ import annotations
6
6
 
7
- from enum import Enum
8
7
  from dataclasses import dataclass
9
-
8
+ from enum import Enum
10
9
  from typing import Optional, Callable, Iterable
11
10
 
12
11
 
13
- @dataclass(init=True, repr=True)
12
+ @dataclass
14
13
  class Language:
15
14
  name: str = None
16
15
  code: str = None
@@ -44,7 +43,7 @@ class _EnumWrapper(Enum):
44
43
 
45
44
  if apply_func(_val) == value:
46
45
  return item_obj
47
-
46
+
48
47
  except TypeError:
49
48
  pass
50
49
 
@@ -167,7 +166,7 @@ class Languages(_EnumWrapper):
167
166
  return super().all_of(attr_name, apply_func)
168
167
 
169
168
 
170
- @dataclass(init=True, repr=True)
169
+ @dataclass
171
170
  class TTSVoice:
172
171
  name: str
173
172
  gender: str
@@ -195,3 +194,43 @@ class TTSVoices(_EnumWrapper):
195
194
  def all_of(cls, attr_name: str = "name", apply_func: Optional[Callable] = None) -> Iterable:
196
195
  return super().all_of(attr_name, apply_func)
197
196
 
197
+
198
+ @dataclass
199
+ class AlertType:
200
+ id: int
201
+ message: str
202
+
203
+
204
+ class AlertTypes(_EnumWrapper):
205
+ """
206
+ Enum for associating alert type indecies with their messages, for use with the str.format() method.
207
+ """
208
+ # Reference: https://github.com/TimMcCool/scratchattach/issues/304#issuecomment-2800110811
209
+ # NOTE: THE TEXT WITHIN THE BRACES HERE MATTERS! IF YOU WANT TO CHANGE IT, MAKE SURE TO EDIT `site.alert.EducatorAlert`!
210
+ ban = AlertType(0, "{username} was banned.")
211
+ unban = AlertType(1, "{username} was unbanned.")
212
+ excluded_from_homepage = AlertType(2, "{username} was excluded from homepage")
213
+ excluded_from_homepage2 = AlertType(3, "{username} was excluded from homepage") # for some reason there are duplicates
214
+ notified = AlertType(4, "{username} was notified by a Scratch Administrator. Notification type: {notification_type}") # not sure what notification type is
215
+ autoban = AlertType(5, "{username} was banned automatically")
216
+ autoremoved = AlertType(6, "{project} by {username} was removed automatically")
217
+ project_censored2 = AlertType(7, "{project} by {username} was censored.") # <empty #7>
218
+ project_censored = AlertType(20, "{project} by {username} was censored.")
219
+ project_uncensored = AlertType(8, "{project} by {username} was uncensored.")
220
+ project_reviewed2 = AlertType(9, "{project} by {username} was reviewed by a Scratch Administrator.") # <empty #9>
221
+ project_reviewed = AlertType(10, "{project} by {username} was reviewed by a Scratch Administrator.")
222
+ project_deleted = AlertType(11, "{project} by {username} was deleted by a Scratch Administrator.")
223
+ user_deleted2 = AlertType(12, "{username} was deleted by a Scratch Administrator") # <empty #12>
224
+ user_deleted = AlertType(17, "{username} was deleted by a Scratch Administrator")
225
+ studio_reviewed2 = AlertType(13, "{studio} was reviewed by a Scratch Administrator.") # <empty #13>
226
+ studio_reviewed = AlertType(14, "{studio} was reviewed by a Scratch Administrator.")
227
+ studio_deleted = AlertType(15, "{studio} was deleted by a Scratch Administrator.")
228
+ email_confirm2 = AlertType(16, "The email address of {username} was confirmed by a Scratch Administrator") # <empty #16>
229
+ email_confirm = AlertType(18, "The email address of {username} was confirmed by a Scratch Administrator") # no '.' in HTML
230
+ email_unconfirm = AlertType(19, "The email address of {username} was set as not confirmed by a Scratch Administrator")
231
+ automute = AlertType(22, "{username} was automatically muted by our comment filters. The comment they tried to post was: {comment}")
232
+ default = AlertType(-1, "{username} had an admin action performed.") # default case
233
+
234
+ @classmethod
235
+ def find(cls, value, by: str = "id", apply_func: Optional[Callable] = None) -> AlertType:
236
+ return super().find(value, by, apply_func)
@@ -231,3 +231,13 @@ class LoginDataWarning(UserWarning):
231
231
  """
232
232
  Warns you not to accidentally share your login data.
233
233
  """
234
+
235
+ class AnonymousSiteComponentWarning(UserWarning):
236
+ """
237
+ Warns about a usage of an anonymous site component.
238
+ """
239
+
240
+ class UnexpectedWebsocketEventWarning(RuntimeWarning):
241
+ """
242
+ Warns about an unexpected occurrence with a websocket.
243
+ """