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.
- cli/__about__.py +1 -0
- cli/__init__.py +26 -0
- cli/cmd/__init__.py +4 -0
- cli/cmd/group.py +127 -0
- cli/cmd/login.py +60 -0
- cli/cmd/profile.py +7 -0
- cli/cmd/sessions.py +5 -0
- cli/context.py +142 -0
- cli/db.py +66 -0
- cli/namespace.py +14 -0
- {scratchattach/cloud → cloud}/_base.py +112 -87
- {scratchattach/cloud → cloud}/cloud.py +16 -16
- {scratchattach/editor → editor}/__init__.py +2 -1
- {scratchattach/editor → editor}/asset.py +26 -14
- {scratchattach/editor → editor}/backpack_json.py +3 -5
- {scratchattach/editor → editor}/base.py +2 -4
- {scratchattach/editor → editor}/block.py +27 -22
- {scratchattach/editor → editor}/blockshape.py +1 -1
- {scratchattach/editor → editor}/build_defaulting.py +2 -2
- editor/commons.py +145 -0
- {scratchattach/editor → editor}/field.py +1 -1
- {scratchattach/editor → editor}/inputs.py +6 -3
- {scratchattach/editor → editor}/meta.py +10 -7
- {scratchattach/editor → editor}/monitor.py +10 -8
- {scratchattach/editor → editor}/mutation.py +68 -11
- {scratchattach/editor → editor}/pallete.py +1 -3
- {scratchattach/editor → editor}/prim.py +4 -0
- {scratchattach/editor → editor}/project.py +118 -16
- {scratchattach/editor → editor}/sprite.py +25 -15
- {scratchattach/editor → editor}/vlb.py +2 -2
- {scratchattach/eventhandlers → eventhandlers}/_base.py +1 -0
- {scratchattach/eventhandlers → eventhandlers}/cloud_events.py +26 -6
- {scratchattach/eventhandlers → eventhandlers}/cloud_recorder.py +4 -4
- {scratchattach/eventhandlers → eventhandlers}/cloud_requests.py +139 -54
- {scratchattach/eventhandlers → eventhandlers}/cloud_server.py +6 -3
- {scratchattach/eventhandlers → eventhandlers}/cloud_storage.py +1 -2
- eventhandlers/filterbot.py +163 -0
- other/other_apis.py +598 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/METADATA +7 -11
- scratchattach-3.0.0b1.dist-info/RECORD +79 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/WHEEL +1 -1
- scratchattach-3.0.0b1.dist-info/entry_points.txt +2 -0
- scratchattach-3.0.0b1.dist-info/top_level.txt +7 -0
- {scratchattach/site → site}/_base.py +32 -5
- site/activity.py +426 -0
- {scratchattach/site → site}/alert.py +4 -5
- {scratchattach/site → site}/backpack_asset.py +2 -1
- {scratchattach/site → site}/classroom.py +80 -73
- {scratchattach/site → site}/cloud_activity.py +43 -29
- {scratchattach/site → site}/comment.py +86 -100
- {scratchattach/site → site}/forum.py +8 -4
- site/placeholder.py +132 -0
- {scratchattach/site → site}/project.py +228 -122
- {scratchattach/site → site}/session.py +156 -71
- {scratchattach/site → site}/studio.py +139 -46
- site/typed_dicts.py +151 -0
- {scratchattach/site → site}/user.py +511 -215
- {scratchattach/utils → utils}/commons.py +12 -4
- {scratchattach/utils → utils}/encoder.py +7 -4
- {scratchattach/utils → utils}/enums.py +1 -0
- {scratchattach/utils → utils}/exceptions.py +36 -2
- utils/optional_async.py +154 -0
- utils/requests.py +306 -0
- scratchattach/__init__.py +0 -29
- scratchattach/editor/commons.py +0 -273
- scratchattach/eventhandlers/filterbot.py +0 -161
- scratchattach/other/other_apis.py +0 -284
- scratchattach/site/activity.py +0 -382
- scratchattach/utils/requests.py +0 -93
- scratchattach-2.1.15b0.dist-info/RECORD +0 -66
- scratchattach-2.1.15b0.dist-info/top_level.txt +0 -1
- {scratchattach/cloud → cloud}/__init__.py +0 -0
- {scratchattach/editor → editor}/code_translation/__init__.py +0 -0
- {scratchattach/editor → editor}/code_translation/parse.py +0 -0
- {scratchattach/editor → editor}/comment.py +0 -0
- {scratchattach/editor → editor}/extension.py +0 -0
- {scratchattach/editor → editor}/twconfig.py +0 -0
- {scratchattach/eventhandlers → eventhandlers}/__init__.py +0 -0
- {scratchattach/eventhandlers → eventhandlers}/combine.py +0 -0
- {scratchattach/eventhandlers → eventhandlers}/message_events.py +0 -0
- {scratchattach/other → other}/__init__.py +0 -0
- {scratchattach/other → other}/project_json_capabilities.py +0 -0
- {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
- {scratchattach/site → site}/__init__.py +0 -0
- {scratchattach/site → site}/browser_cookie3_stub.py +0 -0
- {scratchattach/site → site}/browser_cookies.py +0 -0
- {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,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
|
-
|
|
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 ==
|
|
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
|
|
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 =
|
|
131
|
+
comment_source_type = comment.CommentSource.PROJECT
|
|
133
132
|
elif comment_type == 1:
|
|
134
133
|
# profile
|
|
135
|
-
comment_source_type =
|
|
134
|
+
comment_source_type = comment.CommentSource.USER_PROFILE
|
|
136
135
|
else:
|
|
137
136
|
# probably a studio
|
|
138
|
-
comment_source_type =
|
|
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
|
-
|
|
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
|
|