scratchattach 3.0.0b0__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
- cloud/__init__.py +2 -0
- cloud/_base.py +483 -0
- cloud/cloud.py +183 -0
- editor/__init__.py +22 -0
- editor/asset.py +265 -0
- editor/backpack_json.py +115 -0
- editor/base.py +191 -0
- editor/block.py +584 -0
- editor/blockshape.py +357 -0
- editor/build_defaulting.py +51 -0
- editor/code_translation/__init__.py +0 -0
- editor/code_translation/parse.py +177 -0
- editor/comment.py +80 -0
- editor/commons.py +145 -0
- editor/extension.py +50 -0
- editor/field.py +99 -0
- editor/inputs.py +138 -0
- editor/meta.py +117 -0
- editor/monitor.py +185 -0
- editor/mutation.py +381 -0
- editor/pallete.py +88 -0
- editor/prim.py +174 -0
- editor/project.py +381 -0
- editor/sprite.py +609 -0
- editor/twconfig.py +114 -0
- editor/vlb.py +134 -0
- eventhandlers/__init__.py +0 -0
- eventhandlers/_base.py +101 -0
- eventhandlers/cloud_events.py +130 -0
- eventhandlers/cloud_recorder.py +26 -0
- eventhandlers/cloud_requests.py +544 -0
- eventhandlers/cloud_server.py +249 -0
- eventhandlers/cloud_storage.py +135 -0
- eventhandlers/combine.py +30 -0
- eventhandlers/filterbot.py +163 -0
- eventhandlers/message_events.py +42 -0
- other/__init__.py +0 -0
- other/other_apis.py +598 -0
- other/project_json_capabilities.py +475 -0
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/METADATA +1 -1
- scratchattach-3.0.0b1.dist-info/RECORD +79 -0
- scratchattach-3.0.0b1.dist-info/top_level.txt +7 -0
- site/__init__.py +0 -0
- site/_base.py +93 -0
- site/activity.py +426 -0
- site/alert.py +226 -0
- site/backpack_asset.py +119 -0
- site/browser_cookie3_stub.py +17 -0
- site/browser_cookies.py +61 -0
- site/classroom.py +454 -0
- site/cloud_activity.py +121 -0
- site/comment.py +228 -0
- site/forum.py +436 -0
- site/placeholder.py +132 -0
- site/project.py +932 -0
- site/session.py +1323 -0
- site/studio.py +704 -0
- site/typed_dicts.py +151 -0
- site/user.py +1252 -0
- utils/__init__.py +0 -0
- utils/commons.py +263 -0
- utils/encoder.py +161 -0
- utils/enums.py +237 -0
- utils/exceptions.py +277 -0
- utils/optional_async.py +154 -0
- utils/requests.py +306 -0
- scratchattach/__init__.py +0 -37
- scratchattach/__main__.py +0 -93
- scratchattach-3.0.0b0.dist-info/RECORD +0 -8
- scratchattach-3.0.0b0.dist-info/top_level.txt +0 -1
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/WHEEL +0 -0
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
site/cloud_activity.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Union, TypeGuard, Optional
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
import warnings
|
|
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
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class CloudActivity(BaseSiteComponent[Union[typed_dicts.CloudActivityDict, typed_dicts.CloudLogActivityDict]]):
|
|
16
|
+
"""
|
|
17
|
+
Represents a cloud activity (a cloud variable set / creation / deletion).
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
|
|
21
|
+
:.username: The user who caused the cloud event (the user who added / set / deleted the cloud variable)
|
|
22
|
+
|
|
23
|
+
:.var: The name of the cloud variable that was updated (specified without the cloud emoji)
|
|
24
|
+
|
|
25
|
+
:.name: The name of the cloud variable that was updated (specified without the cloud emoji)
|
|
26
|
+
|
|
27
|
+
:.type: The activity type
|
|
28
|
+
|
|
29
|
+
:.timestamp: Then timestamp of when the action was performed
|
|
30
|
+
|
|
31
|
+
:.value: If the cloud variable was set, then this attribute provides the value the cloud variable was set to
|
|
32
|
+
|
|
33
|
+
:.cloud: The cloud (as object inheriting from scratchattach.Cloud.BaseCloud) that the cloud activity corresponds to
|
|
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)
|
|
43
|
+
|
|
44
|
+
def __init__(self, **entries):
|
|
45
|
+
# Set attributes every CloudActivity object needs to have:
|
|
46
|
+
self._session = None
|
|
47
|
+
self.cloud = None
|
|
48
|
+
self.user = None
|
|
49
|
+
self.username = None
|
|
50
|
+
self.type = None
|
|
51
|
+
self.timestamp = time.time()
|
|
52
|
+
|
|
53
|
+
# Update attributes from entries dict:
|
|
54
|
+
self.__dict__.update(entries)
|
|
55
|
+
|
|
56
|
+
def update(self):
|
|
57
|
+
warnings.warn("CloudActivity objects can't be updated", exceptions.InvalidUpdateWarning)
|
|
58
|
+
return False # Objects of this type cannot be updated
|
|
59
|
+
|
|
60
|
+
def __eq__(self, activity2):
|
|
61
|
+
# CloudLogEvents needs to check if two activites are equal (to finde new ones), therefore CloudActivity objects need to be comparable
|
|
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
|
|
63
|
+
|
|
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"]
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
def load_log_data(self):
|
|
84
|
+
if self.cloud is None:
|
|
85
|
+
print("Warning: There aren't cloud logs available for this cloud, therefore the user and exact timestamp can't be loaded")
|
|
86
|
+
else:
|
|
87
|
+
if hasattr(self.cloud, "logs"):
|
|
88
|
+
logs = self.cloud.logs(filter_by_var_named=self.var, limit=100)
|
|
89
|
+
matching = list(filter(lambda x: x.value == self.value and x.timestamp <= self.timestamp, logs))
|
|
90
|
+
if matching == []:
|
|
91
|
+
return False
|
|
92
|
+
activity = matching[0]
|
|
93
|
+
self.username = activity.username
|
|
94
|
+
self.user = activity.username
|
|
95
|
+
self.timestamp = activity.timestamp
|
|
96
|
+
return True
|
|
97
|
+
else:
|
|
98
|
+
print("Warning: There aren't cloud logs available for this cloud, therefore the user and exact timestamp can't be loaded")
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
def actor(self):
|
|
102
|
+
"""
|
|
103
|
+
Returns the user that performed the cloud activity as scratchattach.user.User object
|
|
104
|
+
"""
|
|
105
|
+
if self.username is None:
|
|
106
|
+
return None
|
|
107
|
+
return self._make_linked_object("username", self.username, user.User, exceptions.UserNotFound)
|
|
108
|
+
|
|
109
|
+
def project(self) -> Optional[project.Project]:
|
|
110
|
+
"""
|
|
111
|
+
Returns the project where the cloud activity was performed as scratchattach.project.Project object
|
|
112
|
+
"""
|
|
113
|
+
def make_linked(cloud: _base.BaseCloud) -> project.Project:
|
|
114
|
+
return self._make_linked_object("id", cloud.project_id, project.Project, exceptions.ProjectNotFound)
|
|
115
|
+
if self.cloud is None:
|
|
116
|
+
return None
|
|
117
|
+
cloud = self.cloud
|
|
118
|
+
if not isinstance(cloud, _base.BaseCloud):
|
|
119
|
+
return None
|
|
120
|
+
return make_linked(cloud)
|
|
121
|
+
|
site/comment.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Comment class"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import warnings
|
|
5
|
+
import html
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing_extensions import assert_never
|
|
9
|
+
from typing import Union, Optional, Any
|
|
10
|
+
from enum import Enum, auto
|
|
11
|
+
|
|
12
|
+
from . import user, project, studio, session
|
|
13
|
+
from ._base import BaseSiteComponent
|
|
14
|
+
from scratchattach.utils import exceptions
|
|
15
|
+
|
|
16
|
+
class CommentSource(Enum):
|
|
17
|
+
PROJECT = auto()
|
|
18
|
+
USER_PROFILE = auto()
|
|
19
|
+
STUDIO = auto()
|
|
20
|
+
UNKNOWN = auto()
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Comment(BaseSiteComponent):
|
|
24
|
+
"""
|
|
25
|
+
Represents a Scratch comment (on a profile, studio or project)
|
|
26
|
+
"""
|
|
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
|
|
41
|
+
|
|
42
|
+
def __str__(self):
|
|
43
|
+
return self.text
|
|
44
|
+
|
|
45
|
+
def update(self):
|
|
46
|
+
warnings.warn("Warning: Comment objects can't be updated")
|
|
47
|
+
return False # Objects of this type cannot be updated
|
|
48
|
+
|
|
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)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def text(self) -> str:
|
|
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
|
|
80
|
+
return self.content
|
|
81
|
+
|
|
82
|
+
return str(html.unescape(self.content))
|
|
83
|
+
|
|
84
|
+
# Methods for getting related entities
|
|
85
|
+
|
|
86
|
+
def author(self) -> user.User:
|
|
87
|
+
return self._make_linked_object("username", self.author_name, user.User, exceptions.UserNotFound)
|
|
88
|
+
|
|
89
|
+
def place(self) -> user.User | studio.Studio | project.Project:
|
|
90
|
+
"""
|
|
91
|
+
Returns the place (the project, profile or studio) where the comment was posted as Project, User, or Studio object.
|
|
92
|
+
|
|
93
|
+
If the place can't be traced back, None is returned.
|
|
94
|
+
"""
|
|
95
|
+
if self.source == CommentSource.USER_PROFILE:
|
|
96
|
+
return self._make_linked_object("username", self.source_id, user.User, exceptions.UserNotFound)
|
|
97
|
+
elif self.source == CommentSource.STUDIO:
|
|
98
|
+
return self._make_linked_object("id", self.source_id, studio.Studio, exceptions.UserNotFound)
|
|
99
|
+
elif self.source == CommentSource.PROJECT:
|
|
100
|
+
return self._make_linked_object("id", self.source_id, project.Project, exceptions.UserNotFound)
|
|
101
|
+
else:
|
|
102
|
+
assert_never(self.source)
|
|
103
|
+
|
|
104
|
+
def parent_comment(self) -> Comment | None:
|
|
105
|
+
if self.parent_id is None:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
if self.cached_parent_comment is not None:
|
|
109
|
+
return self.cached_parent_comment
|
|
110
|
+
|
|
111
|
+
if self.source == "profile":
|
|
112
|
+
self.cached_parent_comment = user.User(username=self.source_id, _session=self._session).comment_by_id(
|
|
113
|
+
self.parent_id)
|
|
114
|
+
|
|
115
|
+
elif self.source == "project":
|
|
116
|
+
p = project.Project(id=self.source_id, _session=self._session)
|
|
117
|
+
p.update()
|
|
118
|
+
self.cached_parent_comment = p.comment_by_id(self.parent_id)
|
|
119
|
+
|
|
120
|
+
elif self.source == "studio":
|
|
121
|
+
self.cached_parent_comment = studio.Studio(id=self.source_id, _session=self._session).comment_by_id(
|
|
122
|
+
self.parent_id)
|
|
123
|
+
|
|
124
|
+
return self.cached_parent_comment
|
|
125
|
+
|
|
126
|
+
def replies(self, *, use_cache: bool = True, limit=40, offset=0):
|
|
127
|
+
"""
|
|
128
|
+
Keyword Arguments:
|
|
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.
|
|
130
|
+
"""
|
|
131
|
+
if (self.cached_replies is None) or (not use_cache):
|
|
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]
|
|
135
|
+
|
|
136
|
+
elif self.source == CommentSource.PROJECT:
|
|
137
|
+
p = project.Project(id=self.source_id, _session=self._session)
|
|
138
|
+
p.update()
|
|
139
|
+
self.cached_replies = p.comment_replies(comment_id=self.id, limit=limit, offset=offset)
|
|
140
|
+
|
|
141
|
+
elif self.source == CommentSource.STUDIO:
|
|
142
|
+
self.cached_replies = studio.Studio(id=self.source_id, _session=self._session).comment_replies(
|
|
143
|
+
comment_id=self.id, limit=limit, offset=offset)
|
|
144
|
+
|
|
145
|
+
return self.cached_replies
|
|
146
|
+
|
|
147
|
+
# Methods for dealing with the comment
|
|
148
|
+
|
|
149
|
+
def reply(self, content, *, commentee_id=None):
|
|
150
|
+
"""
|
|
151
|
+
Posts a reply comment to the comment.
|
|
152
|
+
|
|
153
|
+
Warning:
|
|
154
|
+
Scratch only shows comments replying to top-level comments, and all replies to replies are actually replies to top-level comments in the API.
|
|
155
|
+
|
|
156
|
+
Therefore, if this comment is a reply, this method will not reply to the comment itself but to the corresponding top-level comment.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
content (str): Comment content to post.
|
|
160
|
+
|
|
161
|
+
Keyword args:
|
|
162
|
+
commentee_id (None or str): If set to None (default), it will automatically fill out the commentee ID with the user ID of the parent comment author. Set it to "" to mention no user.
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
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
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
self._assert_auth()
|
|
172
|
+
parent_id = str(self.id)
|
|
173
|
+
if self.parent_id is not None:
|
|
174
|
+
parent_id = str(self.parent_id)
|
|
175
|
+
if commentee_id is None:
|
|
176
|
+
if self.author_id:
|
|
177
|
+
commentee_id = self.author_id
|
|
178
|
+
else:
|
|
179
|
+
commentee_id = ""
|
|
180
|
+
|
|
181
|
+
if self.source == CommentSource.USER_PROFILE:
|
|
182
|
+
return user.User(username=self.source_id, _session=self._session).reply_comment(content,
|
|
183
|
+
parent_id=str(parent_id),
|
|
184
|
+
commentee_id=commentee_id)
|
|
185
|
+
if self.source == CommentSource.PROJECT:
|
|
186
|
+
p = project.Project(id=self.source_id, _session=self._session)
|
|
187
|
+
p.update()
|
|
188
|
+
return p.reply_comment(content, parent_id=str(parent_id), commentee_id=commentee_id)
|
|
189
|
+
|
|
190
|
+
if self.source == CommentSource.STUDIO:
|
|
191
|
+
return studio.Studio(id=self.source_id, _session=self._session).reply_comment(content,
|
|
192
|
+
parent_id=str(parent_id),
|
|
193
|
+
commentee_id=commentee_id)
|
|
194
|
+
raise ValueError(f"Unknown source: {self.source}")
|
|
195
|
+
|
|
196
|
+
def delete(self):
|
|
197
|
+
"""
|
|
198
|
+
Deletes the comment.
|
|
199
|
+
"""
|
|
200
|
+
self._assert_auth()
|
|
201
|
+
if self.source == CommentSource.USER_PROFILE:
|
|
202
|
+
return user.User(username=self.source_id, _session=self._session).delete_comment(comment_id=self.id)
|
|
203
|
+
|
|
204
|
+
elif self.source == CommentSource.PROJECT:
|
|
205
|
+
p = project.Project(id=self.source_id, _session=self._session)
|
|
206
|
+
p.update()
|
|
207
|
+
return p.delete_comment(comment_id=self.id)
|
|
208
|
+
|
|
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?
|
|
213
|
+
|
|
214
|
+
def report(self):
|
|
215
|
+
"""
|
|
216
|
+
Reports the comment to the Scratch team.
|
|
217
|
+
"""
|
|
218
|
+
self._assert_auth()
|
|
219
|
+
if self.source == CommentSource.USER_PROFILE:
|
|
220
|
+
user.User(username=self.source_id, _session=self._session).report_comment(comment_id=self.id)
|
|
221
|
+
|
|
222
|
+
elif self.source == CommentSource.PROJECT:
|
|
223
|
+
p = project.Project(id=self.source_id, _session=self._session)
|
|
224
|
+
p.update()
|
|
225
|
+
p.report_comment(comment_id=self.id)
|
|
226
|
+
|
|
227
|
+
elif self.source == CommentSource.STUDIO:
|
|
228
|
+
studio.Studio(id=self.source_id, _session=self._session).report_comment(comment_id=self.id)
|