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
@@ -0,0 +1,79 @@
1
+ cli/__about__.py,sha256=rWWgBXhOQakEGEip4jZgZsmGHkIU08m01PRgPUfmHdg,17
2
+ cli/__init__.py,sha256=WAJmqnZi5RoJGlx3fngQAi9APNf84QlicncbsPRwhkg,700
3
+ cli/context.py,sha256=7m9i-J2j36MN8uPJm8ydjMFKWZjJQAyu5NBluL8eQUE,4950
4
+ cli/db.py,sha256=sV-AwyYc8WeHalpadAV1vD0HXkEmZrcvhLGJA0q0TD0,1920
5
+ cli/namespace.py,sha256=qWH1QI9ot-5Y1ahpY9JHw8HTGnlFwHRKLoKxvxFqk5A,445
6
+ cli/cmd/__init__.py,sha256=KiNP3GVwCf-NE_yzYgDvza3hR3Sr1B0-wqu5Xaxi_g4,110
7
+ cli/cmd/group.py,sha256=j8jqy2N4KEChe6cUus5MczoqGKDzjOIgG2mBmxVmb-A,4144
8
+ cli/cmd/login.py,sha256=BnPUCUDG58gNly76OZdBT22oxaZY50b_VHnQWx2p40I,1914
9
+ cli/cmd/profile.py,sha256=jT-JoJhFCtzbyskdQpi8_ckVzrVqDghlh-zEEEchFDI,172
10
+ cli/cmd/sessions.py,sha256=JzBx6BTjddqU-UGZz4CtYpBKb9CC7lbpXLk19Pz7Zoo,116
11
+ cloud/__init__.py,sha256=MN7s4grQcciqUO7TiFjTbU2sC69m6XXlwOHNRbN3FKo,41
12
+ cloud/_base.py,sha256=tXVWPsL9Ybvu2H0zQ5R9GexgeHAarEv4GS16foD3_qw,19251
13
+ cloud/cloud.py,sha256=zq0Z-N2EF2uUxpJ0uZlITY2DeKkjb3TxwK_etci-xCM,8470
14
+ editor/__init__.py,sha256=pdq-dg7fa4cj6eksu5yzUe27DGY6CLKVUtCSrbYCsM8,807
15
+ editor/asset.py,sha256=Ut_Em2rzlzswi2aDTvyRZWvF5JKLY8IAmoLMOQLBpjQ,8148
16
+ editor/backpack_json.py,sha256=zhNRKDmlA9Oz-IixjLRnSwVG6JfKURs31KhQPjJu_bs,3880
17
+ editor/base.py,sha256=MUQ4zqw-IaFZDH9o_F9k7F3mptymR67E8ITkaMfnD5M,5311
18
+ editor/block.py,sha256=2UKKQbrDdsI6dbswo2swvIr7jXiXA114TCtXDO9DSFw,19085
19
+ editor/blockshape.py,sha256=Q8CAWsBc2O5VqcGhUxbJoW1RRfy7kBxz1AporkExBi8,25318
20
+ editor/build_defaulting.py,sha256=rRDSXpIugWENYs6X50A_a3BJfWJuzEtwW1bv9IAyo-M,1421
21
+ editor/comment.py,sha256=G_eFxeaC_vKbCFpDfw4oZxIReUBoBUaCA5q35O-AW6Y,2403
22
+ editor/commons.py,sha256=0mm30KjAenl_V2BXbkdirFN62SraYfGUariXB-TXNlU,3583
23
+ editor/extension.py,sha256=PFVl5SrS66M4Md98I_dsfeL9BLeWwvyWezmhnTRseIQ,1541
24
+ editor/field.py,sha256=_AmBwrb5TeG_CRm312I69T2xXlzuKYeudpygPyA4698,2994
25
+ editor/inputs.py,sha256=bmCLsxgGimkESbKEnhsnw_ZSRpBDsWRIcEoYPFMx9Zs,4505
26
+ editor/meta.py,sha256=QCHk5DKvYAw7F5Gg4OIJNcB7PhK8u5YgfDmGQPWobTs,3330
27
+ editor/monitor.py,sha256=A30XadTjw_G0KsTnwo3YEI31nkplc1DnO8AirjmB3zc,5921
28
+ editor/mutation.py,sha256=o2msH-s6UxFZ-LuYF1BlR4CBm-ulKTVpStux54cxnDk,12246
29
+ editor/pallete.py,sha256=OY76grzJLUWsAKi2x-mipEXv5f-9I-s9UjoLZ3FmtBM,2299
30
+ editor/prim.py,sha256=v74mkdLwsXAKPy_KPesugv-xCxA6YcGU6_v7Zvc2YEY,5919
31
+ editor/project.py,sha256=tWf9kULlJjMCV1t5AofIDFqGylBYQ31vpjDMJ3jM3_Q,12884
32
+ editor/sprite.py,sha256=M_rrkErJswH9stKvrywrkaoNEwgRBbF7-Si17W-0vTs,21205
33
+ editor/twconfig.py,sha256=iE6ylAsZzniAfhL09GkZSFn1XacYtCQPzRCUSPIBzDA,3324
34
+ editor/vlb.py,sha256=Fl2gGwZyYh54uOhQ7XITfCgCpJTQB2P8wy47PKY3Qyk,4151
35
+ editor/code_translation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
+ editor/code_translation/parse.py,sha256=FjuHVg_tNzkGcGSNwgH6MzAHcf5YCvaEUSYukyJkwbk,4932
37
+ eventhandlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
+ eventhandlers/_base.py,sha256=97vFbuhSoJeYb-jrEWDdbZWQ7e_54CY3ktrq8d_-w8g,3142
39
+ eventhandlers/cloud_events.py,sha256=LDENLVopVwKDVrcfkR1spKzT45ET-EKqBdtCzXoaS5g,5228
40
+ eventhandlers/cloud_recorder.py,sha256=dvob4-aKLxE9WFqvCNv28M_r3r3OEkET6rwmu5yl4jk,795
41
+ eventhandlers/cloud_requests.py,sha256=CQsA6wzW6C0xJYtw6g0X8x5DY5EHkDE1tAOMVigzhzA,25113
42
+ eventhandlers/cloud_server.py,sha256=j2Iiuxsp9xOLeZ6S7iT7Yto8u599O57kRUZLMMIxjQ0,12431
43
+ eventhandlers/cloud_storage.py,sha256=YYcvRjhIuOboWVemMKFEtUhrd6UeUFO_BlbAIH7oaeQ,4609
44
+ eventhandlers/combine.py,sha256=YiWI6WI1BySioXpfYaMv8noBM14EjZa7dtsJsMTshEU,894
45
+ eventhandlers/filterbot.py,sha256=V5dQErz_yFpSioh5VxUDaBW8L9ny1uMviTt7x-KFC1k,7612
46
+ eventhandlers/message_events.py,sha256=KvznXAeNGk1WWCxd7PI95yovX-58TyCBNDdXbrYgb8Q,1776
47
+ other/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
+ other/other_apis.py,sha256=yZ06rCZmJLSZZdfv1V_VgRBq5dUxS31YHkpsdOxAezw,17509
49
+ other/project_json_capabilities.py,sha256=07t1iMgWm4Qd06jHyQ3vK7tROguvc2RQCo78enrdSlA,22646
50
+ scratchattach-3.0.0b1.dist-info/licenses/LICENSE,sha256=1PRKLhZU4wYt5M-C9f7q0W3go3u_ojnZMNOdR3g3J-E,1080
51
+ site/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
+ site/_base.py,sha256=D2SOp2aFiDP0hG7-xP8kK1pmoAz5TOi7_iWt6Ulpsbg,3283
53
+ site/activity.py,sha256=g5fg97xHUAKOkaZ4VzKl3hVpICZhv1mpDCO6LlmkoTU,17871
54
+ site/alert.py,sha256=V6asmcWy4tcQgWhG95rpqXP0KeUy7VQU9wD9AMhCqds,9324
55
+ site/backpack_asset.py,sha256=__VZomGDJkbgWj2ridQQArMMLWoSMv8dpO7PPpvPgBU,3322
56
+ site/browser_cookie3_stub.py,sha256=codk0knOP5C0YThaRazvqsqX7X7WnrD2UwFd1nFG7mg,1422
57
+ site/browser_cookies.py,sha256=uQyjJJ4HCu46R4tPWCcTUqDMXSXhQ4KQUobqCSxScSA,1864
58
+ site/classroom.py,sha256=lpHh2OkHeFzGUaFbf9PWIrosYOBo2cRrfc6fsex2w44,18139
59
+ site/cloud_activity.py,sha256=vMQy2k3jzPbOa3_TiH23B9dTk4BQA_z0q_Ab6TFkeV8,5397
60
+ site/comment.py,sha256=kUZxbjChs8K66vShz-Q1y1s72yJ44hFXJUYLsSb9rUs,9449
61
+ site/forum.py,sha256=-XLi3UJOwDt0Ye8CqP4c-sSxOkPSe5fPVa9b7MOm64k,16553
62
+ site/placeholder.py,sha256=BTOroGKA3lpgKPJXFeveEuMdcNkOIddpbNTrV-48s-o,5446
63
+ site/project.py,sha256=Zc5TN4PEGC1fTze_rkU7_-Y1YDuSXrkHaJGD3eeLgqU,34750
64
+ site/session.py,sha256=kd4IKaMXmtWGJJnFV3wOoLAF2S_012zHkdGcMZgUFxs,54451
65
+ site/studio.py,sha256=sAqFJDjhxf-9NynLB7mrCgzxq8td1oqa1zTW9MP1yTM,26195
66
+ site/typed_dicts.py,sha256=Hq65u56OSLdcoQ0F0A_CxZ8WBY5d_0VuJmUEMd8KmpU,3611
67
+ site/user.py,sha256=P2nsSGo-Z1QlXzAQxstOOq-E4QGN9HVA4dIQaJd1YFY,47404
68
+ utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
+ utils/commons.py,sha256=DpBTxRC2XeKURW5YJwAT6teAfYG7221GxClTaZ0-yQE,8465
70
+ utils/encoder.py,sha256=1lOXBbqo2JGMdns6Q5WGhAB9p7Q8trduBk-SZgmcEKw,2559
71
+ utils/enums.py,sha256=5HhXidczUYYVwHf6jfJcMA80jK4V0imaltLEu5epIF8,11127
72
+ utils/exceptions.py,sha256=T1nBbbkTZVkZU7uNaqBqSbNTGtOaOnTVLRRqUTatTis,7556
73
+ utils/optional_async.py,sha256=zTCFt6tpSvlcwns1RgAACKplA1SSFyVWAcDRS8kLLjE,4721
74
+ utils/requests.py,sha256=Brl94PCyblaQopanXyyQZ8ZoaWuFrK3NqUTZWW22gpY,9608
75
+ scratchattach-3.0.0b1.dist-info/METADATA,sha256=JnNVUZxbXAuGUH0mn9nE4Cuu17ko-wNNy1OLdPMPSWU,5633
76
+ scratchattach-3.0.0b1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
77
+ scratchattach-3.0.0b1.dist-info/entry_points.txt,sha256=vNXuP05TQKEoIzmzmUzS7zbtSZx0p3JmeUW3QhNdYfg,56
78
+ scratchattach-3.0.0b1.dist-info/top_level.txt,sha256=PFfH9Sb4fMOY99H0Xiuuc8nEGjAT_-anhTaORDZBd7A,48
79
+ scratchattach-3.0.0b1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ scratch = scratchattach.__main__:main
@@ -0,0 +1,7 @@
1
+ cli
2
+ cloud
3
+ editor
4
+ eventhandlers
5
+ other
6
+ site
7
+ utils
@@ -1,18 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from abc import ABC, abstractmethod
4
- from typing import TypeVar, Optional
4
+ from typing import TypeVar, Optional, Self, Union, Any, Generic
5
+ import json
5
6
 
6
7
  import requests
7
- from scratchattach.utils import exceptions, commons
8
+
9
+ from scratchattach.utils import exceptions, commons, optional_async
10
+ from scratchattach.utils import requests as m_requests
8
11
  from . import session
9
12
 
13
+ D = TypeVar("D")
10
14
  C = TypeVar("C", bound="BaseSiteComponent")
11
- class BaseSiteComponent(ABC):
15
+ class BaseSiteComponent(ABC, Generic[D]):
12
16
  _session: Optional[session.Session]
13
17
  update_api: str
14
18
  _headers: dict[str, str]
15
19
  _cookies: dict[str, str]
20
+ oa_http_session: Optional[m_requests.OAHTTPSession] = None
16
21
 
17
22
  # @abstractmethod
18
23
  # def __init__(self): # dataclasses do not implement __init__ directly
@@ -32,7 +37,7 @@ class BaseSiteComponent(ABC):
32
37
  if "429" in str(response):
33
38
  return "429"
34
39
 
35
- if response.text == '{\n "response": "Too many requests"\n}':
40
+ if json.loads(response.text) == {"response": "Too many requests"}:
36
41
  return "429"
37
42
 
38
43
  # If no error: Parse JSON:
@@ -41,9 +46,13 @@ class BaseSiteComponent(ABC):
41
46
  return False
42
47
 
43
48
  return self._update_from_dict(response)
49
+
50
+ def updated(self) -> Self:
51
+ self.update()
52
+ return self
44
53
 
45
54
  @abstractmethod
46
- def _update_from_dict(self, data) -> bool:
55
+ def _update_from_dict(self, data: D) -> bool:
47
56
  """
48
57
  Parses the API response that is fetched in the update-method. Class specific, must be overridden in classes inheriting from this one.
49
58
  """
@@ -59,8 +68,26 @@ class BaseSiteComponent(ABC):
59
68
  Class must inherit from BaseSiteComponent
60
69
  """
61
70
  return commons._get_object(identificator_id, identificator, Class, NotFoundException, self._session)
71
+
72
+ def supply_data_dict(self, data: D) -> bool:
73
+ return self._update_from_dict(data)
62
74
 
63
75
  update_function = requests.get
64
76
  """
65
77
  Internal function run on update. Function is a method of the 'requests' module/class
66
78
  """
79
+
80
+ def _make_request(
81
+ self,
82
+ method: Union[m_requests.HTTPMethod, str],
83
+ url: str,
84
+ *,
85
+ cookies: Optional[dict[str, str]] = None,
86
+ headers: Optional[dict[str, str]] = None,
87
+ params: Optional[dict[str, str]] = None,
88
+ data: Optional[Union[dict[str, str], str]] = None,
89
+ json: Optional[Any] = None
90
+ ) -> optional_async.CARequest:
91
+ if self.oa_http_session is None:
92
+ raise ValueError("This BaseSiteComponent has no oa_http_session.")
93
+ return self.oa_http_session.request(method, url, cookies=cookies, headers=headers, params=params, data=data, json=json)
site/activity.py ADDED
@@ -0,0 +1,426 @@
1
+ """Activity and CloudActivity class"""
2
+ from __future__ import annotations
3
+
4
+ import html
5
+ import warnings
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Optional, Any
9
+ from enum import Enum
10
+
11
+ from bs4 import Tag
12
+
13
+ from . import user, project, studio, session, forum
14
+ from ._base import BaseSiteComponent
15
+ from scratchattach.utils import exceptions
16
+
17
+ class ActivityTypes(Enum):
18
+ loveproject = "loveproject"
19
+ favoriteproject = "favoriteproject"
20
+ becomecurator = "becomecurator"
21
+ followuser = "followuser"
22
+ followstudio = "followstudio"
23
+ shareproject = "shareproject"
24
+ remixproject = "remixproject"
25
+ becomeownerstudio = "becomeownerstudio"
26
+ addcomment = "addcomment"
27
+ curatorinvite = "curatorinvite"
28
+ userjoin = "userjoin"
29
+ studioactivity = "studioactivity"
30
+ forumpost = "forumpost"
31
+ updatestudio = "updatestudio"
32
+ createstudio = "createstudio"
33
+ promotetomanager = "promotetomanager"
34
+ updateprofile = "updateprofile"
35
+ removeprojectfromstudio = "removeprojectfromstudio"
36
+ addprojecttostudio = "addprojecttostudio"
37
+ performaction = "performaction"
38
+
39
+ @dataclass
40
+ class Activity(BaseSiteComponent):
41
+ """
42
+ Represents a Scratch activity (message or other user page activity)
43
+ """
44
+ _session: Optional[session.Session] = None
45
+ raw: Any = None
46
+
47
+ id: Optional[int] = None
48
+ actor_username: Optional[str] = None
49
+
50
+ project_id: Optional[int] = None
51
+ gallery_id: Optional[int] = None
52
+ username: Optional[str] = None
53
+ followed_username: Optional[str] = None
54
+ recipient_username: Optional[str] = None
55
+ title: Optional[str] = None
56
+ project_title: Optional[str] = None
57
+ gallery_title: Optional[str] = None
58
+ topic_title: Optional[str] = None
59
+ topic_id: Optional[int] = None
60
+ target_name: Optional[str] = None
61
+ target_id: Optional[int | str] = None
62
+
63
+ parent_title: Optional[str] = None
64
+ parent_id: Optional[int] = None
65
+
66
+ comment_type: Optional[int] = None
67
+ comment_obj_id = None
68
+ comment_obj_title: Optional[str] = None
69
+ comment_id: Optional[int] = None
70
+ comment_fragment: Optional[str] = None
71
+
72
+ changed_fields: Optional[dict[str, str]] = None
73
+ is_reshare: Optional[bool] = None
74
+
75
+ datetime_created: Optional[str] = None
76
+ time: Any = None
77
+ type: Optional[ActivityTypes] = None
78
+
79
+ def __repr__(self):
80
+ return f"Activity({repr(self.raw)})"
81
+
82
+ def __str__(self):
83
+ return '-A ' + ' '.join(self.parts)
84
+
85
+ @property
86
+ def parts(self):
87
+ """
88
+ Return format: [actor username] + N * [action, object]
89
+ :return: A list of parts of the message. Join the parts to get a readable version, which is done with str(activity)
90
+ """
91
+ match self.type:
92
+ case ActivityTypes.loveproject:
93
+ return [f"{self.actor_username}", "loved", f"-P {self.title!r} ({self.project_id})"]
94
+ case ActivityTypes.favoriteproject:
95
+ return [f"{self.actor_username}", "favorited", f"-P {self.project_title!r} ({self.project_id})"]
96
+ case ActivityTypes.becomecurator:
97
+ return [f"{self.actor_username}", "now curating", f"-S {self.title!r} ({self.gallery_id})"]
98
+ case ActivityTypes.followuser:
99
+ return [f"{self.actor_username}", "followed", f"-U {self.followed_username}"]
100
+ case ActivityTypes.followstudio:
101
+ return [f"{self.actor_username}", "followed", f"-S {self.title!r} ({self.gallery_id})"]
102
+ case ActivityTypes.shareproject:
103
+ return [f"{self.actor_username}", "reshared" if self.is_reshare else "shared",
104
+ f"-P {self.title!r} ({self.project_id})"]
105
+ case ActivityTypes.remixproject:
106
+ return [f"{self.actor_username}", "remixed",
107
+ f"-P {self.parent_title!r} ({self.parent_id}) as -P {self.title!r} ({self.project_id})"]
108
+ case ActivityTypes.becomeownerstudio:
109
+ return [f"{self.actor_username}", "became owner of", f"-S {self.gallery_title!r} ({self.gallery_id})"]
110
+
111
+ case ActivityTypes.addcomment:
112
+ ret = [self.actor_username, "commented on"]
113
+
114
+ match self.comment_type:
115
+ case 0:
116
+ # project
117
+ ret.append(f"-P {self.comment_obj_title!r} ({self.comment_obj_id}")
118
+ case 1:
119
+ # user
120
+ ret.append(f"-U {self.comment_obj_title}")
121
+
122
+ case 2:
123
+ # studio
124
+ ret.append(f"-S {self.comment_obj_title!r} ({self.comment_obj_id}")
125
+
126
+ case _:
127
+ raise ValueError(f"Unknown comment type: {self.comment_type}")
128
+
129
+ ret[-1] += f"#{self.comment_id})"
130
+
131
+ ret.append(f"{html.unescape(self.comment_fragment)}")
132
+
133
+ return ret
134
+
135
+ case ActivityTypes.curatorinvite:
136
+ return [f"{self.actor_username}", "invited you to curate", f"-S {self.title!r} ({self.gallery_id})"]
137
+
138
+ case ActivityTypes.userjoin:
139
+ # This is also the first message you get - 'Welcome to Scratch'
140
+ return [f"{self.actor_username}", "joined Scratch"]
141
+
142
+ case ActivityTypes.studioactivity:
143
+ # the actor username should be systemuser
144
+ return [f"{self.actor_username}", 'Studio activity', '', f"-S {self.title!r} ({self.gallery_id})"]
145
+
146
+ case ActivityTypes.forumpost:
147
+ return [f"{self.actor_username}", "posted in", f"-F {self.topic_title} ({self.topic_id})"]
148
+
149
+ case ActivityTypes.updatestudio:
150
+ return [f"{self.actor_username}", "updated", f"-S {self.gallery_title} ({self.gallery_id})"]
151
+
152
+ case ActivityTypes.createstudio:
153
+ return [f"{self.actor_username}", "created", f"-S {self.gallery_title} ({self.gallery_id})"]
154
+
155
+ case ActivityTypes.promotetomanager:
156
+ return [f"{self.actor_username}", "promoted", f"-U {self.recipient_username}", "in",
157
+ f"-S {self.gallery_title} ({self.gallery_id})"]
158
+
159
+ case ActivityTypes.updateprofile:
160
+ return [f"{self.actor_username}", "updated their profile.", f"Changed fields: {self.changed_fields}"]
161
+
162
+ case ActivityTypes.removeprojectfromstudio:
163
+ return [f"{self.actor_username}", "removed", f"-P {self.project_title} ({self.project_id})", "from",
164
+ f"-S {self.gallery_title} ({self.gallery_id})"]
165
+
166
+ case ActivityTypes.addprojecttostudio:
167
+ return [f"{self.actor_username}", "added", f"-P {self.project_title} ({self.project_id})", "to",
168
+ f"-S {self.gallery_title} ({self.gallery_id})"]
169
+
170
+ case ActivityTypes.performaction:
171
+ return [f"{self.actor_username}", "performed an action"]
172
+
173
+ case _:
174
+ raise NotImplementedError(
175
+ f"Activity type {self.type!r} is not implemented!\n"
176
+ f"{self.raw=}\n"
177
+ f"Raise an issue on github: https://github.com/TimMcCool/scratchattach/issues")
178
+
179
+ def update(self):
180
+ print("Warning: Activity objects can't be updated")
181
+ return False # Objects of this type cannot be updated
182
+
183
+ def _update_from_dict(self, data):
184
+ self.raw = data
185
+
186
+ self._session = data.get("_session", self._session)
187
+ self.raw = data.get("raw", self.raw)
188
+
189
+ self.id = data.get("id", self.id)
190
+ self.actor_username = data.get("actor_username", self.actor_username)
191
+
192
+ self.project_id = data.get("project_id", self.project_id)
193
+ self.gallery_id = data.get("gallery_id", self.gallery_id)
194
+ self.username = data.get("username", self.username)
195
+ self.followed_username = data.get("followed_username", self.followed_username)
196
+ self.recipient_username = data.get("recipient_username", self.recipient_username)
197
+ self.title = data.get("title", self.title)
198
+ self.project_title = data.get("project_title", self.project_title)
199
+ self.gallery_title = data.get("gallery_title", self.gallery_title)
200
+ self.topic_title = data.get("topic_title", self.topic_title)
201
+ self.topic_id = data.get("topic_id", self.topic_id)
202
+ self.target_name = data.get("target_name", self.target_name)
203
+ self.target_id = data.get("target_id", self.target_id)
204
+
205
+ self.parent_title = data.get("parent_title", self.parent_title)
206
+ self.parent_id = data.get("parent_id", self.parent_id)
207
+
208
+ self.comment_type = data.get("comment_type", self.comment_type)
209
+ self.comment_obj_id = data.get("comment_obj_id", self.comment_obj_id)
210
+ self.comment_obj_title = data.get("comment_obj_title", self.comment_obj_title)
211
+ self.comment_id = data.get("comment_id", self.comment_id)
212
+ self.comment_fragment = data.get("comment_fragment", self.comment_fragment)
213
+
214
+ self.changed_fields = data.get("changed_fields", self.changed_fields)
215
+ self.is_reshare = data.get("is_reshare", self.is_reshare)
216
+
217
+ self.datetime_created = data.get("datetime_created", self.datetime_created)
218
+ self.time = data.get("time", self.time)
219
+
220
+ _type = data.get("type", self.type)
221
+ if _type:
222
+ self.type = ActivityTypes[_type]
223
+
224
+ return True
225
+
226
+ def _update_from_json(self, data: dict):
227
+ """
228
+ Update using JSON, used in the classroom API.
229
+ """
230
+ activity_type = data["type"]
231
+
232
+ _time = data["datetime_created"] if "datetime_created" in data else None
233
+
234
+ if "actor" in data:
235
+ username = data["actor"]["username"]
236
+ elif "actor_username" in data:
237
+ username = data["actor_username"]
238
+ else:
239
+ username = None
240
+
241
+ if recipient := data.get("recipient"):
242
+ recipient_username = recipient["username"]
243
+ elif recipient_username := data.get("recipient_username"):
244
+ pass
245
+ elif project_creator := data.get("project_creator"):
246
+ recipient_username = project_creator["username"]
247
+ else:
248
+ recipient_username = None
249
+
250
+ default_case = False
251
+ # Even if `activity_type` is an invalid value; it will default to 'user performed an action'
252
+ self.actor_username = username
253
+ self.username = username
254
+ self.raw = data
255
+ self.datetime_created = _time
256
+ if activity_type == 0:
257
+ self.type = ActivityTypes.followuser
258
+ self.followed_username = data["followed_username"]
259
+
260
+ elif activity_type == 1:
261
+ self.type = ActivityTypes.followstudio
262
+ self.gallery_id = data["gallery"]
263
+
264
+ elif activity_type == 2:
265
+ self.type = ActivityTypes.loveproject
266
+ self.project_id = data["project"]
267
+ self.recipient_username = recipient_username
268
+
269
+ elif activity_type == 3:
270
+ self.type = ActivityTypes.favoriteproject
271
+ self.project_id = data["project"]
272
+ self.recipient_username = recipient_username
273
+
274
+ elif activity_type == 7:
275
+ self.type = ActivityTypes.addprojecttostudio
276
+ self.project_id = data["project"]
277
+ self.gallery_id = data["gallery"]
278
+ self.recipient_username = recipient_username
279
+
280
+ elif activity_type in (8, 9, 10):
281
+ self.type = ActivityTypes.shareproject
282
+ self.is_reshare = data["is_reshare"]
283
+ self.project_id = data["project"]
284
+ self.recipient_username = recipient_username
285
+
286
+ elif activity_type == 11:
287
+ self.type = ActivityTypes.remixproject
288
+ self.parent_id = data["parent"]
289
+ warnings.warn(f"This may be incorrectly implemented.\n"
290
+ f"Raw data: {data}\n"
291
+ f"Please raise an issue on gh: https://github.com/TimMcCool/scratchattach/issues")
292
+ self.recipient_username = recipient_username
293
+
294
+ # type 12 does not exist in the HTML. That's why it was removed, not merged with type 13.
295
+
296
+ elif activity_type == 13:
297
+ self.type = ActivityTypes.createstudio
298
+ self.gallery_id = data["gallery"]
299
+
300
+ elif activity_type == 15:
301
+ self.type = ActivityTypes.updatestudio
302
+ self.gallery_id = data["gallery"]
303
+
304
+ elif activity_type in (16, 17, 18, 19):
305
+ self.type = ActivityTypes.removeprojectfromstudio
306
+ self.gallery_id = data["gallery"]
307
+ self.project_id = data["project"]
308
+
309
+ elif activity_type in (20, 21, 22):
310
+ self.type = ActivityTypes.promotetomanager
311
+ self.recipient_username = recipient_username
312
+ self.gallery_id = data["gallery"]
313
+
314
+ elif activity_type in (23, 24, 25):
315
+ self.type = ActivityTypes.updateprofile
316
+ self.changed_fields = data.get("changed_fields", {})
317
+
318
+ elif activity_type in (26, 27):
319
+ # Comment in either project, user, or studio
320
+ self.type = ActivityTypes.addcomment
321
+ self.comment_fragment = data["comment_fragment"]
322
+ self.comment_type = data["comment_type"]
323
+ self.comment_obj_id = data["comment_obj_id"]
324
+ self.comment_obj_title = data["comment_obj_title"]
325
+ self.comment_id = data["comment_id"]
326
+
327
+ else:
328
+ # This is coded in the scratch HTML, haven't found an example of it though
329
+ self.type = ActivityTypes.performaction
330
+
331
+
332
+ def _update_from_html(self, data: Tag):
333
+
334
+ self.raw = data
335
+
336
+ _time = data.find('div').find('span').find_next().find_next().text.strip()
337
+
338
+ if '\xa0' in _time:
339
+ while '\xa0' in _time:
340
+ _time = _time.replace('\xa0', ' ')
341
+
342
+ self.datetime_created = _time
343
+ self.actor_username = data.find('div').find('span').text
344
+
345
+ self.target_name = data.find('div').find('span').find_next().text
346
+ self.target_link = data.find('div').find('span').find_next()["href"]
347
+ # note that target_id can also be a username, so it isn't exclusively an int
348
+ self.target_id = data.find('div').find('span').find_next()["href"].split("/")[-2]
349
+
350
+ _type = data.find('div').find_all('span')[0].next_sibling.strip()
351
+ if _type == "loved":
352
+ self.type = ActivityTypes.loveproject
353
+
354
+ elif _type == "favorited":
355
+ self.type = ActivityTypes.favoriteproject
356
+
357
+ elif "curator" in _type:
358
+ self.type = ActivityTypes.becomecurator
359
+
360
+ elif "shared" in _type:
361
+ self.type = ActivityTypes.shareproject
362
+
363
+ elif "is now following" in _type:
364
+ if "users" in self.target_link:
365
+ self.type = ActivityTypes.followuser
366
+ else:
367
+ self.type = ActivityTypes.followstudio
368
+
369
+ return True
370
+
371
+ def actor(self):
372
+ """
373
+ Returns the user that performed the activity as User object
374
+ """
375
+ return self._make_linked_object("username", self.actor_username, user.User, exceptions.UserNotFound)
376
+
377
+ def target(self):
378
+ """
379
+ Returns the activity's target (depending on the activity, this is either a User, Project, Studio or Comment object).
380
+ May also return None if the activity type is unknown.
381
+ """
382
+ _type = self.type.value
383
+
384
+ if "project" in _type: # target is a project
385
+ if self.target_id:
386
+ return self._make_linked_object("id", self.target_id, project.Project, exceptions.ProjectNotFound)
387
+ if self.project_id:
388
+ return self._make_linked_object("id", self.project_id, project.Project, exceptions.ProjectNotFound)
389
+
390
+ if _type == "becomecurator" or _type == "followstudio": # target is a studio
391
+ if self.target_id:
392
+ return self._make_linked_object("id", self.target_id, studio.Studio, exceptions.StudioNotFound)
393
+ if self.gallery_id:
394
+ return self._make_linked_object("id", self.gallery_id, studio.Studio, exceptions.StudioNotFound)
395
+ # NOTE: the "becomecurator" type is ambigous - if it is inside the studio activity tab, the target is the user who joined
396
+ if self.username:
397
+ return self._make_linked_object("username", self.username, user.User, exceptions.UserNotFound)
398
+
399
+ if _type == "followuser" or "curator" in _type: # target is a user
400
+ if self.target_name:
401
+ return self._make_linked_object("username", self.target_name, user.User, exceptions.UserNotFound)
402
+ if self.followed_username:
403
+ return self._make_linked_object("username", self.followed_username, user.User, exceptions.UserNotFound)
404
+
405
+ if self.recipient_username: # the recipient_username field always indicates the target is a user
406
+ return self._make_linked_object("username", self.recipient_username, user.User, exceptions.UserNotFound)
407
+
408
+ if _type == "addcomment": # target is a comment
409
+ if self.comment_type == 0:
410
+ # we need author name, but it has not been saved in this object
411
+ _proj = self._session.connect_project(self.comment_obj_id)
412
+ _c = _proj.comment_by_id(self.comment_id)
413
+
414
+ elif self.comment_type == 1:
415
+ _c = user.User(username=self.comment_obj_title, _session=self._session).comment_by_id(self.comment_id)
416
+ elif self.comment_type == 2:
417
+ _c = user.User(id=self.comment_obj_id, _session=self._session).comment_by_id(self.comment_id)
418
+ else:
419
+ raise ValueError(f"{self.comment_type} is an invalid comment type")
420
+
421
+ return _c
422
+
423
+ if _type == "forumpost":
424
+ return forum.ForumTopic(id=603418, _session=self._session, title=self.title)
425
+
426
+ return None
@@ -7,8 +7,7 @@ import pprint
7
7
  import warnings
8
8
  from dataclasses import dataclass, field, KW_ONLY
9
9
  from datetime import datetime
10
- from typing import TYPE_CHECKING, Any, Optional, Union
11
- from typing_extensions import Self
10
+ from typing_extensions import TYPE_CHECKING, Any, Optional, Union, Self
12
11
 
13
12
  from . import user, project, studio, comment, session
14
13
  from scratchattach.utils import enums
@@ -129,13 +128,13 @@ class EducatorAlert:
129
128
 
130
129
  if comment_type == 0:
131
130
  # project
132
- comment_source_type = "project"
131
+ comment_source_type = comment.CommentSource.PROJECT
133
132
  elif comment_type == 1:
134
133
  # profile
135
- comment_source_type = "profile"
134
+ comment_source_type = comment.CommentSource.USER_PROFILE
136
135
  else:
137
136
  # probably a studio
138
- comment_source_type = "studio"
137
+ comment_source_type = comment.CommentSource.STUDIO
139
138
  warnings.warn(
140
139
  f"The parser was not able to recognise the \"comment_type\" of {comment_type} in the alert JSON response.\n"
141
140
  f"Full response: \n{pprint.pformat(data)}.\n\n"
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  import time
5
5
  import logging
6
+ import warnings
6
7
 
7
8
  from ._base import BaseSiteComponent
8
9
  from scratchattach.utils import exceptions
@@ -39,7 +40,7 @@ class BackpackAsset(BaseSiteComponent):
39
40
  self.__dict__.update(entries)
40
41
 
41
42
  def update(self):
42
- print("Warning: BackpackAsset objects can't be updated")
43
+ warnings.warn("Warning: BackpackAsset objects can't be updated")
43
44
  return False # Objects of this type cannot be updated
44
45
 
45
46