scratchattach 3.0.0b0__py3-none-any.whl → 3.0.0b2__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 (80) hide show
  1. scratchattach/cli/__about__.py +1 -0
  2. scratchattach/cli/__init__.py +26 -0
  3. scratchattach/cli/cmd/__init__.py +4 -0
  4. scratchattach/cli/cmd/group.py +127 -0
  5. scratchattach/cli/cmd/login.py +60 -0
  6. scratchattach/cli/cmd/profile.py +7 -0
  7. scratchattach/cli/cmd/sessions.py +5 -0
  8. scratchattach/cli/context.py +142 -0
  9. scratchattach/cli/db.py +66 -0
  10. scratchattach/cli/namespace.py +14 -0
  11. scratchattach/cloud/__init__.py +2 -0
  12. scratchattach/cloud/_base.py +483 -0
  13. scratchattach/cloud/cloud.py +183 -0
  14. scratchattach/editor/__init__.py +22 -0
  15. scratchattach/editor/asset.py +265 -0
  16. scratchattach/editor/backpack_json.py +115 -0
  17. scratchattach/editor/base.py +191 -0
  18. scratchattach/editor/block.py +584 -0
  19. scratchattach/editor/blockshape.py +357 -0
  20. scratchattach/editor/build_defaulting.py +51 -0
  21. scratchattach/editor/code_translation/__init__.py +0 -0
  22. scratchattach/editor/code_translation/parse.py +177 -0
  23. scratchattach/editor/comment.py +80 -0
  24. scratchattach/editor/commons.py +145 -0
  25. scratchattach/editor/extension.py +50 -0
  26. scratchattach/editor/field.py +99 -0
  27. scratchattach/editor/inputs.py +138 -0
  28. scratchattach/editor/meta.py +117 -0
  29. scratchattach/editor/monitor.py +185 -0
  30. scratchattach/editor/mutation.py +381 -0
  31. scratchattach/editor/pallete.py +88 -0
  32. scratchattach/editor/prim.py +174 -0
  33. scratchattach/editor/project.py +381 -0
  34. scratchattach/editor/sprite.py +609 -0
  35. scratchattach/editor/twconfig.py +114 -0
  36. scratchattach/editor/vlb.py +134 -0
  37. scratchattach/eventhandlers/__init__.py +0 -0
  38. scratchattach/eventhandlers/_base.py +101 -0
  39. scratchattach/eventhandlers/cloud_events.py +130 -0
  40. scratchattach/eventhandlers/cloud_recorder.py +26 -0
  41. scratchattach/eventhandlers/cloud_requests.py +544 -0
  42. scratchattach/eventhandlers/cloud_server.py +249 -0
  43. scratchattach/eventhandlers/cloud_storage.py +135 -0
  44. scratchattach/eventhandlers/combine.py +30 -0
  45. scratchattach/eventhandlers/filterbot.py +163 -0
  46. scratchattach/eventhandlers/message_events.py +42 -0
  47. scratchattach/other/__init__.py +0 -0
  48. scratchattach/other/other_apis.py +598 -0
  49. scratchattach/other/project_json_capabilities.py +475 -0
  50. scratchattach/site/__init__.py +0 -0
  51. scratchattach/site/_base.py +93 -0
  52. scratchattach/site/activity.py +426 -0
  53. scratchattach/site/alert.py +226 -0
  54. scratchattach/site/backpack_asset.py +119 -0
  55. scratchattach/site/browser_cookie3_stub.py +17 -0
  56. scratchattach/site/browser_cookies.py +61 -0
  57. scratchattach/site/classroom.py +454 -0
  58. scratchattach/site/cloud_activity.py +121 -0
  59. scratchattach/site/comment.py +228 -0
  60. scratchattach/site/forum.py +436 -0
  61. scratchattach/site/placeholder.py +132 -0
  62. scratchattach/site/project.py +932 -0
  63. scratchattach/site/session.py +1323 -0
  64. scratchattach/site/studio.py +704 -0
  65. scratchattach/site/typed_dicts.py +151 -0
  66. scratchattach/site/user.py +1252 -0
  67. scratchattach/utils/__init__.py +0 -0
  68. scratchattach/utils/commons.py +263 -0
  69. scratchattach/utils/encoder.py +161 -0
  70. scratchattach/utils/enums.py +237 -0
  71. scratchattach/utils/exceptions.py +277 -0
  72. scratchattach/utils/optional_async.py +154 -0
  73. scratchattach/utils/requests.py +306 -0
  74. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/METADATA +1 -1
  75. scratchattach-3.0.0b2.dist-info/RECORD +81 -0
  76. scratchattach-3.0.0b0.dist-info/RECORD +0 -8
  77. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/WHEEL +0 -0
  78. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/entry_points.txt +0 -0
  79. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/licenses/LICENSE +0 -0
  80. {scratchattach-3.0.0b0.dist-info → scratchattach-3.0.0b2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,704 @@
1
+ """Studio class"""
2
+ from __future__ import annotations
3
+
4
+ import warnings
5
+ import json
6
+ import random
7
+
8
+ from dataclasses import dataclass, field
9
+
10
+ from typing_extensions import Optional
11
+
12
+ from . import user, comment, project, activity, session
13
+ from scratchattach.site.typed_dicts import StudioDict, StudioRoleDict
14
+ from ._base import BaseSiteComponent
15
+ from scratchattach.utils import exceptions, commons
16
+ from scratchattach.utils.commons import api_iterative, headers
17
+ from scratchattach.utils.requests import requests
18
+
19
+
20
+ @dataclass
21
+ class Studio(BaseSiteComponent):
22
+ """
23
+ Represents a Scratch studio.
24
+
25
+ Attributes:
26
+
27
+ :.id:
28
+
29
+ :.title:
30
+
31
+ :.description:
32
+
33
+ :.host_id: The user id of the studio host
34
+
35
+ :.open_to_all: Whether everyone is allowed to add projects
36
+
37
+ :.comments_allowed:
38
+
39
+ :.image_url:
40
+
41
+ :.created:
42
+
43
+ :.modified:
44
+
45
+ :.follower_count:
46
+
47
+ :.manager_count:
48
+
49
+ :.project_count:
50
+
51
+ :.update(): Updates the attributes
52
+
53
+ """
54
+ id: int = 0
55
+ title: Optional[str] = None
56
+ description: Optional[str] = None
57
+ host_id: Optional[int] = None
58
+ follower_count: Optional[int] = None
59
+ manager_count: Optional[int] = None
60
+ project_count: Optional[int] = None
61
+ image_url: Optional[str] = None
62
+ open_to_all: Optional[bool] = None
63
+ comments_allowed: Optional[bool] = None
64
+ created: Optional[str] = None
65
+ modified: Optional[str] = None
66
+ _session: Optional[session.Session] = None
67
+
68
+
69
+ def __post_init__(self):
70
+ # Info on how the .update method has to fetch the data:
71
+ self.update_function = requests.get
72
+ self.update_api = f"https://api.scratch.mit.edu/studios/{self.id}"
73
+
74
+ # Headers and cookies:
75
+ if self._session is None:
76
+ self._headers = headers
77
+ self._cookies = {}
78
+ else:
79
+ self._headers = self._session._headers
80
+ self._cookies = self._session._cookies
81
+
82
+ # Headers for operations that require accept and Content-Type fields:
83
+ self._json_headers = dict(self._headers)
84
+ self._json_headers["accept"] = "application/json"
85
+ self._json_headers["Content-Type"] = "application/json"
86
+
87
+ def _update_from_dict(self, studio: StudioDict):
88
+ self.id = int(studio["id"])
89
+ self.title = studio["title"]
90
+ self.description = studio["description"]
91
+ self.host_id = studio["host"]
92
+ self.open_to_all = studio["open_to_all"]
93
+ self.comments_allowed = studio["comments_allowed"]
94
+ self.image_url = studio["image"] # rename/alias to thumbnail_url?
95
+ self.created = studio["history"]["created"]
96
+ self.modified = studio["history"]["modified"]
97
+
98
+ stats = studio.get("stats", {})
99
+ self.follower_count = stats.get("followers", self.follower_count)
100
+ self.manager_count = stats.get("managers", self.manager_count)
101
+ self.project_count = stats.get("projects", self.project_count)
102
+ return True
103
+
104
+ def __str__(self):
105
+ ret = f"-S {self.id}"
106
+ if self.title:
107
+ ret += f" ({self.title})"
108
+ return ret
109
+
110
+ def __rich__(self):
111
+ from rich.panel import Panel
112
+ from rich.table import Table
113
+ from rich import box
114
+ from rich.markup import escape
115
+
116
+ url = f"[link={self.url}]{escape(self.title)}[/]"
117
+
118
+ ret = Table.grid(expand=True)
119
+ ret.add_column(ratio=1)
120
+ ret.add_column(ratio=3)
121
+
122
+ info = Table(box=box.SIMPLE)
123
+ info.add_column(url, overflow="fold")
124
+ info.add_column(f"#{self.id}", overflow="fold")
125
+ info.add_row("Host ID", str(self.host_id))
126
+ info.add_row("Followers", str(self.follower_count))
127
+ info.add_row("Projects", str(self.project_count))
128
+ info.add_row("Managers", str(self.manager_count))
129
+ info.add_row("Comments allowed", str(self.comments_allowed))
130
+ info.add_row("Open", str(self.open_to_all))
131
+ info.add_row("Created", self.created)
132
+ info.add_row("Modified", self.modified)
133
+
134
+ desc = Table(box=box.SIMPLE)
135
+ desc.add_row("Description", escape(self.description))
136
+
137
+ ret.add_row(
138
+ Panel(info, title=url),
139
+ Panel(desc, title="Description"),
140
+ )
141
+
142
+ return ret
143
+
144
+ @property
145
+ def url(self):
146
+ return f"https://scratch.mit.edu/studios/{self.id}"
147
+
148
+ @property
149
+ def thumbnail(self) -> bytes:
150
+ with requests.no_error_handling():
151
+ return requests.get(self.image_url).content
152
+
153
+ def follow(self):
154
+ """
155
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
156
+ """
157
+ self._assert_auth()
158
+ requests.put(
159
+ f"https://scratch.mit.edu/site-api/users/bookmarkers/{self.id}/add/?usernames={self._session._username}",
160
+ headers=headers,
161
+ cookies=self._cookies,
162
+ timeout=10,
163
+ )
164
+
165
+ def unfollow(self):
166
+ """
167
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
168
+ """
169
+ self._assert_auth()
170
+ requests.put(
171
+ f"https://scratch.mit.edu/site-api/users/bookmarkers/{self.id}/remove/?usernames={self._session._username}",
172
+ headers=headers,
173
+ cookies=self._cookies,
174
+ timeout=10,
175
+ )
176
+
177
+ def comments(self, *, limit=40, offset=0) -> list[comment.Comment]:
178
+ """
179
+ Returns the comments posted on the studio (except for replies. To get replies use :meth:`scratchattach.studio.Studio.get_comment_replies`).
180
+
181
+ Keyword Arguments:
182
+ page: The page of the comments that should be returned.
183
+ limit: Max. amount of returned comments.
184
+
185
+ Returns:
186
+ list<Comment>: A list containing the requested comments as Comment objects.
187
+ """
188
+ response = commons.api_iterative(
189
+ f"https://api.scratch.mit.edu/studios/{self.id}/comments/", limit=limit, offset=offset, add_params=f"&cachebust={random.randint(0,9999)}")
190
+ for i in response:
191
+ i["source"] = "studio"
192
+ i["source_id"] = self.id
193
+ return commons.parse_object_list(response, comment.Comment, self._session)
194
+
195
+ def comment_replies(self, *, comment_id, limit=40, offset=0) -> list[comment.Comment]:
196
+ response = commons.api_iterative(
197
+ f"https://api.scratch.mit.edu/studios/{self.id}/comments/{comment_id}/replies", limit=limit, offset=offset, add_params=f"&cachebust={random.randint(0,9999)}")
198
+ for x in response:
199
+ x["parent_id"] = comment_id
200
+ x["source"] = "studio"
201
+ x["source_id"] = self.id
202
+ return commons.parse_object_list(response, comment.Comment, self._session)
203
+
204
+ def comment_by_id(self, comment_id):
205
+ r = requests.get(
206
+ f"https://api.scratch.mit.edu/studios/{self.id}/comments/{comment_id}",
207
+ timeout=10,
208
+ ).json()
209
+ if r is None:
210
+ raise exceptions.CommentNotFound()
211
+ _comment = comment.Comment(id=r["id"], _session=self._session, source=comment.CommentSource.STUDIO, source_id=self.id)
212
+ _comment._update_from_dict(r)
213
+ return _comment
214
+
215
+ def post_comment(self, content, *, parent_id="", commentee_id=""):
216
+ """
217
+ Posts a comment on the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
218
+
219
+ Args:
220
+ content: Content of the comment that should be posted
221
+
222
+ Keyword Arguments:
223
+ parent_id: ID of the comment you want to reply to. If you don't want to mention a user, don't put the argument.
224
+ commentee_id: ID of the user that will be mentioned in your comment and will receive a message about your comment. If you don't want to mention a user, don't put the argument.
225
+
226
+ Returns:
227
+ scratchattach.comment.Comment: The posted comment as Comment object.
228
+ """
229
+ self._assert_auth()
230
+ data = {
231
+ "commentee_id": commentee_id,
232
+ "content": str(content),
233
+ "parent_id": parent_id,
234
+ }
235
+ headers = dict(self._json_headers)
236
+ headers["referer"] = "https://scratch.mit.edu/projects/" + str(self.id) + "/"
237
+ r = requests.post(
238
+ f"https://api.scratch.mit.edu/proxy/comments/studio/{self.id}/",
239
+ headers=headers,
240
+ cookies=self._cookies,
241
+ data=json.dumps(data),
242
+ timeout=10,
243
+ ).json()
244
+ if "id" not in r:
245
+ raise exceptions.CommentPostFailure(r)
246
+ _comment = comment.Comment(id=r["id"], _session=self._session, source=comment.CommentSource.STUDIO, source_id=self.id)
247
+ _comment._update_from_dict(r)
248
+ return _comment
249
+
250
+ def delete_comment(self, *, comment_id):
251
+ # NEEDS TO BE TESTED!
252
+ """
253
+ Deletes a comment by ID. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
254
+
255
+ Args:
256
+ comment_id: The id of the comment that should be deleted
257
+ """
258
+ self._assert_auth()
259
+ return requests.delete(
260
+ f"https://api.scratch.mit.edu/proxy/comments/studio/{self.id}/comment/{comment_id}/",
261
+ headers=self._headers,
262
+ cookies=self._cookies,
263
+ ).headers
264
+
265
+ def report_comment(self, *, comment_id):
266
+ # NEEDS TO BE TESTED!
267
+ """
268
+ Reports a comment by ID to the Scratch team. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_project`
269
+
270
+ Args:
271
+ comment_id: The id of the comment that should be reported
272
+ """
273
+ self._assert_auth()
274
+ return requests.delete(
275
+ f"https://api.scratch.mit.edu/proxy/comments/studio/{self.id}/comment/{comment_id}/report",
276
+ headers=self._headers,
277
+ cookies=self._cookies,
278
+ )
279
+
280
+ def set_thumbnail(self, *, file):
281
+ """
282
+ Sets the studio thumbnail. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
283
+
284
+ Keyword Arguments:
285
+ file: The path to the image file
286
+
287
+ Returns:
288
+ str: Scratch cdn link to the set thumbnail
289
+ """
290
+ self._assert_auth()
291
+ with open(file, "rb") as f:
292
+ thumbnail = f.read()
293
+
294
+ filename = file.replace("\\", "/")
295
+ if filename.endswith("/"):
296
+ filename = filename[:-1]
297
+ filename = filename.split("/").pop()
298
+
299
+ file_type = filename.split(".").pop()
300
+
301
+ payload1 = f'------WebKitFormBoundaryhKZwFjoxAyUTMlSh\r\nContent-Disposition: form-data; name="file"; filename="{filename}"\r\nContent-Type: image/{file_type}\r\n\r\n'
302
+ payload1 = payload1.encode("utf-8")
303
+ payload2 = b"\r\n------WebKitFormBoundaryhKZwFjoxAyUTMlSh--\r\n"
304
+ payload = b"".join([payload1, thumbnail, payload2])
305
+
306
+ r = requests.post(
307
+ f"https://scratch.mit.edu/site-api/galleries/all/{self.id}/",
308
+ headers={
309
+ "accept": "*/",
310
+ "content-type": "multipart/form-data; boundary=----WebKitFormBoundaryhKZwFjoxAyUTMlSh",
311
+ "Referer": "https://scratch.mit.edu/",
312
+ "x-csrftoken": "a",
313
+ "x-requested-with": "XMLHttpRequest",
314
+ },
315
+ data=payload,
316
+ cookies=self._cookies,
317
+ timeout=10,
318
+ ).json()
319
+
320
+ if "errors" in r:
321
+ raise (exceptions.BadRequest(", ".join(r["errors"])))
322
+ else:
323
+ return r["thumbnail_url"]
324
+
325
+ def reply_comment(self, content, *, parent_id, commentee_id=""):
326
+ """
327
+ Posts a reply to a comment on the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
328
+
329
+ Args:
330
+ content: Content of the comment that should be posted
331
+
332
+ Warning:
333
+ Only replies to top-level comments are shown on the Scratch website. Replies to replies are actually replies to the corresponding top-level comment in the API.
334
+
335
+ Therefore, parent_id should be the comment id of a top level comment.
336
+
337
+ Keyword Arguments:
338
+ parent_id: ID of the comment you want to reply to
339
+ commentee_id: ID of the user you are replying to
340
+ """
341
+ self._assert_auth()
342
+ return self.post_comment(
343
+ content, parent_id=parent_id, commentee_id=commentee_id
344
+ )
345
+
346
+ def projects(self, limit=40, offset=0) -> list[project.Project]:
347
+ """
348
+ Gets the studio projects.
349
+
350
+ Keyword arguments:
351
+ limit (int): Max amount of returned projects.
352
+ offset (int): Offset of the first returned project.
353
+
354
+ Returns:
355
+ list<scratchattach.project.Project>: A list containing the studio projects as Project objects
356
+ """
357
+ response = commons.api_iterative(
358
+ f"https://api.scratch.mit.edu/studios/{self.id}/projects", limit=limit, offset=offset)
359
+ return commons.parse_object_list(response, project.Project, self._session)
360
+
361
+ def curators(self, limit=40, offset=0) -> list[user.User]:
362
+ """
363
+ Gets the studio curators.
364
+
365
+ Keyword arguments:
366
+ limit (int): Max amount of returned curators.
367
+ offset (int): Offset of the first returned curator.
368
+
369
+ Returns:
370
+ list<scratchattach.user.User>: A list containing the studio curators as User objects
371
+ """
372
+ response = commons.api_iterative(
373
+ f"https://api.scratch.mit.edu/studios/{self.id}/curators", limit=limit, offset=offset)
374
+ return commons.parse_object_list(response, user.User, self._session, "username")
375
+
376
+
377
+ def invite_curator(self, curator):
378
+ """
379
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
380
+ """
381
+ self._assert_auth()
382
+ try:
383
+ return requests.put(
384
+ f"https://scratch.mit.edu/site-api/users/curators-in/{self.id}/invite_curator/?usernames={curator}",
385
+ headers=headers,
386
+ cookies=self._cookies,
387
+ timeout=10,
388
+ ).json()
389
+ except Exception:
390
+ raise (exceptions.Unauthorized)
391
+
392
+ def promote_curator(self, curator):
393
+ """
394
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
395
+ """
396
+ self._assert_auth()
397
+ try:
398
+ return requests.put(
399
+ f"https://scratch.mit.edu/site-api/users/curators-in/{self.id}/promote/?usernames={curator}",
400
+ headers=headers,
401
+ cookies=self._cookies,
402
+ timeout=10,
403
+ ).json()
404
+ except Exception:
405
+ raise (exceptions.Unauthorized)
406
+
407
+ def remove_curator(self, curator):
408
+ """
409
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
410
+ """
411
+ self._assert_auth()
412
+ try:
413
+ return requests.put(
414
+ f"https://scratch.mit.edu/site-api/users/curators-in/{self.id}/remove/?usernames={curator}",
415
+ headers=headers,
416
+ cookies=self._cookies,
417
+ timeout=10,
418
+ ).json()
419
+ except Exception:
420
+ raise (exceptions.Unauthorized)
421
+
422
+ def transfer_ownership(self, new_owner, *, password):
423
+ """
424
+ Makes another Scratcher studio host. You need to specify your password to do this.
425
+
426
+ Arguments:
427
+ new_owner (str): Username of new host
428
+
429
+ Keyword arguments:
430
+ password (str): The password of your Scratch account
431
+
432
+ Warning:
433
+ This action is irreversible!
434
+ """
435
+ self._assert_auth()
436
+ try:
437
+ return requests.put(
438
+ f"https://api.scratch.mit.edu/studios/{self.id}/transfer/{new_owner}",
439
+ headers=self._headers,
440
+ cookies=self._cookies,
441
+ timeout=10,
442
+ json={"password":password}
443
+ ).json()
444
+ except Exception:
445
+ raise (exceptions.Unauthorized)
446
+
447
+
448
+ def leave(self):
449
+ """
450
+ Removes yourself from the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
451
+ """
452
+ self._assert_auth()
453
+ return self.remove_curator(self._session._username)
454
+
455
+ def add_project(self, project_id):
456
+ """
457
+ Adds a project to the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
458
+
459
+ Args:
460
+ project_id: Project id of the project that should be added
461
+ """
462
+ self._assert_auth()
463
+ return requests.post(
464
+ f"https://api.scratch.mit.edu/studios/{self.id}/project/{project_id}",
465
+ headers=self._headers,
466
+ timeout=10,
467
+ ).json()
468
+
469
+ def remove_project(self, project_id):
470
+ """
471
+ Removes a project from the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
472
+
473
+ Args:
474
+ project_id: Project id of the project that should be removed
475
+ """
476
+ self._assert_auth()
477
+ return requests.delete(
478
+ f"https://api.scratch.mit.edu/studios/{self.id}/project/{project_id}",
479
+ headers=self._headers,
480
+ timeout=10,
481
+ ).json()
482
+
483
+ def managers(self, limit=40, offset=0):
484
+ """
485
+ Gets the studio managers.
486
+
487
+ Keyword arguments:
488
+ limit (int): Max amount of returned managers
489
+ offset (int): Offset of the first returned manager.
490
+
491
+ Returns:
492
+ list<scratchattach.user.User>: A list containing the studio managers as user objects
493
+ """
494
+ response = commons.api_iterative(
495
+ f"https://api.scratch.mit.edu/studios/{self.id}/managers", limit=limit, offset=offset)
496
+ return commons.parse_object_list(response, user.User, self._session, "username")
497
+
498
+ def host(self) -> user.User:
499
+ """
500
+ Gets the studio host.
501
+
502
+ Returns:
503
+ scratchattach.user.User: An object representing the studio host.
504
+ """
505
+ managers = self.managers(limit=1, offset=0)
506
+ try:
507
+ return managers[0]
508
+ except Exception:
509
+ return None
510
+
511
+ def set_fields(self, fields_dict):
512
+ """
513
+ Sets fields. Uses the scratch.mit.edu/site-api PUT API.
514
+ """
515
+ self._assert_auth()
516
+ requests.put(
517
+ f"https://scratch.mit.edu/site-api/galleries/all/{self.id}/",
518
+ headers=headers,
519
+ cookies=self._cookies,
520
+ data=json.dumps(fields_dict),
521
+ timeout=10,
522
+ )
523
+
524
+
525
+ def set_description(self, new):
526
+ """
527
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
528
+ """
529
+ self.set_fields({"description": new + "\n"})
530
+
531
+ def set_title(self, new):
532
+ """
533
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
534
+ """
535
+ self.set_fields({"title": new})
536
+
537
+ def open_projects(self):
538
+ """
539
+ Changes the studio settings so everyone (including non-curators) is able to add projects to the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
540
+ """
541
+ self._assert_auth()
542
+ requests.put(
543
+ f"https://scratch.mit.edu/site-api/galleries/{self.id}/mark/open/",
544
+ headers=headers,
545
+ cookies=self._cookies,
546
+ timeout=10,
547
+ )
548
+
549
+ def close_projects(self):
550
+ """
551
+ Changes the studio settings so only curators can add projects to the studio. You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
552
+ """
553
+ self._assert_auth()
554
+ requests.put(
555
+ f"https://scratch.mit.edu/site-api/galleries/{self.id}/mark/closed/",
556
+ headers=headers,
557
+ cookies=self._cookies,
558
+ timeout=10,
559
+ )
560
+
561
+ def turn_off_commenting(self):
562
+ """
563
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
564
+ """
565
+ self._assert_auth()
566
+ if self.comments_allowed:
567
+ requests.post(
568
+ f"https://scratch.mit.edu/site-api/comments/gallery/{self.id}/toggle-comments/",
569
+ headers=headers,
570
+ cookies=self._cookies,
571
+ timeout=10,
572
+ )
573
+ self.comments_allowed = not self.comments_allowed
574
+
575
+ def turn_on_commenting(self):
576
+ """
577
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
578
+ """
579
+ self._assert_auth()
580
+ if not self.comments_allowed:
581
+ requests.post(
582
+ f"https://scratch.mit.edu/site-api/comments/gallery/{self.id}/toggle-comments/",
583
+ headers=headers,
584
+ cookies=self._cookies,
585
+ timeout=10,
586
+ )
587
+ self.comments_allowed = not self.comments_allowed
588
+
589
+ def toggle_commenting(self):
590
+ """
591
+ You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
592
+ """
593
+ self._assert_auth()
594
+ requests.post(
595
+ f"https://scratch.mit.edu/site-api/comments/gallery/{self.id}/toggle-comments/",
596
+ headers=headers,
597
+ cookies=self._cookies,
598
+ timeout=10,
599
+ )
600
+ self.comments_allowed = not self.comments_allowed
601
+
602
+ def activity(self, *, limit=40, offset=0, date_limit=None):
603
+ add_params = ""
604
+ if date_limit is not None:
605
+ add_params = f"&dateLimit={date_limit}"
606
+ response = commons.api_iterative(
607
+ f"https://api.scratch.mit.edu/studios/{self.id}/activity", limit=limit, offset=offset, add_params=add_params)
608
+ return commons.parse_object_list(response, activity.Activity, self._session)
609
+
610
+ def accept_invite(self):
611
+ self._assert_auth()
612
+ return requests.put(
613
+ f"https://scratch.mit.edu/site-api/users/curators-in/{self.id}/add/?usernames={self._session._username}",
614
+ headers=headers,
615
+ cookies=self._cookies,
616
+ timeout=10,
617
+ ).json()
618
+
619
+ def your_role(self) -> StudioRoleDict:
620
+ """
621
+ Returns a dict with information about your role in the studio (whether you're following, curating, managing it or are invited)
622
+ """
623
+ self._assert_auth()
624
+ return requests.get(
625
+ f"https://api.scratch.mit.edu/studios/{self.id}/users/{self._session.username}",
626
+ headers=self._headers,
627
+ cookies=self._cookies,
628
+ timeout=10,
629
+ ).json()
630
+
631
+ def get_exact_project_count(self) -> int:
632
+ """
633
+ Get the exact project count of a studio using a binary-search-like strategy
634
+ """
635
+ if self.project_count is not None and self.project_count < 100:
636
+ return self.project_count
637
+
638
+ # Get maximum possible project count before binary search
639
+ maximum = 100
640
+ minimum = 0
641
+
642
+ while True:
643
+ if not self.projects(offset=maximum):
644
+ break
645
+ minimum = maximum
646
+ maximum *= 2
647
+
648
+ # Binary search
649
+ while True:
650
+ middle = (minimum + maximum) // 2
651
+ projects = self.projects(limit=40, offset=middle)
652
+
653
+ if not projects:
654
+ # too high - no projects found
655
+ maximum = middle
656
+ elif len(projects) < 40:
657
+ # we are 40 within true value, and can infer the rest
658
+ break
659
+ else:
660
+ # too low - full project list
661
+ minimum = middle
662
+
663
+ return middle + len(projects)
664
+
665
+
666
+
667
+ def get_studio(studio_id) -> Studio:
668
+ """
669
+ Gets a studio without logging in.
670
+
671
+ Args:
672
+ studio_id (int): Studio id of the requested studio
673
+
674
+ Returns:
675
+ scratchattach.studio.Studio: An object representing the requested studio
676
+
677
+ Warning:
678
+ Any methods that authentication (like studio.follow) will not work on the returned object.
679
+
680
+ If you want to use these, get the studio with :meth:`scratchattach.session.Session.connect_studio` instead.
681
+ """
682
+ warnings.warn(
683
+ "Warning: For methods that require authentication, use session.connect_studio instead of get_studio.\n"
684
+ "If you want to remove this warning, use warnings.filterwarnings('ignore', category=scratchattach.StudioAuthenticationWarning).\n"
685
+ "To ignore all warnings of the type GetAuthenticationWarning, which includes this warning, use "
686
+ "`warnings.filterwarnings('ignore', category=scratchattach.GetAuthenticationWarning)`.",
687
+ exceptions.StudioAuthenticationWarning
688
+ )
689
+ return commons._get_object("id", studio_id, Studio, exceptions.StudioNotFound)
690
+
691
+ def search_studios(*, query="", mode="trending", language="en", limit=40, offset=0):
692
+ if not query:
693
+ raise ValueError("The query can't be empty for search")
694
+ response = commons.api_iterative(
695
+ f"https://api.scratch.mit.edu/search/studios", limit=limit, offset=offset, add_params=f"&language={language}&mode={mode}&q={query}")
696
+ return commons.parse_object_list(response, Studio)
697
+
698
+
699
+ def explore_studios(*, query="", mode="trending", language="en", limit=40, offset=0):
700
+ if not query:
701
+ raise ValueError("The query can't be empty for explore")
702
+ response = commons.api_iterative(
703
+ f"https://api.scratch.mit.edu/explore/studios", limit=limit, offset=offset, add_params=f"&language={language}&mode={mode}&q={query}")
704
+ return commons.parse_object_list(response, Studio)