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
utils/__init__.py ADDED
File without changes
utils/commons.py ADDED
@@ -0,0 +1,263 @@
1
+ """v2 ready: Common functions used by various internal modules"""
2
+ from __future__ import annotations
3
+
4
+ import string
5
+
6
+ from typing import Optional, Final, Any, TypeVar, Callable, TYPE_CHECKING, Union, overload
7
+ from threading import Event as ManualResetEvent
8
+ from threading import Lock
9
+
10
+ from . import exceptions
11
+ from .requests import requests
12
+
13
+ from scratchattach.site import _base
14
+
15
+
16
+ headers: Final = {
17
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
18
+ "(KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
19
+ "x-csrftoken": "a",
20
+ "x-requested-with": "XMLHttpRequest",
21
+ "referer": "https://scratch.mit.edu",
22
+ }
23
+ empty_project_json: Final = {
24
+ 'targets': [
25
+ {
26
+ 'isStage': True,
27
+ 'name': 'Stage',
28
+ 'variables': {
29
+ '`jEk@4|i[#Fk?(8x)AV.-my variable': [
30
+ 'my variable',
31
+ 0,
32
+ ],
33
+ },
34
+ 'lists': {},
35
+ 'broadcasts': {},
36
+ 'blocks': {},
37
+ 'comments': {},
38
+ 'currentCostume': 0,
39
+ 'costumes': [
40
+ {
41
+ 'name': '',
42
+ 'bitmapResolution': 1,
43
+ 'dataFormat': 'svg',
44
+ 'assetId': '14e46ec3e2ba471c2adfe8f119052307',
45
+ 'md5ext': '14e46ec3e2ba471c2adfe8f119052307.svg',
46
+ 'rotationCenterX': 0,
47
+ 'rotationCenterY': 0,
48
+ },
49
+ ],
50
+ 'sounds': [],
51
+ 'volume': 100,
52
+ 'layerOrder': 0,
53
+ 'tempo': 60,
54
+ 'videoTransparency': 50,
55
+ 'videoState': 'on',
56
+ 'textToSpeechLanguage': None,
57
+ },
58
+ ],
59
+ 'monitors': [],
60
+ 'extensions': [],
61
+ 'meta': {
62
+ 'semver': '3.0.0',
63
+ 'vm': '2.3.0',
64
+ 'agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
65
+ 'Chrome/124.0.0.0 Safari/537.36',
66
+ },
67
+ }
68
+
69
+
70
+ def api_iterative_data(fetch_func: Callable[[int, int], list], limit: int, offset: int, max_req_limit: int = 40,
71
+ unpack: bool = True) -> list:
72
+ """
73
+ Iteratively gets data by calling fetch_func with a moving offset and a limit.
74
+ Once fetch_func returns None, the retrieval is completed.
75
+ """
76
+ if limit is None:
77
+ limit = max_req_limit
78
+
79
+ end = offset + limit
80
+ api_data = []
81
+ for offs in range(offset, end, max_req_limit):
82
+ # Mimic actual scratch by only requesting the max amount
83
+ data = fetch_func(offs, max_req_limit)
84
+ if data is None:
85
+ break
86
+
87
+ if unpack:
88
+ api_data.extend(data)
89
+ else:
90
+ api_data.append(data)
91
+
92
+ if len(data) < max_req_limit:
93
+ break
94
+
95
+ api_data = api_data[:limit]
96
+ return api_data
97
+
98
+
99
+ def api_iterative(url: str, *, limit: int, offset: int, max_req_limit: int = 40, add_params: str = "",
100
+ _headers: Optional[dict] = None, cookies: Optional[dict] = None):
101
+ """
102
+ Function for getting data from one of Scratch's iterative JSON API endpoints (like /users/<user>/followers, or /users/<user>/projects)
103
+ """
104
+ if _headers is None:
105
+ _headers = headers.copy()
106
+ if cookies is None:
107
+ cookies = {}
108
+
109
+ if offset < 0:
110
+ raise exceptions.BadRequest("offset parameter must be >= 0")
111
+ if limit < 0:
112
+ raise exceptions.BadRequest("limit parameter must be >= 0")
113
+
114
+ def fetch(off: int, lim: int):
115
+ """
116
+ Performs a single API request
117
+ """
118
+ resp = requests.get(
119
+ f"{url}?limit={lim}&offset={off}{add_params}", headers=_headers, cookies=cookies, timeout=10
120
+ ).json()
121
+
122
+ if not resp:
123
+ return None
124
+ if resp == {"code": "BadRequest", "message": ""}:
125
+ raise exceptions.BadRequest("The passed arguments are invalid")
126
+ return resp
127
+
128
+ api_data = api_iterative_data(
129
+ fetch, limit, offset, max_req_limit=max_req_limit, unpack=True
130
+ )
131
+ return api_data
132
+
133
+ def _get_object(identificator_name, identificator, __class: type[C], NotFoundException, session=None) -> C:
134
+ # Internal function: Generalization of the process ran by get_user, get_studio etc.
135
+ # Builds an object of class that is inheriting from BaseSiteComponent
136
+ # # Class must inherit from BaseSiteComponent
137
+ from scratchattach.site import project
138
+ try:
139
+ use_class: type = __class
140
+ if __class is project.PartialProject:
141
+ use_class = project.Project
142
+ assert issubclass(use_class, __class)
143
+ _object = use_class(**{identificator_name: identificator, "_session": session})
144
+ r = _object.update()
145
+ if r == "429":
146
+ raise exceptions.Response429(
147
+ "Your network is blocked or rate-limited by Scratch.\n"
148
+ "If you're using an online IDE like replit.com, try running the code on your computer.")
149
+ if not r:
150
+ # Target is unshared. The cases that this can happen in are hardcoded:
151
+ if __class is project.PartialProject: # Case: Target is an unshared project.
152
+ _object = project.PartialProject(**{identificator_name: identificator,
153
+ "shared": False, "_session": session})
154
+ assert isinstance(_object, __class)
155
+ return _object
156
+ else:
157
+ raise NotFoundException
158
+ else:
159
+ return _object
160
+ except KeyError as e:
161
+ raise NotFoundException(f"Key error at key {e} when reading API response")
162
+ except Exception as e:
163
+ raise e
164
+
165
+ I = TypeVar("I")
166
+ @overload
167
+ def webscrape_count(raw: str, text_before: str, text_after: str, cls: type[I]) -> I:
168
+ pass
169
+
170
+ @overload
171
+ def webscrape_count(raw: str, text_before: str, text_after: str) -> int:
172
+ pass
173
+
174
+ def webscrape_count(raw, text_before, text_after, cls = int):
175
+ return cls(raw.split(text_before)[1].split(text_after)[0])
176
+
177
+
178
+ if TYPE_CHECKING:
179
+ C = TypeVar("C", bound=_base.BaseSiteComponent)
180
+
181
+ def parse_object_list(raw, /, __class: type[C], session=None, primary_key="id") -> list[C]:
182
+ results = []
183
+ for raw_dict in raw:
184
+ try:
185
+ _obj = __class(**{primary_key: raw_dict[primary_key], "_session": session})
186
+ # noinspection PyProtectedMember
187
+ _obj._update_from_dict(raw_dict)
188
+ results.append(_obj)
189
+ except Exception as e:
190
+ print("Warning raised by scratchattach: failed to parse ", raw_dict, "error", e)
191
+ return results
192
+
193
+
194
+ class LockEvent:
195
+ """
196
+ Can be waited on and triggered. Not to be confused with threading.Event, which has to be reset.
197
+ """
198
+ _event: ManualResetEvent
199
+ _locks: list[Lock]
200
+ _access_locks: Lock
201
+ def __init__(self):
202
+ self._event = ManualResetEvent()
203
+ self._locks = []
204
+ self._access_locks = Lock()
205
+
206
+ def wait(self, blocking: bool = True, timeout: Optional[Union[int, float]] = None) -> bool:
207
+ """
208
+ Wait for the event.
209
+ """
210
+ return self._event.wait(timeout if blocking else 0)
211
+
212
+ def trigger(self):
213
+ """
214
+ Trigger all threads waiting on this event to continue.
215
+ """
216
+ with self._access_locks:
217
+ for lock in self._locks:
218
+ try:
219
+ lock.release()
220
+ except RuntimeError:
221
+ pass
222
+ self._locks.clear()
223
+ self._event.set()
224
+ self._event = ManualResetEvent()
225
+
226
+ def on(self) -> Lock:
227
+ """
228
+ Return a lock that will unlock once the event takes place. Return value has to be waited on to wait for the event.
229
+ """
230
+ lock = Lock()
231
+ with self._access_locks:
232
+ self._locks.append(lock)
233
+ lock.acquire(timeout=0)
234
+ return lock
235
+
236
+ def get_class_sort_mode(mode: str) -> tuple[str, str]:
237
+ """
238
+ Returns the sort mode for the given mode for classes only
239
+ """
240
+ ascsort = ''
241
+ descsort = ''
242
+
243
+ mode = mode.lower()
244
+ if mode == "last created":
245
+ pass
246
+ elif mode == "students":
247
+ descsort = "student_count"
248
+ elif mode == "a-z":
249
+ ascsort = "title"
250
+ elif mode == "z-a":
251
+ descsort = "title"
252
+
253
+ return ascsort, descsort
254
+
255
+
256
+ def b62_decode(s: str):
257
+ chars = string.digits + string.ascii_uppercase + string.ascii_lowercase
258
+
259
+ ret = 0
260
+ for char in s:
261
+ ret = ret * 62 + chars.index(char)
262
+
263
+ return ret
utils/encoder.py ADDED
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+ import math
3
+ from . import exceptions
4
+
5
+ letters = [
6
+ None,
7
+ None,
8
+ None,
9
+ None,
10
+ None,
11
+ None,
12
+ None,
13
+ None,
14
+ None,
15
+ None,
16
+ "1",
17
+ "2",
18
+ "3",
19
+ "4",
20
+ "5",
21
+ "6",
22
+ "7",
23
+ "8",
24
+ "9",
25
+ "0",
26
+ " ",
27
+ "a",
28
+ "A",
29
+ "b",
30
+ "B",
31
+ "c",
32
+ "C",
33
+ "d",
34
+ "D",
35
+ "e",
36
+ "E",
37
+ "f",
38
+ "F",
39
+ "g",
40
+ "G",
41
+ "h",
42
+ "H",
43
+ "i",
44
+ "I",
45
+ "j",
46
+ "J",
47
+ "k",
48
+ "K",
49
+ "l",
50
+ "L",
51
+ "m",
52
+ "M",
53
+ "n",
54
+ "N",
55
+ "o",
56
+ "O",
57
+ "p",
58
+ "P",
59
+ "q",
60
+ "Q",
61
+ "r",
62
+ "R",
63
+ "s",
64
+ "S",
65
+ "t",
66
+ "T",
67
+ "u",
68
+ "U",
69
+ "v",
70
+ "V",
71
+ "w",
72
+ "W",
73
+ "x",
74
+ "X",
75
+ "y",
76
+ "Y",
77
+ "z",
78
+ "Z",
79
+ "*",
80
+ "/",
81
+ ".",
82
+ ",",
83
+ "!",
84
+ '"',
85
+ "§",
86
+ "$",
87
+ "%",
88
+ "_",
89
+ "-",
90
+ "(",
91
+ "´",
92
+ ")",
93
+ "`",
94
+ "?",
95
+ "new line",
96
+ "@",
97
+ "#",
98
+ "~",
99
+ ";",
100
+ ":",
101
+ "+",
102
+ "&",
103
+ "|",
104
+ "^",
105
+ "'"
106
+ ]
107
+
108
+
109
+ class Encoding:
110
+ """
111
+ Class that contains tools for encoding / decoding strings. The strings encoded / decoded with these functions can be decoded / encoded with Scratch using this sprite: https://scratch3-assets.1tim.repl.co/Encoder.sprite3
112
+ """
113
+ @staticmethod
114
+ def decode(inp):
115
+ """
116
+ Args:
117
+ inp (str): The encoded input.
118
+
119
+ Returns:
120
+ str: The decoded output.
121
+ """
122
+ try:
123
+ inp = str(inp)
124
+ except Exception:
125
+ raise exceptions.InvalidDecodeInput
126
+
127
+ outp = ""
128
+ # print(f"Encoding.decode({inp=})")
129
+ # This loops through a string like 'abCDefGHijKLmnOP' like so: l1+l2=ab, CD, ef, GH, etc.
130
+ for l1, l2 in zip(inp[::2], inp[1::2]):
131
+ outp += letters[int(l1 + l2)]
132
+
133
+ return outp
134
+
135
+ @staticmethod
136
+ def encode(inp):
137
+ """
138
+ Args:
139
+ inp (str): The decoded input.
140
+
141
+ Returns:
142
+ str: The encoded output.
143
+ """
144
+ inp = str(inp)
145
+ outp = ""
146
+ for i in inp:
147
+ if i in letters:
148
+ outp = f"{outp}{letters.index(i)}"
149
+ else:
150
+ outp += str(letters.index(" "))
151
+ return outp
152
+
153
+ @staticmethod
154
+ def replace_char(old_char, new_char):
155
+ """
156
+ Replaces a character in the list that the encoder uses to encode / decode values.
157
+ You can access this list using `scratchattach.encoder.letters`
158
+ """
159
+ i = letters.index(old_char)
160
+ letters[i] = new_char
161
+
utils/enums.py ADDED
@@ -0,0 +1,237 @@
1
+ """
2
+ List of supported languages of scratch's translate and text2speech extensions.
3
+ Adapted from https://translate-service.scratch.mit.edu/supported?language=en
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from typing import Optional, Callable, Iterable
10
+
11
+
12
+ @dataclass
13
+ class Language:
14
+ name: str = None
15
+ code: str = None
16
+ locales: list[str] = None
17
+ tts_locale: str = None
18
+ single_gender: bool = None
19
+
20
+
21
+ # should this underscore be removed
22
+ class _EnumWrapper(Enum):
23
+ @classmethod
24
+ def find(cls, value, by: str, apply_func: Optional[Callable] = None):
25
+ """
26
+ Finds the enum item with the given attribute that is equal to the given value.
27
+ the apply_func will be applied to the attribute of each language object before comparison.
28
+
29
+ i.e. Languages.find("ukranian", "name", str.lower) will return the Ukrainian language dataclass object
30
+ (even though Ukrainian was spelt lowercase, since str.lower will convert the "Ukrainian" string to lowercase)
31
+ """
32
+ if apply_func is None:
33
+ def apply_func(x):
34
+ return x
35
+
36
+ for item in cls:
37
+ item_obj = item.value
38
+
39
+ try:
40
+ if by is None:
41
+ _val = item_obj
42
+ else:
43
+ _val = getattr(item_obj, by)
44
+
45
+ if apply_func(_val) == value:
46
+ return item_obj
47
+
48
+ except TypeError:
49
+ pass
50
+
51
+ @classmethod
52
+ def all_of(cls, attr_name: str, apply_func: Optional[Callable] = None) -> Iterable:
53
+ """
54
+ Returns the list of each listed enum item's specified attribute by "attr_name"
55
+
56
+ i.e. Languages.all_of("name") will return a list of names:
57
+ ["Albanian", "Amharic", ...]
58
+
59
+ The apply_func function will be applied to every list item,
60
+ i.e. Languages.all_of("name", str.lower) will return the same except in lowercase:
61
+ ["albanian", "amharic", ...]
62
+ """
63
+ if apply_func is None:
64
+ def apply_func(x):
65
+ return x
66
+
67
+ for item in cls:
68
+ item_obj = item.value
69
+ attr = getattr(item_obj, attr_name)
70
+ try:
71
+ yield apply_func(attr)
72
+
73
+ except TypeError:
74
+ yield attr
75
+
76
+ @classmethod
77
+ def find_by_attrs(cls, value, bys: list[str], apply_func: Optional[Callable] = None) -> list:
78
+ """
79
+ Calls the EnumWrapper.by function multiple times until a match is found, using the provided 'by' attribute names
80
+ """
81
+ for by in bys:
82
+ ret = cls.find(value, by, apply_func)
83
+ if ret is not None:
84
+ return ret
85
+
86
+
87
+ class Languages(_EnumWrapper):
88
+ Albanian = Language('Albanian', 'sq', None, None, None)
89
+ Amharic = Language('Amharic', 'am', None, None, None)
90
+ Arabic = Language('Arabic', 'ar', ['ar'], 'arb', True)
91
+ Armenian = Language('Armenian', 'hy', None, None, None)
92
+ Azerbaijani = Language('Azerbaijani', 'az', None, None, None)
93
+ Basque = Language('Basque', 'eu', None, None, None)
94
+ Belarusian = Language('Belarusian', 'be', None, None, None)
95
+ Bulgarian = Language('Bulgarian', 'bg', None, None, None)
96
+ Catalan = Language('Catalan', 'ca', None, None, None)
97
+ Chinese_Traditional = Language('Chinese (Traditional)', 'zh-tw', ['zh-cn', 'zh-tw'], 'cmn-CN', True)
98
+ Croatian = Language('Croatian', 'hr', None, None, None)
99
+ Czech = Language('Czech', 'cs', None, None, None)
100
+ Danish = Language('Danish', 'da', ['da'], 'da-DK', False)
101
+ Dutch = Language('Dutch', 'nl', ['nl'], 'nl-NL', False)
102
+ English = Language('English', 'en', ['en'], 'en-US', False)
103
+ Esperanto = Language('Esperanto', 'eo', None, None, None)
104
+ Estonian = Language('Estonian', 'et', None, None, None)
105
+ Finnish = Language('Finnish', 'fi', None, None, None)
106
+ French = Language('French', 'fr', ['fr'], 'fr-FR', False)
107
+ Galician = Language('Galician', 'gl', None, None, None)
108
+ German = Language('German', 'de', ['de'], 'de-DE', False)
109
+ Greek = Language('Greek', 'el', None, None, None)
110
+ Haitian_Creole = Language('Haitian Creole', 'ht', None, None, None)
111
+ Hindi = Language('Hindi', 'hi', ['hi'], 'hi-IN', True)
112
+ Hungarian = Language('Hungarian', 'hu', None, None, None)
113
+ Icelandic = Language('Icelandic', 'is', ['is'], 'is-IS', False)
114
+ Indonesian = Language('Indonesian', 'id', None, None, None)
115
+ Irish = Language('Irish', 'ga', None, None, None)
116
+ Italian = Language('Italian', 'it', ['it'], 'it-IT', False)
117
+ Japanese = Language('Japanese', 'ja', ['ja', 'ja-hira'], 'ja-JP', False)
118
+ Kannada = Language('Kannada', 'kn', None, None, None)
119
+ Korean = Language('Korean', 'ko', ['ko'], 'ko-KR', True)
120
+ Kurdish_Kurmanji = Language('Kurdish (Kurmanji)', 'ku', None, None, None)
121
+ Latin = Language('Latin', 'la', None, None, None)
122
+ Latvian = Language('Latvian', 'lv', None, None, None)
123
+ Lithuanian = Language('Lithuanian', 'lt', None, None, None)
124
+ Macedonian = Language('Macedonian', 'mk', None, None, None)
125
+ Malay = Language('Malay', 'ms', None, None, None)
126
+ Malayalam = Language('Malayalam', 'ml', None, None, None)
127
+ Maltese = Language('Maltese', 'mt', None, None, None)
128
+ Maori = Language('Maori', 'mi', None, None, None)
129
+ Marathi = Language('Marathi', 'mr', None, None, None)
130
+ Mongolian = Language('Mongolian', 'mn', None, None, None)
131
+ Myanmar_Burmese = Language('Myanmar (Burmese)', 'my', None, None, None)
132
+ Persian = Language('Persian', 'fa', None, None, None)
133
+ Polish = Language('Polish', 'pl', ['pl'], 'pl-PL', False)
134
+ Portuguese = Language('Portuguese', 'pt', ['pt'], 'pt-PT', False)
135
+ Romanian = Language('Romanian', 'ro', ['ro'], 'ro-RO', True)
136
+ Russian = Language('Russian', 'ru', ['ru'], 'ru-RU', False)
137
+ Scots_Gaelic = Language('Scots Gaelic', 'gd', None, None, None)
138
+ Serbian = Language('Serbian', 'sr', None, None, None)
139
+ Slovak = Language('Slovak', 'sk', None, None, None)
140
+ Slovenian = Language('Slovenian', 'sl', None, None, None)
141
+ Spanish = Language('Spanish', 'es', None, None, None)
142
+ Swedish = Language('Swedish', 'sv', ['sv'], 'sv-SE', True)
143
+ Telugu = Language('Telugu', 'te', None, None, None)
144
+ Thai = Language('Thai', 'th', None, None, None)
145
+ Turkish = Language('Turkish', 'tr', ['tr'], 'tr-TR', True)
146
+ Ukrainian = Language('Ukrainian', 'uk', None, None, None)
147
+ Uzbek = Language('Uzbek', 'uz', None, None, None)
148
+ Vietnamese = Language('Vietnamese', 'vi', None, None, None)
149
+ Welsh = Language('Welsh', 'cy', ['cy'], 'cy-GB', True)
150
+ Zulu = Language('Zulu', 'zu', None, None, None)
151
+ Hebrew = Language('Hebrew', 'he', None, None, None)
152
+ Chinese_Simplified = Language('Chinese (Simplified)', 'zh-cn', ['zh-cn', 'zh-tw'], 'cmn-CN', True)
153
+ Mandarin = Chinese_Simplified
154
+
155
+ nb_NO = Language(None, None, ['nb', 'nn'], 'nb-NO', True)
156
+ pt_BR = Language(None, None, ['pt-br'], 'pt-BR', False)
157
+ Brazilian = pt_BR
158
+ es_ES = Language(None, None, ['es'], 'es-ES', False)
159
+ es_US = Language(None, None, ['es-419'], 'es-US', False)
160
+
161
+ @classmethod
162
+ def find(cls, value, by: str = "name", apply_func: Optional[Callable] = None) -> Language:
163
+ return super().find(value, by, apply_func)
164
+
165
+ @classmethod
166
+ def all_of(cls, attr_name: str = "name", apply_func: Optional[Callable] = None) -> list:
167
+ return super().all_of(attr_name, apply_func)
168
+
169
+
170
+ @dataclass
171
+ class TTSVoice:
172
+ name: str
173
+ gender: str
174
+ playback_rate: float | int = 1
175
+
176
+
177
+ class TTSVoices(_EnumWrapper):
178
+ alto = TTSVoice("alto", "female")
179
+ # female is functionally equal to alto
180
+ female = TTSVoice("female", "female")
181
+
182
+ tenor = TTSVoice("tenor", "male")
183
+ # male is functionally equal to tenor
184
+ male = TTSVoice("male", "male")
185
+
186
+ squeak = TTSVoice("squeak", "female", 1.19)
187
+ giant = TTSVoice("giant", "male", .84)
188
+ kitten = TTSVoice("kitten", "female", 1.41)
189
+
190
+ @classmethod
191
+ def find(cls, value, by: str = "name", apply_func: Optional[Callable] = None) -> TTSVoice:
192
+ return super().find(value, by, apply_func)
193
+
194
+ @classmethod
195
+ def all_of(cls, attr_name: str = "name", apply_func: Optional[Callable] = None) -> Iterable:
196
+ return super().all_of(attr_name, apply_func)
197
+
198
+
199
+ @dataclass
200
+ class AlertType:
201
+ id: int
202
+ message: str
203
+
204
+
205
+ class AlertTypes(_EnumWrapper):
206
+ """
207
+ Enum for associating alert type indecies with their messages, for use with the str.format() method.
208
+ """
209
+ # Reference: https://github.com/TimMcCool/scratchattach/issues/304#issuecomment-2800110811
210
+ # NOTE: THE TEXT WITHIN THE BRACES HERE MATTERS! IF YOU WANT TO CHANGE IT, MAKE SURE TO EDIT `site.alert.EducatorAlert`!
211
+ ban = AlertType(0, "{username} was banned.")
212
+ unban = AlertType(1, "{username} was unbanned.")
213
+ excluded_from_homepage = AlertType(2, "{username} was excluded from homepage")
214
+ excluded_from_homepage2 = AlertType(3, "{username} was excluded from homepage") # for some reason there are duplicates
215
+ notified = AlertType(4, "{username} was notified by a Scratch Administrator. Notification type: {notification_type}") # not sure what notification type is
216
+ autoban = AlertType(5, "{username} was banned automatically")
217
+ autoremoved = AlertType(6, "{project} by {username} was removed automatically")
218
+ project_censored2 = AlertType(7, "{project} by {username} was censored.") # <empty #7>
219
+ project_censored = AlertType(20, "{project} by {username} was censored.")
220
+ project_uncensored = AlertType(8, "{project} by {username} was uncensored.")
221
+ project_reviewed2 = AlertType(9, "{project} by {username} was reviewed by a Scratch Administrator.") # <empty #9>
222
+ project_reviewed = AlertType(10, "{project} by {username} was reviewed by a Scratch Administrator.")
223
+ project_deleted = AlertType(11, "{project} by {username} was deleted by a Scratch Administrator.")
224
+ user_deleted2 = AlertType(12, "{username} was deleted by a Scratch Administrator") # <empty #12>
225
+ user_deleted = AlertType(17, "{username} was deleted by a Scratch Administrator")
226
+ studio_reviewed2 = AlertType(13, "{studio} was reviewed by a Scratch Administrator.") # <empty #13>
227
+ studio_reviewed = AlertType(14, "{studio} was reviewed by a Scratch Administrator.")
228
+ studio_deleted = AlertType(15, "{studio} was deleted by a Scratch Administrator.")
229
+ email_confirm2 = AlertType(16, "The email address of {username} was confirmed by a Scratch Administrator") # <empty #16>
230
+ email_confirm = AlertType(18, "The email address of {username} was confirmed by a Scratch Administrator") # no '.' in HTML
231
+ email_unconfirm = AlertType(19, "The email address of {username} was set as not confirmed by a Scratch Administrator")
232
+ automute = AlertType(22, "{username} was automatically muted by our comment filters. The comment they tried to post was: {comment}")
233
+ default = AlertType(-1, "{username} had an admin action performed.") # default case
234
+
235
+ @classmethod
236
+ def find(cls, value, by: str = "id", apply_func: Optional[Callable] = None) -> AlertType:
237
+ return super().find(value, by, apply_func)