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
@@ -1,9 +1,13 @@
1
- from typing import Optional
1
+ from typing import Optional, TYPE_CHECKING
2
+ from typing_extensions import assert_never
2
3
  from http.cookiejar import CookieJar
3
4
  from enum import Enum, auto
4
5
  browsercookie_err = None
5
6
  try:
6
- import browsercookie
7
+ if TYPE_CHECKING:
8
+ from . import browser_cookie3_stub as browser_cookie3
9
+ else:
10
+ import browser_cookie3
7
11
  except Exception as e:
8
12
  browsercookie = None
9
13
  browsercookie_err = e
@@ -15,8 +19,8 @@ class Browser(Enum):
15
19
  EDGE = auto()
16
20
  SAFARI = auto()
17
21
  CHROMIUM = auto()
18
- EDGE_DEV = auto()
19
22
  VIVALDI = auto()
23
+ EDGE_DEV = auto()
20
24
 
21
25
 
22
26
  FIREFOX = Browser.FIREFOX
@@ -24,32 +28,34 @@ CHROME = Browser.CHROME
24
28
  EDGE = Browser.EDGE
25
29
  SAFARI = Browser.SAFARI
26
30
  CHROMIUM = Browser.CHROMIUM
27
- EDGE_DEV = Browser.EDGE_DEV
28
31
  VIVALDI = Browser.VIVALDI
29
32
  ANY = Browser.ANY
33
+ EDGE_DEV = Browser.EDGE_DEV
30
34
 
31
35
  def cookies_from_browser(browser : Browser = ANY) -> dict[str, str]:
32
36
  """
33
37
  Import cookies from browser to login
34
38
  """
35
- if not browsercookie:
39
+ if not browser_cookie3:
36
40
  raise browsercookie_err or ModuleNotFoundError()
37
41
  cookies : Optional[CookieJar] = None
38
- if browser == ANY:
39
- cookies = browsercookie.load()
40
- elif browser == FIREFOX:
41
- cookies = browsercookie.firefox()
42
- elif browser == CHROME:
43
- cookies = browsercookie.chrome()
44
- elif browser == EDGE:
45
- cookies = browsercookie.edge()
46
- elif browser == SAFARI:
47
- cookies = browsercookie.safari()
48
- elif browser == CHROMIUM:
49
- cookies = browsercookie.chromium()
50
- elif browser == EDGE_DEV:
51
- cookies = browsercookie.edge_dev()
52
- elif browser == VIVALDI:
53
- cookies = browsercookie.vivaldi()
42
+ if browser is Browser.ANY:
43
+ cookies = browser_cookie3.load()
44
+ elif browser is Browser.FIREFOX:
45
+ cookies = browser_cookie3.firefox()
46
+ elif browser is Browser.CHROME:
47
+ cookies = browser_cookie3.chrome()
48
+ elif browser is Browser.EDGE:
49
+ cookies = browser_cookie3.edge()
50
+ elif browser is Browser.SAFARI:
51
+ cookies = browser_cookie3.safari()
52
+ elif browser is Browser.CHROMIUM:
53
+ cookies = browser_cookie3.chromium()
54
+ elif browser is Browser.VIVALDI:
55
+ cookies = browser_cookie3.vivaldi()
56
+ elif browser is Browser.EDGE_DEV:
57
+ raise ValueError("EDGE_DEV is not supported anymore.")
58
+ else:
59
+ assert_never(browser)
54
60
  assert isinstance(cookies, CookieJar)
55
61
  return {cookie.name: cookie.value for cookie in cookies if "scratch.mit.edu" in cookie.domain and cookie.value}
@@ -1,58 +1,67 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import datetime
4
+ import json
4
5
  import warnings
5
- from typing import Optional, TYPE_CHECKING, Any
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime
8
+ from typing import Optional, TYPE_CHECKING, Any, Callable
6
9
 
7
10
  import bs4
11
+ from bs4 import BeautifulSoup
8
12
 
9
13
  if TYPE_CHECKING:
10
- from ..site.session import Session
14
+ from scratchattach.site.session import Session
11
15
 
12
- from ..utils.commons import requests
16
+ from scratchattach.utils.commons import requests
13
17
  from . import user, activity
14
18
  from ._base import BaseSiteComponent
15
- from ..utils import exceptions, commons
16
- from ..utils.commons import headers
17
-
18
- from bs4 import BeautifulSoup
19
+ from scratchattach.utils import exceptions, commons
20
+ from scratchattach.utils.commons import headers
19
21
 
20
22
 
23
+ @dataclass
21
24
  class Classroom(BaseSiteComponent):
22
- def __init__(self, **entries):
25
+ title: str = None
26
+ id: int = None
27
+ classtoken: str = None
28
+
29
+ author: user.User = None
30
+ about_class: str = None
31
+ working_on: str = None
32
+
33
+ is_closed: bool = False
34
+ datetime: datetime = None
35
+
36
+
37
+ update_function: Callable = field(repr=False, default=requests.get)
38
+ _session: Optional[Session] = field(repr=False, default=None)
39
+
40
+ def __post_init__(self):
23
41
  # Info on how the .update method has to fetch the data:
24
42
  # NOTE: THIS DOESN'T WORK WITH CLOSED CLASSES!
25
- self.update_function = requests.get
26
- if "id" in entries:
27
- self.update_API = f"https://api.scratch.mit.edu/classrooms/{entries['id']}"
28
- elif "classtoken" in entries:
29
- self.update_API = f"https://api.scratch.mit.edu/classtoken/{entries['classtoken']}"
43
+ if self.id:
44
+ self.update_api = f"https://api.scratch.mit.edu/classrooms/{self.id}"
45
+ elif self.classtoken:
46
+ self.update_api = f"https://api.scratch.mit.edu/classtoken/{self.classtoken}"
30
47
  else:
31
- raise KeyError(f"No class id or token provided! Entries: {entries}")
32
-
33
- # Set attributes every Project object needs to have:
34
- self._session: Session = None
35
- self.id = None
36
- self.classtoken = None
37
-
38
- self.__dict__.update(entries)
48
+ raise KeyError(f"No class id or token provided! {self.__dict__ = }")
39
49
 
40
50
  # Headers and cookies:
41
51
  if self._session is None:
42
- self._headers = headers
52
+ self._headers = commons.headers
43
53
  self._cookies = {}
44
54
  else:
45
55
  self._headers = self._session._headers
46
56
  self._cookies = self._session._cookies
47
57
 
48
58
  # Headers for operations that require accept and Content-Type fields:
49
- self._json_headers = dict(self._headers)
50
- self._json_headers["accept"] = "application/json"
51
- self._json_headers["Content-Type"] = "application/json"
52
- self.is_closed = False
59
+ self._json_headers = {**self._headers,
60
+ "accept": "application/json",
61
+ "Content-Type": "application/json"}
53
62
 
54
- def __repr__(self) -> str:
55
- return f"classroom called {self.title!r}"
63
+ def __str__(self) -> str:
64
+ return f"<Classroom {self.title!r}, id={self.id!r}>"
56
65
 
57
66
  def update(self):
58
67
  try:
@@ -305,7 +314,8 @@ class Classroom(BaseSiteComponent):
305
314
  warnings.warn(f"{self._session} may not be authenticated to edit {self}")
306
315
  raise e
307
316
 
308
- def register_student(self, username: str, password: str = '', birth_month: Optional[int] = None, birth_year: Optional[int] = None,
317
+ def register_student(self, username: str, password: str = '', birth_month: Optional[int] = None,
318
+ birth_year: Optional[int] = None,
309
319
  gender: Optional[str] = None, country: Optional[str] = None, is_robot: bool = False) -> None:
310
320
  return register_by_token(self.id, self.classtoken, username, password, birth_month, birth_year, gender, country,
311
321
  is_robot)
@@ -346,7 +356,8 @@ class Classroom(BaseSiteComponent):
346
356
 
347
357
  return activities
348
358
 
349
- def activity(self, student: str = "all", mode: str = "Last created", page: Optional[int] = None) -> list[dict[str, Any]]:
359
+ def activity(self, student: str = "all", mode: str = "Last created", page: Optional[int] = None) -> list[
360
+ dict[str, Any]]:
350
361
  """
351
362
  Get a list of private activity, only available to the class owner.
352
363
  Returns:
@@ -357,9 +368,15 @@ class Classroom(BaseSiteComponent):
357
368
 
358
369
  ascsort, descsort = commons.get_class_sort_mode(mode)
359
370
 
360
- data = requests.get(f"https://scratch.mit.edu/site-api/classrooms/activity/{self.id}/{student}/",
361
- params={"page": page, "ascsort": ascsort, "descsort": descsort},
362
- headers=self._headers, cookies=self._cookies).json()
371
+ with requests.no_error_handling():
372
+ try:
373
+ data = requests.get(
374
+ f"https://scratch.mit.edu/site-api/classrooms/activity/{self.id}/{student}/",
375
+ params={"page": page, "ascsort": ascsort, "descsort": descsort},
376
+ headers=self._headers, cookies=self._cookies
377
+ ).json()
378
+ except json.JSONDecodeError:
379
+ return []
363
380
 
364
381
  _activity = []
365
382
  for activity_json in data:
@@ -421,7 +438,7 @@ def register_by_token(class_id: int, class_token: str, username: str, password:
421
438
  "is_robot": is_robot}
422
439
 
423
440
  response = requests.post("https://scratch.mit.edu/classes/register_new_student/",
424
- data=data, headers=headers, cookies={"scratchcsrftoken": 'a'})
441
+ data=data, headers=commons.headers, cookies={"scratchcsrftoken": 'a'})
425
442
  ret = response.json()[0]
426
443
 
427
444
  if "username" in ret:
@@ -91,8 +91,8 @@ class CloudActivity(BaseSiteComponent):
91
91
  """
92
92
  if self.username is None:
93
93
  return None
94
- from ..site import user
95
- from ..utils import exceptions
94
+ from scratchattach.site import user
95
+ from scratchattach.utils import exceptions
96
96
  return self._make_linked_object("username", self.username, user.User, exceptions.UserNotFound)
97
97
 
98
98
  def project(self):
@@ -101,7 +101,7 @@ class CloudActivity(BaseSiteComponent):
101
101
  """
102
102
  if self.cloud is None:
103
103
  return None
104
- from ..site import project
105
- from ..utils import exceptions
104
+ from scratchattach.site import project
105
+ from scratchattach.utils import exceptions
106
106
  return self._make_linked_object("id", self.cloud.project_id, project.Project, exceptions.ProjectNotFound)
107
107
 
@@ -1,17 +1,29 @@
1
1
  """Comment class"""
2
2
  from __future__ import annotations
3
3
 
4
+ import html
5
+ from typing import Union, Optional, Any
6
+ from typing_extensions import assert_never # importing from typing caused me errors
7
+ from enum import Enum, auto
8
+
4
9
  from . import user, project, studio
5
10
  from ._base import BaseSiteComponent
6
- from ..utils import exceptions
7
-
11
+ from scratchattach.utils import exceptions
8
12
 
9
13
  class Comment(BaseSiteComponent):
10
14
  """
11
15
  Represents a Scratch comment (on a profile, studio or project)
12
16
  """
13
-
14
- def str(self):
17
+ id: Union[int, str]
18
+ source: str
19
+ source_id: Union[int, str]
20
+ cached_replies: Optional[list[Comment]]
21
+ parent_id: Optional[Union[int, str]]
22
+ cached_parent_comment: Optional[Comment]
23
+ commentee_id: Optional[int]
24
+ content: Any
25
+
26
+ def __str__(self):
15
27
  return str(self.content)
16
28
 
17
29
  def __init__(self, **entries):
@@ -82,6 +94,12 @@ class Comment(BaseSiteComponent):
82
94
  pass
83
95
  return True
84
96
 
97
+ @property
98
+ def text(self) -> str:
99
+ if self.source == "profile":
100
+ return self.content
101
+ return str(html.unescape(self.content))
102
+
85
103
  # Methods for getting related entities
86
104
 
87
105
  def author(self) -> user.User:
@@ -95,10 +113,12 @@ class Comment(BaseSiteComponent):
95
113
  """
96
114
  if self.source == "profile":
97
115
  return self._make_linked_object("username", self.source_id, user.User, exceptions.UserNotFound)
98
- if self.source == "studio":
116
+ elif self.source == "studio":
99
117
  return self._make_linked_object("id", self.source_id, studio.Studio, exceptions.UserNotFound)
100
- if self.source == "project":
118
+ elif self.source == "project":
101
119
  return self._make_linked_object("id", self.source_id, project.Project, exceptions.UserNotFound)
120
+ else:
121
+ raise ValueError("Unknown source.")
102
122
 
103
123
  def parent_comment(self) -> Comment | None:
104
124
  if self.parent_id is None:
@@ -129,8 +149,10 @@ class Comment(BaseSiteComponent):
129
149
  """
130
150
  if (self.cached_replies is None) or (not use_cache):
131
151
  if self.source == "profile":
132
- self.cached_replies = user.User(username=self.source_id, _session=self._session).comment_by_id(
133
- self.id).cached_replies[offset:offset + limit]
152
+ _cached_replies = user.User(username=self.source_id, _session=self._session).comment_by_id(
153
+ self.id).cached_replies
154
+ if _cached_replies is not None:
155
+ self.cached_replies = _cached_replies[offset:offset + limit]
134
156
 
135
157
  elif self.source == "project":
136
158
  p = project.Project(id=self.source_id, _session=self._session)
@@ -1,16 +1,21 @@
1
1
  """ForumTopic and ForumPost classes"""
2
2
  from __future__ import annotations
3
3
 
4
- from . import user
5
- from ..utils.commons import headers
6
- from ..utils import exceptions, commons
7
- from ._base import BaseSiteComponent
8
- import xml.etree.ElementTree as ET
9
- from bs4 import BeautifulSoup
4
+ from dataclasses import dataclass, field
5
+ from typing import Optional, Any
10
6
  from urllib.parse import urlparse, parse_qs
7
+ import xml.etree.ElementTree as ET
11
8
 
12
- from ..utils.requests import Requests as requests
9
+ from bs4 import BeautifulSoup, Tag
10
+
11
+ from . import user
12
+ from . import session as module_session
13
+ from scratchattach.utils.commons import headers
14
+ from scratchattach.utils import exceptions, commons
15
+ from ._base import BaseSiteComponent
16
+ from scratchattach.utils.requests import requests
13
17
 
18
+ @dataclass
14
19
  class ForumTopic(BaseSiteComponent):
15
20
  '''
16
21
  Represents a Scratch forum topic.
@@ -33,28 +38,26 @@ class ForumTopic(BaseSiteComponent):
33
38
 
34
39
  :.update(): Updates the attributes
35
40
  '''
36
-
37
- def __init__(self, **entries):
41
+ id: int
42
+ title: str
43
+ category_name: str
44
+ last_updated: str
45
+ _session: Optional[module_session.Session] = field(default=None)
46
+ reply_count: Optional[int] = field(default=None)
47
+ view_count: Optional[int] = field(default=None)
48
+
49
+ def __post_init__(self):
38
50
  # Info on how the .update method has to fetch the data:
39
51
  self.update_function = requests.get
40
- self.update_API = f"https://scratch.mit.edu/discuss/feeds/topic/{entries['id']}/"
41
-
42
- # Set attributes every Project object needs to have:
43
- self._session = None
44
- self.id = 0
45
- self.reply_count = None
46
- self.view_count = None
47
-
48
- # Update attributes from entries dict:
49
- self.__dict__.update(entries)
52
+ self.update_api = f"https://scratch.mit.edu/discuss/feeds/topic/{self.id}/"
50
53
 
51
54
  # Headers and cookies:
52
55
  if self._session is None:
53
56
  self._headers = headers
54
57
  self._cookies = {}
55
58
  else:
56
- self._headers = self._session._headers
57
- self._cookies = self._session._cookies
59
+ self._headers = self._session.get_headers()
60
+ self._cookies = self._session.get_cookies()
58
61
 
59
62
  # Headers for operations that require accept and Content-Type fields:
60
63
  self._json_headers = dict(self._headers)
@@ -65,7 +68,7 @@ class ForumTopic(BaseSiteComponent):
65
68
  # As there is no JSON API for getting forum topics anymore,
66
69
  # the data has to be retrieved from the XML feed.
67
70
  response = self.update_function(
68
- self.update_API,
71
+ self.update_api,
69
72
  headers = self._headers,
70
73
  cookies = self._cookies, timeout=20 # fetching forums can take very long
71
74
  )
@@ -87,17 +90,22 @@ class ForumTopic(BaseSiteComponent):
87
90
  raise exceptions.ScrapeError(str(e))
88
91
  else:
89
92
  raise exceptions.ForumContentNotFound
90
-
91
- return self._update_from_dict(dict(
92
- title = title, category_name = category_name, last_updated = last_updated
93
- ))
94
-
95
-
96
- def _update_from_dict(self, data):
97
- self.__dict__.update(data)
93
+ self.title = title
94
+ self.category_name = category_name
95
+ self.last_updated = last_updated
98
96
  return True
97
+
98
+ @classmethod
99
+ def from_id(cls, __id: int, session: module_session.Session, update: bool = False):
100
+ new = cls(id=__id, _session=session, title="", last_updated="", category_name="")
101
+ if update:
102
+ new.update()
103
+ return new
104
+
105
+ def _update_from_dict(self, data: dict[str, Any]):
106
+ self.__dict__.update(data)
99
107
 
100
- def posts(self, *, page=1, order="oldest"):
108
+ def posts(self, *, page=1, order="oldest") -> list[ForumPost]:
101
109
  """
102
110
  Args:
103
111
  page (int): The page of the forum topic that should be returned. First page is at index 1.
@@ -117,10 +125,11 @@ class ForumTopic(BaseSiteComponent):
117
125
  raise exceptions.FetchError(str(e))
118
126
  try:
119
127
  soup = BeautifulSoup(response.content, 'html.parser')
120
- soup = soup.find("div", class_="djangobb")
121
-
128
+ soup_elm = soup.find("div", class_="djangobb")
129
+ assert isinstance(soup_elm, Tag)
122
130
  try:
123
- pagination_div = soup.find('div', class_='pagination')
131
+ pagination_div = soup_elm.find('div', class_='pagination')
132
+ assert isinstance(pagination_div, Tag)
124
133
  num_pages = int(pagination_div.find_all('a', class_='page')[-1].text)
125
134
  except Exception:
126
135
  num_pages = 1
@@ -128,8 +137,9 @@ class ForumTopic(BaseSiteComponent):
128
137
  try:
129
138
  # get topic category:
130
139
  topic_category = ""
131
- breadcrumb_ul = soup.find_all('ul')[1] # Find the second ul element
140
+ breadcrumb_ul = soup_elm.find_all('ul')[1] # Find the second ul element
132
141
  if breadcrumb_ul:
142
+ assert isinstance(breadcrumb_ul, Tag)
133
143
  link = breadcrumb_ul.find_all('a')[1] # Get the right anchor tag
134
144
  topic_category = link.text.strip() # Extract and strip text content
135
145
  except Exception as e:
@@ -139,12 +149,14 @@ class ForumTopic(BaseSiteComponent):
139
149
  # get corresponding posts:
140
150
  post_htmls = soup.find_all('div', class_='blockpost')
141
151
  for raw_post in post_htmls:
142
- post = ForumPost(id=int(raw_post['id'].replace("p", "")), topic_id=self.id, _session=self._session, topic_category=topic_category, topic_num_pages=num_pages)
143
- post._update_from_html(raw_post)
152
+ if not isinstance(raw_post, Tag):
153
+ continue
154
+ post = ForumPost(id=int(str(raw_post['id']).replace("p", "")), topic_id=self.id, _session=self._session, topic_category=topic_category, topic_num_pages=num_pages)
155
+ post.update_from_html(raw_post)
144
156
 
145
157
  posts.append(post)
146
158
  except Exception as e:
147
- raise exceptions.ScrapeError(str(e))
159
+ raise exceptions.ScrapeError() from e
148
160
 
149
161
  return posts
150
162
 
@@ -157,7 +169,7 @@ class ForumTopic(BaseSiteComponent):
157
169
  if len(posts) > 0:
158
170
  return posts[0]
159
171
 
160
-
172
+ @dataclass
161
173
  class ForumPost(BaseSiteComponent):
162
174
  '''
163
175
  Represents a Scratch forum post.
@@ -190,34 +202,39 @@ class ForumPost(BaseSiteComponent):
190
202
 
191
203
  :.update(): Updates the attributes
192
204
  '''
193
-
194
- def __init__(self, **entries):
205
+ id: int = field(default=0)
206
+ topic_id: int = field(default=0)
207
+ topic_name: str = field(default="")
208
+ topic_category: str = field(default="")
209
+ topic_num_pages: int = field(default=0)
210
+ author_name: str = field(default="")
211
+ author_avatar_url: str = field(default="")
212
+ posted: str = field(default="")
213
+ deleted: bool = field(default=False)
214
+ html_content: str = field(default="")
215
+ content: str = field(default="")
216
+ post_index: int = field(default=0)
217
+ _session: Optional[module_session.Session] = field(default=None)
218
+ def __post_init__(self):
195
219
 
196
220
  # A forum post can't be updated the usual way as there is no API anymore
197
- self.update_function = None
198
- self.update_API = None
199
-
200
- # Set attributes every Project object needs to have:
201
- self._session = None
202
- self.id = 0
203
- self.topic_id = 0
204
- self.deleted = False
205
-
206
- # Update attributes from entries dict:
207
- self.__dict__.update(entries)
221
+ self.update_api = ""
208
222
 
209
223
  # Headers and cookies:
210
224
  if self._session is None:
211
225
  self._headers = headers
212
226
  self._cookies = {}
213
227
  else:
214
- self._headers = self._session._headers
215
- self._cookies = self._session._cookies
228
+ self._headers = self._session.get_headers()
229
+ self._cookies = self._session.get_cookies()
216
230
 
217
231
  # Headers for operations that require accept and Content-Type fields:
218
232
  self._json_headers = dict(self._headers)
219
233
  self._json_headers["accept"] = "application/json"
220
234
  self._json_headers["Content-Type"] = "application/json"
235
+
236
+ def update_function(self, *args, **kwargs):
237
+ raise TypeError("Forum posts cannot be updated like this")
221
238
 
222
239
  def update(self):
223
240
  """
@@ -225,32 +242,47 @@ class ForumPost(BaseSiteComponent):
225
242
  As there is no API for retrieving a single post anymore, this requires reloading the forum page.
226
243
  """
227
244
  page = 1
228
- posts = ForumTopic(id=self.topic_id, _session=self._session).posts(page=1)
245
+ posts = ForumTopic.from_id(self.topic_id, session=self._session).posts(page=1)
229
246
  while posts != []:
230
247
  matching = list(filter(lambda x : int(x.id) == int(self.id), posts))
231
248
  if len(matching) > 0:
232
249
  this = matching[0]
233
250
  break
234
251
  page += 1
235
- posts = ForumTopic(id=self.topic_id, _session=self._session).posts(page=page)
252
+ posts = ForumTopic.from_id(self.topic_id, session=self._session).posts(page=page)
236
253
  else:
237
254
  return False
238
-
239
- return self._update_from_dict(this.__dict__)
255
+ self._update_from_dict(vars(this))
240
256
 
241
- def _update_from_dict(self, data):
257
+ def _update_from_dict(self, data: dict[str, Any]):
242
258
  self.__dict__.update(data)
243
259
  return True
244
-
245
- def _update_from_html(self, soup_html):
246
- self.post_index = int(soup_html.find('span', class_='conr').text.strip('#'))
247
- self.id = int(soup_html['id'].replace("p", ""))
248
- self.posted = soup_html.find('a', href=True).text.strip()
249
- self.content = soup_html.find('div', class_='post_body_html').text.strip()
260
+
261
+ def update_from_html(self, soup_html: Tag):
262
+ return self._update_from_html(soup_html)
263
+
264
+ def _update_from_html(self, soup_html: Tag):
265
+ post_index_elm = soup_html.find('span', class_='conr')
266
+ assert isinstance(post_index_elm, Tag)
267
+ id_attr = soup_html['id']
268
+ assert isinstance(id_attr, str)
269
+ posted_elm = soup_html.find('a', href=True)
270
+ assert isinstance(posted_elm, Tag)
271
+ content_elm = soup_html.find('div', class_='post_body_html')
272
+ assert isinstance(content_elm, Tag)
273
+ author_name_elm = soup_html.select_one('dl dt a')
274
+ assert isinstance(author_name_elm, Tag)
275
+ topic_name_elm = soup_html.find('h3')
276
+ assert isinstance(topic_name_elm, Tag)
277
+
278
+ self.post_index = int(post_index_elm.text.strip('#'))
279
+ self.id = int(id_attr.replace("p", ""))
280
+ self.posted = posted_elm.text.strip()
281
+ self.content = content_elm.text.strip()
250
282
  self.html_content = str(soup_html.find('div', class_='post_body_html'))
251
- self.author_name = soup_html.find('dl').find('dt').find('a').text.strip()
252
- self.author_avatar_url = soup_html.find('dl').find('dt').find('a')['href']
253
- self.topic_name = soup_html.find('h3').text.strip()
283
+ self.author_name = author_name_elm.text.strip()
284
+ self.author_avatar_url = str(author_name_elm['href'])
285
+ self.topic_name = topic_name_elm.text.strip()
254
286
  return True
255
287
 
256
288
  def topic(self):
@@ -270,7 +302,7 @@ class ForumPost(BaseSiteComponent):
270
302
  """
271
303
  return self._make_linked_object("username", self.author_name, user.User, exceptions.UserNotFound)
272
304
 
273
- def edit(self, new_content):
305
+ def edit(self, new_content: str):
274
306
  """
275
307
  Changes the content of the forum post. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_post` or through another method that requires authentication. You must own the forum post.
276
308