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.
Files changed (46) hide show
  1. {scratchattach-2.1.5 → scratchattach-2.1.7}/PKG-INFO +3 -3
  2. {scratchattach-2.1.5 → scratchattach-2.1.7}/README.md +1 -1
  3. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/__init__.py +2 -1
  4. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/cloud_server.py +2 -2
  5. scratchattach-2.1.7/scratchattach/other/other_apis.py +212 -0
  6. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/classroom.py +2 -2
  7. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/forum.py +15 -1
  8. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/project.py +9 -1
  9. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/session.py +77 -20
  10. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/studio.py +1 -1
  11. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/user.py +1 -1
  12. scratchattach-2.1.7/scratchattach/utils/enums.py +190 -0
  13. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/utils/exceptions.py +28 -5
  14. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach.egg-info/PKG-INFO +3 -3
  15. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach.egg-info/SOURCES.txt +1 -0
  16. {scratchattach-2.1.5 → scratchattach-2.1.7}/setup.py +2 -2
  17. scratchattach-2.1.5/scratchattach/other/other_apis.py +0 -103
  18. {scratchattach-2.1.5 → scratchattach-2.1.7}/LICENSE +0 -0
  19. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/cloud/__init__.py +0 -0
  20. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/cloud/_base.py +0 -0
  21. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/cloud/cloud.py +0 -0
  22. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/__init__.py +0 -0
  23. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/_base.py +0 -0
  24. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/cloud_events.py +0 -0
  25. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/cloud_recorder.py +0 -0
  26. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/cloud_requests.py +0 -0
  27. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/cloud_storage.py +0 -0
  28. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/combine.py +0 -0
  29. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/filterbot.py +0 -0
  30. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/eventhandlers/message_events.py +0 -0
  31. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/other/__init__.py +0 -0
  32. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/other/project_json_capabilities.py +0 -0
  33. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/__init__.py +0 -0
  34. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/_base.py +0 -0
  35. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/activity.py +0 -0
  36. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/backpack_asset.py +0 -0
  37. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/cloud_activity.py +0 -0
  38. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/site/comment.py +0 -0
  39. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/utils/__init__.py +0 -0
  40. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/utils/commons.py +0 -0
  41. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/utils/encoder.py +0 -0
  42. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach/utils/requests.py +0 -0
  43. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach.egg-info/dependency_links.txt +0 -0
  44. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach.egg-info/requires.txt +0 -0
  45. {scratchattach-2.1.5 → scratchattach-2.1.7}/scratchattach.egg-info/top_level.txt +0 -0
  46. {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.5
3
+ Version: 2.1.7
4
4
  Summary: A Scratch API Wrapper
5
- Home-page: https://github.com/TimMcCool/scratchattach
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://github.com/TimMcCool/scratchattach/blob/main/logos/logo.svg">
28
+ <img width="160" src="https://raw.githubusercontent.com/TimMcCool/scratchattach/refs/heads/main/logos/logo.svg">
29
29
 
30
30
  [![PyPI status](https://img.shields.io/pypi/status/scratchattach.svg)](https://pypi.python.org/pypi/scratchattach/)
31
31
  [![PyPI download month](https://img.shields.io/pypi/dm/scratchattach.svg)](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://github.com/TimMcCool/scratchattach/blob/main/logos/logo.svg">
7
+ <img width="160" src="https://raw.githubusercontent.com/TimMcCool/scratchattach/refs/heads/main/logos/logo.svg">
8
8
 
9
9
  [![PyPI status](https://img.shields.io/pypi/status/scratchattach.svg)](https://pypi.python.org/pypi/scratchattach/)
10
10
  [![PyPI download month](https://img.shields.io/pypi/dm/scratchattach.svg)](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 _update(self):
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:
@@ -575,7 +575,7 @@ class Studio(BaseSiteComponent):
575
575
  ).json()
576
576
 
577
577
 
578
- def get_studio(studio_id):
578
+ def get_studio(studio_id) -> Studio:
579
579
  """
580
580
  Gets a studio without logging in.
581
581
 
@@ -818,7 +818,7 @@ class User(BaseSiteComponent):
818
818
 
819
819
  # ------ #
820
820
 
821
- def get_user(username):
821
+ def get_user(username) -> User:
822
822
  """
823
823
  Gets a user without logging in.
824
824
 
@@ -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 ConnectionError(Exception):
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.5
3
+ Version: 2.1.7
4
4
  Summary: A Scratch API Wrapper
5
- Home-page: https://github.com/TimMcCool/scratchattach
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://github.com/TimMcCool/scratchattach/blob/main/logos/logo.svg">
28
+ <img width="160" src="https://raw.githubusercontent.com/TimMcCool/scratchattach/refs/heads/main/logos/logo.svg">
29
29
 
30
30
  [![PyPI status](https://img.shields.io/pypi/status/scratchattach.svg)](https://pypi.python.org/pypi/scratchattach/)
31
31
  [![PyPI download month](https://img.shields.io/pypi/dm/scratchattach.svg)](https://pypi.python.org/pypi/scratchattach/)
@@ -38,5 +38,6 @@ scratchattach/site/user.py
38
38
  scratchattach/utils/__init__.py
39
39
  scratchattach/utils/commons.py
40
40
  scratchattach/utils/encoder.py
41
+ scratchattach/utils/enums.py
41
42
  scratchattach/utils/exceptions.py
42
43
  scratchattach/utils/requests.py
@@ -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'
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://github.com/TimMcCool/scratchattach',
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