Skip to content

Commit 03c0475

Browse files
authored
Merge branch 'main' into main
2 parents 7269930 + 7a1e1e6 commit 03c0475

21 files changed

+180
-54
lines changed

scratchattach/__init__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,22 @@
1111
# from .other.project_json_capabilities import ProjectBody, get_empty_project_pb, get_pb_from_dict, read_sb3_file, download_asset
1212
from .utils.encoder import Encoding
1313
from .utils.enums import Languages, TTSVoices
14-
from .utils.exceptions import LoginDataWarning
14+
from .utils.exceptions import (
15+
LoginDataWarning,
16+
GetAuthenticationWarning,
17+
StudioAuthenticationWarning,
18+
ClassroomAuthenticationWarning,
19+
ProjectAuthenticationWarning,
20+
UserAuthenticationWarning)
1521

1622
from .site.activity import Activity, ActvityTypes
1723
from .site.backpack_asset import BackpackAsset
1824
from .site.comment import Comment, CommentSource
1925
from .site.cloud_activity import CloudActivity
2026
from .site.forum import ForumPost, ForumTopic, get_topic, get_topic_list, youtube_link_to_scratch
2127
from .site.project import Project, get_project, search_projects, explore_projects
22-
from .site.session import Session, login, login_by_id, login_by_session_string, login_by_io, login_by_file, login_from_browser
28+
from .site.session import Session, login, login_by_id, login_by_session_string, login_by_io, login_by_file, \
29+
login_from_browser
2330
from .site.studio import Studio, get_studio, search_studios, explore_studios
2431
from .site.classroom import Classroom, get_classroom
2532
from .site.user import User, get_user, Rank

scratchattach/cloud/cloud.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from scratchattach.utils import exceptions, commons
99
from scratchattach.site import cloud_activity
1010

11+
from websocket import WebSocketBadStatusException
1112

1213
class ScratchCloud(BaseCloud):
1314
def __init__(self, *, project_id, _session=None):
@@ -26,7 +27,10 @@ def __init__(self, *, project_id, _session=None):
2627

2728
def connect(self):
2829
self._assert_auth() # Connecting to Scratch's cloud websocket requires a login to the Scratch website
29-
super().connect()
30+
try:
31+
super().connect()
32+
except WebSocketBadStatusException as e:
33+
raise WebSocketBadStatusException(f"Error: Scratch's Cloud system may be down. Please try again later.") from e
3034

3135
def set_var(self, variable, value):
3236
self._assert_auth() # Setting a cloud var requires a login to the Scratch website

scratchattach/site/classroom.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,17 @@ def update(self):
9191
if pfx in script.text:
9292
educator_username = commons.webscrape_count(script.text, pfx, sfx, str)
9393

94-
ret = {"id": self.id,
95-
"title": title,
96-
"description": description,
97-
"status": status,
98-
"educator": {"username": educator_username},
99-
"is_closed": True
100-
}
94+
ret: typed_dicts.ClassroomDict = {
95+
"id": self.id,
96+
"title": title,
97+
"description": description,
98+
"educator": {},
99+
"status": status,
100+
"is_closed": True
101+
}
102+
103+
if educator_username:
104+
ret["educator"]["username"] = educator_username
101105

102106
return self._update_from_dict(ret)
103107
return success
@@ -136,7 +140,7 @@ def student_names(self, *, page=1) -> list[str]:
136140
response = requests.get(f"https://scratch.mit.edu/classes/{self.id}/")
137141
soup = BeautifulSoup(response.text, "html.parser")
138142
found = set("")
139-
143+
140144
for result in soup.css.select("ul.scroll-content .user a"):
141145
result_text = result.text.strip()
142146
if result_text in found:
@@ -185,7 +189,7 @@ def class_studio_ids(self, *, page: int = 1) -> list[int]:
185189
ret = []
186190
response = requests.get(f"https://scratch.mit.edu/classes/{self.id}/")
187191
soup = BeautifulSoup(response.text, "html.parser")
188-
192+
189193
for result in soup.css.select("ul.scroll-content .gallery a[href]:not([class])"):
190194
value = result["href"]
191195
if not isinstance(value, str):
@@ -392,7 +396,13 @@ def get_classroom(class_id: str) -> Classroom:
392396
393397
If you want to use these, get the user with :meth:`scratchattach.session.Session.connect_classroom` instead.
394398
"""
395-
warnings.warn("For methods that require authentication, use session.connect_classroom instead of get_classroom")
399+
warnings.warn(
400+
"For methods that require authentication, use session.connect_classroom instead of get_classroom\n"
401+
"If you want to remove this warning, use warnings.filterwarnings('ignore', category=scratchattach.ClassroomAuthenticationWarning)\n"
402+
"To ignore all warnings of the type GetAuthenticationWarning, which includes this warning, use "
403+
"`warnings.filterwarnings('ignore', category=scratchattach.GetAuthenticationWarning)`.",
404+
exceptions.ClassroomAuthenticationWarning
405+
)
396406
return commons._get_object("id", class_id, Classroom, exceptions.ClassroomNotFound)
397407

398408

@@ -411,7 +421,13 @@ def get_classroom_from_token(class_token) -> Classroom:
411421
412422
If you want to use these, get the user with :meth:`scratchattach.session.Session.connect_classroom` instead.
413423
"""
414-
warnings.warn("For methods that require authentication, use session.connect_classroom instead of get_classroom")
424+
warnings.warn(
425+
"For methods that require authentication, use session.connect_classroom instead of get_classroom. "
426+
"If you want to remove this warning, use warnings.filterwarnings('ignore', category=ClassroomAuthenticationWarning). "
427+
"To ignore all warnings of the type GetAuthenticationWarning, which includes this warning, use "
428+
"warnings.filterwarnings('ignore', category=GetAuthenticationWarning).",
429+
exceptions.ClassroomAuthenticationWarning
430+
)
415431
return commons._get_object("classtoken", class_token, Classroom, exceptions.ClassroomNotFound)
416432

417433

scratchattach/site/project.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import random
77
import base64
88
import time
9+
import warnings
910
import zipfile
1011
from io import BytesIO
1112
from typing import Callable
@@ -933,7 +934,14 @@ def get_project(project_id) -> Project:
933934
934935
If you want to use these methods, get the project with :meth:`scratchattach.session.Session.connect_project` instead.
935936
"""
936-
print("Warning: For methods that require authentication, use session.connect_project instead of get_project")
937+
warnings.warn(
938+
"Warning: For methods that require authentication, use session.connect_project instead of get_project.\n"
939+
"If you want to remove this warning, "
940+
"use `warnings.filterwarnings('ignore', category=scratchattach.ProjectAuthenticationWarning)`.\n"
941+
"To ignore all warnings of the type GetAuthenticationWarning, which includes this warning, use "
942+
"`warnings.filterwarnings('ignore', category=scratchattach.GetAuthenticationWarning)`.",
943+
exceptions.ProjectAuthenticationWarning
944+
)
937945
return commons._get_object("id", project_id, Project, exceptions.ProjectNotFound)
938946

939947

scratchattach/site/session.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def enforce_ratelimit(__type: str, name: str, amount: int = 5, duration: int = 6
4949
cache = ratelimit_cache
5050
cache.setdefault(__type, [])
5151
uses = cache[__type]
52-
while uses[-1] < time.time() - duration:
52+
while uses and uses[-1] < time.time() - duration:
5353
uses.pop()
5454
if len(uses) < amount:
5555
uses.insert(0, time.time())
@@ -94,6 +94,7 @@ class Session(BaseSiteComponent):
9494

9595
has_outstanding_email_confirmation: bool = field(repr=False, default=False)
9696
is_teacher: bool = field(repr=False, default=False)
97+
is_teacher_invitee: bool = field(repr=False, default=False)
9798
_session: Optional[Session] = field(kw_only=True, default=None)
9899

99100
def __str__(self) -> str:
@@ -139,6 +140,7 @@ def _update_from_dict(self, data: Union[dict, typed_dicts.SessionDict]):
139140

140141
self.new_scratcher = data["permissions"]["new_scratcher"]
141142
self.is_teacher = data["permissions"]["educator"]
143+
self.is_teacher_invitee = data["permissions"]["educator_invitee"]
142144

143145
self.mute_status = data["permissions"]["mute_status"]
144146

scratchattach/site/studio.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Studio class"""
22
from __future__ import annotations
33

4+
import warnings
45
import json
56
import random
67
from . import user, comment, project, activity
@@ -592,7 +593,13 @@ def get_studio(studio_id) -> Studio:
592593
593594
If you want to use these, get the studio with :meth:`scratchattach.session.Session.connect_studio` instead.
594595
"""
595-
print("Warning: For methods that require authentication, use session.connect_studio instead of get_studio")
596+
warnings.warn(
597+
"Warning: For methods that require authentication, use session.connect_studio instead of get_studio.\n"
598+
"If you want to remove this warning, use warnings.filterwarnings('ignore', category=scratchattach.StudioAuthenticationWarning).\n"
599+
"To ignore all warnings of the type GetAuthenticationWarning, which includes this warning, use "
600+
"`warnings.filterwarnings('ignore', category=scratchattach.GetAuthenticationWarning)`.",
601+
exceptions.StudioAuthenticationWarning
602+
)
596603
return commons._get_object("id", studio_id, Studio, exceptions.StudioNotFound)
597604

598605
def search_studios(*, query="", mode="trending", language="en", limit=40, offset=0):

scratchattach/site/typed_dicts.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from scratchattach.cloud import _base
4-
from typing import TypedDict, Union, Optional
4+
from typing import TypedDict, Union, Optional, Required, NotRequired
55

66
class SessionUserDict(TypedDict):
77
id: int
@@ -67,11 +67,11 @@ class UserProfileDict(TypedDict):
6767
images: dict[str, str]
6868

6969
class UserDict(TypedDict):
70-
id: int
71-
username: str
72-
scratchteam: bool
73-
history: UserHistoryDict
74-
profile: UserProfileDict
70+
id: NotRequired[int]
71+
username: NotRequired[str]
72+
scratchteam: NotRequired[bool]
73+
history: NotRequired[UserHistoryDict]
74+
profile: NotRequired[UserProfileDict]
7575

7676
class CloudLogActivityDict(TypedDict):
7777
user: str
@@ -93,7 +93,8 @@ class ClassroomDict(TypedDict):
9393
title: str
9494
description: str
9595
status: str
96-
date_start: str
97-
date_end: Optional[str]
98-
images: dict[str, str]
96+
date_start: NotRequired[str]
97+
date_end: NotRequired[Optional[str]]
98+
images: NotRequired[dict[str, str]]
9999
educator: UserDict
100+
is_closed: NotRequired[bool]

scratchattach/site/user.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -990,5 +990,11 @@ def get_user(username) -> User:
990990
991991
If you want to use these, get the user with :meth:`scratchattach.session.Session.connect_user` instead.
992992
"""
993-
print("Warning: For methods that require authentication, use session.connect_user instead of get_user")
993+
warnings.warn(
994+
"Warning: For methods that require authentication, use session.connect_user instead of get_user.\n"
995+
"To ignore this warning, use warnings.filterwarnings('ignore', category=scratchattach.UserAuthenticationWarning).\n"
996+
"To ignore all warnings of the type GetAuthenticationWarning, which includes this warning, use "
997+
"`warnings.filterwarnings('ignore', category=scratchattach.GetAuthenticationWarning)`.",
998+
exceptions.UserAuthenticationWarning
999+
)
9941000
return commons._get_object("username", username, User, exceptions.UserNotFound)

scratchattach/utils/exceptions.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,32 @@ class InvalidUpdateWarning(UserWarning):
236236
"""
237237
Warns you that something cannot be updated.
238238
"""
239+
240+
class GetAuthenticationWarning(UserWarning):
241+
"""
242+
All authentication warnings.
243+
"""
244+
245+
class UserAuthenticationWarning(GetAuthenticationWarning):
246+
"""
247+
Warns you to use session.connect_user instead of user.get_user
248+
for actions that require authentication.
249+
"""
250+
251+
class ProjectAuthenticationWarning(GetAuthenticationWarning):
252+
"""
253+
Warns you to use session.connect_project instead of project.get_project
254+
for actions that require authentication.
255+
"""
256+
257+
class StudioAuthenticationWarning(GetAuthenticationWarning):
258+
"""
259+
Warns you to use session.connect_studio instead of studio.get_studio
260+
for actions that require authentication.
261+
"""
262+
263+
class ClassroomAuthenticationWarning(GetAuthenticationWarning):
264+
"""
265+
Warns you to use session.connect_classroom or session.connect_classroom_from_token instead of classroom.get_classroom
266+
for actions that require authentication.
267+
"""

tests/test_activity.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import sys
22
from datetime import datetime, timedelta, timezone
3+
import warnings
34

45

56

@@ -8,6 +9,9 @@ def test_activity():
89
import scratchattach as sa
910
from scratchattach.utils import exceptions
1011
import util
12+
if not util.credentials_available():
13+
warnings.warn("Skipped test_activity because there were no credentials available.")
14+
return
1115
sess = util.session()
1216

1317
# we cannot do assertions, but we can probe for any errors.

0 commit comments

Comments
 (0)