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