scratchattach 2.1.15b0__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 (87) 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. {scratchattach/cloud → cloud}/_base.py +112 -87
  12. {scratchattach/cloud → cloud}/cloud.py +16 -16
  13. {scratchattach/editor → editor}/__init__.py +2 -1
  14. {scratchattach/editor → editor}/asset.py +26 -14
  15. {scratchattach/editor → editor}/backpack_json.py +3 -5
  16. {scratchattach/editor → editor}/base.py +2 -4
  17. {scratchattach/editor → editor}/block.py +27 -22
  18. {scratchattach/editor → editor}/blockshape.py +1 -1
  19. {scratchattach/editor → editor}/build_defaulting.py +2 -2
  20. editor/commons.py +145 -0
  21. {scratchattach/editor → editor}/field.py +1 -1
  22. {scratchattach/editor → editor}/inputs.py +6 -3
  23. {scratchattach/editor → editor}/meta.py +10 -7
  24. {scratchattach/editor → editor}/monitor.py +10 -8
  25. {scratchattach/editor → editor}/mutation.py +68 -11
  26. {scratchattach/editor → editor}/pallete.py +1 -3
  27. {scratchattach/editor → editor}/prim.py +4 -0
  28. {scratchattach/editor → editor}/project.py +118 -16
  29. {scratchattach/editor → editor}/sprite.py +25 -15
  30. {scratchattach/editor → editor}/vlb.py +2 -2
  31. {scratchattach/eventhandlers → eventhandlers}/_base.py +1 -0
  32. {scratchattach/eventhandlers → eventhandlers}/cloud_events.py +26 -6
  33. {scratchattach/eventhandlers → eventhandlers}/cloud_recorder.py +4 -4
  34. {scratchattach/eventhandlers → eventhandlers}/cloud_requests.py +139 -54
  35. {scratchattach/eventhandlers → eventhandlers}/cloud_server.py +6 -3
  36. {scratchattach/eventhandlers → eventhandlers}/cloud_storage.py +1 -2
  37. eventhandlers/filterbot.py +163 -0
  38. other/other_apis.py +598 -0
  39. {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/METADATA +7 -11
  40. scratchattach-3.0.0b1.dist-info/RECORD +79 -0
  41. {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/WHEEL +1 -1
  42. scratchattach-3.0.0b1.dist-info/entry_points.txt +2 -0
  43. scratchattach-3.0.0b1.dist-info/top_level.txt +7 -0
  44. {scratchattach/site → site}/_base.py +32 -5
  45. site/activity.py +426 -0
  46. {scratchattach/site → site}/alert.py +4 -5
  47. {scratchattach/site → site}/backpack_asset.py +2 -1
  48. {scratchattach/site → site}/classroom.py +80 -73
  49. {scratchattach/site → site}/cloud_activity.py +43 -29
  50. {scratchattach/site → site}/comment.py +86 -100
  51. {scratchattach/site → site}/forum.py +8 -4
  52. site/placeholder.py +132 -0
  53. {scratchattach/site → site}/project.py +228 -122
  54. {scratchattach/site → site}/session.py +156 -71
  55. {scratchattach/site → site}/studio.py +139 -46
  56. site/typed_dicts.py +151 -0
  57. {scratchattach/site → site}/user.py +511 -215
  58. {scratchattach/utils → utils}/commons.py +12 -4
  59. {scratchattach/utils → utils}/encoder.py +7 -4
  60. {scratchattach/utils → utils}/enums.py +1 -0
  61. {scratchattach/utils → utils}/exceptions.py +36 -2
  62. utils/optional_async.py +154 -0
  63. utils/requests.py +306 -0
  64. scratchattach/__init__.py +0 -29
  65. scratchattach/editor/commons.py +0 -273
  66. scratchattach/eventhandlers/filterbot.py +0 -161
  67. scratchattach/other/other_apis.py +0 -284
  68. scratchattach/site/activity.py +0 -382
  69. scratchattach/utils/requests.py +0 -93
  70. scratchattach-2.1.15b0.dist-info/RECORD +0 -66
  71. scratchattach-2.1.15b0.dist-info/top_level.txt +0 -1
  72. {scratchattach/cloud → cloud}/__init__.py +0 -0
  73. {scratchattach/editor → editor}/code_translation/__init__.py +0 -0
  74. {scratchattach/editor → editor}/code_translation/parse.py +0 -0
  75. {scratchattach/editor → editor}/comment.py +0 -0
  76. {scratchattach/editor → editor}/extension.py +0 -0
  77. {scratchattach/editor → editor}/twconfig.py +0 -0
  78. {scratchattach/eventhandlers → eventhandlers}/__init__.py +0 -0
  79. {scratchattach/eventhandlers → eventhandlers}/combine.py +0 -0
  80. {scratchattach/eventhandlers → eventhandlers}/message_events.py +0 -0
  81. {scratchattach/other → other}/__init__.py +0 -0
  82. {scratchattach/other → other}/project_json_capabilities.py +0 -0
  83. {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
  84. {scratchattach/site → site}/__init__.py +0 -0
  85. {scratchattach/site → site}/browser_cookie3_stub.py +0 -0
  86. {scratchattach/site → site}/browser_cookies.py +0 -0
  87. {scratchattach/utils → utils}/__init__.py +0 -0
@@ -2,23 +2,29 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import json
5
+ import pprint
5
6
  import random
6
7
  import base64
7
8
  import time
9
+ import warnings
8
10
  import zipfile
9
11
  from io import BytesIO
12
+ from typing import Callable
10
13
 
11
- from typing import Any
14
+ from typing import Any, Optional
15
+ from typing_extensions import deprecated
12
16
  from . import user, comment, studio
13
17
  from scratchattach.utils import exceptions
14
18
  from scratchattach.utils import commons
15
19
  from scratchattach.utils.commons import empty_project_json, headers
16
20
  from ._base import BaseSiteComponent
17
- from scratchattach.other.project_json_capabilities import ProjectBody
21
+ # from scratchattach.other.project_json_capabilities import ProjectBody
22
+ from scratchattach import editor
18
23
  from scratchattach.utils.requests import requests
19
24
 
20
25
  CREATE_PROJECT_USES: list[float] = []
21
26
 
27
+
22
28
  class PartialProject(BaseSiteComponent):
23
29
  """
24
30
  Represents an unshared Scratch project that can't be accessed.
@@ -27,10 +33,10 @@ class PartialProject(BaseSiteComponent):
27
33
  def __str__(self):
28
34
  return f"Unshared project with id {self.id}"
29
35
 
30
- def __init__(self, **entries):
36
+ def __init__(self, **entries) -> None:
31
37
 
32
38
  # Info on how the .update method has to fetch the data:
33
- self.update_function = requests.get
39
+ self.update_function: Callable = requests.get
34
40
  self.update_api = f"https://api.scratch.mit.edu/projects/{entries['id']}"
35
41
 
36
42
  # Set attributes every Project object needs to have:
@@ -39,10 +45,11 @@ class PartialProject(BaseSiteComponent):
39
45
  self.id = 0
40
46
  self.instructions = None
41
47
  self.parent_title = None
48
+ self._moderation_status = None
42
49
 
43
50
  # Update attributes from entries dict:
44
51
  self.__dict__.update(entries)
45
-
52
+
46
53
  # Headers and cookies:
47
54
  if self._session is None:
48
55
  self._headers = headers
@@ -61,50 +68,114 @@ class PartialProject(BaseSiteComponent):
61
68
  self.id = int(data["id"])
62
69
  except KeyError:
63
70
  pass
64
- try: self.url = "https://scratch.mit.edu/projects/" + str(self.id)
65
- except Exception: pass
66
- try: self.author_name = data["author"]["username"]
67
- except Exception: pass
68
- try: self.author_name = data["username"]
69
- except Exception: pass
70
- try: self.comments_allowed = data["comments_allowed"]
71
- except Exception: pass
72
- try: self.instructions = data["instructions"]
73
- except Exception: pass
74
- try: self.notes = data["description"]
75
- except Exception: pass
76
- try: self.created = data["history"]["created"]
77
- except Exception: pass
78
- try: self.last_modified = data["history"]["modified"]
79
- except Exception: pass
80
- try: self.share_date = data["history"]["shared"]
81
- except Exception: pass
82
- try: self.thumbnail_url = data["image"]
83
- except Exception: pass
71
+ try:
72
+ self.url = "https://scratch.mit.edu/projects/" + str(self.id)
73
+ except Exception:
74
+ pass
75
+ try:
76
+ self.author_name = data["author"]["username"]
77
+ except Exception:
78
+ pass
79
+ try:
80
+ self.author_name = data["username"]
81
+ except Exception:
82
+ pass
83
+ try:
84
+ self.comments_allowed = data["comments_allowed"]
85
+ except Exception:
86
+ pass
87
+ try:
88
+ self.instructions = data["instructions"]
89
+ except Exception:
90
+ pass
91
+ try:
92
+ self.notes = data["description"]
93
+ except Exception:
94
+ pass
95
+ try:
96
+ self.created = data["history"]["created"]
97
+ except Exception:
98
+ pass
99
+ try:
100
+ self.last_modified = data["history"]["modified"]
101
+ except Exception:
102
+ pass
103
+ try:
104
+ self.share_date = data["history"]["shared"]
105
+ except Exception:
106
+ pass
107
+ try:
108
+ self.thumbnail_url = data["image"]
109
+ except Exception:
110
+ pass
84
111
  try:
85
112
  self.remix_parent = data["remix"]["parent"]
86
113
  self.remix_root = data["remix"]["root"]
87
114
  except Exception:
88
115
  self.remix_parent = None
89
116
  self.remix_root = None
90
- try: self.favorites = data["stats"]["favorites"]
91
- except Exception: pass
92
- try: self.loves = data["stats"]["loves"]
93
- except Exception: pass
94
- try: self.remix_count = data["stats"]["remixes"]
95
- except Exception: pass
96
- try: self.views = data["stats"]["views"]
97
- except Exception: pass
98
- try: self.title = data["title"]
99
- except Exception: pass
117
+ try:
118
+ self.favorites = data["stats"]["favorites"]
119
+ except Exception:
120
+ pass
121
+ try:
122
+ self.loves = data["stats"]["loves"]
123
+ except Exception:
124
+ pass
125
+ try:
126
+ self.remix_count = data["stats"]["remixes"]
127
+ except Exception:
128
+ pass
129
+ try:
130
+ self.views = data["stats"]["views"]
131
+ except Exception:
132
+ pass
133
+ try:
134
+ self.title = data["title"]
135
+ except Exception:
136
+ pass
100
137
  try:
101
138
  self.project_token = data["project_token"]
102
139
  except Exception:
103
140
  self.project_token = None
104
- if "code" in data: # Project is unshared -> return false
141
+ if "code" in data: # Project is unshared -> return false
105
142
  return False
106
143
  return True
107
144
 
145
+ def __rich__(self):
146
+ from rich.panel import Panel
147
+ from rich.table import Table
148
+ from rich import box
149
+ from rich.markup import escape
150
+
151
+ url = f"[link={self.url}]{self.title}[/]"
152
+
153
+ ret = Table.grid(expand=True)
154
+ ret.add_column(ratio=1)
155
+ ret.add_column(ratio=3)
156
+
157
+ info = Table(box=box.SIMPLE)
158
+ info.add_column(url, overflow="fold")
159
+ info.add_column(f"#{self.id}", overflow="fold")
160
+
161
+ info.add_row("By", self.author_name)
162
+ info.add_row("Created", escape(self.created))
163
+ info.add_row("Shared", escape(self.share_date))
164
+ info.add_row("Modified", escape(self.last_modified))
165
+ info.add_row("Comments allowed", escape(str(self.comments_allowed)))
166
+ info.add_row("Loves", str(self.loves))
167
+ info.add_row("Faves", str(self.favorites))
168
+ info.add_row("Remixes", str(self.remix_count))
169
+ info.add_row("Views", str(self.views))
170
+
171
+ desc = Table(box=box.SIMPLE)
172
+ desc.add_row("Instructions", escape(self.instructions))
173
+ desc.add_row("Notes & Credits", escape(self.notes))
174
+
175
+ ret.add_row(Panel(info, title=url), Panel(desc, title="Description"))
176
+
177
+ return ret
178
+
108
179
  @property
109
180
  def embed_url(self):
110
181
  """
@@ -113,7 +184,7 @@ class PartialProject(BaseSiteComponent):
113
184
  """
114
185
  return f"{self.url}/embed"
115
186
 
116
- def remixes(self, *, limit=40, offset=0):
187
+ def remixes(self, *, limit=40, offset=0) -> list[Project]:
117
188
  """
118
189
  Returns:
119
190
  list<scratchattach.project.Project>: A list containing the remixes of the project, each project is represented by a Project object.
@@ -129,11 +200,11 @@ class PartialProject(BaseSiteComponent):
129
200
  """
130
201
  p = get_project(self.id)
131
202
  return isinstance(p, Project)
132
-
203
+
133
204
  def raw_json_or_empty(self) -> dict[str, Any]:
134
205
  return empty_project_json
135
-
136
- def create_remix(self, *, title=None, project_json=None): # not working
206
+
207
+ def create_remix(self, *, title=None, project_json=None): # not working
137
208
  """
138
209
  Creates a project on the Scratch website.
139
210
 
@@ -145,7 +216,7 @@ class PartialProject(BaseSiteComponent):
145
216
 
146
217
  if title is None:
147
218
  if "title" in self.__dict__:
148
- title = self.title+" remix"
219
+ title = self.title + " remix"
149
220
  else:
150
221
  title = " remix"
151
222
  if project_json is None:
@@ -157,7 +228,8 @@ class PartialProject(BaseSiteComponent):
157
228
  if CREATE_PROJECT_USES[-1] < time.time() - 300:
158
229
  CREATE_PROJECT_USES.pop()
159
230
  else:
160
- raise exceptions.BadRequest("Rate limit for remixing Scratch projects exceeded.\nThis rate limit is enforced by scratchattach, not by the Scratch API.\nFor security reasons, it cannot be turned off.\n\nDon't spam-create projects, it WILL get you banned.")
231
+ raise exceptions.BadRequest(
232
+ "Rate limit for remixing Scratch projects exceeded.\nThis rate limit is enforced by scratchattach, not by the Scratch API.\nFor security reasons, it cannot be turned off.\n\nDon't spam-create projects, it WILL get you banned.")
161
233
  return
162
234
  CREATE_PROJECT_USES.insert(0, time.time())
163
235
 
@@ -167,11 +239,12 @@ class PartialProject(BaseSiteComponent):
167
239
  'title': title,
168
240
  }
169
241
 
170
- response = requests.post('https://projects.scratch.mit.edu/', params=params, cookies=self._cookies, headers=self._headers, json=project_json).json()
242
+ response = requests.post('https://projects.scratch.mit.edu/', params=params, cookies=self._cookies,
243
+ headers=self._headers, json=project_json).json()
171
244
  _project = self._session.connect_project(response["content-name"])
172
245
  _project.parent_title = base64.b64decode(response['content-title']).decode('utf-8').split(' remix')[0]
173
246
  return _project
174
-
247
+
175
248
  def load_description(self):
176
249
  """
177
250
  Gets the instructions of the unshared project. Requires authentication.
@@ -212,7 +285,7 @@ class Project(PartialProject):
212
285
  :.share_date:
213
286
 
214
287
  :.thumbnail_url:
215
-
288
+
216
289
  :.remix_parent:
217
290
 
218
291
  :.remix_root:
@@ -220,7 +293,7 @@ class Project(PartialProject):
220
293
  :.loves: The project's love count
221
294
 
222
295
  :.favorites: The project's favorite count
223
-
296
+
224
297
  :.remix_count: The number of remixes
225
298
 
226
299
  :.views: The view count
@@ -230,8 +303,16 @@ class Project(PartialProject):
230
303
  :.update(): Updates the attributes
231
304
  """
232
305
 
306
+ def __repr__(self):
307
+ return f"-P {self.id} ({self.title})"
308
+
233
309
  def __str__(self):
234
- return str(self.title)
310
+ return repr(self)
311
+
312
+ @property
313
+ def thumbnail(self) -> bytes:
314
+ with requests.no_error_handling():
315
+ return requests.get(self.thumbnail_url).content
235
316
 
236
317
  def _assert_permission(self):
237
318
  self._assert_auth()
@@ -242,7 +323,9 @@ class Project(PartialProject):
242
323
  def load_description(self):
243
324
  # Overrides the load_description method that exists for unshared projects
244
325
  self.update()
245
-
326
+
327
+ # -- Project contents (body/json) -- #
328
+
246
329
  def download(self, *, filename=None, dir="."):
247
330
  """
248
331
  Downloads the project json to the given directory.
@@ -270,7 +353,8 @@ class Project(PartialProject):
270
353
  "Method only works for projects created with Scratch 3"
271
354
  )
272
355
  )
273
-
356
+
357
+ @deprecated("Use raw_json instead")
274
358
  def get_json(self) -> str:
275
359
  """
276
360
  Downloads the project json and returns it as a string
@@ -290,17 +374,15 @@ class Project(PartialProject):
290
374
  )
291
375
  )
292
376
 
293
- def body(self):
377
+ def body(self) -> editor.Project:
294
378
  """
295
379
  Method only works for project created with Scratch 3.
296
380
 
297
381
  Returns:
298
- scratchattach.ProjectBody: The contents of the project as ProjectBody object
382
+ scratchattach.editor.Project: The contents of the project as editor Project object
299
383
  """
300
384
  raw_json = self.raw_json()
301
- pb = ProjectBody()
302
- pb.from_json(raw_json)
303
- return pb
385
+ return editor.Project.from_json(raw_json)
304
386
 
305
387
  def raw_json(self):
306
388
  """
@@ -337,7 +419,7 @@ class Project(PartialProject):
337
419
 
338
420
  def raw_json_or_empty(self):
339
421
  return self.raw_json()
340
-
422
+
341
423
  def creator_agent(self):
342
424
  """
343
425
  Method only works for project created with Scratch 3.
@@ -347,7 +429,50 @@ class Project(PartialProject):
347
429
  """
348
430
  return self.raw_json()["meta"]["agent"]
349
431
 
350
- def author(self):
432
+ def set_body(self, project_body: editor.Project):
433
+ """
434
+ Sets the project's contents You can use this to upload projects to the Scratch website.
435
+ Returns a dict with Scratch's raw JSON API response.
436
+
437
+ Args:
438
+ project_body (scratchattach.ProjectBody): A ProjectBody object containing the contents of the project
439
+ """
440
+ self._assert_permission()
441
+
442
+ return self.set_json(project_body.to_json())
443
+
444
+ def set_json(self, json_data):
445
+ """
446
+ Sets the project json. You can use this to upload projects to the Scratch website.
447
+ Returns a dict with Scratch's raw JSON API response.
448
+
449
+ Args:
450
+ json_data (dict or JSON): The new project JSON as encoded JSON object or as dict
451
+ """
452
+
453
+ self._assert_permission()
454
+
455
+ if not isinstance(json_data, dict):
456
+ json_data = json.loads(json_data)
457
+
458
+ return requests.put(
459
+ f"https://projects.scratch.mit.edu/{self.id}",
460
+ headers=self._headers,
461
+ cookies=self._cookies,
462
+ json=json_data,
463
+ ).json()
464
+
465
+ def upload_json_from(self, project_id: int | str):
466
+ """
467
+ Uploads the project json from the project with the given id to the project represented by this Project object
468
+ """
469
+ self._assert_auth()
470
+ other_project = self._session.connect_project(project_id) # type: ignore
471
+ self.set_json(other_project.raw_json())
472
+
473
+ # -- other -- #
474
+
475
+ def author(self) -> user.User:
351
476
  """
352
477
  Returns:
353
478
  scratchattach.user.User: An object representing the Scratch user who created this project.
@@ -360,7 +485,8 @@ class Project(PartialProject):
360
485
  list<scratchattach.studio.Studio>: A list containing the studios this project is in, each studio is represented by a Studio object.
361
486
  """
362
487
  response = commons.api_iterative(
363
- f"https://api.scratch.mit.edu/users/{self.author_name}/projects/{self.id}/studios", limit=limit, offset=offset, add_params=f"&cachebust={random.randint(0,9999)}")
488
+ f"https://api.scratch.mit.edu/users/{self.author_name}/projects/{self.id}/studios", limit=limit,
489
+ offset=offset, add_params=f"&cachebust={random.randint(0, 9999)}")
364
490
  return commons.parse_object_list(response, studio.Studio, self._session)
365
491
 
366
492
  def comments(self, *, limit=40, offset=0) -> list['comment.Comment']:
@@ -376,7 +502,8 @@ class Project(PartialProject):
376
502
  """
377
503
 
378
504
  response = commons.api_iterative(
379
- f"https://api.scratch.mit.edu/users/{self.author_name}/projects/{self.id}/comments/", limit=limit, offset=offset, add_params=f"&cachebust={random.randint(0,9999)}")
505
+ f"https://api.scratch.mit.edu/users/{self.author_name}/projects/{self.id}/comments/", limit=limit,
506
+ offset=offset, add_params=f"&cachebust={random.randint(0, 9999)}")
380
507
  for i in response:
381
508
  i["source"] = "project"
382
509
  i["source_id"] = self.id
@@ -384,7 +511,8 @@ class Project(PartialProject):
384
511
 
385
512
  def comment_replies(self, *, comment_id, limit=40, offset=0):
386
513
  response = commons.api_iterative(
387
- f"https://api.scratch.mit.edu/users/{self.author_name}/projects/{self.id}/comments/{comment_id}/replies/", limit=limit, offset=offset, add_params=f"&cachebust={random.randint(0,9999)}")
514
+ f"https://api.scratch.mit.edu/users/{self.author_name}/projects/{self.id}/comments/{comment_id}/replies/",
515
+ limit=limit, offset=offset, add_params=f"&cachebust={random.randint(0, 9999)}")
388
516
  for x in response:
389
517
  x["parent_id"] = comment_id
390
518
  x["source"] = "project"
@@ -396,17 +524,22 @@ class Project(PartialProject):
396
524
  Returns:
397
525
  scratchattach.comments.Comment: A Comment object representing the requested comment.
398
526
  """
399
- r = requests.get(
527
+ # https://api.scratch.mit.edu/users/TimMcCool/projects/404369790/comments/439984518
528
+ data = requests.get(
400
529
  f"https://api.scratch.mit.edu/users/{self.author_name}/projects/{self.id}/comments/{comment_id}",
401
530
  headers=self._headers,
402
531
  cookies=self._cookies
403
532
  ).json()
404
- if r is None:
405
- raise exceptions.CommentNotFound()
406
- _comment = comment.Comment(id=r["id"], _session=self._session, source="project", source_id=self.id)
407
- _comment._update_from_dict(r)
533
+
534
+ if data is None or data.get("code") == "NotFound":
535
+ raise exceptions.CommentNotFound(
536
+ f"Cannot find comment #{comment_id} on -P {self.id} by -U {self.author_name}")
537
+
538
+ _comment = comment.Comment(id=data["id"], _session=self._session, source=comment.CommentSource.PROJECT,
539
+ source_id=self.id)
540
+ _comment._update_from_dict(data)
408
541
  return _comment
409
-
542
+
410
543
  def love(self):
411
544
  """
412
545
  Posts a love on the project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
@@ -488,8 +621,8 @@ class Project(PartialProject):
488
621
  use_site_api (bool):
489
622
  When enabled, the fields are set using the scratch.mit.edu/site-api API.
490
623
  This function allows setting more fields than Project.set_fields.
491
- For example you can also share / unshare the project by setting the "shared" field.
492
- According to the Scratch team, this API is deprecated. As of 2024 it's still fully functional tho.
624
+ For example, you can also share / unshare the project by setting the "shared" field.
625
+ According to the Scratch team, this API is deprecated. As of 2024 it's still fully functional though.
493
626
  """
494
627
  self._assert_permission()
495
628
  if use_site_api:
@@ -507,7 +640,7 @@ class Project(PartialProject):
507
640
  json=fields_dict,
508
641
  ).json()
509
642
  return self._update_from_dict(r)
510
-
643
+
511
644
  def turn_off_commenting(self):
512
645
  """
513
646
  Disables commenting on the project. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
@@ -584,7 +717,7 @@ class Project(PartialProject):
584
717
  f"https://api.scratch.mit.edu/proxy/comments/project/{self.id}/comment/{comment_id}/",
585
718
  headers=self._headers,
586
719
  cookies=self._cookies,
587
- ).headers
720
+ )
588
721
 
589
722
  def report_comment(self, *, comment_id):
590
723
  """
@@ -632,7 +765,8 @@ class Project(PartialProject):
632
765
  )
633
766
  if "id" not in r:
634
767
  raise exceptions.CommentPostFailure(r)
635
- _comment = comment.Comment(id=r["id"], _session=self._session, source="project", source_id=self.id)
768
+ _comment = comment.Comment(id=r["id"], _session=self._session, source=comment.CommentSource.PROJECT,
769
+ source_id=self.id)
636
770
  _comment._update_from_dict(r)
637
771
  return _comment
638
772
 
@@ -655,55 +789,12 @@ class Project(PartialProject):
655
789
  return self.post_comment(
656
790
  content, parent_id=parent_id, commentee_id=commentee_id
657
791
  )
658
-
659
- def set_body(self, project_body:ProjectBody):
660
- """
661
- Sets the project's contents You can use this to upload projects to the Scratch website.
662
- Returns a dict with Scratch's raw JSON API response.
663
-
664
- Args:
665
- project_body (scratchattach.ProjectBody): A ProjectBody object containing the contents of the project
666
- """
667
- self._assert_permission()
668
-
669
- return self.set_json(project_body.to_json())
670
-
671
-
672
- def set_json(self, json_data):
673
- """
674
- Sets the project json. You can use this to upload projects to the Scratch website.
675
- Returns a dict with Scratch's raw JSON API response.
676
-
677
- Args:
678
- json_data (dict or JSON): The new project JSON as encoded JSON object or as dict
679
- """
680
-
681
- self._assert_permission()
682
-
683
- if not isinstance(json_data, dict):
684
- json_data = json.loads(json_data)
685
-
686
- return requests.put(
687
- f"https://projects.scratch.mit.edu/{self.id}",
688
- headers=self._headers,
689
- cookies=self._cookies,
690
- json=json_data,
691
- ).json()
692
-
693
- def upload_json_from(self, project_id):
694
- """
695
- Uploads the project json from the project with the given id to the project represented by this Project object
696
- """
697
- self._assert_auth()
698
- other_project = self._session.connect_project(project_id)
699
- self.set_json(other_project.get_raw_json())
700
792
 
701
793
  def set_title(self, text):
702
794
  """
703
795
  Changes the projects title. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
704
796
  """
705
797
  self.set_fields({"title": text})
706
-
707
798
 
708
799
  def set_instructions(self, text):
709
800
  """
@@ -717,7 +808,7 @@ class Project(PartialProject):
717
808
  """
718
809
  self.set_fields({"description": text})
719
810
 
720
-
811
+ @deprecated("Deprecated because ScratchDB is down indefinitely.")
721
812
  def ranks(self):
722
813
  """
723
814
  Gets information about the project's ranks. Fetched from ScratchDB.
@@ -728,12 +819,11 @@ class Project(PartialProject):
728
819
  Returns:
729
820
  dict: A dict containing the project's ranks. If the ranks aren't available, all values will be -1.
730
821
  """
731
- print("Warning: Project.ranks method is deprecated because ScratchDB is down indefinitely.")
732
822
  return requests.get(
733
823
  f"https://scratchdb.lefty.one/v3/project/info/{self.id}"
734
824
  ).json()["statistics"]["ranks"]
735
825
 
736
- def moderation_status(self):
826
+ def moderation_status(self, *, reload: bool = False):
737
827
  """
738
828
  Gets information about the project's moderation status. Fetched from jeffalo's API.
739
829
 
@@ -750,19 +840,24 @@ class Project(PartialProject):
750
840
 
751
841
  no_remixes: Unable to fetch the project's moderation status.
752
842
  """
843
+ if self._moderation_status and not reload:
844
+ return self._moderation_status
845
+
753
846
  try:
754
847
  return requests.get(
755
848
  f"https://jeffalo.net/api/nfe/?project={self.id}"
756
849
  ).json()["status"]
757
850
  except Exception:
758
- raise (exceptions.FetchError)
851
+ raise exceptions.FetchError
759
852
 
760
853
  def visibility(self):
761
854
  """
762
855
  Returns info about the project's visibility. Requires authentication.
763
856
  """
764
857
  self._assert_auth()
765
- return requests.get(f"https://api.scratch.mit.edu/users/{self._session.username}/projects/{self.id}/visibility", headers=self._headers, cookies=self._cookies).json()
858
+ return requests.get(f"https://api.scratch.mit.edu/users/{self._session.username}/projects/{self.id}/visibility",
859
+ headers=self._headers, cookies=self._cookies).json()
860
+
766
861
 
767
862
  # ------ #
768
863
 
@@ -782,9 +877,17 @@ def get_project(project_id) -> Project:
782
877
 
783
878
  If you want to use these methods, get the project with :meth:`scratchattach.session.Session.connect_project` instead.
784
879
  """
785
- print("Warning: For methods that require authentication, use session.connect_project instead of get_project")
880
+ warnings.warn(
881
+ "For methods that require authentication, use session.connect_project instead of get_project.\n"
882
+ "If you want to remove this warning, "
883
+ "use `warnings.filterwarnings('ignore', category=scratchattach.ProjectAuthenticationWarning)`.\n"
884
+ "To ignore all warnings of the type GetAuthenticationWarning, which includes this warning, use "
885
+ "`warnings.filterwarnings('ignore', category=scratchattach.GetAuthenticationWarning)`.",
886
+ exceptions.ProjectAuthenticationWarning
887
+ )
786
888
  return commons._get_object("id", project_id, Project, exceptions.ProjectNotFound)
787
889
 
890
+
788
891
  def search_projects(*, query="", mode="trending", language="en", limit=40, offset=0):
789
892
  '''
790
893
  Uses the Scratch search to search projects.
@@ -802,9 +905,11 @@ def search_projects(*, query="", mode="trending", language="en", limit=40, offse
802
905
  if not query:
803
906
  raise ValueError("The query can't be empty for search")
804
907
  response = commons.api_iterative(
805
- f"https://api.scratch.mit.edu/search/projects", limit=limit, offset=offset, add_params=f"&language={language}&mode={mode}&q={query}")
908
+ f"https://api.scratch.mit.edu/search/projects", limit=limit, offset=offset,
909
+ add_params=f"&language={language}&mode={mode}&q={query}")
806
910
  return commons.parse_object_list(response, Project)
807
911
 
912
+
808
913
  def explore_projects(*, query="*", mode="trending", language="en", limit=40, offset=0):
809
914
  '''
810
915
  Gets projects from the explore page.
@@ -822,5 +927,6 @@ def explore_projects(*, query="*", mode="trending", language="en", limit=40, off
822
927
  if not query:
823
928
  raise ValueError("The query can't be empty for search")
824
929
  response = commons.api_iterative(
825
- f"https://api.scratch.mit.edu/explore/projects", limit=limit, offset=offset, add_params=f"&language={language}&mode={mode}&q={query}")
826
- return commons.parse_object_list(response, Project)
930
+ f"https://api.scratch.mit.edu/explore/projects", limit=limit, offset=offset,
931
+ add_params=f"&language={language}&mode={mode}&q={query}")
932
+ return commons.parse_object_list(response, Project)