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
@@ -1,16 +1,23 @@
1
1
  """Studio class"""
2
2
  from __future__ import annotations
3
3
 
4
+ import warnings
4
5
  import json
5
6
  import random
6
- from . import user, comment, project, activity
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
7
15
  from scratchattach.utils import exceptions, commons
8
16
  from scratchattach.utils.commons import api_iterative, headers
9
- from ._base import BaseSiteComponent
10
-
11
17
  from scratchattach.utils.requests import requests
12
18
 
13
19
 
20
+ @dataclass
14
21
  class Studio(BaseSiteComponent):
15
22
  """
16
23
  Represents a Scratch studio.
@@ -44,19 +51,25 @@ class Studio(BaseSiteComponent):
44
51
  :.update(): Updates the attributes
45
52
 
46
53
  """
47
-
48
- def __init__(self, **entries):
49
-
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):
50
70
  # Info on how the .update method has to fetch the data:
51
71
  self.update_function = requests.get
52
- self.update_api = f"https://api.scratch.mit.edu/studios/{entries['id']}"
53
-
54
- # Set attributes every Project object needs to have:
55
- self._session = None
56
- self.id = 0
57
-
58
- # Update attributes from entries dict:
59
- self.__dict__.update(entries)
72
+ self.update_api = f"https://api.scratch.mit.edu/studios/{self.id}"
60
73
 
61
74
  # Headers and cookies:
62
75
  if self._session is None:
@@ -71,33 +84,72 @@ class Studio(BaseSiteComponent):
71
84
  self._json_headers["accept"] = "application/json"
72
85
  self._json_headers["Content-Type"] = "application/json"
73
86
 
74
- def _update_from_dict(self, studio):
75
- try: self.id = int(studio["id"])
76
- except Exception: pass
77
- try: self.title = studio["title"]
78
- except Exception: pass
79
- try: self.description = studio["description"]
80
- except Exception: pass
81
- try: self.host_id = studio["host"]
82
- except Exception: pass
83
- try: self.open_to_all = studio["open_to_all"]
84
- except Exception: pass
85
- try: self.comments_allowed = studio["comments_allowed"]
86
- except Exception: pass
87
- try: self.image_url = studio["image"]
88
- except Exception: pass
89
- try: self.created = studio["history"]["created"]
90
- except Exception: pass
91
- try: self.modified = studio["history"]["modified"]
92
- except Exception: pass
93
- try: self.follower_count = studio["stats"]["followers"]
94
- except Exception: pass
95
- try: self.manager_count = studio["stats"]["managers"]
96
- except Exception: pass
97
- try: self.project_count = studio["stats"]["projects"]
98
- except Exception: pass
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)
99
102
  return True
100
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
+
101
153
  def follow(self):
102
154
  """
103
155
  You can only use this function if this object was created using :meth:`scratchattach.session.Session.connect_studio`
@@ -156,7 +208,7 @@ class Studio(BaseSiteComponent):
156
208
  ).json()
157
209
  if r is None:
158
210
  raise exceptions.CommentNotFound()
159
- _comment = comment.Comment(id=r["id"], _session=self._session, source="studio", source_id=self.id)
211
+ _comment = comment.Comment(id=r["id"], _session=self._session, source=comment.CommentSource.STUDIO, source_id=self.id)
160
212
  _comment._update_from_dict(r)
161
213
  return _comment
162
214
 
@@ -191,7 +243,7 @@ class Studio(BaseSiteComponent):
191
243
  ).json()
192
244
  if "id" not in r:
193
245
  raise exceptions.CommentPostFailure(r)
194
- _comment = comment.Comment(id=r["id"], _session=self._session, source="studio", source_id=self.id)
246
+ _comment = comment.Comment(id=r["id"], _session=self._session, source=comment.CommentSource.STUDIO, source_id=self.id)
195
247
  _comment._update_from_dict(r)
196
248
  return _comment
197
249
 
@@ -291,7 +343,7 @@ class Studio(BaseSiteComponent):
291
343
  content, parent_id=parent_id, commentee_id=commentee_id
292
344
  )
293
345
 
294
- def projects(self, limit=40, offset=0):
346
+ def projects(self, limit=40, offset=0) -> list[project.Project]:
295
347
  """
296
348
  Gets the studio projects.
297
349
 
@@ -306,7 +358,7 @@ class Studio(BaseSiteComponent):
306
358
  f"https://api.scratch.mit.edu/studios/{self.id}/projects", limit=limit, offset=offset)
307
359
  return commons.parse_object_list(response, project.Project, self._session)
308
360
 
309
- def curators(self, limit=40, offset=0):
361
+ def curators(self, limit=40, offset=0) -> list[user.User]:
310
362
  """
311
363
  Gets the studio curators.
312
364
 
@@ -443,7 +495,7 @@ class Studio(BaseSiteComponent):
443
495
  f"https://api.scratch.mit.edu/studios/{self.id}/managers", limit=limit, offset=offset)
444
496
  return commons.parse_object_list(response, user.User, self._session, "username")
445
497
 
446
- def host(self):
498
+ def host(self) -> user.User:
447
499
  """
448
500
  Gets the studio host.
449
501
 
@@ -564,7 +616,7 @@ class Studio(BaseSiteComponent):
564
616
  timeout=10,
565
617
  ).json()
566
618
 
567
- def your_role(self):
619
+ def your_role(self) -> StudioRoleDict:
568
620
  """
569
621
  Returns a dict with information about your role in the studio (whether you're following, curating, managing it or are invited)
570
622
  """
@@ -576,6 +628,41 @@ class Studio(BaseSiteComponent):
576
628
  timeout=10,
577
629
  ).json()
578
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
+
579
666
 
580
667
  def get_studio(studio_id) -> Studio:
581
668
  """
@@ -592,7 +679,13 @@ def get_studio(studio_id) -> Studio:
592
679
 
593
680
  If you want to use these, get the studio with :meth:`scratchattach.session.Session.connect_studio` instead.
594
681
  """
595
- print("Warning: For methods that require authentication, use session.connect_studio instead of get_studio")
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
+ )
596
689
  return commons._get_object("id", studio_id, Studio, exceptions.StudioNotFound)
597
690
 
598
691
  def search_studios(*, query="", mode="trending", language="en", limit=40, offset=0):
site/typed_dicts.py ADDED
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ from typing_extensions import OrderedDict
4
+
5
+ from scratchattach.cloud import _base
6
+ from typing import TypedDict, Union, Optional, Required, NotRequired
7
+
8
+ class SessionUserDict(TypedDict):
9
+ id: int
10
+ banned: bool
11
+ should_vpn: bool
12
+ username: str
13
+ token: str
14
+ thumbnailUrl: str
15
+ dateJoined: str
16
+ email: str
17
+ birthYear: int
18
+ birthMonth: int
19
+ gender: str
20
+
21
+ class SessionOffenseDict(TypedDict):
22
+ expiresAt: float
23
+ messageType: str
24
+ createdAt: float
25
+
26
+ class SessionOffensesDict(TypedDict):
27
+ offenses: list[SessionOffenseDict]
28
+ showWarning: bool
29
+ muteExpiresAt: float
30
+ currentMessageType: str
31
+
32
+ class SessionPermissionsDict(TypedDict):
33
+ admin: bool
34
+ scratcher: bool
35
+ new_scratcher: bool
36
+ invited_scratcher: bool
37
+ social: bool
38
+ educator: bool
39
+ educator_invitee: bool
40
+ student: bool
41
+ mute_status: Union[dict, SessionOffensesDict]
42
+
43
+ class SessionFlagsDict(TypedDict):
44
+ must_reset_password: bool
45
+ must_complete_registration: bool
46
+ has_outstanding_email_confirmation: bool
47
+ show_welcome: bool
48
+ confirm_email_banner: bool
49
+ unsupported_browser_banner: bool
50
+ with_parent_email: bool
51
+ project_comments_enabled: bool
52
+ gallery_comments_enabled: bool
53
+ userprofile_comments_enabled: bool
54
+ everything_is_totally_normal: bool
55
+
56
+ class SessionDict(TypedDict):
57
+ user: SessionUserDict
58
+ permissions: SessionPermissionsDict
59
+ flags: SessionFlagsDict
60
+
61
+ class OcularUserMetaDict(TypedDict):
62
+ updated: str
63
+ updatedBy: str
64
+
65
+ class OcularUserDict(TypedDict):
66
+ _id: str
67
+ name: str
68
+ status: str
69
+ color: str
70
+ meta: OcularUserMetaDict
71
+
72
+ class UserHistoryDict(TypedDict):
73
+ joined: str
74
+
75
+ class UserProfileDict(TypedDict):
76
+ id: int
77
+ status: str
78
+ bio: str
79
+ country: str
80
+ images: dict[str, str]
81
+ membership_label: NotRequired[int]
82
+ membership_avatar_badge: NotRequired[int]
83
+
84
+ class UserDict(TypedDict):
85
+ id: NotRequired[int]
86
+ username: NotRequired[str]
87
+ scratchteam: NotRequired[bool]
88
+ history: NotRequired[UserHistoryDict]
89
+ profile: NotRequired[UserProfileDict]
90
+
91
+ class CloudLogActivityDict(TypedDict):
92
+ user: str
93
+ verb: str
94
+ name: str
95
+ value: Union[str, float, int]
96
+ timestamp: int
97
+ cloud: _base.AnyCloud
98
+
99
+ class CloudActivityDict(TypedDict):
100
+ method: str
101
+ name: str
102
+ value: Union[str, float, int]
103
+ project_id: int
104
+ cloud: _base.AnyCloud
105
+
106
+ class ClassroomDict(TypedDict):
107
+ id: int
108
+ title: str
109
+ description: str
110
+ status: str
111
+ date_start: NotRequired[str]
112
+ date_end: NotRequired[Optional[str]]
113
+ images: NotRequired[dict[str, str]]
114
+ educator: UserDict
115
+ is_closed: NotRequired[bool]
116
+
117
+ class StudioHistoryDict(TypedDict):
118
+ created: str
119
+ modified: str
120
+
121
+ class StudioStatsDict(TypedDict):
122
+ followers: int
123
+ managers: int
124
+ projects: int
125
+
126
+ class StudioDict(TypedDict):
127
+ id: int
128
+ title: str
129
+ description: str
130
+ host: int
131
+ open_to_all: bool
132
+ comments_allowed: bool
133
+ image: str
134
+ history: StudioHistoryDict
135
+ stats: NotRequired[StudioStatsDict]
136
+
137
+ class StudioRoleDict(TypedDict):
138
+ manager: bool
139
+ curator: bool
140
+ invited: bool
141
+ following: bool
142
+
143
+ class PlaceholderProjectDataMetadataDict(TypedDict):
144
+ title: str
145
+ description: str
146
+
147
+ # https://github.com/GarboMuffin/placeholder/blob/e1e98953342a40abbd626a111f621711f74e783b/src/routes/projects/%5Bproject%5D/%2Bpage.server.ts#L19
148
+ class PlaceholderProjectDataDict(TypedDict):
149
+ metadata: PlaceholderProjectDataMetadataDict
150
+ md5extsToSha256: OrderedDict[str, str]
151
+ adminOwnershipToken: Optional[str]