scratchattach 2.1.12__py3-none-any.whl → 2.1.14__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 (56) hide show
  1. scratchattach/cloud/_base.py +12 -8
  2. scratchattach/cloud/cloud.py +19 -7
  3. scratchattach/editor/asset.py +59 -5
  4. scratchattach/editor/base.py +82 -31
  5. scratchattach/editor/block.py +87 -15
  6. scratchattach/editor/blockshape.py +8 -4
  7. scratchattach/editor/build_defaulting.py +6 -2
  8. scratchattach/editor/code_translation/__init__.py +0 -0
  9. scratchattach/editor/code_translation/parse.py +177 -0
  10. scratchattach/editor/comment.py +6 -0
  11. scratchattach/editor/commons.py +82 -19
  12. scratchattach/editor/extension.py +10 -3
  13. scratchattach/editor/field.py +9 -0
  14. scratchattach/editor/inputs.py +4 -1
  15. scratchattach/editor/meta.py +11 -3
  16. scratchattach/editor/monitor.py +46 -38
  17. scratchattach/editor/mutation.py +11 -4
  18. scratchattach/editor/pallete.py +24 -25
  19. scratchattach/editor/prim.py +2 -2
  20. scratchattach/editor/project.py +9 -3
  21. scratchattach/editor/sprite.py +19 -6
  22. scratchattach/editor/twconfig.py +2 -1
  23. scratchattach/editor/vlb.py +1 -1
  24. scratchattach/eventhandlers/_base.py +3 -3
  25. scratchattach/eventhandlers/cloud_events.py +2 -2
  26. scratchattach/eventhandlers/cloud_requests.py +4 -7
  27. scratchattach/eventhandlers/cloud_server.py +3 -3
  28. scratchattach/eventhandlers/combine.py +2 -2
  29. scratchattach/eventhandlers/message_events.py +1 -1
  30. scratchattach/other/other_apis.py +4 -4
  31. scratchattach/other/project_json_capabilities.py +3 -3
  32. scratchattach/site/_base.py +13 -12
  33. scratchattach/site/activity.py +11 -43
  34. scratchattach/site/alert.py +227 -0
  35. scratchattach/site/backpack_asset.py +2 -2
  36. scratchattach/site/browser_cookie3_stub.py +17 -0
  37. scratchattach/site/browser_cookies.py +27 -21
  38. scratchattach/site/classroom.py +51 -34
  39. scratchattach/site/cloud_activity.py +4 -4
  40. scratchattach/site/comment.py +30 -8
  41. scratchattach/site/forum.py +101 -69
  42. scratchattach/site/project.py +37 -17
  43. scratchattach/site/session.py +177 -83
  44. scratchattach/site/studio.py +4 -4
  45. scratchattach/site/user.py +184 -62
  46. scratchattach/utils/commons.py +35 -23
  47. scratchattach/utils/enums.py +44 -5
  48. scratchattach/utils/exceptions.py +10 -0
  49. scratchattach/utils/requests.py +57 -31
  50. {scratchattach-2.1.12.dist-info → scratchattach-2.1.14.dist-info}/METADATA +9 -3
  51. scratchattach-2.1.14.dist-info/RECORD +66 -0
  52. {scratchattach-2.1.12.dist-info → scratchattach-2.1.14.dist-info}/WHEEL +1 -1
  53. scratchattach/editor/sbuild.py +0 -2837
  54. scratchattach-2.1.12.dist-info/RECORD +0 -63
  55. {scratchattach-2.1.12.dist-info → scratchattach-2.1.14.dist-info}/licenses/LICENSE +0 -0
  56. {scratchattach-2.1.12.dist-info → scratchattach-2.1.14.dist-info}/top_level.txt +0 -0
@@ -17,7 +17,7 @@ You can move, resize, and minimize this comment, but don't edit it by hand. This
17
17
  _END = " // _twconfig_"
18
18
 
19
19
 
20
- @dataclass(init=True, repr=True)
20
+ @dataclass
21
21
  class TWConfig(base.JSONSerializable):
22
22
  framerate: int = None,
23
23
  interpolation: bool = False,
@@ -100,6 +100,7 @@ def get_twconfig_data(string: str) -> dict | None:
100
100
  return None
101
101
 
102
102
 
103
+ # todo: move this to commons.py?
103
104
  def none_if_eq(data, compare) -> Any | None:
104
105
  """
105
106
  Returns None if data and compare are the same
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
  from typing import Optional, Literal
10
10
 
11
11
  from . import base, sprite, build_defaulting
12
- from ..utils import exceptions
12
+ from scratchattach.utils import exceptions
13
13
 
14
14
 
15
15
  class Variable(base.NamedIDComponent):
@@ -5,8 +5,8 @@ from collections import defaultdict
5
5
  from threading import Thread
6
6
  from collections.abc import Callable
7
7
  import traceback
8
- from ..utils.requests import Requests as requests
9
- from ..utils import exceptions
8
+ from scratchattach.utils.requests import requests
9
+ from scratchattach.utils import exceptions
10
10
 
11
11
  class BaseEventHandler(ABC):
12
12
  _events: defaultdict[str, list[Callable]]
@@ -36,7 +36,7 @@ class BaseEventHandler(ABC):
36
36
  self._thread = None
37
37
  self._updater()
38
38
 
39
- def call_event(self, event_name, args=[]):
39
+ def call_event(self, event_name, args : list = []):
40
40
  try:
41
41
  if event_name in self._threaded_events:
42
42
  for func in self._threaded_events[event_name]:
@@ -1,9 +1,9 @@
1
1
  """CloudEvents class"""
2
2
  from __future__ import annotations
3
3
 
4
- from ..cloud import _base
4
+ from scratchattach.cloud import _base
5
5
  from ._base import BaseEventHandler
6
- from ..site import cloud_activity
6
+ from scratchattach.site import cloud_activity
7
7
  import time
8
8
  import json
9
9
  from collections.abc import Iterator
@@ -2,13 +2,13 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  from .cloud_events import CloudEvents
5
- from ..site import project
5
+ from scratchattach.site import project
6
6
  from threading import Thread, Event, current_thread
7
7
  import time
8
8
  import random
9
9
  import traceback
10
- from ..utils.encoder import Encoding
11
- from ..utils import exceptions
10
+ from scratchattach.utils.encoder import Encoding
11
+ from scratchattach.utils import exceptions
12
12
 
13
13
  class Request:
14
14
 
@@ -69,7 +69,6 @@ class CloudRequests(CloudEvents):
69
69
  self._requests = {}
70
70
  self.event(self.on_set, thread=False)
71
71
  self.event(self.on_reconnect, thread=True)
72
- self.respond_in_thread = False
73
72
  self.no_packet_loss = no_packet_loss # When enabled, query the clouddata log regularly for missed requests and reconnect after every single request (reduces packet loss a lot, but is spammy and can make response duration longer)
74
73
  self.used_cloud_vars = used_cloud_vars
75
74
  self.respond_order = respond_order
@@ -106,8 +105,6 @@ class CloudRequests(CloudEvents):
106
105
  """
107
106
  def inner(function):
108
107
  # called if the decorator provides arguments
109
- if thread:
110
- self.respond_in_thread = True
111
108
  self._requests[function.__name__ if name is None else name] = Request(
112
109
  function.__name__ if name is None else name,
113
110
  enabled = enabled,
@@ -302,7 +299,7 @@ class CloudRequests(CloudEvents):
302
299
  request_id=request_id,
303
300
  activity=activity
304
301
  )
305
- self.call_event("on_request", received_request)
302
+ self.call_event("on_request", [received_request])
306
303
  if received_request.request.thread:
307
304
  self.executed_requests[request_id] = received_request
308
305
  Thread(target=received_request.request, args=[received_request]).start() # Execute the request function directly in a thread
@@ -2,11 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket
4
4
  from threading import Thread
5
- from ..utils import exceptions
5
+ from scratchattach.utils import exceptions
6
6
  import json
7
7
  import time
8
- from ..site import cloud_activity
9
- from ..site.user import User
8
+ from scratchattach.site import cloud_activity
9
+ from scratchattach.site.user import User
10
10
  from ._base import BaseEventHandler
11
11
 
12
12
  class TwCloudSocket(WebSocket):
@@ -11,7 +11,7 @@ class MultiEventHandler:
11
11
 
12
12
  def event(self, function, *args, **kwargs):
13
13
  for handler in self.handlers:
14
- handler.request(function, *args, **kwargs)
14
+ handler.event(function, *args, **kwargs)
15
15
 
16
16
  def start(self, *args, **kwargs):
17
17
  for handler in self.handlers:
@@ -27,4 +27,4 @@ class MultiEventHandler:
27
27
 
28
28
  def resume(self, *args, **kwargs):
29
29
  for handler in self.handlers:
30
- handler.resume(*args, **kwargs)
30
+ handler.resume(*args, **kwargs)
@@ -1,7 +1,7 @@
1
1
  """MessageEvents class"""
2
2
  from __future__ import annotations
3
3
 
4
- from ..site import user
4
+ from scratchattach.site import user
5
5
  from ._base import BaseEventHandler
6
6
  import time
7
7
 
@@ -4,10 +4,10 @@ from __future__ import annotations
4
4
  import json
5
5
  from dataclasses import dataclass, field
6
6
 
7
- from ..utils import commons
8
- from ..utils.enums import Languages, Language, TTSVoices, TTSVoice
9
- from ..utils.exceptions import BadRequest, InvalidLanguage, InvalidTTSGender
10
- from ..utils.requests import Requests as requests
7
+ from scratchattach.utils import commons
8
+ from scratchattach.utils.enums import Languages, Language, TTSVoices, TTSVoice
9
+ from scratchattach.utils.exceptions import BadRequest, InvalidLanguage, InvalidTTSGender
10
+ from scratchattach.utils.requests import requests
11
11
  from typing import Optional
12
12
 
13
13
 
@@ -10,9 +10,9 @@ import random
10
10
  import string
11
11
  import zipfile
12
12
  from abc import ABC, abstractmethod
13
- from ..utils import exceptions
14
- from ..utils.commons import empty_project_json
15
- from ..utils.requests import Requests as requests
13
+ from scratchattach.utils import exceptions
14
+ from scratchattach.utils.commons import empty_project_json
15
+ from scratchattach.utils.requests import requests
16
16
  # noinspection PyPep8Naming
17
17
  def load_components(json_data: list, ComponentClass: type, target_list: list):
18
18
  for element in json_data:
@@ -1,27 +1,29 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from abc import ABC, abstractmethod
4
+ from typing import TypeVar, Optional
4
5
 
5
6
  import requests
6
- from ..utils import exceptions, commons
7
- from typing import TypeVar
8
- from types import FunctionType
7
+ from scratchattach.utils import exceptions, commons
8
+ from . import session
9
9
 
10
10
  C = TypeVar("C", bound="BaseSiteComponent")
11
11
  class BaseSiteComponent(ABC):
12
- @abstractmethod
13
- def __init__(self):
14
- self._session = None
15
- self._cookies = None
16
- self._headers = None
17
- self.update_API = None
12
+ _session: Optional[session.Session]
13
+ update_api: str
14
+ _headers: dict[str, str]
15
+ _cookies: dict[str, str]
16
+
17
+ # @abstractmethod
18
+ # def __init__(self): # dataclasses do not implement __init__ directly
19
+ # pass
18
20
 
19
21
  def update(self):
20
22
  """
21
23
  Updates the attributes of the object by performing an API response. Returns True if the update was successful.
22
24
  """
23
25
  response = self.update_function(
24
- self.update_API,
26
+ self.update_api,
25
27
  headers=self._headers,
26
28
  cookies=self._cookies, timeout=10
27
29
  )
@@ -45,7 +47,6 @@ class BaseSiteComponent(ABC):
45
47
  """
46
48
  Parses the API response that is fetched in the update-method. Class specific, must be overridden in classes inheriting from this one.
47
49
  """
48
- pass
49
50
 
50
51
  def _assert_auth(self):
51
52
  if self._session is None:
@@ -59,7 +60,7 @@ class BaseSiteComponent(ABC):
59
60
  """
60
61
  return commons._get_object(identificator_id, identificator, Class, NotFoundException, self._session)
61
62
 
62
- update_function: FunctionType = requests.get
63
+ update_function = requests.get
63
64
  """
64
65
  Internal function run on update. Function is a method of the 'requests' module/class
65
66
  """
@@ -5,7 +5,7 @@ from bs4 import PageElement
5
5
 
6
6
  from . import user, project, studio
7
7
  from ._base import BaseSiteComponent
8
- from ..utils import exceptions
8
+ from scratchattach.utils import exceptions
9
9
 
10
10
 
11
11
  class Activity(BaseSiteComponent):
@@ -20,7 +20,6 @@ class Activity(BaseSiteComponent):
20
20
  return str(self.raw)
21
21
 
22
22
  def __init__(self, **entries):
23
-
24
23
  # Set attributes every Activity object needs to have:
25
24
  self._session = None
26
25
  self.raw = None
@@ -81,7 +80,8 @@ class Activity(BaseSiteComponent):
81
80
  recipient_username = None
82
81
 
83
82
  default_case = False
84
- """Whether this is 'blank'; it will default to 'user performed an action'"""
83
+ # Even if `activity_type` is an invalid value; it will default to 'user performed an action'
84
+
85
85
  if activity_type == 0:
86
86
  # follow
87
87
  followed_username = data["followed_username"]
@@ -150,13 +150,7 @@ class Activity(BaseSiteComponent):
150
150
  self.project_id = project_id
151
151
  self.recipient_username = recipient_username
152
152
 
153
- elif activity_type == 8:
154
- default_case = True
155
-
156
- elif activity_type == 9:
157
- default_case = True
158
-
159
- elif activity_type == 10:
153
+ elif activity_type in (8, 9, 10):
160
154
  # Share/Reshare project
161
155
  project_id = data["project"]
162
156
  is_reshare = data["is_reshare"]
@@ -187,9 +181,8 @@ class Activity(BaseSiteComponent):
187
181
  self.project_id = parent_id
188
182
  self.recipient_username = recipient_username
189
183
 
190
- elif activity_type == 12:
191
- default_case = True
192
-
184
+ # type 12 does not exist in the HTML. That's why it was removed, not merged with type 13.
185
+
193
186
  elif activity_type == 13:
194
187
  # Create ('add') studio
195
188
  studio_id = data["gallery"]
@@ -216,16 +209,7 @@ class Activity(BaseSiteComponent):
216
209
  self.username = username
217
210
  self.gallery_id = studio_id
218
211
 
219
- elif activity_type == 16:
220
- default_case = True
221
-
222
- elif activity_type == 17:
223
- default_case = True
224
-
225
- elif activity_type == 18:
226
- default_case = True
227
-
228
- elif activity_type == 19:
212
+ elif activity_type in (16, 17, 18, 19):
229
213
  # Remove project from studio
230
214
 
231
215
  project_id = data["project"]
@@ -240,13 +224,7 @@ class Activity(BaseSiteComponent):
240
224
  self.username = username
241
225
  self.project_id = project_id
242
226
 
243
- elif activity_type == 20:
244
- default_case = True
245
-
246
- elif activity_type == 21:
247
- default_case = True
248
-
249
- elif activity_type == 22:
227
+ elif activity_type in (20, 21, 22):
250
228
  # Was promoted to manager for studio
251
229
  studio_id = data["gallery"]
252
230
 
@@ -260,13 +238,7 @@ class Activity(BaseSiteComponent):
260
238
  self.recipient_username = recipient_username
261
239
  self.gallery_id = studio_id
262
240
 
263
- elif activity_type == 23:
264
- default_case = True
265
-
266
- elif activity_type == 24:
267
- default_case = True
268
-
269
- elif activity_type == 25:
241
+ elif activity_type in (23, 24, 25):
270
242
  # Update profile
271
243
  raw = f"{username} made a profile update"
272
244
 
@@ -276,10 +248,7 @@ class Activity(BaseSiteComponent):
276
248
 
277
249
  self.username = username
278
250
 
279
- elif activity_type == 26:
280
- default_case = True
281
-
282
- elif activity_type == 27:
251
+ elif activity_type in (26, 27):
283
252
  # Comment (quite complicated)
284
253
  comment_type: int = data["comment_type"]
285
254
  fragment = data["comment_fragment"]
@@ -313,13 +282,12 @@ class Activity(BaseSiteComponent):
313
282
  self.comment_obj_id = comment_obj_id
314
283
  self.comment_obj_title = comment_obj_title
315
284
  self.comment_id = comment_id
316
-
317
285
  else:
318
286
  default_case = True
319
287
 
320
288
  if default_case:
321
289
  # This is coded in the scratch HTML, haven't found an example of it though
322
- raw = f"{username} performed an action"
290
+ raw = f"{username} performed an action."
323
291
 
324
292
  self.raw = raw
325
293
  self.datetime_created = _time
@@ -0,0 +1,227 @@
1
+ # classroom alerts (& normal alerts in the future)
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import pprint
7
+ import warnings
8
+ from dataclasses import dataclass, field, KW_ONLY
9
+ from datetime import datetime
10
+ from typing import TYPE_CHECKING, Any, Optional, Union
11
+ from typing_extensions import Self
12
+
13
+ from . import user, project, studio, comment, session
14
+ from scratchattach.utils import enums
15
+
16
+ if TYPE_CHECKING:
17
+ ...
18
+
19
+
20
+ # todo: implement regular alerts
21
+ # If you implement regular alerts, it may be applicable to make EducatorAlert a subclass.
22
+
23
+
24
+ @dataclass
25
+ class EducatorAlert:
26
+ """
27
+ Represents an alert for student activity, viewable at https://scratch.mit.edu/site-api/classrooms/alerts/
28
+
29
+ Attributes:
30
+ model: The type of alert (presumably); should always equal "educators.educatoralert" in this class
31
+ type: An integer that identifies the type of alert, differentiating e.g. against bans or autoban or censored comments etc
32
+ raw: The raw JSON data from the API
33
+ id: The ID of the alert (internally called 'pk' by scratch, not sure what this is for)
34
+ time_read: The time the alert was read
35
+ time_created: The time the alert was created
36
+ target: The user that the alert is about (the student)
37
+ actor: The user that created the alert (the admin)
38
+ target_object: The object that the alert is about (e.g. a project, studio, or comment)
39
+ notification_type: not sure what this is for, but inferred from the scratch HTML reference
40
+ """
41
+ _: KW_ONLY
42
+ # required attrs
43
+ target: user.User
44
+ actor: user.User
45
+ target_object: Optional[Union[project.Project, studio.Studio, comment.Comment, studio.Studio]]
46
+ notification_type: str
47
+ _session: Optional[session.Session]
48
+
49
+ # defaulted attrs
50
+ model: str = "educators.educatoralert"
51
+ type: int = -1
52
+ raw: dict = field(repr=False, default_factory=dict)
53
+ id: int = -1
54
+ time_read: datetime = datetime.fromtimestamp(0.0)
55
+ time_created: datetime = datetime.fromtimestamp(0.0)
56
+
57
+
58
+ @classmethod
59
+ def from_json(cls, data: dict[str, Any], _session: Optional[session.Session] = None) -> Self:
60
+ """
61
+ Load an EducatorAlert from a JSON object.
62
+
63
+ Arguments:
64
+ data (dict): The JSON object
65
+ _session (session.Session): The session object used to load this data, to 'connect' to the alerts rather than just 'get' them
66
+
67
+ Returns:
68
+ EducatorAlert: The loaded EducatorAlert object
69
+ """
70
+ model = data.get("model") # With this class, should be equal to educators.educatoralert
71
+ assert isinstance(model, str)
72
+ alert_id = data.get("pk") # not sure what kind of pk/id this is. Doesn't seem to be a user or class id.
73
+ assert isinstance(alert_id, int)
74
+
75
+ fields = data.get("fields")
76
+ assert isinstance(fields, dict)
77
+
78
+ time_read_raw = fields.get("educator_datetime_read")
79
+ assert isinstance(time_read_raw, str)
80
+ time_read: datetime = datetime.fromisoformat(time_read_raw)
81
+
82
+ admin_action = fields.get("admin_action")
83
+ assert isinstance(admin_action, dict)
84
+
85
+ time_created_raw = admin_action.get("datetime_created")
86
+ assert isinstance(time_created_raw, str)
87
+ time_created: datetime = datetime.fromisoformat(time_created_raw)
88
+
89
+ alert_type = admin_action.get("type")
90
+ assert isinstance(alert_type, int)
91
+
92
+ target_data = admin_action.get("target_user")
93
+ assert isinstance(target_data, dict)
94
+ target = user.User(username=target_data.get("username"),
95
+ id=target_data.get("pk"),
96
+ icon_url=target_data.get("thumbnail_url"),
97
+ admin=target_data.get("admin", False),
98
+ _session=_session)
99
+
100
+ actor_data = admin_action.get("actor")
101
+ assert isinstance(actor_data, dict)
102
+ actor = user.User(username=actor_data.get("username"),
103
+ id=actor_data.get("pk"),
104
+ icon_url=actor_data.get("thumbnail_url"),
105
+ admin=actor_data.get("admin", False),
106
+ _session=_session)
107
+
108
+ object_id = admin_action.get("object_id") # this could be a comment id, a project id, etc.
109
+ assert isinstance(object_id, int)
110
+ target_object: project.Project | studio.Studio | comment.Comment | None = None
111
+
112
+ extra_data: dict[str, Any] = json.loads(admin_action.get("extra_data", "{}"))
113
+ # todo: if possible, properly implement the incomplete parts of this parser (look for warning.warn())
114
+ notification_type: str = ""
115
+
116
+ if "project_title" in extra_data:
117
+ # project
118
+ target_object = project.Project(id=object_id,
119
+ title=extra_data["project_title"],
120
+ _session=_session)
121
+ elif "comment_content" in extra_data:
122
+ # comment
123
+ comment_data: dict[str, Any] = extra_data["comment_content"]
124
+ content: str | None = comment_data.get("content")
125
+
126
+ comment_obj_id: int | None = comment_data.get("comment_obj_id")
127
+
128
+ comment_type: int | None = comment_data.get("comment_type")
129
+
130
+ if comment_type == 0:
131
+ # project
132
+ comment_source_type = "project"
133
+ elif comment_type == 1:
134
+ # profile
135
+ comment_source_type = "profile"
136
+ else:
137
+ # probably a studio
138
+ comment_source_type = "studio"
139
+ warnings.warn(
140
+ f"The parser was not able to recognise the \"comment_type\" of {comment_type} in the alert JSON response.\n"
141
+ f"Full response: \n{pprint.pformat(data)}.\n\n"
142
+ f"Please draft an issue on github: https://github.com/TimMcCool/scratchattach/issues, providing this "
143
+ f"whole error message. This will allow us to implement an incomplete part of this parser")
144
+
145
+ # the comment_obj's corresponding attribute of comment.Comment is the place() method. As it has no cache, the title data is wasted.
146
+ # if the comment_obj is deleted, this is still a valid way of working out the title/username
147
+
148
+ target_object = comment.Comment(
149
+ id=object_id,
150
+ content=content,
151
+ source=comment_source_type,
152
+ source_id=comment_obj_id,
153
+ _session=_session
154
+ )
155
+
156
+ elif "gallery_title" in extra_data:
157
+ # studio
158
+ # possible implemented incorrectly
159
+ target_object = studio.Studio(
160
+ id=object_id,
161
+ title=extra_data["gallery_title"],
162
+ _session=_session
163
+ )
164
+ elif "notification_type" in extra_data:
165
+ # possible implemented incorrectly
166
+ notification_type = extra_data["notification_type"]
167
+ else:
168
+ warnings.warn(
169
+ f"The parser was not able to recognise the \"extra_data\" in the alert JSON response.\n"
170
+ f"Full response: \n{pprint.pformat(data)}.\n\n"
171
+ f"Please draft an issue on github: https://github.com/TimMcCool/scratchattach/issues, providing this "
172
+ f"whole error message. This will allow us to implement an incomplete part of this parser")
173
+
174
+ return cls(
175
+ id=alert_id,
176
+ model=model,
177
+ type=alert_type,
178
+ raw=data,
179
+ time_read=time_read,
180
+ time_created=time_created,
181
+ target=target,
182
+ actor=actor,
183
+ target_object=target_object,
184
+ notification_type=notification_type,
185
+ _session=_session
186
+ )
187
+
188
+ def __str__(self):
189
+ return f"EducatorAlert: {self.message}"
190
+
191
+ @property
192
+ def alert_type(self) -> enums.AlertType:
193
+ """
194
+ Get an associated AlertType object for this alert (based on the type index)
195
+ """
196
+ alert_type = enums.AlertTypes.find(self.type)
197
+ if not alert_type:
198
+ alert_type = enums.AlertTypes.default.value
199
+
200
+ return alert_type
201
+
202
+ @property
203
+ def message(self):
204
+ """
205
+ Format the alert message using the alert type's message template, as it would be on the website.
206
+ """
207
+ raw_message = self.alert_type.message
208
+ comment_content = ""
209
+ if isinstance(self.target_object, comment.Comment):
210
+ comment_content = self.target_object.content
211
+
212
+ return raw_message.format(username=self.target.username,
213
+ project=self.target_object_title,
214
+ studio=self.target_object_title,
215
+ notification_type=self.notification_type,
216
+ comment=comment_content)
217
+
218
+ @property
219
+ def target_object_title(self):
220
+ """
221
+ Get the title of the target object (if applicable)
222
+ """
223
+ if isinstance(self.target_object, project.Project):
224
+ return self.target_object.title
225
+ if isinstance(self.target_object, studio.Studio):
226
+ return self.target_object.title
227
+ return None # explicit
@@ -5,8 +5,8 @@ import time
5
5
  import logging
6
6
 
7
7
  from ._base import BaseSiteComponent
8
- from ..utils import exceptions
9
- from ..utils.requests import Requests as requests
8
+ from scratchattach.utils import exceptions
9
+ from scratchattach.utils.requests import requests
10
10
 
11
11
 
12
12
 
@@ -0,0 +1,17 @@
1
+ # browser_cookie3.pyi
2
+
3
+ import http.cookiejar
4
+ from typing import Optional
5
+
6
+ def chrome(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
7
+ def chromium(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
8
+ def firefox(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
9
+ def opera(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
10
+ def edge(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
11
+ def brave(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
12
+ def vivaldi(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
13
+ def safari(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
14
+ def lynx(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
15
+ def w3m(cookie_file: Optional[str] = None, key_file: Optional[str] = None) -> http.cookiejar.CookieJar: return NotImplemented
16
+
17
+ def load() -> http.cookiejar.CookieJar: return NotImplemented