satori-python 0.10.0__tar.gz → 0.11.0__tar.gz

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 (27) hide show
  1. {satori_python-0.10.0 → satori_python-0.11.0}/PKG-INFO +1 -1
  2. {satori_python-0.10.0 → satori_python-0.11.0}/pyproject.toml +9 -4
  3. {satori_python-0.10.0 → satori_python-0.11.0}/src/satori/__init__.py +1 -1
  4. satori_python-0.11.0/src/satori/client/__init__.py +218 -0
  5. {satori_python-0.10.0 → satori_python-0.11.0}/src/satori/client/account.pyi +3 -0
  6. {satori_python-0.10.0 → satori_python-0.11.0}/src/satori/client/network/base.py +2 -4
  7. {satori_python-0.10.0 → satori_python-0.11.0}/src/satori/client/network/websocket.py +5 -3
  8. {satori_python-0.10.0 → satori_python-0.11.0}/src/satori/const.py +2 -0
  9. {satori_python-0.10.0 → satori_python-0.11.0}/src/satori/element.py +8 -2
  10. satori_python-0.11.0/src/satori/event.py +63 -0
  11. {satori_python-0.10.0 → satori_python-0.11.0}/src/satori/model.py +63 -15
  12. {satori_python-0.10.0 → satori_python-0.11.0}/src/satori/server/__init__.py +175 -5
  13. {satori_python-0.10.0 → satori_python-0.11.0}/src/satori/server/adapter.py +7 -16
  14. satori_python-0.11.0/src/satori/server/model.py +36 -0
  15. satori_python-0.11.0/src/satori/server/route.py +240 -0
  16. satori_python-0.10.0/src/satori/client/__init__.py +0 -108
  17. satori_python-0.10.0/src/satori/server/model.py +0 -40
  18. {satori_python-0.10.0 → satori_python-0.11.0}/LICENSE +0 -0
  19. {satori_python-0.10.0 → satori_python-0.11.0}/README.md +0 -0
  20. {satori_python-0.10.0 → satori_python-0.11.0}/src/satori/client/account.py +0 -0
  21. {satori_python-0.10.0 → satori_python-0.11.0}/src/satori/client/network/__init__.py +0 -0
  22. {satori_python-0.10.0 → satori_python-0.11.0}/src/satori/client/network/webhook.py +0 -0
  23. {satori_python-0.10.0 → satori_python-0.11.0}/src/satori/client/session.py +0 -0
  24. {satori_python-0.10.0 → satori_python-0.11.0}/src/satori/config.py +0 -0
  25. {satori_python-0.10.0 → satori_python-0.11.0}/src/satori/exception.py +0 -0
  26. {satori_python-0.10.0 → satori_python-0.11.0}/src/satori/parser.py +0 -0
  27. {satori_python-0.10.0 → satori_python-0.11.0}/src/satori/server/conection.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: satori-python
3
- Version: 0.10.0
3
+ Version: 0.11.0
4
4
  Summary: Satori Protocol SDK for python
5
5
  Home-page: https://github.com/RF-Tar-Railt/satori-python
6
6
  Author-Email: RF-Tar-Railt <rf_tar_railt@qq.com>
@@ -28,7 +28,7 @@ classifiers = [
28
28
  "Programming Language :: Python :: 3.12",
29
29
  "Operating System :: OS Independent",
30
30
  ]
31
- version = "0.10.0"
31
+ version = "0.11.0"
32
32
 
33
33
  [project.license]
34
34
  text = "MIT"
@@ -63,7 +63,7 @@ includes = [
63
63
  composite = [
64
64
  "isort ./src/ ./example/",
65
65
  "black ./src/ ./example/",
66
- "ruff ./src/ ./example/",
66
+ "ruff check ./src/ ./example/",
67
67
  ]
68
68
 
69
69
  [tool.pdm.version]
@@ -84,6 +84,13 @@ extra_standard_library = [
84
84
  ]
85
85
 
86
86
  [tool.ruff]
87
+ line-length = 110
88
+ target-version = "py38"
89
+ exclude = [
90
+ "exam.py",
91
+ ]
92
+
93
+ [tool.ruff.lint]
87
94
  select = [
88
95
  "E",
89
96
  "W",
@@ -100,8 +107,6 @@ ignore = [
100
107
  "C901",
101
108
  "UP037",
102
109
  ]
103
- line-length = 110
104
- target-version = "py38"
105
110
 
106
111
  [tool.pyright]
107
112
  pythonPlatform = "All"
@@ -39,4 +39,4 @@ from .model import MessageObject as MessageObject
39
39
  from .model import Role as Role
40
40
  from .model import User as User
41
41
 
42
- __version__ = "0.10.0"
42
+ __version__ = "0.11.0"
@@ -0,0 +1,218 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import signal
5
+ from functools import wraps
6
+ from typing import Any, Awaitable, Callable, Iterable, Literal, TypeVar, overload
7
+
8
+ from creart import it
9
+ from launart import Launart, Service, any_completed
10
+ from loguru import logger
11
+
12
+ from satori import event
13
+ from satori.config import Config, WebhookInfo, WebsocketsInfo
14
+ from satori.const import EventType
15
+ from satori.model import Event, LoginStatus
16
+
17
+ from .account import Account as Account
18
+ from .account import ApiInfo as ApiInfo
19
+ from .network.base import BaseNetwork as BaseNetwork
20
+ from .network.webhook import WebhookNetwork
21
+ from .network.websocket import WsNetwork
22
+
23
+ TConfig = TypeVar("TConfig", bound=Config)
24
+ TE = TypeVar("TE", bound=Event, contravariant=True)
25
+
26
+ MAPPING: dict[type[Config], type[BaseNetwork]] = {
27
+ WebhookInfo: WebhookNetwork,
28
+ WebsocketsInfo: WsNetwork,
29
+ }
30
+
31
+
32
+ class App(Service):
33
+ id = "satori-python.client"
34
+ required: set[str] = set()
35
+ stages: set[str] = {"preparing", "blocking", "cleanup"}
36
+
37
+ accounts: dict[str, Account]
38
+ connections: list[BaseNetwork]
39
+ event_callbacks: list[Callable[[Account, Event], Awaitable[Any]]]
40
+ lifecycle_callbacks: list[Callable[[Account, LoginStatus], Awaitable[Any]]]
41
+
42
+ @classmethod
43
+ def register_config(cls, tc: type[TConfig], tn: type[BaseNetwork[TConfig]]):
44
+ MAPPING[tc] = tn
45
+
46
+ def __init__(self, *configs: Config):
47
+ self.accounts = {}
48
+ self.connections = []
49
+ self.event_callbacks = []
50
+ self.lifecycle_callbacks = []
51
+ super().__init__()
52
+ for config in configs:
53
+ self.apply(config)
54
+
55
+ def apply(self, config: Config):
56
+ try:
57
+ connection = MAPPING[config.__class__](self, config)
58
+ except KeyError:
59
+ raise TypeError(f"Unknown config type: {config}")
60
+ self.connections.append(connection)
61
+
62
+ def get_account(self, self_id: str) -> Account:
63
+ return self.accounts[self_id]
64
+
65
+ def register(self, callback: Callable[[Account, Event], Awaitable[Any]]):
66
+ self.event_callbacks.append(callback)
67
+
68
+ @overload
69
+ def register_on(self, event_type: Literal[EventType.FRIEND_REQUEST]) -> Callable[
70
+ [Callable[[Account, event.UserEvent], Awaitable[Any]]],
71
+ Callable[[Account, event.UserEvent], Awaitable[Any]],
72
+ ]: ...
73
+
74
+ @overload
75
+ def register_on(
76
+ self,
77
+ event_type: Literal[
78
+ EventType.GUILD_ADDED, EventType.GUILD_REMOVED, EventType.GUILD_REQUEST, EventType.GUILD_UPDATED
79
+ ],
80
+ ) -> Callable[
81
+ [Callable[[Account, event.GuildEvent], Awaitable[Any]]],
82
+ Callable[[Account, event.GuildEvent], Awaitable[Any]],
83
+ ]: ...
84
+
85
+ @overload
86
+ def register_on(
87
+ self,
88
+ event_type: Literal[
89
+ EventType.GUILD_MEMBER_ADDED,
90
+ EventType.GUILD_MEMBER_REMOVED,
91
+ EventType.GUILD_MEMBER_UPDATED,
92
+ EventType.GUILD_MEMBER_REQUEST,
93
+ ],
94
+ ) -> Callable[
95
+ [Callable[[Account, event.GuildMemberEvent], Awaitable[Any]]],
96
+ Callable[[Account, event.GuildMemberEvent], Awaitable[Any]],
97
+ ]: ...
98
+
99
+ @overload
100
+ def register_on(
101
+ self,
102
+ event_type: Literal[
103
+ EventType.GUILD_ROLE_CREATED, EventType.GUILD_ROLE_DELETED, EventType.GUILD_ROLE_UPDATED
104
+ ],
105
+ ) -> Callable[
106
+ [Callable[[Account, event.GuildRoleEvent], Awaitable[Any]]],
107
+ Callable[[Account, event.GuildRoleEvent], Awaitable[Any]],
108
+ ]: ...
109
+
110
+ @overload
111
+ def register_on(
112
+ self, event_type: Literal[EventType.LOGIN_ADDED, EventType.LOGIN_REMOVED, EventType.LOGIN_UPDATED]
113
+ ) -> Callable[
114
+ [Callable[[Account, event.LoginEvent], Awaitable[Any]]],
115
+ Callable[[Account, event.LoginEvent], Awaitable[Any]],
116
+ ]: ...
117
+
118
+ @overload
119
+ def register_on(
120
+ self,
121
+ event_type: Literal[EventType.MESSAGE_CREATED, EventType.MESSAGE_DELETED, EventType.MESSAGE_UPDATED],
122
+ ) -> Callable[
123
+ [Callable[[Account, event.MessageEvent], Awaitable[Any]]],
124
+ Callable[[Account, event.MessageEvent], Awaitable[Any]],
125
+ ]: ...
126
+
127
+ @overload
128
+ def register_on(
129
+ self, event_type: Literal[EventType.REACTION_ADDED, EventType.REACTION_REMOVED]
130
+ ) -> Callable[
131
+ [Callable[[Account, event.ReactionEvent], Awaitable[Any]]],
132
+ Callable[[Account, event.ReactionEvent], Awaitable[Any]],
133
+ ]: ...
134
+
135
+ @overload
136
+ def register_on(self, event_type: Literal[EventType.INTERACTION_BUTTON]) -> Callable[
137
+ [Callable[[Account, event.ButtonInteractionEvent], Awaitable[Any]]],
138
+ Callable[[Account, event.ButtonInteractionEvent], Awaitable[Any]],
139
+ ]: ...
140
+
141
+ @overload
142
+ def register_on(self, event_type: Literal[EventType.INTERACTION_COMMAND]) -> Callable[
143
+ [Callable[[Account, event.ArgvInteractionEvent | event.MessageEvent], Awaitable[Any]]],
144
+ Callable[[Account, event.ArgvInteractionEvent | event.MessageEvent], Awaitable[Any]],
145
+ ]: ...
146
+
147
+ @overload
148
+ def register_on(self, event_type: Literal[EventType.INTERNAL]) -> Callable[
149
+ [Callable[[Account, event.InternalEvent], Awaitable[Any]]],
150
+ Callable[[Account, event.InternalEvent], Awaitable[Any]],
151
+ ]: ...
152
+
153
+ def register_on(self, event_type: EventType):
154
+ def decorator(
155
+ func: Callable[[Account, Any], Awaitable[Any]], /
156
+ ) -> Callable[[Account, Any], Awaitable[Any]]:
157
+ @wraps(func)
158
+ async def wrapper(account: Account, event: Event) -> Any:
159
+ if event.type == event_type.value:
160
+ return await func(account, event)
161
+
162
+ self.register(wrapper)
163
+ return wrapper
164
+
165
+ return decorator
166
+
167
+ def lifecycle(self, callback: Callable[[Account, LoginStatus], Awaitable[Any]]):
168
+ self.lifecycle_callbacks.append(callback)
169
+
170
+ async def account_update(self, account: Account, state: LoginStatus):
171
+ if self.lifecycle_callbacks:
172
+ await asyncio.gather(*(callback(account, state) for callback in self.lifecycle_callbacks))
173
+
174
+ async def post(self, event: Event):
175
+ if not self.event_callbacks:
176
+ return
177
+ identity = f"{event.platform}/{event.self_id}"
178
+ if identity not in self.accounts:
179
+ logger.warning(f"Received event for unknown account: {event}")
180
+ return
181
+ account = self.accounts[identity]
182
+ await asyncio.gather(*(callback(account, event) for callback in self.event_callbacks))
183
+
184
+ async def launch(self, manager: Launart):
185
+ for conn in self.connections:
186
+ manager.add_component(conn)
187
+
188
+ async with self.stage("preparing"):
189
+ ...
190
+
191
+ async with self.stage("blocking"):
192
+ await any_completed(
193
+ manager.status.wait_for_sigexit(),
194
+ *(conn.status.wait_for("blocking-completed") for conn in self.connections),
195
+ )
196
+
197
+ async with self.stage("cleanup"):
198
+ for account in self.accounts.values():
199
+ await self.account_update(account, LoginStatus.OFFLINE)
200
+ self.accounts.clear()
201
+
202
+ def run(
203
+ self,
204
+ manager: Launart | None = None,
205
+ *,
206
+ loop: asyncio.AbstractEventLoop | None = None,
207
+ stop_signal: Iterable[signal.Signals] = (signal.SIGINT,),
208
+ ):
209
+ if manager is None:
210
+ manager = it(Launart)
211
+ manager.add_component(self)
212
+ manager.launch_blocking(loop=loop, stop_signal=stop_signal)
213
+
214
+ async def run_async(self, manager: Launart | None = None):
215
+ if manager is None:
216
+ manager = it(Launart)
217
+ manager.add_component(self)
218
+ await manager.launch()
@@ -55,6 +55,7 @@ class Account:
55
55
  message: 要发送的消息
56
56
  """
57
57
  ...
58
+
58
59
  async def send_private_message(
59
60
  self,
60
61
  user_id: str,
@@ -67,6 +68,7 @@ class Account:
67
68
  message: 要发送的消息
68
69
  """
69
70
  ...
71
+
70
72
  async def update_message(
71
73
  self,
72
74
  channel_id: str,
@@ -81,6 +83,7 @@ class Account:
81
83
  message: 要更新的消息
82
84
  """
83
85
  ...
86
+
84
87
  async def message_create(
85
88
  self,
86
89
  *,
@@ -27,12 +27,10 @@ class BaseNetwork(Generic[TConfig], Service):
27
27
  self.close_signal = asyncio.Event()
28
28
  self.sequence = -1
29
29
 
30
- async def wait_for_available(self):
31
- ...
30
+ async def wait_for_available(self): ...
32
31
 
33
32
  @property
34
- def alive(self) -> bool:
35
- ...
33
+ def alive(self) -> bool: ...
36
34
 
37
35
  async def connection_closed(self):
38
36
  self.close_signal.set()
@@ -101,9 +101,11 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
101
101
  else:
102
102
  account = Account(platform, self_id, self.config)
103
103
  logger.info(f"account registered: {account}")
104
- account.connected.set() if login[
105
- "status"
106
- ] == LoginStatus.ONLINE else account.connected.clear()
104
+ (
105
+ account.connected.set()
106
+ if login["status"] == LoginStatus.ONLINE
107
+ else account.connected.clear()
108
+ )
107
109
  self.app.accounts[identity] = account
108
110
  self.accounts[identity] = account
109
111
  await self.app.account_update(account, LoginStatus.ONLINE)
@@ -13,6 +13,7 @@ class Api(str, Enum):
13
13
  CHANNEL_CREATE = "channel.create"
14
14
  CHANNEL_UPDATE = "channel.update"
15
15
  CHANNEL_DELETE = "channel.delete"
16
+ CHANNEL_MUTE = "channel.mute"
16
17
  USER_CHANNEL_CREATE = "user.channel.create"
17
18
 
18
19
  GUILD_GET = "guild.get"
@@ -22,6 +23,7 @@ class Api(str, Enum):
22
23
  GUILD_MEMBER_LIST = "guild.member.list"
23
24
  GUILD_MEMBER_GET = "guild.member.get"
24
25
  GUILD_MEMBER_KICK = "guild.member.kick"
26
+ GUILD_MEMBER_MUTE = "guild.member.mute"
25
27
  GUILD_MEMBER_APPROVE = "guild.member.approve"
26
28
  GUILD_MEMBER_ROLE_SET = "guild.member.role.set"
27
29
  GUILD_MEMBER_ROLE_UNSET = "guild.member.role.unset"
@@ -24,6 +24,13 @@ class Element:
24
24
  for f in fields(self):
25
25
  if f.name in ("_attrs", "_children"):
26
26
  continue
27
+ if f.type is not str and isinstance(attr := getattr(self, f.name), str):
28
+ if f.type is bool:
29
+ if attr.lower() not in ("true", "false"):
30
+ raise TypeError(f.name, attr)
31
+ setattr(self, f.name, attr.lower() == "true")
32
+ else:
33
+ setattr(self, f.name, f.type(attr))
27
34
  self._attrs[f.name] = getattr(self, f.name)
28
35
  self._attrs = {k: v for k, v in self._attrs.items() if v is not None}
29
36
 
@@ -59,8 +66,7 @@ class Element:
59
66
  self.__post_call__()
60
67
  return self
61
68
 
62
- def __post_call__(self):
63
- ...
69
+ def __post_call__(self): ...
64
70
 
65
71
 
66
72
  @dataclass
@@ -0,0 +1,63 @@
1
+ from satori.model import (
2
+ ArgvInteraction,
3
+ ButtonInteraction,
4
+ Channel,
5
+ Event,
6
+ Guild,
7
+ Login,
8
+ Member,
9
+ MessageObject,
10
+ Role,
11
+ User,
12
+ )
13
+
14
+
15
+ class MessageEvent(Event):
16
+ channel: Channel
17
+ member: Member
18
+ message: MessageObject
19
+ user: User
20
+
21
+
22
+ class UserEvent(Event):
23
+ user: User
24
+
25
+
26
+ class GuildEvent(Event):
27
+ guild: Guild
28
+
29
+
30
+ class GuildMemberEvent(GuildEvent):
31
+ user: User
32
+ member: Member
33
+
34
+
35
+ class GuildRoleEvent(GuildEvent):
36
+ role: Role
37
+
38
+
39
+ class LoginEvent(Event):
40
+ login: Login
41
+
42
+
43
+ class ReactionEvent(Event):
44
+ channel: Channel
45
+ user: User
46
+ message: MessageObject
47
+
48
+
49
+ class ButtonInteractionEvent(Event):
50
+ button: ButtonInteraction
51
+ user: User
52
+ channel: Channel
53
+
54
+
55
+ class ArgvInteractionEvent(Event):
56
+ argv: ArgvInteraction
57
+ user: User
58
+ channel: Channel
59
+
60
+
61
+ class InternalEvent(Event):
62
+ _type: str
63
+ _data: dict
@@ -7,6 +7,15 @@ from .element import Element, transform
7
7
  from .parser import parse
8
8
 
9
9
 
10
+ class ModelBase:
11
+ @classmethod
12
+ def parse(cls, raw: dict):
13
+ raise NotImplementedError
14
+
15
+ def dump(self) -> dict:
16
+ raise NotImplementedError
17
+
18
+
10
19
  class ChannelType(IntEnum):
11
20
  TEXT = 0
12
21
  DIRECT = 1
@@ -15,7 +24,7 @@ class ChannelType(IntEnum):
15
24
 
16
25
 
17
26
  @dataclass
18
- class Channel:
27
+ class Channel(ModelBase):
19
28
  id: str
20
29
  type: ChannelType
21
30
  name: Optional[str] = None
@@ -37,7 +46,7 @@ class Channel:
37
46
 
38
47
 
39
48
  @dataclass
40
- class Guild:
49
+ class Guild(ModelBase):
41
50
  id: str
42
51
  name: Optional[str] = None
43
52
  avatar: Optional[str] = None
@@ -56,7 +65,7 @@ class Guild:
56
65
 
57
66
 
58
67
  @dataclass
59
- class User:
68
+ class User(ModelBase):
60
69
  id: str
61
70
  name: Optional[str] = None
62
71
  nick: Optional[str] = None
@@ -81,7 +90,7 @@ class User:
81
90
 
82
91
 
83
92
  @dataclass
84
- class Member:
93
+ class Member(ModelBase):
85
94
  user: Optional[User] = None
86
95
  nick: Optional[str] = None
87
96
  name: Optional[str] = None
@@ -111,7 +120,7 @@ class Member:
111
120
 
112
121
 
113
122
  @dataclass
114
- class Role:
123
+ class Role(ModelBase):
115
124
  id: str
116
125
  name: Optional[str] = None
117
126
 
@@ -135,7 +144,7 @@ class LoginStatus(IntEnum):
135
144
 
136
145
 
137
146
  @dataclass
138
- class Login:
147
+ class Login(ModelBase):
139
148
  status: LoginStatus
140
149
  user: Optional[User] = None
141
150
  self_id: Optional[str] = None
@@ -161,19 +170,27 @@ class Login:
161
170
 
162
171
 
163
172
  @dataclass
164
- class ArgvInteraction:
173
+ class ArgvInteraction(ModelBase):
165
174
  name: str
166
175
  arguments: list
167
176
  options: Any
168
177
 
178
+ @classmethod
179
+ def parse(cls, raw: dict):
180
+ return cls(**raw)
181
+
169
182
  def dump(self):
170
183
  return asdict(self)
171
184
 
172
185
 
173
186
  @dataclass
174
- class ButtonInteraction:
187
+ class ButtonInteraction(ModelBase):
175
188
  id: str
176
189
 
190
+ @classmethod
191
+ def parse(cls, raw: dict):
192
+ return cls(**raw)
193
+
177
194
  def dump(self):
178
195
  return asdict(self)
179
196
 
@@ -187,18 +204,18 @@ class Opcode(IntEnum):
187
204
 
188
205
 
189
206
  @dataclass
190
- class Identify:
207
+ class Identify(ModelBase):
191
208
  token: Optional[str] = None
192
209
  sequence: Optional[int] = None
193
210
 
194
211
 
195
212
  @dataclass
196
- class Ready:
213
+ class Ready(ModelBase):
197
214
  logins: List[Login]
198
215
 
199
216
 
200
217
  @dataclass
201
- class MessageObject:
218
+ class MessageObject(ModelBase):
202
219
  id: str
203
220
  content: str
204
221
  channel: Optional[Channel] = None
@@ -208,6 +225,20 @@ class MessageObject:
208
225
  created_at: Optional[datetime] = None
209
226
  updated_at: Optional[datetime] = None
210
227
 
228
+ @classmethod
229
+ def from_elements(
230
+ cls,
231
+ id: str,
232
+ content: List[Element],
233
+ channel: Optional[Channel] = None,
234
+ guild: Optional[Guild] = None,
235
+ member: Optional[Member] = None,
236
+ user: Optional[User] = None,
237
+ created_at: Optional[datetime] = None,
238
+ updated_at: Optional[datetime] = None,
239
+ ):
240
+ return cls(id, "".join(str(i) for i in content), channel, guild, member, user, created_at, updated_at)
241
+
211
242
  @property
212
243
  def message(self) -> List[Element]:
213
244
  return transform(parse(self.content))
@@ -267,6 +298,9 @@ class Event:
267
298
  role: Optional[Role] = None
268
299
  user: Optional[User] = None
269
300
 
301
+ _type: Optional[str] = None
302
+ _data: Optional[dict] = None
303
+
270
304
  @classmethod
271
305
  def parse(cls, raw: dict):
272
306
  data = {
@@ -277,9 +311,9 @@ class Event:
277
311
  "timestamp": datetime.fromtimestamp(int(raw["timestamp"]) / 1000),
278
312
  }
279
313
  if "argv" in raw:
280
- data["argv"] = ArgvInteraction(**raw["argv"])
314
+ data["argv"] = ArgvInteraction.parse(raw["argv"])
281
315
  if "button" in raw:
282
- data["button"] = ButtonInteraction(**raw["button"])
316
+ data["button"] = ButtonInteraction.parse(raw["button"])
283
317
  if "channel" in raw:
284
318
  data["channel"] = Channel.parse(raw["channel"])
285
319
  if "guild" in raw:
@@ -296,6 +330,10 @@ class Event:
296
330
  data["role"] = Role.parse(raw["role"])
297
331
  if "user" in raw:
298
332
  data["user"] = User.parse(raw["user"])
333
+ if "_type" in raw:
334
+ data["_type"] = raw["_type"]
335
+ if "_data" in raw:
336
+ data["_data"] = raw["_data"]
299
337
  return cls(**data)
300
338
 
301
339
  def dump(self):
@@ -326,14 +364,18 @@ class Event:
326
364
  res["role"] = self.role.dump()
327
365
  if self.user:
328
366
  res["user"] = self.user.dump()
367
+ if self._type:
368
+ res["_type"] = self._type
369
+ if self._data:
370
+ res["_data"] = self._data
329
371
  return res
330
372
 
331
373
 
332
- T = TypeVar("T")
374
+ T = TypeVar("T", bound=ModelBase)
333
375
 
334
376
 
335
377
  @dataclass
336
- class PageResult(Generic[T]):
378
+ class PageResult(ModelBase, Generic[T]):
337
379
  data: List[T]
338
380
  next: Optional[str] = None
339
381
 
@@ -341,3 +383,9 @@ class PageResult(Generic[T]):
341
383
  def parse(cls, raw: dict, parser: Callable[[dict], T]) -> "PageResult[T]":
342
384
  data = [parser(item) for item in raw["data"]]
343
385
  return cls(data, raw.get("next"))
386
+
387
+ def dump(self):
388
+ res: dict = {"data": [item.dump() for item in self.data]}
389
+ if self.next:
390
+ res["next"] = self.next
391
+ return res