scratchattach 2.1.13__py3-none-any.whl → 2.1.15b0__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 (55) 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 +86 -15
  6. scratchattach/editor/blockshape.py +10 -6
  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 +49 -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 +2 -2
  25. scratchattach/eventhandlers/cloud_events.py +2 -2
  26. scratchattach/eventhandlers/cloud_requests.py +3 -3
  27. scratchattach/eventhandlers/cloud_server.py +3 -3
  28. scratchattach/eventhandlers/message_events.py +1 -1
  29. scratchattach/other/other_apis.py +4 -4
  30. scratchattach/other/project_json_capabilities.py +3 -3
  31. scratchattach/site/_base.py +13 -12
  32. scratchattach/site/activity.py +11 -43
  33. scratchattach/site/alert.py +227 -0
  34. scratchattach/site/backpack_asset.py +2 -2
  35. scratchattach/site/browser_cookie3_stub.py +17 -0
  36. scratchattach/site/browser_cookies.py +27 -21
  37. scratchattach/site/classroom.py +51 -34
  38. scratchattach/site/cloud_activity.py +4 -4
  39. scratchattach/site/comment.py +30 -8
  40. scratchattach/site/forum.py +101 -69
  41. scratchattach/site/project.py +42 -21
  42. scratchattach/site/session.py +170 -80
  43. scratchattach/site/studio.py +4 -4
  44. scratchattach/site/user.py +179 -64
  45. scratchattach/utils/commons.py +35 -23
  46. scratchattach/utils/enums.py +44 -5
  47. scratchattach/utils/exceptions.py +10 -0
  48. scratchattach/utils/requests.py +57 -31
  49. {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/METADATA +8 -3
  50. scratchattach-2.1.15b0.dist-info/RECORD +66 -0
  51. {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/WHEEL +1 -1
  52. scratchattach/editor/sbuild.py +0 -2837
  53. scratchattach-2.1.13.dist-info/RECORD +0 -63
  54. {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/licenses/LICENSE +0 -0
  55. {scratchattach-2.1.13.dist-info → scratchattach-2.1.15b0.dist-info}/top_level.txt +0 -0
@@ -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
 
@@ -5,15 +5,19 @@ import json
5
5
  import random
6
6
  import base64
7
7
  import time
8
+ import zipfile
9
+ from io import BytesIO
10
+
11
+ from typing import Any
8
12
  from . import user, comment, studio
9
- from ..utils import exceptions
10
- from ..utils import commons
11
- from ..utils.commons import empty_project_json, headers
13
+ from scratchattach.utils import exceptions
14
+ from scratchattach.utils import commons
15
+ from scratchattach.utils.commons import empty_project_json, headers
12
16
  from ._base import BaseSiteComponent
13
- from ..other.project_json_capabilities import ProjectBody
14
- from ..utils.requests import Requests as requests
17
+ from scratchattach.other.project_json_capabilities import ProjectBody
18
+ from scratchattach.utils.requests import requests
15
19
 
16
- CREATE_PROJECT_USES = []
20
+ CREATE_PROJECT_USES: list[float] = []
17
21
 
18
22
  class PartialProject(BaseSiteComponent):
19
23
  """
@@ -27,7 +31,7 @@ class PartialProject(BaseSiteComponent):
27
31
 
28
32
  # Info on how the .update method has to fetch the data:
29
33
  self.update_function = requests.get
30
- self.update_API = f"https://api.scratch.mit.edu/projects/{entries['id']}"
34
+ self.update_api = f"https://api.scratch.mit.edu/projects/{entries['id']}"
31
35
 
32
36
  # Set attributes every Project object needs to have:
33
37
  self._session = None
@@ -126,6 +130,9 @@ class PartialProject(BaseSiteComponent):
126
130
  p = get_project(self.id)
127
131
  return isinstance(p, Project)
128
132
 
133
+ def raw_json_or_empty(self) -> dict[str, Any]:
134
+ return empty_project_json
135
+
129
136
  def create_remix(self, *, title=None, project_json=None): # not working
130
137
  """
131
138
  Creates a project on the Scratch website.
@@ -142,10 +149,7 @@ class PartialProject(BaseSiteComponent):
142
149
  else:
143
150
  title = " remix"
144
151
  if project_json is None:
145
- if "title" in self.__dict__:
146
- project_json = self.raw_json()
147
- else:
148
- project_json = empty_project_json
152
+ project_json = self.raw_json_or_empty()
149
153
 
150
154
  if len(CREATE_PROJECT_USES) < 5:
151
155
  CREATE_PROJECT_USES.insert(0, time.time())
@@ -239,7 +243,7 @@ class Project(PartialProject):
239
243
  # Overrides the load_description method that exists for unshared projects
240
244
  self.update()
241
245
 
242
- def download(self, *, filename=None, dir=""):
246
+ def download(self, *, filename=None, dir="."):
243
247
  """
244
248
  Downloads the project json to the given directory.
245
249
 
@@ -251,14 +255,15 @@ class Project(PartialProject):
251
255
  if filename is None:
252
256
  filename = str(self.id)
253
257
  if not (dir.endswith("/") or dir.endswith("\\")):
254
- dir = dir+"/"
258
+ dir += "/"
255
259
  self.update()
256
260
  response = requests.get(
257
261
  f"https://projects.scratch.mit.edu/{self.id}?token={self.project_token}",
258
262
  timeout=10,
259
263
  )
260
- filename = filename.replace(".sb3", "")
261
- open(f"{dir}{filename}.sb3", "wb").write(response.content)
264
+ filename = filename.removesuffix(".sb3")
265
+ with open(f"{dir}{filename}.sb3", "wb") as f:
266
+ f.write(response.content)
262
267
  except Exception:
263
268
  raise (
264
269
  exceptions.FetchError(
@@ -306,16 +311,32 @@ class Project(PartialProject):
306
311
  """
307
312
  try:
308
313
  self.update()
309
- return requests.get(
310
- f"https://projects.scratch.mit.edu/{self.id}?token={self.project_token}",
311
- timeout=10,
312
- ).json()
313
- except Exception:
314
+
315
+ except Exception as e:
314
316
  raise (
315
317
  exceptions.FetchError(
316
- "Either the project was created with an old Scratch version, or you're not authorized for accessing it"
318
+ f"You're not authorized for accessing {self}.\nException: {e}"
317
319
  )
318
320
  )
321
+
322
+ with requests.no_error_handling():
323
+ resp = requests.get(
324
+ f"https://projects.scratch.mit.edu/{self.id}?token={self.project_token}",
325
+ timeout=10,
326
+ )
327
+
328
+ try:
329
+ return resp.json()
330
+ except json.JSONDecodeError:
331
+ # I am not aware of any cases where this will not be a zip file
332
+ # in the future, cache a projectbody object here and just return the json
333
+ # that is fetched from there to not waste existing asset data from this zip file
334
+
335
+ with zipfile.ZipFile(BytesIO(resp.content)) as zipf:
336
+ return json.load(zipf.open("project.json"))
337
+
338
+ def raw_json_or_empty(self):
339
+ return self.raw_json()
319
340
 
320
341
  def creator_agent(self):
321
342
  """