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