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,142 +1,430 @@
1
- import datetime
2
- import requests
3
- from . import user, session
4
- from ..utils.commons import api_iterative, headers
5
- from ..utils import exceptions, commons
6
- from ._base import BaseSiteComponent
7
-
8
- class Classroom(BaseSiteComponent):
9
- def __init__(self, **entries):
10
- # Info on how the .update method has to fetch the data:
11
- self.update_function = requests.get
12
- if "id" in entries:
13
- self.update_API = f"https://api.scratch.mit.edu/classrooms/{entries['id']}"
14
- elif "classtoken" in entries:
15
- self.update_API = f"https://api.scratch.mit.edu/classtoken/{entries['classtoken']}"
16
- else:
17
- raise KeyError
18
-
19
- # Set attributes every Project object needs to have:
20
- self._session = None
21
- self.id = None
22
- self.classtoken = None
23
-
24
- self.__dict__.update(entries)
25
-
26
- # Headers and cookies:
27
- if self._session is None:
28
- self._headers = headers
29
- self._cookies = {}
30
- else:
31
- self._headers = self._session._headers
32
- self._cookies = self._session._cookies
33
-
34
- # Headers for operations that require accept and Content-Type fields:
35
- self._json_headers = dict(self._headers)
36
- self._json_headers["accept"] = "application/json"
37
- self._json_headers["Content-Type"] = "application/json"
38
-
39
- def _update_from_dict(self, classrooms):
40
- try: self.id = int(classrooms["id"])
41
- except Exception: pass
42
- try: self.title = classrooms["title"]
43
- except Exception: pass
44
- try: self.about_class = classrooms["description"]
45
- except Exception: pass
46
- try: self.working_on = classrooms["status"]
47
- except Exception: pass
48
- try: self.datetime = datetime.datetime.fromisoformat(classrooms["date_start"])
49
- except Exception: pass
50
- try: self.author = user.User(username=classrooms["educator"]["username"],_session=self._session)
51
- except Exception: pass
52
- try: self.author._update_from_dict(classrooms["educator"])
53
- except Exception: pass
54
- return True
55
-
56
- def student_count(self):
57
- # student count
58
- text = requests.get(
59
- f"https://scratch.mit.edu/classes/{self.id}/",
60
- headers = self._headers
61
- ).text
62
- return commons.webscrape_count(text, "Students (", ")")
63
-
64
- def student_names(self, *, page=1):
65
- """
66
- Returns the student on the class.
67
-
68
- Keyword Arguments:
69
- page: The page of the students that should be returned.
70
-
71
- Returns:
72
- list<str>: The usernames of the class students
73
- """
74
- text = requests.get(
75
- f"https://scratch.mit.edu/classes/{self.id}/students/?page={page}",
76
- headers = self._headers
77
- ).text
78
- textlist = [i.split('/">')[0] for i in text.split(' <a href="/users/')[1:]]
79
- return textlist
80
-
81
- def class_studio_count(self):
82
- # student count
83
- text = requests.get(
84
- f"https://scratch.mit.edu/classes/{self.id}/",
85
- headers = self._headers
86
- ).text
87
- return commons.webscrape_count(text, "Class Studios (", ")")
88
-
89
- def class_studio_ids(self, *, page=1):
90
- """
91
- Returns the class studio on the class.
92
-
93
- Keyword Arguments:
94
- page: The page of the students that should be returned.
95
-
96
- Returns:
97
- list<str>: The id of the class studios
98
- """
99
- text = requests.get(
100
- f"https://scratch.mit.edu/classes/{self.id}/studios/?page={page}",
101
- headers = self._headers
102
- ).text
103
- textlist = [int(i.split('/">')[0]) for i in text.split('<span class="title">\n <a href="/studios/')[1:]]
104
- return textlist
105
-
106
-
107
-
108
- def get_classroom(class_id) -> Classroom:
109
- """
110
- Gets a class without logging in.
111
-
112
- Args:
113
- class_id (str): class id of the requested class
114
-
115
- Returns:
116
- scratchattach.classroom.Classroom: An object representing the requested classroom
117
-
118
- Warning:
119
- Any methods that require authentication will not work on the returned object.
120
-
121
- If you want to use these, get the user with :meth:`scratchattach.session.Session.connect_classroom` instead.
122
- """
123
- print("Warning: For methods that require authentication, use session.connect_classroom instead of get_classroom")
124
- return commons._get_object("id", class_id, Classroom, exceptions.ClassroomNotFound)
125
-
126
- def get_classroom_from_token(class_token) -> Classroom:
127
- """
128
- Gets a class without logging in.
129
-
130
- Args:
131
- class_token (str): class token of the requested class
132
-
133
- Returns:
134
- scratchattach.classroom.Classroom: An object representing the requested classroom
135
-
136
- Warning:
137
- Any methods that require authentication will not work on the returned object.
138
-
139
- If you want to use these, get the user with :meth:`scratchattach.session.Session.connect_classroom` instead.
140
- """
141
- print("Warning: For methods that require authentication, use session.connect_classroom instead of get_classroom")
142
- return commons._get_object("classtoken", class_token, Classroom, exceptions.ClassroomNotFound)
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import warnings
5
+ from typing import Optional, TYPE_CHECKING, Any
6
+
7
+ import bs4
8
+
9
+ if TYPE_CHECKING:
10
+ from ..site.session import Session
11
+
12
+ from ..utils.commons import requests
13
+ from . import user, activity
14
+ from ._base import BaseSiteComponent
15
+ from ..utils import exceptions, commons
16
+ from ..utils.commons import headers
17
+
18
+ from bs4 import BeautifulSoup
19
+
20
+
21
+ class Classroom(BaseSiteComponent):
22
+ def __init__(self, **entries):
23
+ # Info on how the .update method has to fetch the data:
24
+ # NOTE: THIS DOESN'T WORK WITH CLOSED CLASSES!
25
+ self.update_function = requests.get
26
+ if "id" in entries:
27
+ self.update_API = f"https://api.scratch.mit.edu/classrooms/{entries['id']}"
28
+ elif "classtoken" in entries:
29
+ self.update_API = f"https://api.scratch.mit.edu/classtoken/{entries['classtoken']}"
30
+ else:
31
+ raise KeyError(f"No class id or token provided! Entries: {entries}")
32
+
33
+ # Set attributes every Project object needs to have:
34
+ self._session: Session = None
35
+ self.id = None
36
+ self.classtoken = None
37
+
38
+ self.__dict__.update(entries)
39
+
40
+ # Headers and cookies:
41
+ if self._session is None:
42
+ self._headers = headers
43
+ self._cookies = {}
44
+ else:
45
+ self._headers = self._session._headers
46
+ self._cookies = self._session._cookies
47
+
48
+ # Headers for operations that require accept and Content-Type fields:
49
+ self._json_headers = dict(self._headers)
50
+ self._json_headers["accept"] = "application/json"
51
+ self._json_headers["Content-Type"] = "application/json"
52
+ self.is_closed = False
53
+
54
+ def __repr__(self) -> str:
55
+ return f"classroom called {self.title!r}"
56
+
57
+ def update(self):
58
+ try:
59
+ success = super().update()
60
+ except exceptions.ClassroomNotFound:
61
+ success = False
62
+
63
+ if not success:
64
+ response = requests.get(f"https://scratch.mit.edu/classes/{self.id}/")
65
+ soup = BeautifulSoup(response.text, "html.parser")
66
+
67
+ headings = soup.find_all("h1")
68
+ for heading in headings:
69
+ if heading.text == "Whoops! Our server is Scratch'ing its head":
70
+ raise exceptions.ClassroomNotFound(f"Classroom id {self.id} is not closed and cannot be found.")
71
+
72
+ # id, title, description, status, date_start (iso format), educator/username
73
+
74
+ title = soup.find("title").contents[0][:-len(" on Scratch")]
75
+
76
+ overviews = soup.find_all("p", {"class": "overview"})
77
+ description, status = overviews[0].text, overviews[1].text
78
+
79
+ educator_username = None
80
+ pfx = "Scratch.INIT_DATA.PROFILE = {\n model: {\n id: '"
81
+ sfx = "',\n userId: "
82
+ for script in soup.find_all("script"):
83
+ if pfx in script.text:
84
+ educator_username = commons.webscrape_count(script.text, pfx, sfx, str)
85
+
86
+ ret = {"id": self.id,
87
+ "title": title,
88
+ "description": description,
89
+ "status": status,
90
+ "educator": {"username": educator_username},
91
+ "is_closed": True
92
+ }
93
+
94
+ return self._update_from_dict(ret)
95
+ return success
96
+
97
+ def _update_from_dict(self, classrooms):
98
+ try:
99
+ self.id = int(classrooms["id"])
100
+ except Exception:
101
+ pass
102
+ try:
103
+ self.title = classrooms["title"]
104
+ except Exception:
105
+ pass
106
+ try:
107
+ self.about_class = classrooms["description"]
108
+ except Exception:
109
+ pass
110
+ try:
111
+ self.working_on = classrooms["status"]
112
+ except Exception:
113
+ pass
114
+ try:
115
+ self.datetime = datetime.datetime.fromisoformat(classrooms["date_start"])
116
+ except Exception:
117
+ pass
118
+ try:
119
+ self.author = user.User(username=classrooms["educator"]["username"], _session=self._session)
120
+ except Exception:
121
+ pass
122
+ try:
123
+ self.author._update_from_dict(classrooms["educator"])
124
+ except Exception:
125
+ pass
126
+ self.is_closed = classrooms.get("is_closed", False)
127
+ return True
128
+
129
+ def student_count(self) -> int:
130
+ # student count
131
+ text = requests.get(
132
+ f"https://scratch.mit.edu/classes/{self.id}/",
133
+ headers=self._headers
134
+ ).text
135
+ return commons.webscrape_count(text, "Students (", ")")
136
+
137
+ def student_names(self, *, page=1) -> list[str]:
138
+ """
139
+ Returns the student on the class.
140
+
141
+ Keyword Arguments:
142
+ page: The page of the students that should be returned.
143
+
144
+ Returns:
145
+ list<str>: The usernames of the class students
146
+ """
147
+ if self.is_closed:
148
+ ret = []
149
+ response = requests.get(f"https://scratch.mit.edu/classes/{self.id}/")
150
+ soup = BeautifulSoup(response.text, "html.parser")
151
+
152
+ for scrollable in soup.find_all("ul", {"class": "scroll-content"}):
153
+ for item in scrollable.contents:
154
+ if not isinstance(item, bs4.NavigableString):
155
+ if "user" in item.attrs["class"]:
156
+ anchors = item.find_all("a")
157
+ if len(anchors) == 2:
158
+ ret.append(anchors[1].text.strip())
159
+
160
+ return ret
161
+
162
+ text = requests.get(
163
+ f"https://scratch.mit.edu/classes/{self.id}/students/?page={page}",
164
+ headers=self._headers
165
+ ).text
166
+ textlist = [i.split('/">')[0] for i in text.split(' <a href="/users/')[1:]]
167
+ return textlist
168
+
169
+ def class_studio_count(self) -> int:
170
+ # studio count
171
+ text = requests.get(
172
+ f"https://scratch.mit.edu/classes/{self.id}/",
173
+ headers=self._headers
174
+ ).text
175
+ return commons.webscrape_count(text, "Class Studios (", ")")
176
+
177
+ def class_studio_ids(self, *, page: int = 1) -> list[int]:
178
+ """
179
+ Returns the class studio on the class.
180
+
181
+ Keyword Arguments:
182
+ page: The page of the students that should be returned.
183
+
184
+ Returns:
185
+ list<int>: The id of the class studios
186
+ """
187
+ if self.is_closed:
188
+ ret = []
189
+ response = requests.get(f"https://scratch.mit.edu/classes/{self.id}/")
190
+ soup = BeautifulSoup(response.text, "html.parser")
191
+
192
+ for scrollable in soup.find_all("ul", {"class": "scroll-content"}):
193
+ for item in scrollable.contents:
194
+ if not isinstance(item, bs4.NavigableString):
195
+ if "gallery" in item.attrs["class"]:
196
+ anchor = item.find("a")
197
+ if "href" in anchor.attrs:
198
+ ret.append(commons.webscrape_count(anchor.attrs["href"], "/studios/", "/"))
199
+ return ret
200
+
201
+ text = requests.get(
202
+ f"https://scratch.mit.edu/classes/{self.id}/studios/?page={page}",
203
+ headers=self._headers
204
+ ).text
205
+ textlist = [int(i.split('/">')[0]) for i in text.split('<span class="title">\n <a href="/studios/')[1:]]
206
+ return textlist
207
+
208
+ def _check_session(self) -> None:
209
+ if self._session is None:
210
+ raise exceptions.Unauthenticated(
211
+ f"Classroom {self} has no associated session. Use session.connect_classroom() instead of sa.get_classroom()")
212
+
213
+ def set_thumbnail(self, thumbnail: bytes) -> None:
214
+ self._check_session()
215
+ requests.post(f"https://scratch.mit.edu/site-api/classrooms/all/{self.id}/",
216
+ headers=self._headers, cookies=self._cookies,
217
+ files={"file": thumbnail})
218
+
219
+ def set_description(self, desc: str) -> None:
220
+ self._check_session()
221
+ response = requests.put(f"https://scratch.mit.edu/site-api/classrooms/all/{self.id}/",
222
+ headers=self._headers, cookies=self._cookies,
223
+ json={"description": desc})
224
+
225
+ try:
226
+ data = response.json()
227
+ if data["description"] == desc:
228
+ # Success!
229
+ return
230
+ else:
231
+ warnings.warn(f"{self._session} may not be authenticated to edit {self}")
232
+
233
+ except Exception as e:
234
+ warnings.warn(f"{self._session} may not be authenticated to edit {self}")
235
+ raise e
236
+
237
+ def set_working_on(self, status: str) -> None:
238
+ self._check_session()
239
+ response = requests.put(f"https://scratch.mit.edu/site-api/classrooms/all/{self.id}/",
240
+ headers=self._headers, cookies=self._cookies,
241
+ json={"status": status})
242
+
243
+ try:
244
+ data = response.json()
245
+ if data["status"] == status:
246
+ # Success!
247
+ return
248
+ else:
249
+ warnings.warn(f"{self._session} may not be authenticated to edit {self}")
250
+
251
+ except Exception as e:
252
+ warnings.warn(f"{self._session} may not be authenticated to edit {self}")
253
+ raise e
254
+
255
+ def set_title(self, title: str) -> None:
256
+ self._check_session()
257
+ response = requests.put(f"https://scratch.mit.edu/site-api/classrooms/all/{self.id}/",
258
+ headers=self._headers, cookies=self._cookies,
259
+ json={"title": title})
260
+
261
+ try:
262
+ data = response.json()
263
+ if data["title"] == title:
264
+ # Success!
265
+ return
266
+ else:
267
+ warnings.warn(f"{self._session} may not be authenticated to edit {self}")
268
+
269
+ except Exception as e:
270
+ warnings.warn(f"{self._session} may not be authenticated to edit {self}")
271
+ raise e
272
+
273
+ def add_studio(self, name: str, description: str = '') -> None:
274
+ self._check_session()
275
+ requests.post("https://scratch.mit.edu/classes/create_classroom_gallery/",
276
+ json={
277
+ "classroom_id": str(self.id),
278
+ "classroom_token": self.classtoken,
279
+ "title": name,
280
+ "description": description},
281
+ headers=self._headers, cookies=self._cookies)
282
+
283
+ def reopen(self) -> None:
284
+ self._check_session()
285
+ response = requests.put(f"https://scratch.mit.edu/site-api/classrooms/all/{self.id}/",
286
+ headers=self._headers, cookies=self._cookies,
287
+ json={"visibility": "visible"})
288
+
289
+ try:
290
+ response.json()
291
+
292
+ except Exception as e:
293
+ warnings.warn(f"{self._session} may not be authenticated to edit {self}")
294
+ raise e
295
+
296
+ def close(self) -> None:
297
+ self._check_session()
298
+ response = requests.post(f"https://scratch.mit.edu/site-api/classrooms/close_classroom/{self.id}/",
299
+ headers=self._headers, cookies=self._cookies)
300
+
301
+ try:
302
+ response.json()
303
+
304
+ except Exception as e:
305
+ warnings.warn(f"{self._session} may not be authenticated to edit {self}")
306
+ raise e
307
+
308
+ def register_student(self, username: str, password: str = '', birth_month: Optional[int] = None, birth_year: Optional[int] = None,
309
+ gender: Optional[str] = None, country: Optional[str] = None, is_robot: bool = False) -> None:
310
+ return register_by_token(self.id, self.classtoken, username, password, birth_month, birth_year, gender, country,
311
+ is_robot)
312
+
313
+ def generate_signup_link(self):
314
+ if self.classtoken is not None:
315
+ return f"https://scratch.mit.edu/signup/{self.classtoken}"
316
+
317
+ self._check_session()
318
+
319
+ response = requests.get(f"https://scratch.mit.edu/site-api/classrooms/generate_registration_link/{self.id}/",
320
+ headers=self._headers, cookies=self._cookies)
321
+ # Should really check for '404' page
322
+ data = response.json()
323
+ if "reg_link" in data:
324
+ return data["reg_link"]
325
+ else:
326
+ raise exceptions.Unauthorized(f"{self._session} is not authorised to generate a signup link of {self}")
327
+
328
+ def public_activity(self, *, limit=20):
329
+ """
330
+ Returns:
331
+ list<scratchattach.Activity>: The user's activity data as parsed list of scratchattach.activity.Activity objects
332
+ """
333
+ if limit > 20:
334
+ warnings.warn("The limit is set to more than 20. There may be an error")
335
+ soup = BeautifulSoup(
336
+ requests.get(f"https://scratch.mit.edu/site-api/classrooms/activity/public/{self.id}/?limit={limit}").text,
337
+ 'html.parser')
338
+
339
+ activities = []
340
+ source = soup.find_all("li")
341
+
342
+ for data in source:
343
+ _activity = activity.Activity(_session=self._session, raw=data)
344
+ _activity._update_from_html(data)
345
+ activities.append(_activity)
346
+
347
+ return activities
348
+
349
+ def activity(self, student: str = "all", mode: str = "Last created", page: Optional[int] = None) -> list[dict[str, Any]]:
350
+ """
351
+ Get a list of private activity, only available to the class owner.
352
+ Returns:
353
+ list<activity.Activity> The private activity of users in the class
354
+ """
355
+
356
+ self._check_session()
357
+
358
+ ascsort, descsort = commons.get_class_sort_mode(mode)
359
+
360
+ data = requests.get(f"https://scratch.mit.edu/site-api/classrooms/activity/{self.id}/{student}/",
361
+ params={"page": page, "ascsort": ascsort, "descsort": descsort},
362
+ headers=self._headers, cookies=self._cookies).json()
363
+
364
+ _activity = []
365
+ for activity_json in data:
366
+ _activity.append(activity.Activity(_session=self._session))
367
+ _activity[-1]._update_from_json(activity_json)
368
+
369
+ return _activity
370
+
371
+
372
+ def get_classroom(class_id: str) -> Classroom:
373
+ """
374
+ Gets a class without logging in.
375
+
376
+ Args:
377
+ class_id (str): class id of the requested class
378
+
379
+ Returns:
380
+ scratchattach.classroom.Classroom: An object representing the requested classroom
381
+
382
+ Warning:
383
+ Any methods that require authentication will not work on the returned object.
384
+
385
+ If you want to use these, get the user with :meth:`scratchattach.session.Session.connect_classroom` instead.
386
+ """
387
+ warnings.warn("For methods that require authentication, use session.connect_classroom instead of get_classroom")
388
+ return commons._get_object("id", class_id, Classroom, exceptions.ClassroomNotFound)
389
+
390
+
391
+ def get_classroom_from_token(class_token) -> Classroom:
392
+ """
393
+ Gets a class without logging in.
394
+
395
+ Args:
396
+ class_token (str): class token of the requested class
397
+
398
+ Returns:
399
+ scratchattach.classroom.Classroom: An object representing the requested classroom
400
+
401
+ Warning:
402
+ Any methods that require authentication will not work on the returned object.
403
+
404
+ If you want to use these, get the user with :meth:`scratchattach.session.Session.connect_classroom` instead.
405
+ """
406
+ warnings.warn("For methods that require authentication, use session.connect_classroom instead of get_classroom")
407
+ return commons._get_object("classtoken", class_token, Classroom, exceptions.ClassroomNotFound)
408
+
409
+
410
+ def register_by_token(class_id: int, class_token: str, username: str, password: str, birth_month: int, birth_year: int,
411
+ gender: str, country: str, is_robot: bool = False) -> None:
412
+ data = {"classroom_id": class_id,
413
+ "classroom_token": class_token,
414
+
415
+ "username": username,
416
+ "password": password,
417
+ "birth_month": birth_month,
418
+ "birth_year": birth_year,
419
+ "gender": gender,
420
+ "country": country,
421
+ "is_robot": is_robot}
422
+
423
+ response = requests.post("https://scratch.mit.edu/classes/register_new_student/",
424
+ data=data, headers=headers, cookies={"scratchcsrftoken": 'a'})
425
+ ret = response.json()[0]
426
+
427
+ if "username" in ret:
428
+ return
429
+ else:
430
+ raise exceptions.Unauthorized(f"Can't create account: {response.text}")