scratchattach 2.1.5__tar.gz → 2.1.7__tar.gz
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-2.1.5 → scratchattach-2.1.7}/PKG-INFO +3 -3
- {scratchattach-2.1.5 → scratchattach-2.1.7}/README.md +1 -1
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/__init__.py +2 -1
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/cloud_server.py +2 -2
- scratchattach-2.1.7/scratchattach/other/other_apis.py +212 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/classroom.py +2 -2
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/forum.py +15 -1
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/project.py +9 -1
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/session.py +77 -20
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/studio.py +1 -1
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/user.py +1 -1
- scratchattach-2.1.7/scratchattach/utils/enums.py +190 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/utils/exceptions.py +28 -5
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach.egg-info/PKG-INFO +3 -3
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach.egg-info/SOURCES.txt +1 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/setup.py +2 -2
- scratchattach-2.1.5/scratchattach/other/other_apis.py +0 -103
- {scratchattach-2.1.5 → scratchattach-2.1.7}/LICENSE +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/cloud/__init__.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/cloud/_base.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/cloud/cloud.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/__init__.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/_base.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/cloud_events.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/cloud_recorder.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/cloud_requests.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/cloud_storage.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/combine.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/filterbot.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/message_events.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/other/__init__.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/other/project_json_capabilities.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/__init__.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/_base.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/activity.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/backpack_asset.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/cloud_activity.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/comment.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/utils/__init__.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/utils/commons.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/utils/encoder.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/utils/requests.py +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach.egg-info/dependency_links.txt +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach.egg-info/requires.txt +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach.egg-info/top_level.txt +0 -0
- {scratchattach-2.1.5 → scratchattach-2.1.7}/setup.cfg +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: scratchattach
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.7
|
|
4
4
|
Summary: A Scratch API Wrapper
|
|
5
|
-
Home-page: https://
|
|
5
|
+
Home-page: https://scratchattach.tim1de.net
|
|
6
6
|
Author: TimMcCool
|
|
7
7
|
Author-email: timmccool.scratch@gmail.com
|
|
8
8
|
Keywords: scratch api,scratchattach,scratch api python,scratch python,scratch for python,scratch,scratch cloud,scratch cloud variables,scratch bot
|
|
@@ -25,7 +25,7 @@ The library allows setting cloud variables, following users, updating your profi
|
|
|
25
25
|
so much more! Additionally, it provides frameworks that simplify sending data through cloud variables.
|
|
26
26
|
|
|
27
27
|
<p align="left" style="margin:10px">
|
|
28
|
-
<img width="160" src="https://
|
|
28
|
+
<img width="160" src="https://raw.githubusercontent.com/TimMcCool/scratchattach/refs/heads/main/logos/logo.svg">
|
|
29
29
|
|
|
30
30
|
[](https://pypi.python.org/pypi/scratchattach/)
|
|
31
31
|
[](https://pypi.python.org/pypi/scratchattach/)
|
|
@@ -4,7 +4,7 @@ The library allows setting cloud variables, following users, updating your profi
|
|
|
4
4
|
so much more! Additionally, it provides frameworks that simplify sending data through cloud variables.
|
|
5
5
|
|
|
6
6
|
<p align="left" style="margin:10px">
|
|
7
|
-
<img width="160" src="https://
|
|
7
|
+
<img width="160" src="https://raw.githubusercontent.com/TimMcCool/scratchattach/refs/heads/main/logos/logo.svg">
|
|
8
8
|
|
|
9
9
|
[](https://pypi.python.org/pypi/scratchattach/)
|
|
10
10
|
[](https://pypi.python.org/pypi/scratchattach/)
|
|
@@ -10,12 +10,13 @@ from .eventhandlers.combine import MultiEventHandler
|
|
|
10
10
|
from .other.other_apis import *
|
|
11
11
|
from .other.project_json_capabilities import ProjectBody, get_empty_project_pb, get_pb_from_dict, read_sb3_file, download_asset
|
|
12
12
|
from .utils.encoder import Encoding
|
|
13
|
+
from .utils.enums import Languages, TTSVoices
|
|
13
14
|
|
|
14
15
|
from .site.activity import Activity
|
|
15
16
|
from .site.backpack_asset import BackpackAsset
|
|
16
17
|
from .site.comment import Comment
|
|
17
18
|
from .site.cloud_activity import CloudActivity
|
|
18
|
-
from .site.forum import ForumPost, ForumTopic, get_topic, get_topic_list
|
|
19
|
+
from .site.forum import ForumPost, ForumTopic, get_topic, get_topic_list, youtube_link_to_scratch
|
|
19
20
|
from .site.project import Project, get_project, search_projects, explore_projects
|
|
20
21
|
from .site.session import Session, login, login_by_id, login_by_session_string
|
|
21
22
|
from .site.studio import Studio, get_studio, search_studios, explore_studios
|
|
@@ -223,7 +223,7 @@ def init_cloud_server(hostname='127.0.0.1', port=8080, *, thread=True, length_li
|
|
|
223
223
|
return False
|
|
224
224
|
return True
|
|
225
225
|
|
|
226
|
-
def
|
|
226
|
+
def _updater(self):
|
|
227
227
|
try:
|
|
228
228
|
# Function called when .start() is executed (.start is inherited from BaseEventHandler)
|
|
229
229
|
print(f"Serving websocket server: ws://{hostname}:{port}")
|
|
@@ -241,4 +241,4 @@ def init_cloud_server(hostname='127.0.0.1', port=8080, *, thread=True, length_li
|
|
|
241
241
|
self.running = False
|
|
242
242
|
self.close()
|
|
243
243
|
|
|
244
|
-
return TwCloudServer(hostname, port=port, websocketclass=TwCloudSocket)
|
|
244
|
+
return TwCloudServer(hostname, port=port, websocketclass=TwCloudSocket)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Other Scratch API-related functions"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from ..utils import commons
|
|
6
|
+
from ..utils.exceptions import BadRequest, InvalidLanguage, InvalidTTSGender
|
|
7
|
+
from ..utils.requests import Requests as requests
|
|
8
|
+
from ..utils.enums import Languages, Language, TTSVoices, TTSVoice
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# --- Front page ---
|
|
12
|
+
|
|
13
|
+
def get_news(*, limit=10, offset=0):
|
|
14
|
+
return commons.api_iterative("https://api.scratch.mit.edu/news", limit=limit, offset=offset)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def featured_data():
|
|
18
|
+
return requests.get("https://api.scratch.mit.edu/proxy/featured").json()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def featured_projects():
|
|
22
|
+
return featured_data()["community_featured_projects"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def featured_studios():
|
|
26
|
+
return featured_data()["community_featured_studios"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def top_loved():
|
|
30
|
+
return featured_data()["community_most_loved_projects"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def top_remixed():
|
|
34
|
+
return featured_data()["community_most_remixed_projects"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def newest_projects():
|
|
38
|
+
return featured_data()["community_newest_projects"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def curated_projects():
|
|
42
|
+
return featured_data()["curator_top_projects"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def design_studio_projects():
|
|
46
|
+
return featured_data()["scratch_design_studio"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# --- Statistics ---
|
|
50
|
+
|
|
51
|
+
def total_site_stats():
|
|
52
|
+
data = requests.get("https://scratch.mit.edu/statistics/data/daily/").json()
|
|
53
|
+
data.pop("_TS")
|
|
54
|
+
return data
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def monthly_site_traffic():
|
|
58
|
+
data = requests.get("https://scratch.mit.edu/statistics/data/monthly-ga/").json()
|
|
59
|
+
data.pop("_TS")
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def country_counts():
|
|
64
|
+
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["country_distribution"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def age_distribution():
|
|
68
|
+
data = requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["age_distribution_data"][0]["values"]
|
|
69
|
+
return_data = {}
|
|
70
|
+
for value in data:
|
|
71
|
+
return_data[value["x"]] = value["y"]
|
|
72
|
+
return return_data
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def monthly_comment_activity():
|
|
76
|
+
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["comment_data"]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def monthly_project_shares():
|
|
80
|
+
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["project_data"]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def monthly_active_users():
|
|
84
|
+
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["active_user_data"]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def monthly_activity_trends():
|
|
88
|
+
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["activity_data"]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# --- CSRF Token Generation API ---
|
|
92
|
+
|
|
93
|
+
def get_csrf_token():
|
|
94
|
+
"""
|
|
95
|
+
Generates a scratchcsrftoken using Scratch's API.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
str: The generated scratchcsrftoken
|
|
99
|
+
"""
|
|
100
|
+
return requests.get(
|
|
101
|
+
"https://scratch.mit.edu/csrf_token/"
|
|
102
|
+
).headers["set-cookie"].split(";")[3][len(" Path=/, scratchcsrftoken="):]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# --- Various other api.scratch.mit.edu API endpoints ---
|
|
106
|
+
|
|
107
|
+
def get_health():
|
|
108
|
+
return requests.get("https://api.scratch.mit.edu/health").json()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_total_project_count() -> int:
|
|
112
|
+
return requests.get("https://api.scratch.mit.edu/projects/count/all").json()["count"]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def check_username(username):
|
|
116
|
+
return requests.get(f"https://api.scratch.mit.edu/accounts/checkusername/{username}").json()["msg"]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def check_password(password):
|
|
120
|
+
return requests.post("https://api.scratch.mit.edu/accounts/checkpassword/", json={"password": password}).json()[
|
|
121
|
+
"msg"]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# --- April fools endpoints ---
|
|
125
|
+
|
|
126
|
+
def aprilfools_get_counter() -> int:
|
|
127
|
+
return requests.get("https://api.scratch.mit.edu/surprise").json()["surprise"]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def aprilfools_increment_counter() -> int:
|
|
131
|
+
return requests.post("https://api.scratch.mit.edu/surprise").json()["surprise"]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# --- Resources ---
|
|
135
|
+
def get_resource_urls():
|
|
136
|
+
return requests.get("https://resources.scratch.mit.edu/localized-urls.json").json()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# --- Misc ---
|
|
140
|
+
# I'm not sure what to label this as
|
|
141
|
+
def scratch_team_members() -> dict:
|
|
142
|
+
# Unfortunately, the only place to find this is a js file, not a json file, which is annoying
|
|
143
|
+
text = requests.get("https://scratch.mit.edu/js/credits.bundle.js").text
|
|
144
|
+
text = "[{\"userName\"" + text.split("JSON.parse('[{\"userName\"")[1]
|
|
145
|
+
text = text.split("\"}]')")[0] + "\"}]"
|
|
146
|
+
|
|
147
|
+
return json.loads(text)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def translate(language: str | Languages, text: str = "hello"):
|
|
151
|
+
if isinstance(language, str):
|
|
152
|
+
lang = Languages.find_by_attrs(language.lower(), ["code", "tts_locale", "name"], str.lower)
|
|
153
|
+
elif isinstance(language, Languages):
|
|
154
|
+
lang = language.value
|
|
155
|
+
else:
|
|
156
|
+
lang = language
|
|
157
|
+
|
|
158
|
+
if not isinstance(lang, Language):
|
|
159
|
+
raise InvalidLanguage(f"{language} is not a language")
|
|
160
|
+
|
|
161
|
+
if lang.code is None:
|
|
162
|
+
raise InvalidLanguage(f"{lang} is not a valid translate language")
|
|
163
|
+
|
|
164
|
+
response_json = requests.get(
|
|
165
|
+
f"https://translate-service.scratch.mit.edu/translate?language={lang.code}&text={text}").json()
|
|
166
|
+
|
|
167
|
+
if "result" in response_json:
|
|
168
|
+
return response_json["result"]
|
|
169
|
+
else:
|
|
170
|
+
raise BadRequest(f"Language '{language}' does not seem to be valid.\nResponse: {response_json}")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def text2speech(text: str = "hello", voice_name: str = "female", language: str = "en-US"):
|
|
174
|
+
"""
|
|
175
|
+
Sends a request to Scratch's TTS synthesis service.
|
|
176
|
+
Returns:
|
|
177
|
+
- The TTS audio (mp3) as bytes
|
|
178
|
+
- The playback rate (e.g. for giant it would be 0.84)
|
|
179
|
+
"""
|
|
180
|
+
if isinstance(voice_name, str):
|
|
181
|
+
voice = TTSVoices.find_by_attrs(voice_name.lower(), ["name", "gender"], str.lower)
|
|
182
|
+
elif isinstance(voice_name, TTSVoices):
|
|
183
|
+
voice = voice_name.value
|
|
184
|
+
else:
|
|
185
|
+
voice = voice_name
|
|
186
|
+
|
|
187
|
+
if not isinstance(voice, TTSVoice):
|
|
188
|
+
raise InvalidTTSGender(f"TTS Gender {voice_name} is not supported.")
|
|
189
|
+
|
|
190
|
+
# If it's kitten, make sure to change everything to just meows
|
|
191
|
+
if voice.name == "kitten":
|
|
192
|
+
text = ''
|
|
193
|
+
for word in text.split(' '):
|
|
194
|
+
if word.strip() != '':
|
|
195
|
+
text += "meow "
|
|
196
|
+
|
|
197
|
+
if isinstance(language, str):
|
|
198
|
+
lang = Languages.find_by_attrs(language.lower(), ["code", "tts_locale", "name"], str.lower)
|
|
199
|
+
elif isinstance(language, Languages):
|
|
200
|
+
lang = language.value
|
|
201
|
+
else:
|
|
202
|
+
lang = language
|
|
203
|
+
|
|
204
|
+
if not isinstance(lang, Language):
|
|
205
|
+
raise InvalidLanguage(f"Language '{language}' is not a language")
|
|
206
|
+
|
|
207
|
+
if lang.tts_locale is None:
|
|
208
|
+
raise InvalidLanguage(f"Language '{language}' is not a valid TTS language")
|
|
209
|
+
|
|
210
|
+
response = requests.get(f"https://synthesis-service.scratch.mit.edu/synth"
|
|
211
|
+
f"?locale={lang.tts_locale}&gender={voice.gender}&text={text}")
|
|
212
|
+
return response.content, voice.playback_rate
|
|
@@ -105,7 +105,7 @@ class Classroom(BaseSiteComponent):
|
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
def get_classroom(class_id):
|
|
108
|
+
def get_classroom(class_id) -> Classroom:
|
|
109
109
|
"""
|
|
110
110
|
Gets a class without logging in.
|
|
111
111
|
|
|
@@ -123,7 +123,7 @@ def get_classroom(class_id):
|
|
|
123
123
|
print("Warning: For methods that require authentication, use session.connect_classroom instead of get_classroom")
|
|
124
124
|
return commons._get_object("id", class_id, Classroom, exceptions.ClassroomNotFound)
|
|
125
125
|
|
|
126
|
-
def get_classroom_from_token(class_token):
|
|
126
|
+
def get_classroom_from_token(class_token) -> Classroom:
|
|
127
127
|
"""
|
|
128
128
|
Gets a class without logging in.
|
|
129
129
|
|
|
@@ -6,6 +6,7 @@ from ..utils import exceptions, commons
|
|
|
6
6
|
from ._base import BaseSiteComponent
|
|
7
7
|
import xml.etree.ElementTree as ET
|
|
8
8
|
from bs4 import BeautifulSoup
|
|
9
|
+
from urllib.parse import urlparse, parse_qs
|
|
9
10
|
|
|
10
11
|
from ..utils.requests import Requests as requests
|
|
11
12
|
|
|
@@ -306,7 +307,7 @@ class ForumPost(BaseSiteComponent):
|
|
|
306
307
|
)
|
|
307
308
|
|
|
308
309
|
|
|
309
|
-
def get_topic(topic_id):
|
|
310
|
+
def get_topic(topic_id) -> ForumTopic:
|
|
310
311
|
|
|
311
312
|
"""
|
|
312
313
|
Gets a forum topic without logging in. Data received from Scratch's RSS feed XML API.
|
|
@@ -383,3 +384,16 @@ def get_topic_list(category_id, *, page=1):
|
|
|
383
384
|
except Exception as e:
|
|
384
385
|
raise exceptions.ScrapeError(str(e))
|
|
385
386
|
|
|
387
|
+
|
|
388
|
+
def youtube_link_to_scratch(link: str):
|
|
389
|
+
"""
|
|
390
|
+
Converts a YouTube url (in multiple formats) like https://youtu.be/1JTgg4WVAX8?si=fIEskaEaOIRZyTAz
|
|
391
|
+
to a link like https://scratch.mit.edu/discuss/youtube/1JTgg4WVAX8
|
|
392
|
+
"""
|
|
393
|
+
url_parse = urlparse(link)
|
|
394
|
+
query_parse = parse_qs(url_parse.query)
|
|
395
|
+
if 'v' in query_parse:
|
|
396
|
+
video_id = query_parse['v'][0]
|
|
397
|
+
else:
|
|
398
|
+
video_id = url_parse.path.split('/')[-1]
|
|
399
|
+
return f"https://scratch.mit.edu/discuss/youtube/{video_id}"
|
|
@@ -100,6 +100,14 @@ class PartialProject(BaseSiteComponent):
|
|
|
100
100
|
return False
|
|
101
101
|
return True
|
|
102
102
|
|
|
103
|
+
@property
|
|
104
|
+
def embed_url(self):
|
|
105
|
+
"""
|
|
106
|
+
Returns:
|
|
107
|
+
the url of the embed of the project
|
|
108
|
+
"""
|
|
109
|
+
return f"{self.url}/embed"
|
|
110
|
+
|
|
103
111
|
def remixes(self, *, limit=40, offset=0):
|
|
104
112
|
"""
|
|
105
113
|
Returns:
|
|
@@ -721,7 +729,7 @@ class Project(PartialProject):
|
|
|
721
729
|
# ------ #
|
|
722
730
|
|
|
723
731
|
|
|
724
|
-
def get_project(project_id):
|
|
732
|
+
def get_project(project_id) -> Project:
|
|
725
733
|
"""
|
|
726
734
|
Gets a project without logging in.
|
|
727
735
|
|
|
@@ -54,7 +54,7 @@ class Session(BaseSiteComponent):
|
|
|
54
54
|
'''
|
|
55
55
|
|
|
56
56
|
def __str__(self):
|
|
57
|
-
return "Login for account: {self.username}"
|
|
57
|
+
return f"Login for account: {self.username}"
|
|
58
58
|
|
|
59
59
|
def __init__(self, **entries):
|
|
60
60
|
|
|
@@ -85,19 +85,31 @@ class Session(BaseSiteComponent):
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
def _update_from_dict(self, data):
|
|
88
|
+
# Note: there are a lot more things you can get from this data dict.
|
|
89
|
+
# Maybe it would be a good idea to also store the dict itself?
|
|
90
|
+
# self.data = data
|
|
91
|
+
|
|
88
92
|
self.xtoken = data['user']['token']
|
|
89
93
|
self._headers["X-Token"] = self.xtoken
|
|
94
|
+
|
|
95
|
+
self.has_outstanding_email_confirmation = data["flags"]["has_outstanding_email_confirmation"]
|
|
96
|
+
|
|
90
97
|
self.email = data["user"]["email"]
|
|
98
|
+
|
|
91
99
|
self.new_scratcher = data["permissions"]["new_scratcher"]
|
|
92
100
|
self.mute_status = data["permissions"]["mute_status"]
|
|
101
|
+
|
|
93
102
|
self.username = data["user"]["username"]
|
|
94
103
|
self._username = data["user"]["username"]
|
|
95
104
|
self.banned = data["user"]["banned"]
|
|
105
|
+
|
|
96
106
|
if self.banned:
|
|
97
107
|
warnings.warn(f"Warning: The account {self._username} you logged in to is BANNED. Some features may not work properly.")
|
|
108
|
+
if self.has_outstanding_email_confirmation:
|
|
109
|
+
warnings.warn(f"Warning: The account {self._username} you logged is not email confirmed. Some features may not work properly.")
|
|
98
110
|
return True
|
|
99
111
|
|
|
100
|
-
def connect_linked_user(self):
|
|
112
|
+
def connect_linked_user(self) -> 'user.User':
|
|
101
113
|
'''
|
|
102
114
|
Gets the user associated with the log in / session.
|
|
103
115
|
|
|
@@ -115,6 +127,51 @@ class Session(BaseSiteComponent):
|
|
|
115
127
|
# backwards compatibility with v1
|
|
116
128
|
return self.connect_linked_user() # To avoid inconsistencies with "connect" and "get", this function was renamed
|
|
117
129
|
|
|
130
|
+
def set_country(self, country: str="Antarctica"):
|
|
131
|
+
requests.post("https://scratch.mit.edu/accounts/settings/",
|
|
132
|
+
data={"country": country},
|
|
133
|
+
headers=self._headers, cookies=self._cookies)
|
|
134
|
+
|
|
135
|
+
def resend_email(self, password: str):
|
|
136
|
+
"""
|
|
137
|
+
Sends a request to resend a confirmation email for this session's account
|
|
138
|
+
|
|
139
|
+
Keyword arguments:
|
|
140
|
+
password (str): Password associated with the session (not stored)
|
|
141
|
+
"""
|
|
142
|
+
requests.post("https://scratch.mit.edu/accounts/email_change/",
|
|
143
|
+
data={"email_address": self.new_email_address,
|
|
144
|
+
"password": password},
|
|
145
|
+
headers=self._headers, cookies=self._cookies)
|
|
146
|
+
@property
|
|
147
|
+
def new_email_address(self) -> str | None:
|
|
148
|
+
"""
|
|
149
|
+
Gets the (unconfirmed) email address that this session has requested to transfer to, if any, otherwise the current address.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
str: The email that this session wants to switch to
|
|
153
|
+
"""
|
|
154
|
+
response = requests.get("https://scratch.mit.edu/accounts/email_change/",
|
|
155
|
+
headers=self._headers, cookies=self._cookies)
|
|
156
|
+
|
|
157
|
+
soup = BeautifulSoup(response.content, "html.parser")
|
|
158
|
+
|
|
159
|
+
email = None
|
|
160
|
+
for label_span in soup.find_all("span", {"class": "label"}):
|
|
161
|
+
if label_span.contents[0] == "New Email Address":
|
|
162
|
+
return label_span.parent.contents[-1].text.strip("\n ")
|
|
163
|
+
elif label_span.contents[0] == "Current Email Address":
|
|
164
|
+
email = label_span.parent.contents[-1].text.strip("\n ")
|
|
165
|
+
|
|
166
|
+
return email
|
|
167
|
+
|
|
168
|
+
def logout(self):
|
|
169
|
+
"""
|
|
170
|
+
Sends a logout request to scratch. Might not do anything, might log out this account on other ips/sessions? I am not sure
|
|
171
|
+
"""
|
|
172
|
+
requests.post("https://scratch.mit.edu/accounts/logout/",
|
|
173
|
+
headers=self._headers, cookies=self._cookies)
|
|
174
|
+
|
|
118
175
|
def messages(self, *, limit=40, offset=0, date_limit=None, filter_by=None):
|
|
119
176
|
'''
|
|
120
177
|
Returns the messages.
|
|
@@ -241,21 +298,21 @@ class Session(BaseSiteComponent):
|
|
|
241
298
|
|
|
242
299
|
# -- Project JSON editing capabilities ---
|
|
243
300
|
|
|
244
|
-
def connect_empty_project_pb():
|
|
301
|
+
def connect_empty_project_pb() -> 'project_json_capabilities.ProjectBody':
|
|
245
302
|
pb = project_json_capabilities.ProjectBody()
|
|
246
303
|
pb.from_json(empty_project_json)
|
|
247
304
|
return pb
|
|
248
305
|
|
|
249
|
-
def connect_pb_from_dict(project_json:dict):
|
|
306
|
+
def connect_pb_from_dict(project_json:dict) -> 'project_json_capabilities.ProjectBody':
|
|
250
307
|
pb = project_json_capabilities.ProjectBody()
|
|
251
308
|
pb.from_json(project_json)
|
|
252
309
|
return pb
|
|
253
310
|
|
|
254
|
-
def connect_pb_from_file(path_to_file):
|
|
311
|
+
def connect_pb_from_file(path_to_file) -> 'project_json_capabilities.ProjectBody':
|
|
255
312
|
pb = project_json_capabilities.ProjectBody()
|
|
256
313
|
pb.from_json(project_json_capabilities._load_sb3_file(path_to_file))
|
|
257
314
|
return pb
|
|
258
|
-
|
|
315
|
+
|
|
259
316
|
def download_asset(asset_id_with_file_ext, *, filename=None, dir=""):
|
|
260
317
|
if not (dir.endswith("/") or dir.endswith("\\")):
|
|
261
318
|
dir = dir+"/"
|
|
@@ -463,7 +520,7 @@ class Session(BaseSiteComponent):
|
|
|
463
520
|
except Exception:
|
|
464
521
|
raise(exceptions.FetchError)
|
|
465
522
|
|
|
466
|
-
|
|
523
|
+
|
|
467
524
|
def backpack(self,limit=20, offset=0):
|
|
468
525
|
'''
|
|
469
526
|
Lists the assets that are in the backpack of the user associated with the session.
|
|
@@ -476,7 +533,7 @@ class Session(BaseSiteComponent):
|
|
|
476
533
|
limit = limit, offset = offset, headers = self._headers
|
|
477
534
|
)
|
|
478
535
|
return commons.parse_object_list(data, backpack_asset.BackpackAsset, self)
|
|
479
|
-
|
|
536
|
+
|
|
480
537
|
def delete_from_backpack(self, backpack_asset_id):
|
|
481
538
|
'''
|
|
482
539
|
Deletes an asset from the backpack.
|
|
@@ -510,14 +567,14 @@ class Session(BaseSiteComponent):
|
|
|
510
567
|
"""
|
|
511
568
|
return CloudClass(project_id=project_id, _session=self)
|
|
512
569
|
|
|
513
|
-
def connect_scratch_cloud(self, project_id):
|
|
570
|
+
def connect_scratch_cloud(self, project_id) -> 'cloud.ScratchCloud':
|
|
514
571
|
"""
|
|
515
572
|
Returns:
|
|
516
573
|
scratchattach.cloud.ScratchCloud: An object representing the Scratch cloud of a project.
|
|
517
574
|
"""
|
|
518
575
|
return cloud.ScratchCloud(project_id=project_id, _session=self)
|
|
519
576
|
|
|
520
|
-
def connect_tw_cloud(self, project_id, *, purpose="", contact="", cloud_host="wss://clouddata.turbowarp.org"):
|
|
577
|
+
def connect_tw_cloud(self, project_id, *, purpose="", contact="", cloud_host="wss://clouddata.turbowarp.org") -> 'cloud.TwCloud':
|
|
521
578
|
"""
|
|
522
579
|
Returns:
|
|
523
580
|
scratchattach.cloud.TwCloud: An object representing the TurboWarp cloud of a project.
|
|
@@ -576,7 +633,7 @@ class Session(BaseSiteComponent):
|
|
|
576
633
|
return username
|
|
577
634
|
|
|
578
635
|
|
|
579
|
-
def connect_user_by_id(self, user_id:int):
|
|
636
|
+
def connect_user_by_id(self, user_id:int) -> 'user.User':
|
|
580
637
|
"""
|
|
581
638
|
Gets a user using this session, connects the session to the User object to allow authenticated actions
|
|
582
639
|
|
|
@@ -596,7 +653,7 @@ class Session(BaseSiteComponent):
|
|
|
596
653
|
"""
|
|
597
654
|
return self._make_linked_object("username", self.find_username_from_id(user_id), user.User, exceptions.UserNotFound)
|
|
598
655
|
|
|
599
|
-
def connect_project(self, project_id):
|
|
656
|
+
def connect_project(self, project_id) -> 'project.Project':
|
|
600
657
|
"""
|
|
601
658
|
Gets a project using this session, connects the session to the Project object to allow authenticated actions
|
|
602
659
|
sess
|
|
@@ -608,7 +665,7 @@ sess
|
|
|
608
665
|
"""
|
|
609
666
|
return self._make_linked_object("id", int(project_id), project.Project, exceptions.ProjectNotFound)
|
|
610
667
|
|
|
611
|
-
def connect_studio(self, studio_id):
|
|
668
|
+
def connect_studio(self, studio_id) -> 'studio.Studio':
|
|
612
669
|
"""
|
|
613
670
|
Gets a studio using this session, connects the session to the Studio object to allow authenticated actions
|
|
614
671
|
|
|
@@ -620,7 +677,7 @@ sess
|
|
|
620
677
|
"""
|
|
621
678
|
return self._make_linked_object("id", int(studio_id), studio.Studio, exceptions.StudioNotFound)
|
|
622
679
|
|
|
623
|
-
def connect_classroom(self, class_id):
|
|
680
|
+
def connect_classroom(self, class_id) -> 'classroom.Classroom':
|
|
624
681
|
"""
|
|
625
682
|
Gets a class using this session.
|
|
626
683
|
|
|
@@ -632,7 +689,7 @@ sess
|
|
|
632
689
|
"""
|
|
633
690
|
return self._make_linked_object("id", int(class_id), classroom.Classroom, exceptions.ClassroomNotFound)
|
|
634
691
|
|
|
635
|
-
def connect_classroom_from_token(self, class_token):
|
|
692
|
+
def connect_classroom_from_token(self, class_token) -> 'classroom.Classroom':
|
|
636
693
|
"""
|
|
637
694
|
Gets a class using this session.
|
|
638
695
|
|
|
@@ -644,7 +701,7 @@ sess
|
|
|
644
701
|
"""
|
|
645
702
|
return self._make_linked_object("classtoken", int(class_token), classroom.Classroom, exceptions.ClassroomNotFound)
|
|
646
703
|
|
|
647
|
-
def connect_topic(self, topic_id):
|
|
704
|
+
def connect_topic(self, topic_id) -> 'forum.ForumTopic':
|
|
648
705
|
"""
|
|
649
706
|
Gets a forum topic using this session, connects the session to the ForumTopic object to allow authenticated actions
|
|
650
707
|
Data is up-to-date. Data received from Scratch's RSS feed XML API.
|
|
@@ -710,11 +767,11 @@ sess
|
|
|
710
767
|
|
|
711
768
|
# --- Connect classes inheriting from BaseEventHandler ---
|
|
712
769
|
|
|
713
|
-
def connect_message_events(self, *, update_interval=2):
|
|
770
|
+
def connect_message_events(self, *, update_interval=2) -> 'message_events.MessageEvents':
|
|
714
771
|
# shortcut for connect_linked_user().message_events()
|
|
715
772
|
return message_events.MessageEvents(user.User(username=self.username, _session=self), update_interval=update_interval)
|
|
716
773
|
|
|
717
|
-
def connect_filterbot(self, *, log_deletions=True):
|
|
774
|
+
def connect_filterbot(self, *, log_deletions=True) -> 'filterbot.Filterbot':
|
|
718
775
|
return filterbot.Filterbot(user.User(username=self.username, _session=self), log_deletions=log_deletions)
|
|
719
776
|
|
|
720
777
|
# ------ #
|
|
@@ -788,12 +845,12 @@ def login(username, password, *, timeout=10) -> Session:
|
|
|
788
845
|
except Exception:
|
|
789
846
|
raise exceptions.LoginFailure(
|
|
790
847
|
"Either the provided authentication data is wrong or your network is banned from Scratch.\n\nIf you're using an online IDE (like replit.com) Scratch possibly banned its IP adress. In this case, try logging in with your session id: https://github.com/TimMcCool/scratchattach/wiki#logging-in")
|
|
791
|
-
|
|
848
|
+
|
|
792
849
|
# Create session object:
|
|
793
850
|
return login_by_id(session_id, username=username, password=password)
|
|
794
851
|
|
|
795
852
|
|
|
796
|
-
def login_by_session_string(session_string):
|
|
853
|
+
def login_by_session_string(session_string) -> Session:
|
|
797
854
|
session_string = base64.b64decode(session_string).decode() # unobfuscate
|
|
798
855
|
session_data = json.loads(session_string)
|
|
799
856
|
try:
|
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from typing import Callable, Iterable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(init=True, repr=True)
|
|
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
|
+
class _EnumWrapper(Enum):
|
|
22
|
+
@classmethod
|
|
23
|
+
def find(cls, value, by: str, apply_func: Callable = None):
|
|
24
|
+
"""
|
|
25
|
+
Finds the enum item with the given attribute that is equal to the given value.
|
|
26
|
+
the apply_func will be applied to the attribute of each language object before comparison.
|
|
27
|
+
|
|
28
|
+
i.e. Languages.find("ukranian", "name", str.lower) will return the Ukrainian language dataclass object
|
|
29
|
+
(even though Ukrainian was spelt lowercase, since str.lower will convert the "Ukrainian" string to lowercase)
|
|
30
|
+
"""
|
|
31
|
+
if apply_func is None:
|
|
32
|
+
def apply_func(x):
|
|
33
|
+
return x
|
|
34
|
+
|
|
35
|
+
for item in cls:
|
|
36
|
+
item_obj = item.value
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
if apply_func(getattr(item_obj, by)) == value:
|
|
40
|
+
return item_obj
|
|
41
|
+
except TypeError:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def all_of(cls, attr_name: str, apply_func: Callable = None) -> Iterable:
|
|
46
|
+
"""
|
|
47
|
+
Returns the list of each listed enum item's specified attribute by "attr_name"
|
|
48
|
+
|
|
49
|
+
i.e. Languages.all_of("name") will return a list of names:
|
|
50
|
+
["Albanian", "Amharic", ...]
|
|
51
|
+
|
|
52
|
+
The apply_func function will be applied to every list item,
|
|
53
|
+
i.e. Languages.all_of("name", str.lower) will return the same except in lowercase:
|
|
54
|
+
["albanian", "amharic", ...]
|
|
55
|
+
"""
|
|
56
|
+
if apply_func is None:
|
|
57
|
+
def apply_func(x):
|
|
58
|
+
return x
|
|
59
|
+
|
|
60
|
+
for item in cls:
|
|
61
|
+
item_obj = item.value
|
|
62
|
+
attr = getattr(item_obj, attr_name)
|
|
63
|
+
try:
|
|
64
|
+
yield apply_func(attr)
|
|
65
|
+
|
|
66
|
+
except TypeError:
|
|
67
|
+
yield attr
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def find_by_attrs(cls, value, bys: list[str], apply_func: Callable = None) -> list:
|
|
71
|
+
"""
|
|
72
|
+
Calls the EnumWrapper.by function multiple times until a match is found, using the provided 'by' attribute names
|
|
73
|
+
"""
|
|
74
|
+
for by in bys:
|
|
75
|
+
ret = cls.find(value, by, apply_func)
|
|
76
|
+
if ret is not None:
|
|
77
|
+
return ret
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Languages(_EnumWrapper):
|
|
81
|
+
Albanian = Language('Albanian', 'sq', None, None, None)
|
|
82
|
+
Amharic = Language('Amharic', 'am', None, None, None)
|
|
83
|
+
Arabic = Language('Arabic', 'ar', ['ar'], 'arb', True)
|
|
84
|
+
Armenian = Language('Armenian', 'hy', None, None, None)
|
|
85
|
+
Azerbaijani = Language('Azerbaijani', 'az', None, None, None)
|
|
86
|
+
Basque = Language('Basque', 'eu', None, None, None)
|
|
87
|
+
Belarusian = Language('Belarusian', 'be', None, None, None)
|
|
88
|
+
Bulgarian = Language('Bulgarian', 'bg', None, None, None)
|
|
89
|
+
Catalan = Language('Catalan', 'ca', None, None, None)
|
|
90
|
+
Chinese_Traditional = Language('Chinese (Traditional)', 'zh-tw', ['zh-cn', 'zh-tw'], 'cmn-CN', True)
|
|
91
|
+
Croatian = Language('Croatian', 'hr', None, None, None)
|
|
92
|
+
Czech = Language('Czech', 'cs', None, None, None)
|
|
93
|
+
Danish = Language('Danish', 'da', ['da'], 'da-DK', False)
|
|
94
|
+
Dutch = Language('Dutch', 'nl', ['nl'], 'nl-NL', False)
|
|
95
|
+
English = Language('English', 'en', ['en'], 'en-US', False)
|
|
96
|
+
Esperanto = Language('Esperanto', 'eo', None, None, None)
|
|
97
|
+
Estonian = Language('Estonian', 'et', None, None, None)
|
|
98
|
+
Finnish = Language('Finnish', 'fi', None, None, None)
|
|
99
|
+
French = Language('French', 'fr', ['fr'], 'fr-FR', False)
|
|
100
|
+
Galician = Language('Galician', 'gl', None, None, None)
|
|
101
|
+
German = Language('German', 'de', ['de'], 'de-DE', False)
|
|
102
|
+
Greek = Language('Greek', 'el', None, None, None)
|
|
103
|
+
Haitian_Creole = Language('Haitian Creole', 'ht', None, None, None)
|
|
104
|
+
Hindi = Language('Hindi', 'hi', ['hi'], 'hi-IN', True)
|
|
105
|
+
Hungarian = Language('Hungarian', 'hu', None, None, None)
|
|
106
|
+
Icelandic = Language('Icelandic', 'is', ['is'], 'is-IS', False)
|
|
107
|
+
Indonesian = Language('Indonesian', 'id', None, None, None)
|
|
108
|
+
Irish = Language('Irish', 'ga', None, None, None)
|
|
109
|
+
Italian = Language('Italian', 'it', ['it'], 'it-IT', False)
|
|
110
|
+
Japanese = Language('Japanese', 'ja', ['ja', 'ja-hira'], 'ja-JP', False)
|
|
111
|
+
Kannada = Language('Kannada', 'kn', None, None, None)
|
|
112
|
+
Korean = Language('Korean', 'ko', ['ko'], 'ko-KR', True)
|
|
113
|
+
Kurdish_Kurmanji = Language('Kurdish (Kurmanji)', 'ku', None, None, None)
|
|
114
|
+
Latin = Language('Latin', 'la', None, None, None)
|
|
115
|
+
Latvian = Language('Latvian', 'lv', None, None, None)
|
|
116
|
+
Lithuanian = Language('Lithuanian', 'lt', None, None, None)
|
|
117
|
+
Macedonian = Language('Macedonian', 'mk', None, None, None)
|
|
118
|
+
Malay = Language('Malay', 'ms', None, None, None)
|
|
119
|
+
Malayalam = Language('Malayalam', 'ml', None, None, None)
|
|
120
|
+
Maltese = Language('Maltese', 'mt', None, None, None)
|
|
121
|
+
Maori = Language('Maori', 'mi', None, None, None)
|
|
122
|
+
Marathi = Language('Marathi', 'mr', None, None, None)
|
|
123
|
+
Mongolian = Language('Mongolian', 'mn', None, None, None)
|
|
124
|
+
Myanmar_Burmese = Language('Myanmar (Burmese)', 'my', None, None, None)
|
|
125
|
+
Persian = Language('Persian', 'fa', None, None, None)
|
|
126
|
+
Polish = Language('Polish', 'pl', ['pl'], 'pl-PL', False)
|
|
127
|
+
Portuguese = Language('Portuguese', 'pt', ['pt'], 'pt-PT', False)
|
|
128
|
+
Romanian = Language('Romanian', 'ro', ['ro'], 'ro-RO', True)
|
|
129
|
+
Russian = Language('Russian', 'ru', ['ru'], 'ru-RU', False)
|
|
130
|
+
Scots_Gaelic = Language('Scots Gaelic', 'gd', None, None, None)
|
|
131
|
+
Serbian = Language('Serbian', 'sr', None, None, None)
|
|
132
|
+
Slovak = Language('Slovak', 'sk', None, None, None)
|
|
133
|
+
Slovenian = Language('Slovenian', 'sl', None, None, None)
|
|
134
|
+
Spanish = Language('Spanish', 'es', None, None, None)
|
|
135
|
+
Swedish = Language('Swedish', 'sv', ['sv'], 'sv-SE', True)
|
|
136
|
+
Telugu = Language('Telugu', 'te', None, None, None)
|
|
137
|
+
Thai = Language('Thai', 'th', None, None, None)
|
|
138
|
+
Turkish = Language('Turkish', 'tr', ['tr'], 'tr-TR', True)
|
|
139
|
+
Ukrainian = Language('Ukrainian', 'uk', None, None, None)
|
|
140
|
+
Uzbek = Language('Uzbek', 'uz', None, None, None)
|
|
141
|
+
Vietnamese = Language('Vietnamese', 'vi', None, None, None)
|
|
142
|
+
Welsh = Language('Welsh', 'cy', ['cy'], 'cy-GB', True)
|
|
143
|
+
Zulu = Language('Zulu', 'zu', None, None, None)
|
|
144
|
+
Hebrew = Language('Hebrew', 'he', None, None, None)
|
|
145
|
+
Chinese_Simplified = Language('Chinese (Simplified)', 'zh-cn', ['zh-cn', 'zh-tw'], 'cmn-CN', True)
|
|
146
|
+
Mandarin = Chinese_Simplified
|
|
147
|
+
|
|
148
|
+
nb_NO = Language(None, None, ['nb', 'nn'], 'nb-NO', True)
|
|
149
|
+
pt_BR = Language(None, None, ['pt-br'], 'pt-BR', False)
|
|
150
|
+
Brazilian = pt_BR
|
|
151
|
+
es_ES = Language(None, None, ['es'], 'es-ES', False)
|
|
152
|
+
es_US = Language(None, None, ['es-419'], 'es-US', False)
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def find(cls, value, by: str = "name", apply_func: Callable = None) -> Language:
|
|
156
|
+
return super().find(value, by, apply_func)
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def all_of(cls, attr_name: str = "name", apply_func: Callable = None) -> list:
|
|
160
|
+
return super().all_of(attr_name, apply_func)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(init=True, repr=True)
|
|
164
|
+
class TTSVoice:
|
|
165
|
+
name: str
|
|
166
|
+
gender: str
|
|
167
|
+
playback_rate: float | int = 1
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TTSVoices(_EnumWrapper):
|
|
171
|
+
alto = TTSVoice("alto", "female")
|
|
172
|
+
# female is functionally equal to alto
|
|
173
|
+
female = TTSVoice("female", "female")
|
|
174
|
+
|
|
175
|
+
tenor = TTSVoice("tenor", "male")
|
|
176
|
+
# male is functionally equal to tenor
|
|
177
|
+
male = TTSVoice("male", "male")
|
|
178
|
+
|
|
179
|
+
squeak = TTSVoice("squeak", "female", 1.19)
|
|
180
|
+
giant = TTSVoice("giant", "male", .84)
|
|
181
|
+
kitten = TTSVoice("kitten", "female", 1.41)
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def find(cls, value, by: str = "name", apply_func: Callable = None) -> TTSVoice:
|
|
185
|
+
return super().find(value, by, apply_func)
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def all_of(cls, attr_name: str = "name", apply_func: Callable = None) -> Iterable:
|
|
189
|
+
return super().all_of(attr_name, apply_func)
|
|
190
|
+
|
|
@@ -18,7 +18,6 @@ class Unauthenticated(Exception):
|
|
|
18
18
|
def __init__(self, message=""):
|
|
19
19
|
self.message = "No login / session connected.\n\nThe object on which the method was called was created using scratchattach.get_xyz()\nUse session.connect_xyz() instead (xyz is a placeholder for user / project / cloud / ...).\n\nMore information: https://scratchattach.readthedocs.io/en/latest/scratchattach.html#scratchattach.utils.exceptions.Unauthenticated"
|
|
20
20
|
super().__init__(self.message)
|
|
21
|
-
pass
|
|
22
21
|
|
|
23
22
|
|
|
24
23
|
class Unauthorized(Exception):
|
|
@@ -32,7 +31,6 @@ class Unauthorized(Exception):
|
|
|
32
31
|
self.message = "The user corresponding to the connected login / session is not allowed to perform this action."
|
|
33
32
|
super().__init__(self.message)
|
|
34
33
|
|
|
35
|
-
pass
|
|
36
34
|
|
|
37
35
|
class XTokenError(Exception):
|
|
38
36
|
"""
|
|
@@ -43,6 +41,7 @@ class XTokenError(Exception):
|
|
|
43
41
|
|
|
44
42
|
pass
|
|
45
43
|
|
|
44
|
+
|
|
46
45
|
# Not found errors:
|
|
47
46
|
|
|
48
47
|
class UserNotFound(Exception):
|
|
@@ -60,6 +59,7 @@ class ProjectNotFound(Exception):
|
|
|
60
59
|
|
|
61
60
|
pass
|
|
62
61
|
|
|
62
|
+
|
|
63
63
|
class ClassroomNotFound(Exception):
|
|
64
64
|
"""
|
|
65
65
|
Raised when a non-existent Classroom is requested.
|
|
@@ -75,15 +75,32 @@ class StudioNotFound(Exception):
|
|
|
75
75
|
|
|
76
76
|
pass
|
|
77
77
|
|
|
78
|
+
|
|
78
79
|
class ForumContentNotFound(Exception):
|
|
79
80
|
"""
|
|
80
81
|
Raised when a non-existent forum topic / post is requested.
|
|
81
82
|
"""
|
|
82
83
|
pass
|
|
83
84
|
|
|
85
|
+
|
|
84
86
|
class CommentNotFound(Exception):
|
|
85
87
|
pass
|
|
86
88
|
|
|
89
|
+
|
|
90
|
+
# Invalid inputs
|
|
91
|
+
class InvalidLanguage(Exception):
|
|
92
|
+
"""
|
|
93
|
+
Raised when an invalid language/language code/language object is provided, for TTS or Translate
|
|
94
|
+
"""
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class InvalidTTSGender(Exception):
|
|
99
|
+
"""
|
|
100
|
+
Raised when an invalid TTS gender is provided.
|
|
101
|
+
"""
|
|
102
|
+
pass
|
|
103
|
+
|
|
87
104
|
# API errors:
|
|
88
105
|
|
|
89
106
|
class LoginFailure(Exception):
|
|
@@ -95,6 +112,7 @@ class LoginFailure(Exception):
|
|
|
95
112
|
|
|
96
113
|
pass
|
|
97
114
|
|
|
115
|
+
|
|
98
116
|
class FetchError(Exception):
|
|
99
117
|
"""
|
|
100
118
|
Raised when getting information from the Scratch API fails. This can have various reasons. Make sure all provided arguments are valid.
|
|
@@ -102,6 +120,7 @@ class FetchError(Exception):
|
|
|
102
120
|
|
|
103
121
|
pass
|
|
104
122
|
|
|
123
|
+
|
|
105
124
|
class BadRequest(Exception):
|
|
106
125
|
"""
|
|
107
126
|
Raised when the Scratch API responds with a "Bad Request" error message. This can have various reasons. Make sure all provided arguments are valid.
|
|
@@ -117,6 +136,7 @@ class Response429(Exception):
|
|
|
117
136
|
|
|
118
137
|
pass
|
|
119
138
|
|
|
139
|
+
|
|
120
140
|
class CommentPostFailure(Exception):
|
|
121
141
|
"""
|
|
122
142
|
Raised when a comment fails to post. This can have various reasons.
|
|
@@ -124,12 +144,14 @@ class CommentPostFailure(Exception):
|
|
|
124
144
|
|
|
125
145
|
pass
|
|
126
146
|
|
|
147
|
+
|
|
127
148
|
class APIError(Exception):
|
|
128
149
|
"""
|
|
129
150
|
For API errors that can't be classified into one of the above errors
|
|
130
151
|
"""
|
|
131
152
|
pass
|
|
132
153
|
|
|
154
|
+
|
|
133
155
|
class ScrapeError(Exception):
|
|
134
156
|
"""
|
|
135
157
|
Raised when something goes wrong while web-scraping a page with bs4.
|
|
@@ -137,9 +159,10 @@ class ScrapeError(Exception):
|
|
|
137
159
|
|
|
138
160
|
pass
|
|
139
161
|
|
|
162
|
+
|
|
140
163
|
# Cloud / encoding errors:
|
|
141
164
|
|
|
142
|
-
class
|
|
165
|
+
class CloudConnectionError(Exception):
|
|
143
166
|
"""
|
|
144
167
|
Raised when connecting to Scratch's cloud server fails. This can have various reasons.
|
|
145
168
|
"""
|
|
@@ -172,12 +195,12 @@ class RequestNotFound(Exception):
|
|
|
172
195
|
|
|
173
196
|
pass
|
|
174
197
|
|
|
198
|
+
|
|
175
199
|
# Websocket server errors:
|
|
176
200
|
|
|
177
201
|
class WebsocketServerError(Exception):
|
|
178
|
-
|
|
179
202
|
"""
|
|
180
203
|
Raised when the self-hosted cloud websocket server fails to start.
|
|
181
204
|
"""
|
|
182
205
|
|
|
183
|
-
pass
|
|
206
|
+
pass
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: scratchattach
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.7
|
|
4
4
|
Summary: A Scratch API Wrapper
|
|
5
|
-
Home-page: https://
|
|
5
|
+
Home-page: https://scratchattach.tim1de.net
|
|
6
6
|
Author: TimMcCool
|
|
7
7
|
Author-email: timmccool.scratch@gmail.com
|
|
8
8
|
Keywords: scratch api,scratchattach,scratch api python,scratch python,scratch for python,scratch,scratch cloud,scratch cloud variables,scratch bot
|
|
@@ -25,7 +25,7 @@ The library allows setting cloud variables, following users, updating your profi
|
|
|
25
25
|
so much more! Additionally, it provides frameworks that simplify sending data through cloud variables.
|
|
26
26
|
|
|
27
27
|
<p align="left" style="margin:10px">
|
|
28
|
-
<img width="160" src="https://
|
|
28
|
+
<img width="160" src="https://raw.githubusercontent.com/TimMcCool/scratchattach/refs/heads/main/logos/logo.svg">
|
|
29
29
|
|
|
30
30
|
[](https://pypi.python.org/pypi/scratchattach/)
|
|
31
31
|
[](https://pypi.python.org/pypi/scratchattach/)
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
import codecs
|
|
3
3
|
import os
|
|
4
4
|
|
|
5
|
-
VERSION = '2.1.
|
|
5
|
+
VERSION = '2.1.7'
|
|
6
6
|
DESCRIPTION = 'A Scratch API Wrapper'
|
|
7
7
|
LONG_DESCRIPTION = DESCRIPTION
|
|
8
8
|
|
|
@@ -18,7 +18,7 @@ setup(
|
|
|
18
18
|
packages=find_packages(),
|
|
19
19
|
install_requires=["websocket-client","requests","bs4","SimpleWebSocketServer"],
|
|
20
20
|
keywords=['scratch api', 'scratchattach', 'scratch api python', 'scratch python', 'scratch for python', 'scratch', 'scratch cloud', 'scratch cloud variables', 'scratch bot'],
|
|
21
|
-
url='https://
|
|
21
|
+
url='https://scratchattach.tim1de.net',
|
|
22
22
|
classifiers=[
|
|
23
23
|
"Development Status :: 5 - Production/Stable",
|
|
24
24
|
"Intended Audience :: Developers",
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
"""Other Scratch API-related functions"""
|
|
2
|
-
|
|
3
|
-
from ..utils import commons
|
|
4
|
-
from ..utils.requests import Requests as requests
|
|
5
|
-
import json
|
|
6
|
-
|
|
7
|
-
# --- Front page ---
|
|
8
|
-
|
|
9
|
-
def get_news(*, limit=10, offset=0):
|
|
10
|
-
return commons.api_iterative("https://api.scratch.mit.edu/news", limit = limit, offset = offset)
|
|
11
|
-
|
|
12
|
-
def featured_data():
|
|
13
|
-
return requests.get("https://api.scratch.mit.edu/proxy/featured").json()
|
|
14
|
-
|
|
15
|
-
def featured_projects():
|
|
16
|
-
return featured_data()["community_featured_projects"]
|
|
17
|
-
|
|
18
|
-
def featured_studios():
|
|
19
|
-
return featured_data()["community_featured_studios"]
|
|
20
|
-
|
|
21
|
-
def top_loved():
|
|
22
|
-
return featured_data()["community_most_loved_projects"]
|
|
23
|
-
|
|
24
|
-
def top_remixed():
|
|
25
|
-
return featured_data()["community_most_remixed_projects"]
|
|
26
|
-
|
|
27
|
-
def newest_projects():
|
|
28
|
-
return featured_data()["community_newest_projects"]
|
|
29
|
-
|
|
30
|
-
def curated_projects():
|
|
31
|
-
return featured_data()["curator_top_projects"]
|
|
32
|
-
|
|
33
|
-
def design_studio_projects():
|
|
34
|
-
return featured_data()["scratch_design_studio"]
|
|
35
|
-
|
|
36
|
-
# --- Statistics ---
|
|
37
|
-
|
|
38
|
-
def total_site_stats():
|
|
39
|
-
data = requests.get("https://scratch.mit.edu/statistics/data/daily/").json()
|
|
40
|
-
data.pop("_TS")
|
|
41
|
-
return data
|
|
42
|
-
|
|
43
|
-
def monthly_site_traffic():
|
|
44
|
-
data = requests.get("https://scratch.mit.edu/statistics/data/monthly-ga/").json()
|
|
45
|
-
data.pop("_TS")
|
|
46
|
-
return data
|
|
47
|
-
|
|
48
|
-
def country_counts():
|
|
49
|
-
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["country_distribution"]
|
|
50
|
-
|
|
51
|
-
def age_distribution():
|
|
52
|
-
data = requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["age_distribution_data"][0]["values"]
|
|
53
|
-
return_data = {}
|
|
54
|
-
for value in data:
|
|
55
|
-
return_data[value["x"]] = value["y"]
|
|
56
|
-
return return_data
|
|
57
|
-
|
|
58
|
-
def monthly_comment_activity():
|
|
59
|
-
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["comment_data"]
|
|
60
|
-
|
|
61
|
-
def monthly_project_shares():
|
|
62
|
-
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["project_data"]
|
|
63
|
-
|
|
64
|
-
def monthly_active_users():
|
|
65
|
-
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["active_user_data"]
|
|
66
|
-
|
|
67
|
-
def monthly_activity_trends():
|
|
68
|
-
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["activity_data"]
|
|
69
|
-
|
|
70
|
-
# --- CSRF Token Generation API ---
|
|
71
|
-
|
|
72
|
-
def get_csrf_token():
|
|
73
|
-
"""
|
|
74
|
-
Generates a scratchcsrftoken using Scratch's API.
|
|
75
|
-
|
|
76
|
-
Returns:
|
|
77
|
-
str: The generated scratchcsrftoken
|
|
78
|
-
"""
|
|
79
|
-
return requests.get(
|
|
80
|
-
"https://scratch.mit.edu/csrf_token/"
|
|
81
|
-
).headers["set-cookie"].split(";")[3][len(" Path=/, scratchcsrftoken="):]
|
|
82
|
-
|
|
83
|
-
# --- Various other api.scratch.mit.edu API endpoints ---
|
|
84
|
-
|
|
85
|
-
def get_health():
|
|
86
|
-
return requests.get("https://api.scratch.mit.edu/health").json()
|
|
87
|
-
|
|
88
|
-
def get_total_project_count() -> int:
|
|
89
|
-
return requests.get("https://api.scratch.mit.edu/projects/count/all").json()["count"]
|
|
90
|
-
|
|
91
|
-
def check_username(username):
|
|
92
|
-
return requests.get(f"https://api.scratch.mit.edu/accounts/checkusername/{username}").json()["msg"]
|
|
93
|
-
|
|
94
|
-
def check_password(password):
|
|
95
|
-
return requests.post("https://api.scratch.mit.edu/accounts/checkpassword/", json={"password":password}).json()["msg"]
|
|
96
|
-
|
|
97
|
-
# --- April fools endpoints ---
|
|
98
|
-
|
|
99
|
-
def aprilfools_get_counter() -> int:
|
|
100
|
-
return requests.get("https://api.scratch.mit.edu/surprise").json()["surprise"]
|
|
101
|
-
|
|
102
|
-
def aprilfools_increment_counter() -> int:
|
|
103
|
-
return requests.post("https://api.scratch.mit.edu/surprise").json()["surprise"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/other/project_json_capabilities.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|