scratchattach 2.1.15b0__py3-none-any.whl → 3.0.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli/__about__.py +1 -0
- cli/__init__.py +26 -0
- cli/cmd/__init__.py +4 -0
- cli/cmd/group.py +127 -0
- cli/cmd/login.py +60 -0
- cli/cmd/profile.py +7 -0
- cli/cmd/sessions.py +5 -0
- cli/context.py +142 -0
- cli/db.py +66 -0
- cli/namespace.py +14 -0
- {scratchattach/cloud → cloud}/_base.py +112 -87
- {scratchattach/cloud → cloud}/cloud.py +16 -16
- {scratchattach/editor → editor}/__init__.py +2 -1
- {scratchattach/editor → editor}/asset.py +26 -14
- {scratchattach/editor → editor}/backpack_json.py +3 -5
- {scratchattach/editor → editor}/base.py +2 -4
- {scratchattach/editor → editor}/block.py +27 -22
- {scratchattach/editor → editor}/blockshape.py +1 -1
- {scratchattach/editor → editor}/build_defaulting.py +2 -2
- editor/commons.py +145 -0
- {scratchattach/editor → editor}/field.py +1 -1
- {scratchattach/editor → editor}/inputs.py +6 -3
- {scratchattach/editor → editor}/meta.py +10 -7
- {scratchattach/editor → editor}/monitor.py +10 -8
- {scratchattach/editor → editor}/mutation.py +68 -11
- {scratchattach/editor → editor}/pallete.py +1 -3
- {scratchattach/editor → editor}/prim.py +4 -0
- {scratchattach/editor → editor}/project.py +118 -16
- {scratchattach/editor → editor}/sprite.py +25 -15
- {scratchattach/editor → editor}/vlb.py +2 -2
- {scratchattach/eventhandlers → eventhandlers}/_base.py +1 -0
- {scratchattach/eventhandlers → eventhandlers}/cloud_events.py +26 -6
- {scratchattach/eventhandlers → eventhandlers}/cloud_recorder.py +4 -4
- {scratchattach/eventhandlers → eventhandlers}/cloud_requests.py +139 -54
- {scratchattach/eventhandlers → eventhandlers}/cloud_server.py +6 -3
- {scratchattach/eventhandlers → eventhandlers}/cloud_storage.py +1 -2
- eventhandlers/filterbot.py +163 -0
- other/other_apis.py +598 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/METADATA +7 -11
- scratchattach-3.0.0b1.dist-info/RECORD +79 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/WHEEL +1 -1
- scratchattach-3.0.0b1.dist-info/entry_points.txt +2 -0
- scratchattach-3.0.0b1.dist-info/top_level.txt +7 -0
- {scratchattach/site → site}/_base.py +32 -5
- site/activity.py +426 -0
- {scratchattach/site → site}/alert.py +4 -5
- {scratchattach/site → site}/backpack_asset.py +2 -1
- {scratchattach/site → site}/classroom.py +80 -73
- {scratchattach/site → site}/cloud_activity.py +43 -29
- {scratchattach/site → site}/comment.py +86 -100
- {scratchattach/site → site}/forum.py +8 -4
- site/placeholder.py +132 -0
- {scratchattach/site → site}/project.py +228 -122
- {scratchattach/site → site}/session.py +156 -71
- {scratchattach/site → site}/studio.py +139 -46
- site/typed_dicts.py +151 -0
- {scratchattach/site → site}/user.py +511 -215
- {scratchattach/utils → utils}/commons.py +12 -4
- {scratchattach/utils → utils}/encoder.py +7 -4
- {scratchattach/utils → utils}/enums.py +1 -0
- {scratchattach/utils → utils}/exceptions.py +36 -2
- utils/optional_async.py +154 -0
- utils/requests.py +306 -0
- scratchattach/__init__.py +0 -29
- scratchattach/editor/commons.py +0 -273
- scratchattach/eventhandlers/filterbot.py +0 -161
- scratchattach/other/other_apis.py +0 -284
- scratchattach/site/activity.py +0 -382
- scratchattach/utils/requests.py +0 -93
- scratchattach-2.1.15b0.dist-info/RECORD +0 -66
- scratchattach-2.1.15b0.dist-info/top_level.txt +0 -1
- {scratchattach/cloud → cloud}/__init__.py +0 -0
- {scratchattach/editor → editor}/code_translation/__init__.py +0 -0
- {scratchattach/editor → editor}/code_translation/parse.py +0 -0
- {scratchattach/editor → editor}/comment.py +0 -0
- {scratchattach/editor → editor}/extension.py +0 -0
- {scratchattach/editor → editor}/twconfig.py +0 -0
- {scratchattach/eventhandlers → eventhandlers}/__init__.py +0 -0
- {scratchattach/eventhandlers → eventhandlers}/combine.py +0 -0
- {scratchattach/eventhandlers → eventhandlers}/message_events.py +0 -0
- {scratchattach/other → other}/__init__.py +0 -0
- {scratchattach/other → other}/project_json_capabilities.py +0 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
- {scratchattach/site → site}/__init__.py +0 -0
- {scratchattach/site → site}/browser_cookie3_stub.py +0 -0
- {scratchattach/site → site}/browser_cookies.py +0 -0
- {scratchattach/utils → utils}/__init__.py +0 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import datetime
|
|
4
3
|
import json
|
|
5
4
|
import warnings
|
|
6
5
|
from dataclasses import dataclass, field
|
|
@@ -14,7 +13,7 @@ if TYPE_CHECKING:
|
|
|
14
13
|
from scratchattach.site.session import Session
|
|
15
14
|
|
|
16
15
|
from scratchattach.utils.commons import requests
|
|
17
|
-
from . import user, activity
|
|
16
|
+
from . import user, activity, typed_dicts
|
|
18
17
|
from ._base import BaseSiteComponent
|
|
19
18
|
from scratchattach.utils import exceptions, commons
|
|
20
19
|
from scratchattach.utils.commons import headers
|
|
@@ -22,16 +21,16 @@ from scratchattach.utils.commons import headers
|
|
|
22
21
|
|
|
23
22
|
@dataclass
|
|
24
23
|
class Classroom(BaseSiteComponent):
|
|
25
|
-
title: str =
|
|
26
|
-
id: int =
|
|
27
|
-
classtoken: str =
|
|
24
|
+
title: str = ""
|
|
25
|
+
id: int = 0
|
|
26
|
+
classtoken: str = ""
|
|
28
27
|
|
|
29
|
-
author: user.User = None
|
|
30
|
-
about_class: str =
|
|
31
|
-
working_on: str =
|
|
28
|
+
author: Optional[user.User] = None
|
|
29
|
+
about_class: str = ""
|
|
30
|
+
working_on: str = ""
|
|
32
31
|
|
|
33
32
|
is_closed: bool = False
|
|
34
|
-
datetime: datetime =
|
|
33
|
+
datetime: datetime = datetime.fromtimestamp(0.0)
|
|
35
34
|
|
|
36
35
|
|
|
37
36
|
update_function: Callable = field(repr=False, default=requests.get)
|
|
@@ -92,47 +91,30 @@ class Classroom(BaseSiteComponent):
|
|
|
92
91
|
if pfx in script.text:
|
|
93
92
|
educator_username = commons.webscrape_count(script.text, pfx, sfx, str)
|
|
94
93
|
|
|
95
|
-
ret = {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
102
105
|
|
|
103
106
|
return self._update_from_dict(ret)
|
|
104
107
|
return success
|
|
105
108
|
|
|
106
|
-
def _update_from_dict(self,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
try:
|
|
116
|
-
self.about_class = classrooms["description"]
|
|
117
|
-
except Exception:
|
|
118
|
-
pass
|
|
119
|
-
try:
|
|
120
|
-
self.working_on = classrooms["status"]
|
|
121
|
-
except Exception:
|
|
122
|
-
pass
|
|
123
|
-
try:
|
|
124
|
-
self.datetime = datetime.datetime.fromisoformat(classrooms["date_start"])
|
|
125
|
-
except Exception:
|
|
126
|
-
pass
|
|
127
|
-
try:
|
|
128
|
-
self.author = user.User(username=classrooms["educator"]["username"], _session=self._session)
|
|
129
|
-
except Exception:
|
|
130
|
-
pass
|
|
131
|
-
try:
|
|
132
|
-
self.author._update_from_dict(classrooms["educator"])
|
|
133
|
-
except Exception:
|
|
134
|
-
pass
|
|
135
|
-
self.is_closed = classrooms.get("is_closed", False)
|
|
109
|
+
def _update_from_dict(self, data: typed_dicts.ClassroomDict):
|
|
110
|
+
self.id = int(data["id"])
|
|
111
|
+
self.title = data["title"]
|
|
112
|
+
self.about_class = data["description"]
|
|
113
|
+
self.working_on = data["status"]
|
|
114
|
+
self.datetime = datetime.fromisoformat(data["date_start"])
|
|
115
|
+
self.author = user.User(username=data["educator"]["username"], _session=self._session)
|
|
116
|
+
self.author.supply_data_dict(data["educator"])
|
|
117
|
+
self.is_closed = bool(data["date_end"])
|
|
136
118
|
return True
|
|
137
119
|
|
|
138
120
|
def student_count(self) -> int:
|
|
@@ -157,14 +139,24 @@ class Classroom(BaseSiteComponent):
|
|
|
157
139
|
ret = []
|
|
158
140
|
response = requests.get(f"https://scratch.mit.edu/classes/{self.id}/")
|
|
159
141
|
soup = BeautifulSoup(response.text, "html.parser")
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
142
|
+
found = set("")
|
|
143
|
+
|
|
144
|
+
for result in soup.css.select("ul.scroll-content .user a"):
|
|
145
|
+
result_text = result.text.strip()
|
|
146
|
+
if result_text in found:
|
|
147
|
+
continue
|
|
148
|
+
found.add(result_text)
|
|
149
|
+
ret.append(result_text)
|
|
150
|
+
|
|
151
|
+
# for scrollable in soup.find_all("ul", {"class": "scroll-content"}):
|
|
152
|
+
# if not isinstance(scrollable, Tag):
|
|
153
|
+
# continue
|
|
154
|
+
# for item in scrollable.contents:
|
|
155
|
+
# if not isinstance(item, bs4.NavigableString):
|
|
156
|
+
# if "user" in item.attrs["class"]:
|
|
157
|
+
# anchors = item.find_all("a")
|
|
158
|
+
# if len(anchors) == 2:
|
|
159
|
+
# ret.append(anchors[1].text.strip())
|
|
168
160
|
|
|
169
161
|
return ret
|
|
170
162
|
|
|
@@ -198,13 +190,19 @@ class Classroom(BaseSiteComponent):
|
|
|
198
190
|
response = requests.get(f"https://scratch.mit.edu/classes/{self.id}/")
|
|
199
191
|
soup = BeautifulSoup(response.text, "html.parser")
|
|
200
192
|
|
|
201
|
-
for
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
193
|
+
for result in soup.css.select("ul.scroll-content .gallery a[href]:not([class])"):
|
|
194
|
+
value = result["href"]
|
|
195
|
+
if not isinstance(value, str):
|
|
196
|
+
value = value[0]
|
|
197
|
+
ret.append(commons.webscrape_count(value, "/studios/", "/"))
|
|
198
|
+
|
|
199
|
+
# for scrollable in soup.find_all("ul", {"class": "scroll-content"}):
|
|
200
|
+
# for item in scrollable.contents:
|
|
201
|
+
# if not isinstance(item, bs4.NavigableString):
|
|
202
|
+
# if "gallery" in item.attrs["class"]:
|
|
203
|
+
# anchor = item.find("a")
|
|
204
|
+
# if "href" in anchor.attrs:
|
|
205
|
+
# ret.append(commons.webscrape_count(anchor.attrs["href"], "/studios/", "/"))
|
|
208
206
|
return ret
|
|
209
207
|
|
|
210
208
|
text = requests.get(
|
|
@@ -317,7 +315,7 @@ class Classroom(BaseSiteComponent):
|
|
|
317
315
|
def register_student(self, username: str, password: str = '', birth_month: Optional[int] = None,
|
|
318
316
|
birth_year: Optional[int] = None,
|
|
319
317
|
gender: Optional[str] = None, country: Optional[str] = None, is_robot: bool = False) -> None:
|
|
320
|
-
return register_by_token(self.id, self.classtoken, username, password, birth_month, birth_year, gender, country,
|
|
318
|
+
return register_by_token(self.id, self.classtoken, username, password, birth_month or 1, birth_year or 2000, gender or "(Prefer not to say)", country or "United+States",
|
|
321
319
|
is_robot)
|
|
322
320
|
|
|
323
321
|
def generate_signup_link(self):
|
|
@@ -356,8 +354,7 @@ class Classroom(BaseSiteComponent):
|
|
|
356
354
|
|
|
357
355
|
return activities
|
|
358
356
|
|
|
359
|
-
def activity(self, student: str = "all", mode: str = "Last created", page: Optional[int] = None) -> list[
|
|
360
|
-
dict[str, Any]]:
|
|
357
|
+
def activity(self, student: str = "all", mode: str = "Last created", page: Optional[int] = None) -> list[activity.Activity]:
|
|
361
358
|
"""
|
|
362
359
|
Get a list of private activity, only available to the class owner.
|
|
363
360
|
Returns:
|
|
@@ -370,18 +367,16 @@ class Classroom(BaseSiteComponent):
|
|
|
370
367
|
|
|
371
368
|
with requests.no_error_handling():
|
|
372
369
|
try:
|
|
373
|
-
data = requests.get(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
headers=self._headers, cookies=self._cookies
|
|
377
|
-
).json()
|
|
370
|
+
data = requests.get(f"https://scratch.mit.edu/site-api/classrooms/activity/{self.id}/{student}/",
|
|
371
|
+
params={"page": page, "ascsort": ascsort, "descsort": descsort},
|
|
372
|
+
headers=self._headers, cookies=self._cookies).json()
|
|
378
373
|
except json.JSONDecodeError:
|
|
379
374
|
return []
|
|
380
375
|
|
|
381
|
-
_activity = []
|
|
376
|
+
_activity: list[activity.Activity] = []
|
|
382
377
|
for activity_json in data:
|
|
383
378
|
_activity.append(activity.Activity(_session=self._session))
|
|
384
|
-
_activity[-1]._update_from_json(activity_json)
|
|
379
|
+
_activity[-1]._update_from_json(activity_json) # NOT the same as _update_from_dict
|
|
385
380
|
|
|
386
381
|
return _activity
|
|
387
382
|
|
|
@@ -401,7 +396,13 @@ def get_classroom(class_id: str) -> Classroom:
|
|
|
401
396
|
|
|
402
397
|
If you want to use these, get the user with :meth:`scratchattach.session.Session.connect_classroom` instead.
|
|
403
398
|
"""
|
|
404
|
-
warnings.warn(
|
|
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
|
+
)
|
|
405
406
|
return commons._get_object("id", class_id, Classroom, exceptions.ClassroomNotFound)
|
|
406
407
|
|
|
407
408
|
|
|
@@ -420,7 +421,13 @@ def get_classroom_from_token(class_token) -> Classroom:
|
|
|
420
421
|
|
|
421
422
|
If you want to use these, get the user with :meth:`scratchattach.session.Session.connect_classroom` instead.
|
|
422
423
|
"""
|
|
423
|
-
warnings.warn(
|
|
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
|
+
)
|
|
424
431
|
return commons._get_object("classtoken", class_token, Classroom, exceptions.ClassroomNotFound)
|
|
425
432
|
|
|
426
433
|
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
-
from
|
|
5
|
-
|
|
4
|
+
from typing import Union, TypeGuard, Optional
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
import warnings
|
|
6
7
|
|
|
8
|
+
from scratchattach.cloud import _base
|
|
9
|
+
from scratchattach.utils import exceptions
|
|
10
|
+
from scratchattach.site import project, user
|
|
11
|
+
from ._base import BaseSiteComponent
|
|
12
|
+
from . import typed_dicts, session
|
|
7
13
|
|
|
8
|
-
|
|
14
|
+
@dataclass
|
|
15
|
+
class CloudActivity(BaseSiteComponent[Union[typed_dicts.CloudActivityDict, typed_dicts.CloudLogActivityDict]]):
|
|
9
16
|
"""
|
|
10
17
|
Represents a cloud activity (a cloud variable set / creation / deletion).
|
|
11
18
|
|
|
@@ -25,6 +32,14 @@ class CloudActivity(BaseSiteComponent):
|
|
|
25
32
|
|
|
26
33
|
:.cloud: The cloud (as object inheriting from scratchattach.Cloud.BaseCloud) that the cloud activity corresponds to
|
|
27
34
|
"""
|
|
35
|
+
username: str = field(kw_only=True, default="")
|
|
36
|
+
var: str = field(kw_only=True, default="")
|
|
37
|
+
name: str = field(kw_only=True, default="")
|
|
38
|
+
type: str = field(kw_only=True, default="set")
|
|
39
|
+
timestamp: float = field(kw_only=True, default=0.0)
|
|
40
|
+
value: Union[float, int, str] = field(kw_only=True, default="0.0")
|
|
41
|
+
cloud: _base.AnyCloud = field(kw_only=True, default_factory=lambda : _base.DummyCloud())
|
|
42
|
+
_session: Optional[session.Session] = field(kw_only=True, default=None)
|
|
28
43
|
|
|
29
44
|
def __init__(self, **entries):
|
|
30
45
|
# Set attributes every CloudActivity object needs to have:
|
|
@@ -39,32 +54,30 @@ class CloudActivity(BaseSiteComponent):
|
|
|
39
54
|
self.__dict__.update(entries)
|
|
40
55
|
|
|
41
56
|
def update(self):
|
|
42
|
-
|
|
57
|
+
warnings.warn("CloudActivity objects can't be updated", exceptions.InvalidUpdateWarning)
|
|
43
58
|
return False # Objects of this type cannot be updated
|
|
44
59
|
|
|
45
60
|
def __eq__(self, activity2):
|
|
46
61
|
# CloudLogEvents needs to check if two activites are equal (to finde new ones), therefore CloudActivity objects need to be comparable
|
|
47
62
|
return self.user == activity2.user and self.type == activity2.type and self.timestamp == activity2.timestamp and self.value == activity2.value and self.name == activity2.name
|
|
48
63
|
|
|
49
|
-
def _update_from_dict(self, data) -> bool:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
try: self.cloud = data["cloud"]
|
|
67
|
-
except Exception: pass
|
|
64
|
+
def _update_from_dict(self, data: Union[typed_dicts.CloudActivityDict, typed_dicts.CloudLogActivityDict]) -> bool:
|
|
65
|
+
def is_cloud_log_activity(activity: Union[typed_dicts.CloudActivityDict, typed_dicts.CloudLogActivityDict]) -> TypeGuard[typed_dicts.CloudLogActivityDict]:
|
|
66
|
+
return "verb" in activity
|
|
67
|
+
def is_cloud_activity(activity: Union[typed_dicts.CloudActivityDict, typed_dicts.CloudLogActivityDict]) -> TypeGuard[typed_dicts.CloudActivityDict]:
|
|
68
|
+
return "method" in activity
|
|
69
|
+
self.name = data["name"]
|
|
70
|
+
self.var = data["name"]
|
|
71
|
+
self.value = data["value"]
|
|
72
|
+
if is_cloud_log_activity(data):
|
|
73
|
+
self.user = data["user"]
|
|
74
|
+
self.username = data["user"]
|
|
75
|
+
self.timestamp = data["timestamp"]
|
|
76
|
+
self.type = data["verb"].removesuffix("_var")
|
|
77
|
+
elif is_cloud_activity(data):
|
|
78
|
+
self.type = data["method"]
|
|
79
|
+
if "cloud" in data:
|
|
80
|
+
self.cloud = data["cloud"]
|
|
68
81
|
return True
|
|
69
82
|
|
|
70
83
|
def load_log_data(self):
|
|
@@ -91,17 +104,18 @@ class CloudActivity(BaseSiteComponent):
|
|
|
91
104
|
"""
|
|
92
105
|
if self.username is None:
|
|
93
106
|
return None
|
|
94
|
-
from scratchattach.site import user
|
|
95
|
-
from scratchattach.utils import exceptions
|
|
96
107
|
return self._make_linked_object("username", self.username, user.User, exceptions.UserNotFound)
|
|
97
108
|
|
|
98
|
-
def project(self):
|
|
109
|
+
def project(self) -> Optional[project.Project]:
|
|
99
110
|
"""
|
|
100
111
|
Returns the project where the cloud activity was performed as scratchattach.project.Project object
|
|
101
112
|
"""
|
|
113
|
+
def make_linked(cloud: _base.BaseCloud) -> project.Project:
|
|
114
|
+
return self._make_linked_object("id", cloud.project_id, project.Project, exceptions.ProjectNotFound)
|
|
102
115
|
if self.cloud is None:
|
|
103
116
|
return None
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
cloud = self.cloud
|
|
118
|
+
if not isinstance(cloud, _base.BaseCloud):
|
|
119
|
+
return None
|
|
120
|
+
return make_linked(cloud)
|
|
107
121
|
|
|
@@ -1,103 +1,84 @@
|
|
|
1
1
|
"""Comment class"""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import warnings
|
|
4
5
|
import html
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing_extensions import assert_never
|
|
5
9
|
from typing import Union, Optional, Any
|
|
6
|
-
from typing_extensions import assert_never # importing from typing caused me errors
|
|
7
10
|
from enum import Enum, auto
|
|
8
11
|
|
|
9
|
-
from . import user, project, studio
|
|
12
|
+
from . import user, project, studio, session
|
|
10
13
|
from ._base import BaseSiteComponent
|
|
11
14
|
from scratchattach.utils import exceptions
|
|
12
15
|
|
|
16
|
+
class CommentSource(Enum):
|
|
17
|
+
PROJECT = auto()
|
|
18
|
+
USER_PROFILE = auto()
|
|
19
|
+
STUDIO = auto()
|
|
20
|
+
UNKNOWN = auto()
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
13
23
|
class Comment(BaseSiteComponent):
|
|
14
24
|
"""
|
|
15
25
|
Represents a Scratch comment (on a profile, studio or project)
|
|
16
26
|
"""
|
|
17
|
-
id:
|
|
18
|
-
source:
|
|
19
|
-
source_id:
|
|
20
|
-
cached_replies: Optional[list[Comment]]
|
|
21
|
-
parent_id: Optional[
|
|
22
|
-
cached_parent_comment: Optional[Comment]
|
|
23
|
-
commentee_id: Optional[int]
|
|
24
|
-
content:
|
|
27
|
+
id: Optional[int | str] = None
|
|
28
|
+
source: CommentSource = CommentSource.UNKNOWN
|
|
29
|
+
source_id: Optional[int | str] = None
|
|
30
|
+
cached_replies: Optional[list[Comment]] = None
|
|
31
|
+
parent_id: Optional[int | str] = None
|
|
32
|
+
cached_parent_comment: Optional[Comment] = None
|
|
33
|
+
commentee_id: Optional[int] = None
|
|
34
|
+
content: Optional[str] = None
|
|
35
|
+
reply_count: Optional[int] = None
|
|
36
|
+
written_by_scratchteam: Optional[bool] = None
|
|
37
|
+
author_id: Optional[int] = None
|
|
38
|
+
author_name: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
_session: Optional[session.Session] = None
|
|
25
41
|
|
|
26
42
|
def __str__(self):
|
|
27
|
-
return
|
|
28
|
-
|
|
29
|
-
def __init__(self, **entries):
|
|
30
|
-
|
|
31
|
-
# Set attributes every Comment object needs to have:
|
|
32
|
-
self.id = None
|
|
33
|
-
self._session = None
|
|
34
|
-
self.source = None
|
|
35
|
-
self.source_id = None
|
|
36
|
-
self.cached_replies = None
|
|
37
|
-
self.parent_id = None
|
|
38
|
-
self.cached_parent_comment = None
|
|
39
|
-
|
|
40
|
-
# Update attributes from entries dict:
|
|
41
|
-
self.__dict__.update(entries)
|
|
42
|
-
|
|
43
|
-
if "source" not in entries:
|
|
44
|
-
self.source = "Unknown"
|
|
43
|
+
return self.text
|
|
45
44
|
|
|
46
45
|
def update(self):
|
|
47
|
-
|
|
46
|
+
warnings.warn("Warning: Comment objects can't be updated")
|
|
48
47
|
return False # Objects of this type cannot be updated
|
|
49
48
|
|
|
50
|
-
def _update_from_dict(self, data):
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
except Exception:
|
|
74
|
-
pass
|
|
75
|
-
try:
|
|
76
|
-
self.author_id = data["author"]["id"]
|
|
77
|
-
except Exception:
|
|
78
|
-
pass
|
|
79
|
-
try:
|
|
80
|
-
self.written_by_scratchteam = data["author"]["scratchteam"]
|
|
81
|
-
except Exception:
|
|
82
|
-
pass
|
|
83
|
-
try:
|
|
84
|
-
self.reply_count = data["reply_count"]
|
|
85
|
-
except Exception:
|
|
86
|
-
pass
|
|
87
|
-
try:
|
|
88
|
-
self.source = data["source"]
|
|
89
|
-
except Exception:
|
|
90
|
-
pass
|
|
91
|
-
try:
|
|
92
|
-
self.source_id = data["source_id"]
|
|
93
|
-
except Exception:
|
|
94
|
-
pass
|
|
95
|
-
return True
|
|
49
|
+
def _update_from_dict(self, data: dict[str, str | dict | Any]):
|
|
50
|
+
self.id = data["id"]
|
|
51
|
+
self.parent_id = data.get("parent_id")
|
|
52
|
+
self.commentee_id = data.get("commentee_id")
|
|
53
|
+
self.content = str(data["content"])
|
|
54
|
+
self.datetime_created = data["datetime_created"]
|
|
55
|
+
|
|
56
|
+
author = data.get("author", {})
|
|
57
|
+
self.author_name = author.get("username", self.author_name)
|
|
58
|
+
self.author_id = author.get("id", self.author_id)
|
|
59
|
+
self.written_by_scratchteam = author.get("scratchteam", self.written_by_scratchteam)
|
|
60
|
+
self.reply_count = data.get("reply_count", self.reply_count)
|
|
61
|
+
|
|
62
|
+
source: str = data.get("source")
|
|
63
|
+
if self.source is CommentSource.UNKNOWN:
|
|
64
|
+
self.source = {
|
|
65
|
+
"project": CommentSource.PROJECT,
|
|
66
|
+
"studio": CommentSource.STUDIO,
|
|
67
|
+
"profile": CommentSource.USER_PROFILE,
|
|
68
|
+
None: CommentSource.UNKNOWN,
|
|
69
|
+
}[source]
|
|
70
|
+
|
|
71
|
+
self.source_id = data.get("source_id", self.source_id)
|
|
96
72
|
|
|
97
73
|
@property
|
|
98
74
|
def text(self) -> str:
|
|
99
|
-
|
|
75
|
+
"""
|
|
76
|
+
Parsed version of Comment.content. This removes any escape codes, e.g. ''' becomes ', an apostrophe
|
|
77
|
+
"""
|
|
78
|
+
if self.source is CommentSource.USER_PROFILE:
|
|
79
|
+
# user profile comments do not seem to be escaped
|
|
100
80
|
return self.content
|
|
81
|
+
|
|
101
82
|
return str(html.unescape(self.content))
|
|
102
83
|
|
|
103
84
|
# Methods for getting related entities
|
|
@@ -111,14 +92,14 @@ class Comment(BaseSiteComponent):
|
|
|
111
92
|
|
|
112
93
|
If the place can't be traced back, None is returned.
|
|
113
94
|
"""
|
|
114
|
-
if self.source ==
|
|
95
|
+
if self.source == CommentSource.USER_PROFILE:
|
|
115
96
|
return self._make_linked_object("username", self.source_id, user.User, exceptions.UserNotFound)
|
|
116
|
-
elif self.source ==
|
|
97
|
+
elif self.source == CommentSource.STUDIO:
|
|
117
98
|
return self._make_linked_object("id", self.source_id, studio.Studio, exceptions.UserNotFound)
|
|
118
|
-
elif self.source ==
|
|
99
|
+
elif self.source == CommentSource.PROJECT:
|
|
119
100
|
return self._make_linked_object("id", self.source_id, project.Project, exceptions.UserNotFound)
|
|
120
101
|
else:
|
|
121
|
-
|
|
102
|
+
assert_never(self.source)
|
|
122
103
|
|
|
123
104
|
def parent_comment(self) -> Comment | None:
|
|
124
105
|
if self.parent_id is None:
|
|
@@ -148,18 +129,16 @@ class Comment(BaseSiteComponent):
|
|
|
148
129
|
use_cache (bool): Returns the replies cached on the first reply fetch. This makes it SIGNIFICANTLY faster for profile comments. Warning: For profile comments, the replies are retrieved and cached on object creation.
|
|
149
130
|
"""
|
|
150
131
|
if (self.cached_replies is None) or (not use_cache):
|
|
151
|
-
if self.source ==
|
|
152
|
-
|
|
153
|
-
self.id).cached_replies
|
|
154
|
-
if _cached_replies is not None:
|
|
155
|
-
self.cached_replies = _cached_replies[offset:offset + limit]
|
|
132
|
+
if self.source == CommentSource.USER_PROFILE:
|
|
133
|
+
self.cached_replies = user.User(username=self.source_id, _session=self._session).comment_by_id(
|
|
134
|
+
self.id).cached_replies[offset:offset + limit]
|
|
156
135
|
|
|
157
|
-
elif self.source ==
|
|
136
|
+
elif self.source == CommentSource.PROJECT:
|
|
158
137
|
p = project.Project(id=self.source_id, _session=self._session)
|
|
159
138
|
p.update()
|
|
160
139
|
self.cached_replies = p.comment_replies(comment_id=self.id, limit=limit, offset=offset)
|
|
161
140
|
|
|
162
|
-
elif self.source ==
|
|
141
|
+
elif self.source == CommentSource.STUDIO:
|
|
163
142
|
self.cached_replies = studio.Studio(id=self.source_id, _session=self._session).comment_replies(
|
|
164
143
|
comment_id=self.id, limit=limit, offset=offset)
|
|
165
144
|
|
|
@@ -185,6 +164,8 @@ class Comment(BaseSiteComponent):
|
|
|
185
164
|
|
|
186
165
|
Returns:
|
|
187
166
|
scratchattach.Comment: The created comment.
|
|
167
|
+
:param content: Content of the comment to send
|
|
168
|
+
:param commentee_id: ID of user to reply to
|
|
188
169
|
"""
|
|
189
170
|
|
|
190
171
|
self._assert_auth()
|
|
@@ -192,51 +173,56 @@ class Comment(BaseSiteComponent):
|
|
|
192
173
|
if self.parent_id is not None:
|
|
193
174
|
parent_id = str(self.parent_id)
|
|
194
175
|
if commentee_id is None:
|
|
195
|
-
if
|
|
176
|
+
if self.author_id:
|
|
196
177
|
commentee_id = self.author_id
|
|
197
178
|
else:
|
|
198
179
|
commentee_id = ""
|
|
199
|
-
|
|
180
|
+
|
|
181
|
+
if self.source == CommentSource.USER_PROFILE:
|
|
200
182
|
return user.User(username=self.source_id, _session=self._session).reply_comment(content,
|
|
201
183
|
parent_id=str(parent_id),
|
|
202
184
|
commentee_id=commentee_id)
|
|
203
|
-
if self.source ==
|
|
185
|
+
if self.source == CommentSource.PROJECT:
|
|
204
186
|
p = project.Project(id=self.source_id, _session=self._session)
|
|
205
187
|
p.update()
|
|
206
188
|
return p.reply_comment(content, parent_id=str(parent_id), commentee_id=commentee_id)
|
|
207
|
-
|
|
189
|
+
|
|
190
|
+
if self.source == CommentSource.STUDIO:
|
|
208
191
|
return studio.Studio(id=self.source_id, _session=self._session).reply_comment(content,
|
|
209
192
|
parent_id=str(parent_id),
|
|
210
193
|
commentee_id=commentee_id)
|
|
194
|
+
raise ValueError(f"Unknown source: {self.source}")
|
|
211
195
|
|
|
212
196
|
def delete(self):
|
|
213
197
|
"""
|
|
214
198
|
Deletes the comment.
|
|
215
199
|
"""
|
|
216
200
|
self._assert_auth()
|
|
217
|
-
if self.source ==
|
|
218
|
-
user.User(username=self.source_id, _session=self._session).delete_comment(comment_id=self.id)
|
|
201
|
+
if self.source == CommentSource.USER_PROFILE:
|
|
202
|
+
return user.User(username=self.source_id, _session=self._session).delete_comment(comment_id=self.id)
|
|
219
203
|
|
|
220
|
-
elif self.source ==
|
|
204
|
+
elif self.source == CommentSource.PROJECT:
|
|
221
205
|
p = project.Project(id=self.source_id, _session=self._session)
|
|
222
206
|
p.update()
|
|
223
|
-
p.delete_comment(comment_id=self.id)
|
|
207
|
+
return p.delete_comment(comment_id=self.id)
|
|
224
208
|
|
|
225
|
-
elif self.source ==
|
|
226
|
-
studio.Studio(id=self.source_id, _session=self._session).delete_comment(comment_id=self.id)
|
|
209
|
+
elif self.source == CommentSource.STUDIO:
|
|
210
|
+
return studio.Studio(id=self.source_id, _session=self._session).delete_comment(comment_id=self.id)
|
|
211
|
+
|
|
212
|
+
return None # raise error?
|
|
227
213
|
|
|
228
214
|
def report(self):
|
|
229
215
|
"""
|
|
230
216
|
Reports the comment to the Scratch team.
|
|
231
217
|
"""
|
|
232
218
|
self._assert_auth()
|
|
233
|
-
if self.source ==
|
|
219
|
+
if self.source == CommentSource.USER_PROFILE:
|
|
234
220
|
user.User(username=self.source_id, _session=self._session).report_comment(comment_id=self.id)
|
|
235
221
|
|
|
236
|
-
elif self.source ==
|
|
222
|
+
elif self.source == CommentSource.PROJECT:
|
|
237
223
|
p = project.Project(id=self.source_id, _session=self._session)
|
|
238
224
|
p.update()
|
|
239
225
|
p.report_comment(comment_id=self.id)
|
|
240
226
|
|
|
241
|
-
elif self.source ==
|
|
227
|
+
elif self.source == CommentSource.STUDIO:
|
|
242
228
|
studio.Studio(id=self.source_id, _session=self._session).report_comment(comment_id=self.id)
|