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.
Files changed (83) hide show
  1. cli/__about__.py +1 -0
  2. cli/__init__.py +26 -0
  3. cli/cmd/__init__.py +4 -0
  4. cli/cmd/group.py +127 -0
  5. cli/cmd/login.py +60 -0
  6. cli/cmd/profile.py +7 -0
  7. cli/cmd/sessions.py +5 -0
  8. cli/context.py +142 -0
  9. cli/db.py +66 -0
  10. cli/namespace.py +14 -0
  11. cloud/__init__.py +2 -0
  12. cloud/_base.py +483 -0
  13. cloud/cloud.py +183 -0
  14. editor/__init__.py +22 -0
  15. editor/asset.py +265 -0
  16. editor/backpack_json.py +115 -0
  17. editor/base.py +191 -0
  18. editor/block.py +584 -0
  19. editor/blockshape.py +357 -0
  20. editor/build_defaulting.py +51 -0
  21. editor/code_translation/__init__.py +0 -0
  22. editor/code_translation/parse.py +177 -0
  23. editor/comment.py +80 -0
  24. editor/commons.py +145 -0
  25. editor/extension.py +50 -0
  26. editor/field.py +99 -0
  27. editor/inputs.py +138 -0
  28. editor/meta.py +117 -0
  29. editor/monitor.py +185 -0
  30. editor/mutation.py +381 -0
  31. editor/pallete.py +88 -0
  32. editor/prim.py +174 -0
  33. editor/project.py +381 -0
  34. editor/sprite.py +609 -0
  35. editor/twconfig.py +114 -0
  36. editor/vlb.py +134 -0
  37. eventhandlers/__init__.py +0 -0
  38. eventhandlers/_base.py +101 -0
  39. eventhandlers/cloud_events.py +130 -0
  40. eventhandlers/cloud_recorder.py +26 -0
  41. eventhandlers/cloud_requests.py +544 -0
  42. eventhandlers/cloud_server.py +249 -0
  43. eventhandlers/cloud_storage.py +135 -0
  44. eventhandlers/combine.py +30 -0
  45. eventhandlers/filterbot.py +163 -0
  46. eventhandlers/message_events.py +42 -0
  47. other/__init__.py +0 -0
  48. other/other_apis.py +598 -0
  49. other/project_json_capabilities.py +475 -0
  50. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/METADATA +1 -1
  51. scratchattach-3.0.0b1.dist-info/RECORD +79 -0
  52. scratchattach-3.0.0b1.dist-info/top_level.txt +7 -0
  53. site/__init__.py +0 -0
  54. site/_base.py +93 -0
  55. site/activity.py +426 -0
  56. site/alert.py +226 -0
  57. site/backpack_asset.py +119 -0
  58. site/browser_cookie3_stub.py +17 -0
  59. site/browser_cookies.py +61 -0
  60. site/classroom.py +454 -0
  61. site/cloud_activity.py +121 -0
  62. site/comment.py +228 -0
  63. site/forum.py +436 -0
  64. site/placeholder.py +132 -0
  65. site/project.py +932 -0
  66. site/session.py +1323 -0
  67. site/studio.py +704 -0
  68. site/typed_dicts.py +151 -0
  69. site/user.py +1252 -0
  70. utils/__init__.py +0 -0
  71. utils/commons.py +263 -0
  72. utils/encoder.py +161 -0
  73. utils/enums.py +237 -0
  74. utils/exceptions.py +277 -0
  75. utils/optional_async.py +154 -0
  76. utils/requests.py +306 -0
  77. scratchattach/__init__.py +0 -37
  78. scratchattach/__main__.py +0 -93
  79. scratchattach-3.0.0b0.dist-info/RECORD +0 -8
  80. scratchattach-3.0.0b0.dist-info/top_level.txt +0 -1
  81. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/WHEEL +0 -0
  82. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b1.dist-info}/entry_points.txt +0 -0
  83. {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. '&apos;' 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)