satori-python 0.13.3__tar.gz → 0.14.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 (29) hide show
  1. {satori_python-0.13.3 → satori_python-0.14.0}/PKG-INFO +1 -1
  2. {satori_python-0.13.3 → satori_python-0.14.0}/pyproject.toml +3 -3
  3. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/__init__.py +1 -1
  4. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/__init__.py +2 -1
  5. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/account.pyi +2 -1
  6. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/network/websocket.py +2 -3
  7. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/protocol.py +2 -1
  8. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/element.py +20 -20
  9. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/model.py +11 -11
  10. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/parser.py +20 -19
  11. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/server/__init__.py +16 -3
  12. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/server/adapter.py +5 -4
  13. satori_python-0.14.0/src/satori/server/conection.py +44 -0
  14. satori_python-0.14.0/src/satori/server/deque.py +33 -0
  15. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/server/model.py +4 -5
  16. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/server/route.py +22 -21
  17. satori_python-0.13.3/src/satori/server/conection.py +0 -40
  18. {satori_python-0.13.3 → satori_python-0.14.0}/LICENSE +0 -0
  19. {satori_python-0.13.3 → satori_python-0.14.0}/README.md +0 -0
  20. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/account.py +0 -0
  21. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/network/__init__.py +0 -0
  22. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/network/base.py +0 -0
  23. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/network/util.py +0 -0
  24. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/client/network/webhook.py +0 -0
  25. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/config.py +0 -0
  26. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/const.py +0 -0
  27. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/event.py +0 -0
  28. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/exception.py +0 -0
  29. {satori_python-0.13.3 → satori_python-0.14.0}/src/satori/server/formdata.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: satori-python
3
- Version: 0.13.3
3
+ Version: 0.14.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>
@@ -29,7 +29,7 @@ classifiers = [
29
29
  "Programming Language :: Python :: 3.12",
30
30
  "Operating System :: OS Independent",
31
31
  ]
32
- version = "0.13.3"
32
+ version = "0.14.0"
33
33
 
34
34
  [project.license]
35
35
  text = "MIT"
@@ -86,7 +86,7 @@ extra_standard_library = [
86
86
 
87
87
  [tool.ruff]
88
88
  line-length = 110
89
- target-version = "py38"
89
+ target-version = "py39"
90
90
  exclude = [
91
91
  "exam.py",
92
92
  ]
@@ -111,7 +111,7 @@ ignore = [
111
111
 
112
112
  [tool.pyright]
113
113
  pythonPlatform = "All"
114
- pythonVersion = "3.8"
114
+ pythonVersion = "3.9"
115
115
  typeCheckingMode = "basic"
116
116
  reportShadowedImports = false
117
117
  disableBytesTypePromotions = true
@@ -43,4 +43,4 @@ from .model import Role as Role
43
43
  from .model import Upload as Upload
44
44
  from .model import User as User
45
45
 
46
- __version__ = "0.13.3"
46
+ __version__ = "0.14.0"
@@ -4,8 +4,9 @@ import asyncio
4
4
  import functools
5
5
  import signal
6
6
  import threading
7
+ from collections.abc import Awaitable, Iterable
7
8
  from functools import wraps
8
- from typing import TYPE_CHECKING, Any, Awaitable, Callable, Iterable, Literal, TypeVar, overload
9
+ from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, overload
9
10
 
10
11
  from creart import it
11
12
  from graia.amnesia.builtins.aiohttp import AiohttpClientService
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
- from typing import Any, Generic, Iterable, Protocol, TypeVar, overload
2
+ from collections.abc import Iterable
3
+ from typing import Any, Generic, Protocol, TypeVar, overload
3
4
 
4
5
  from yarl import URL
5
6
 
@@ -119,9 +119,8 @@ class WsNetwork(BaseNetwork[WebsocketsInfo]):
119
119
  async def _heartbeat(self):
120
120
  """心跳"""
121
121
  while True:
122
- if self.sequence:
123
- with suppress(Exception):
124
- await self.send({"op": 1})
122
+ with suppress(Exception):
123
+ await self.send({"op": 1})
125
124
  await asyncio.sleep(9)
126
125
 
127
126
  async def daemon(self, manager: Launart, session: aiohttp.ClientSession):
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any, Iterable, cast, overload
3
+ from collections.abc import Iterable
4
+ from typing import TYPE_CHECKING, Any, cast, overload
4
5
 
5
6
  from aiohttp import FormData
6
7
  from graia.amnesia.builtins.aiohttp import AiohttpClientService
@@ -2,7 +2,7 @@ from base64 import b64encode
2
2
  from dataclasses import InitVar, dataclass, field, fields
3
3
  from io import BytesIO
4
4
  from pathlib import Path
5
- from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, TypeVar, Union, get_args, overload
5
+ from typing import Any, ClassVar, Optional, TypeVar, Union, get_args, overload
6
6
  from typing_extensions import override
7
7
 
8
8
  from .parser import Element as RawElement
@@ -14,13 +14,13 @@ TE = TypeVar("TE", bound="Element")
14
14
 
15
15
  @dataclass(repr=False)
16
16
  class Element:
17
- _attrs: Dict[str, Any] = field(init=False, default_factory=dict)
18
- _children: List["Element"] = field(init=False, default_factory=list)
17
+ _attrs: dict[str, Any] = field(init=False, default_factory=dict)
18
+ _children: list["Element"] = field(init=False, default_factory=list)
19
19
 
20
- __names__: ClassVar[Tuple[str, ...]]
20
+ __names__: ClassVar[tuple[str, ...]]
21
21
 
22
22
  @property
23
- def children(self) -> List["Element"]:
23
+ def children(self) -> list["Element"]:
24
24
  return self._children
25
25
 
26
26
  @property
@@ -28,7 +28,7 @@ class Element:
28
28
  return self.__class__.__name__.lower()
29
29
 
30
30
  @classmethod
31
- def unpack(cls, attrs: Dict[str, Any]):
31
+ def unpack(cls, attrs: dict[str, Any]):
32
32
  obj = cls(**{k: v for k, v in attrs.items() if k in cls.__names__}) # type: ignore
33
33
  obj._attrs.update({k: v for k, v in attrs.items() if k not in cls.__names__})
34
34
  return obj
@@ -166,7 +166,7 @@ class Link(Element):
166
166
  class Resource(Element):
167
167
  src: str
168
168
  title: Optional[str] = None
169
- extra: InitVar[Optional[Dict[str, Any]]] = None
169
+ extra: InitVar[Optional[dict[str, Any]]] = None
170
170
  cache: Optional[bool] = None
171
171
  timeout: Optional[int] = None
172
172
 
@@ -181,19 +181,19 @@ class Resource(Element):
181
181
  mime: Optional[str] = None,
182
182
  name: Optional[str] = None,
183
183
  poster: Optional[str] = None,
184
- extra: Optional[Dict[str, Any]] = None,
184
+ extra: Optional[dict[str, Any]] = None,
185
185
  cache: Optional[bool] = None,
186
186
  timeout: Optional[int] = None,
187
187
  **kwargs,
188
188
  ):
189
- data: Dict[str, Any] = {"extra": extra}
189
+ data: dict[str, Any] = {"extra": extra or kwargs}
190
190
  if url is not None:
191
- data = {"src": url}
191
+ data |= {"src": url}
192
192
  elif path:
193
- data = {"src": Path(path).as_uri()}
193
+ data |= {"src": Path(path).as_uri()}
194
194
  elif raw and mime:
195
195
  bd = raw.getvalue() if isinstance(raw, BytesIO) else raw
196
- data = {"src": f"data:{mime};base64,{b64encode(bd).decode('utf-8')}"}
196
+ data |= {"src": f"data:{mime};base64,{b64encode(bd).decode('utf-8')}"}
197
197
  else:
198
198
  raise ValueError(f"{cls} need at least one of url, path and raw")
199
199
  if name is not None:
@@ -206,7 +206,7 @@ class Resource(Element):
206
206
  data["timeout"] = timeout
207
207
  return cls(**data)
208
208
 
209
- def __post_init__(self, extra: Optional[Dict[str, Any]] = None):
209
+ def __post_init__(self, extra: Optional[dict[str, Any]] = None):
210
210
  super().__post_init__()
211
211
  if extra:
212
212
  self._attrs.update(extra)
@@ -380,7 +380,7 @@ class Message(Element):
380
380
  self,
381
381
  id: Optional[str] = None,
382
382
  forward: Optional[bool] = None,
383
- content: Optional[List[Union[str, Element]]] = None,
383
+ content: Optional[list[Union[str, Element]]] = None,
384
384
  ):
385
385
  self.id = id
386
386
  self.forward = forward
@@ -464,8 +464,8 @@ class Custom(Element):
464
464
  def __init__(
465
465
  self,
466
466
  type: str,
467
- attrs: Optional[Dict[str, Any]] = None,
468
- children: Optional[List[Union[str, Element]]] = None,
467
+ attrs: Optional[dict[str, Any]] = None,
468
+ children: Optional[list[Union[str, Element]]] = None,
469
469
  ):
470
470
  self.type = type
471
471
  super().__init__()
@@ -536,7 +536,7 @@ STYLE_TYPE_MAP = {
536
536
  }
537
537
 
538
538
 
539
- def transform(elements: List[RawElement]) -> List[Element]:
539
+ def transform(elements: list[RawElement]) -> list[Element]:
540
540
  msg = []
541
541
  for elem in elements:
542
542
  tag = elem.tag()
@@ -568,14 +568,14 @@ def transform(elements: List[RawElement]) -> List[Element]:
568
568
 
569
569
 
570
570
  @overload
571
- def select(elements: Union[Element, List[Element]], query: Type[TE]) -> List[TE]: ...
571
+ def select(elements: Union[Element, list[Element]], query: type[TE]) -> list[TE]: ...
572
572
 
573
573
 
574
574
  @overload
575
- def select(elements: Union[Element, List[Element]], query: str) -> List[Element]: ...
575
+ def select(elements: Union[Element, list[Element]], query: str) -> list[Element]: ...
576
576
 
577
577
 
578
- def select(elements: Union[Element, List[Element]], query: Union[Type[TE], str]):
578
+ def select(elements: Union[Element, list[Element]], query: Union[type[TE], str]):
579
579
  if not elements:
580
580
  return []
581
581
  if isinstance(elements, Element):
@@ -4,7 +4,7 @@ from datetime import datetime
4
4
  from enum import IntEnum
5
5
  from os import PathLike
6
6
  from pathlib import Path
7
- from typing import IO, Any, Callable, ClassVar, Dict, Generic, List, Literal, Optional, TypeVar, Union
7
+ from typing import IO, Any, Callable, ClassVar, Generic, Literal, Optional, TypeVar, Union
8
8
  from typing_extensions import TypeAlias
9
9
 
10
10
  from .element import Element, transform
@@ -14,7 +14,7 @@ from .parser import parse
14
14
 
15
15
  @dataclass
16
16
  class ModelBase:
17
- __converter__: ClassVar[Dict[str, Callable[[Any], Any]]] = {}
17
+ __converter__: ClassVar[dict[str, Callable[[Any], Any]]] = {}
18
18
 
19
19
  @classmethod
20
20
  def parse(cls, raw: dict):
@@ -81,7 +81,7 @@ class User(ModelBase):
81
81
  is_bot: Optional[bool] = None
82
82
 
83
83
  def dump(self):
84
- res: Dict[str, Any] = {"id": self.id}
84
+ res: dict[str, Any] = {"id": self.id}
85
85
  if self.name:
86
86
  res["name"] = self.name
87
87
  if self.nick:
@@ -141,13 +141,13 @@ class Login(ModelBase):
141
141
  user: Optional[User] = None
142
142
  self_id: Optional[str] = None
143
143
  platform: Optional[str] = None
144
- features: List[str] = field(default_factory=list)
145
- proxy_urls: List[str] = field(default_factory=list)
144
+ features: list[str] = field(default_factory=list)
145
+ proxy_urls: list[str] = field(default_factory=list)
146
146
 
147
147
  __converter__ = {"user": User.parse, "status": LoginStatus}
148
148
 
149
149
  def dump(self):
150
- res: Dict[str, Any] = {
150
+ res: dict[str, Any] = {
151
151
  "status": self.status.value,
152
152
  "features": self.features,
153
153
  "proxy_urls": self.proxy_urls,
@@ -195,7 +195,7 @@ class Identify(ModelBase):
195
195
 
196
196
  @dataclass
197
197
  class Ready(ModelBase):
198
- logins: List[Login]
198
+ logins: list[Login]
199
199
 
200
200
 
201
201
  @dataclass
@@ -213,7 +213,7 @@ class MessageObject(ModelBase):
213
213
  def from_elements(
214
214
  cls,
215
215
  id: str,
216
- content: List[Element],
216
+ content: list[Element],
217
217
  channel: Optional[Channel] = None,
218
218
  guild: Optional[Guild] = None,
219
219
  member: Optional[Member] = None,
@@ -224,7 +224,7 @@ class MessageObject(ModelBase):
224
224
  return cls(id, "".join(str(i) for i in content), channel, guild, member, user, created_at, updated_at)
225
225
 
226
226
  @property
227
- def message(self) -> List[Element]:
227
+ def message(self) -> list[Element]:
228
228
  return transform(parse(self.content))
229
229
 
230
230
  @classmethod
@@ -244,7 +244,7 @@ class MessageObject(ModelBase):
244
244
  }
245
245
 
246
246
  def dump(self):
247
- res: Dict[str, Any] = {"id": self.id, "content": self.content}
247
+ res: dict[str, Any] = {"id": self.id, "content": self.content}
248
248
  if self.channel:
249
249
  res["channel"] = self.channel.dump()
250
250
  if self.guild:
@@ -335,7 +335,7 @@ T = TypeVar("T", bound=ModelBase)
335
335
 
336
336
  @dataclass
337
337
  class PageResult(ModelBase, Generic[T]):
338
- data: List[T]
338
+ data: list[T]
339
339
  next: Optional[str] = None
340
340
 
341
341
  @classmethod
@@ -1,7 +1,8 @@
1
1
  import re
2
+ from collections.abc import Iterable
2
3
  from dataclasses import dataclass, field
3
4
  from enum import IntEnum
4
- from typing import Any, Callable, Dict, Iterable, List, Literal, Optional, TypedDict, TypeVar, Union, cast
5
+ from typing import Any, Callable, Literal, Optional, TypedDict, TypeVar, Union, cast
5
6
  from typing_extensions import TypeAlias
6
7
 
7
8
  T = TypeVar("T")
@@ -39,13 +40,13 @@ def snake_case(source: str) -> str:
39
40
  )
40
41
 
41
42
 
42
- def ensure_list(value: Union[T, List[T], None]) -> List[T]:
43
+ def ensure_list(value: Union[T, list[T], None]) -> list[T]:
43
44
  return value if isinstance(value, list) else [value] if value else []
44
45
 
45
46
 
46
47
  S = TypeVar("S")
47
- Fragment: TypeAlias = Union[str, "Element", List[Union[str, "Element"]]]
48
- Render: TypeAlias = Callable[[dict, List["Element"], S], T]
48
+ Fragment: TypeAlias = Union[str, "Element", list[Union[str, "Element"]]]
49
+ Render: TypeAlias = Callable[[dict, list["Element"], S], T]
49
50
  Visitor: TypeAlias = Callable[["Element", S], T]
50
51
 
51
52
 
@@ -60,7 +61,7 @@ def make_element(content: Union[str, bool, int, float, "Element"]) -> Optional["
60
61
  raise ValueError(f"Invalid content: {content!r}")
61
62
 
62
63
 
63
- def make_elements(content: Fragment) -> List["Element"]:
64
+ def make_elements(content: Fragment) -> list["Element"]:
64
65
  if isinstance(content, list):
65
66
  res = [make_element(c) for c in content]
66
67
  else:
@@ -70,14 +71,14 @@ def make_elements(content: Fragment) -> List["Element"]:
70
71
 
71
72
  class Element:
72
73
  type: str
73
- attrs: Dict[str, Any]
74
- children: List["Element"]
74
+ attrs: dict[str, Any]
75
+ children: list["Element"]
75
76
  source: Optional[str] = None
76
77
 
77
78
  def __init__(
78
79
  self,
79
80
  type: Union[str, Render[Fragment, Any]],
80
- attrs: Optional[Dict[str, Any]] = None,
81
+ attrs: Optional[dict[str, Any]] = None,
81
82
  *children: Fragment,
82
83
  ) -> None:
83
84
  self.attrs = {}
@@ -154,8 +155,8 @@ class Selector:
154
155
  comb_pat = re.compile(" *([ >+~]) *")
155
156
 
156
157
 
157
- def parse_selector(input: str) -> List[List[Selector]]:
158
- def _quert(query: str) -> List[Selector]:
158
+ def parse_selector(input: str) -> list[list[Selector]]:
159
+ def _quert(query: str) -> list[Selector]:
159
160
  selectors = []
160
161
  combinator = " "
161
162
  while mat := comb_pat.search(query):
@@ -173,7 +174,7 @@ def parse_selector(input: str) -> List[List[Selector]]:
173
174
  return [_quert(q) for q in input.split(",")]
174
175
 
175
176
 
176
- def select(source: Union[str, List[Element]], query: Union[str, List[List[Selector]]]) -> List[Element]:
177
+ def select(source: Union[str, list[Element]], query: Union[str, list[list[Selector]]]) -> list[Element]:
177
178
  if not source or not query:
178
179
  return []
179
180
  if isinstance(source, str):
@@ -182,10 +183,10 @@ def select(source: Union[str, List[Element]], query: Union[str, List[List[Select
182
183
  query = parse_selector(query)
183
184
  if not query:
184
185
  return []
185
- adjacent: List[List[Selector]] = []
186
+ adjacent: list[list[Selector]] = []
186
187
  results = []
187
188
  for index, elem in enumerate(source):
188
- inner: List[List[Selector]] = []
189
+ inner: list[list[Selector]] = []
189
190
  local = [*query, *adjacent]
190
191
  adjacent = []
191
192
  matched = False
@@ -255,7 +256,7 @@ class Token:
255
256
  positon: Position
256
257
  source: str
257
258
  extra: str
258
- children: Dict[str, List[Union[str, "Token"]]] = field(default_factory=dict)
259
+ children: dict[str, list[Union[str, "Token"]]] = field(default_factory=dict)
259
260
 
260
261
 
261
262
  class StackItem(TypedDict):
@@ -263,8 +264,8 @@ class StackItem(TypedDict):
263
264
  slot: str
264
265
 
265
266
 
266
- def fold_tokens(tokens: List[Union[str, Token]]) -> List[Union[str, Token]]:
267
- stack: List[StackItem] = [
267
+ def fold_tokens(tokens: list[Union[str, Token]]) -> list[Union[str, Token]]:
268
+ stack: list[StackItem] = [
268
269
  {
269
270
  "token": Token(
270
271
  type="angle",
@@ -301,8 +302,8 @@ def fold_tokens(tokens: List[Union[str, Token]]) -> List[Union[str, Token]]:
301
302
  return stack[-1]["token"].children["default"]
302
303
 
303
304
 
304
- def parse_tokens(tokens: List[Union[str, Token]], context: Optional[dict] = None) -> List[Element]:
305
- result: List[Element] = []
305
+ def parse_tokens(tokens: list[Union[str, Token]], context: Optional[dict] = None) -> list[Element]:
306
+ result: list[Element] = []
306
307
  for token in tokens:
307
308
  if isinstance(token, str):
308
309
  result.append(Element(type="text", attrs={"text": token}))
@@ -348,7 +349,7 @@ def parse_tokens(tokens: List[Union[str, Token]], context: Optional[dict] = None
348
349
 
349
350
 
350
351
  def parse(src: str, context: Optional[dict] = None):
351
- tokens: List[Union[str, Token]] = []
352
+ tokens: list[Union[str, Token]] = []
352
353
 
353
354
  def push_text(text: str):
354
355
  if text:
@@ -7,11 +7,12 @@ import secrets
7
7
  import signal
8
8
  import threading
9
9
  import urllib.parse
10
+ from collections.abc import Iterable
10
11
  from contextlib import suppress
11
12
  from pathlib import Path
12
13
  from tempfile import TemporaryDirectory
13
14
  from traceback import print_exc
14
- from typing import Any, Iterable, cast
15
+ from typing import Any, cast
15
16
 
16
17
  import aiohttp
17
18
  from creart import it
@@ -32,6 +33,7 @@ from satori.model import Event, ModelBase, Opcode
32
33
 
33
34
  from .adapter import Adapter as Adapter
34
35
  from .conection import WebsocketConnection
36
+ from .deque import Deque
35
37
  from .formdata import parse_content_disposition
36
38
  from .model import Provider as Provider
37
39
  from .model import Request as Request
@@ -99,6 +101,8 @@ class Server(Service, RouterMixin):
99
101
  self.webhooks = webhooks or []
100
102
  self._tempdir = TemporaryDirectory()
101
103
  self.proxy_url_mapping = {}
104
+ self._sequence = 0
105
+ self._event_cache = Deque(maxlen=100)
102
106
  super().__init__()
103
107
 
104
108
  def apply(self, item: Provider | Router | Adapter):
@@ -117,6 +121,9 @@ class Server(Service, RouterMixin):
117
121
  raise TypeError(f"Unknown config type: {item}")
118
122
 
119
123
  async def event_callback(self, event: Event):
124
+ event.id = self._sequence
125
+ self._event_cache.append(event)
126
+ self._sequence += 1
120
127
  for connection in self.connections:
121
128
  try:
122
129
  await connection.send({"op": Opcode.EVENT, "body": event.dump()})
@@ -146,16 +153,22 @@ class Server(Service, RouterMixin):
146
153
  identity = await ws.receive_json()
147
154
  if not isinstance(identity, dict) or identity.get("op") != Opcode.IDENTIFY:
148
155
  return await ws.close(code=3000, reason="Unauthorized")
149
- token = identity["body"]["token"]
156
+ body = identity["body"]
157
+ token = identity["body"].get("token")
150
158
  logins = []
151
159
  for provider in self.providers:
152
160
  if not provider.authenticate(token):
153
161
  return await ws.close(code=3000, reason="Unauthorized")
154
162
  logins.extend(await provider.get_logins())
163
+ sequence = body.get("sequence", -1)
155
164
  await connection.send({"op": Opcode.READY, "body": {"logins": [lo.dump() for lo in logins]}})
156
165
  self.connections.append(connection)
157
-
166
+ logger.debug(f"New connection: {id(connection)}")
158
167
  try:
168
+ if sequence > -1:
169
+ for event in self._event_cache.after(sequence):
170
+ await connection.send({"op": Opcode.EVENT, "body": event.dump()})
171
+ await asyncio.sleep(0.1)
159
172
  await any_completed(connection.heartbeat(), connection.close_signal.wait())
160
173
  finally:
161
174
  self.connections.remove(connection)
@@ -1,5 +1,6 @@
1
1
  from abc import abstractmethod
2
- from typing import AsyncIterator, List
2
+ from collections.abc import AsyncIterator
3
+ from typing import Optional
3
4
 
4
5
  from launart import Service
5
6
 
@@ -18,17 +19,17 @@ class Adapter(Service, RouterMixin):
18
19
  def ensure(self, platform: str, self_id: str) -> bool: ...
19
20
 
20
21
  @abstractmethod
21
- def authenticate(self, token: str) -> bool: ...
22
+ def authenticate(self, token: Optional[str]) -> bool: ...
22
23
 
23
24
  @staticmethod
24
- def proxy_urls() -> List[str]:
25
+ def proxy_urls() -> list[str]:
25
26
  return []
26
27
 
27
28
  @abstractmethod
28
29
  async def download_uploaded(self, platform: str, self_id: str, path: str) -> bytes: ...
29
30
 
30
31
  @abstractmethod
31
- async def get_logins(self) -> List[Login]: ...
32
+ async def get_logins(self) -> list[Login]: ...
32
33
 
33
34
  def __init__(self):
34
35
  super().__init__()
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ from loguru import logger
6
+ from starlette.websockets import WebSocket, WebSocketDisconnect
7
+
8
+ from satori.model import Opcode
9
+
10
+
11
+ class WebsocketConnection:
12
+ connection: WebSocket
13
+
14
+ def __init__(self, connection: WebSocket):
15
+ self.connection = connection
16
+ self.close_signal: asyncio.Event = asyncio.Event()
17
+
18
+ @property
19
+ def alive(self) -> bool:
20
+ return not self.close_signal.is_set()
21
+
22
+ async def heartbeat(self):
23
+ while True:
24
+ try:
25
+ msg = await asyncio.wait_for(self.connection.receive_json(), timeout=10)
26
+ if not isinstance(msg, dict) or msg.get("op") != Opcode.PING:
27
+ continue
28
+ await self.connection.send_json({"op": Opcode.PONG})
29
+ except asyncio.TimeoutError:
30
+ logger.warning(f"Connection {id(self)} heartbeat timeout, closing connection.")
31
+ await self.connection.close()
32
+ await self.connection_closed()
33
+ break
34
+ except WebSocketDisconnect:
35
+ return
36
+
37
+ async def connection_closed(self):
38
+ self.close_signal.set()
39
+
40
+ async def wait_for_available(self):
41
+ return
42
+
43
+ async def send(self, payload: dict) -> None:
44
+ return await self.connection.send_json(payload)
@@ -0,0 +1,33 @@
1
+ from collections import deque
2
+
3
+
4
+ class Deque:
5
+ def __init__(self, maxlen: int):
6
+ self.data = deque(maxlen=maxlen)
7
+ self.offset = 0
8
+
9
+ def append(self, x):
10
+ if len(self.data) == self.data.maxlen:
11
+ self.offset += 1
12
+ self.data.append(x)
13
+
14
+ def __getitem__(self, i: int):
15
+ index = i - self.offset
16
+ if index < 0 or index >= len(self.data):
17
+ return
18
+ return self.data[index]
19
+
20
+ def after(self, i: int):
21
+ if i < self.offset:
22
+ i = self.offset - 1
23
+ return list(self.data)[i + 1 - self.offset :]
24
+
25
+
26
+ if __name__ == "__main__":
27
+ d = Deque(3)
28
+ d.append(0)
29
+ d.append(1)
30
+ d.append(2)
31
+ d.append(3)
32
+ d.append(4)
33
+ d.append(5)
@@ -1,7 +1,6 @@
1
- from __future__ import annotations
2
-
1
+ from collections.abc import AsyncIterator
3
2
  from dataclasses import dataclass
4
- from typing import TYPE_CHECKING, Any, AsyncIterator, Generic, Protocol, TypeVar, Union, runtime_checkable
3
+ from typing import TYPE_CHECKING, Any, Generic, Optional, Protocol, TypeVar, Union, runtime_checkable
5
4
 
6
5
  from satori.const import Api
7
6
  from satori.model import Event, Login
@@ -25,7 +24,7 @@ class Request(Generic[TP]):
25
24
  class Provider(Protocol):
26
25
  def publisher(self) -> AsyncIterator[Event]: ...
27
26
 
28
- def authenticate(self, token: str) -> bool: ...
27
+ def authenticate(self, token: Optional[str]) -> bool: ...
29
28
 
30
29
  async def get_logins(self) -> list[Login]: ...
31
30
 
@@ -39,4 +38,4 @@ class Provider(Protocol):
39
38
 
40
39
  @runtime_checkable
41
40
  class Router(Protocol):
42
- routes: dict[str, RouteCall[Any, Any]]
41
+ routes: dict[str, "RouteCall[Any, Any]"]
@@ -1,4 +1,5 @@
1
- from typing import Any, Awaitable, Callable, Dict, List, Literal, Protocol, TypeVar, Union, overload
1
+ from collections.abc import Awaitable
2
+ from typing import Any, Callable, Literal, Protocol, TypeVar, Union, overload
2
3
  from typing_extensions import NotRequired, TypeAlias, TypedDict
3
4
 
4
5
  from starlette.datastructures import FormData
@@ -30,7 +31,7 @@ class RouteCall(Protocol[T, R]):
30
31
 
31
32
 
32
33
  INTERAL: TypeAlias = RouteCall[
33
- Any, Union[ModelBase, List[ModelBase], Dict[str, Any], List[Dict[str, Any]], None]
34
+ Any, Union[ModelBase, list[ModelBase], dict[str, Any], list[dict[str, Any]], None]
34
35
  ]
35
36
 
36
37
 
@@ -39,7 +40,7 @@ class MessageParam(TypedDict):
39
40
  content: str
40
41
 
41
42
 
42
- MESSAGE_CREATE: TypeAlias = RouteCall[MessageParam, Union[List[MessageObject], List[Dict[str, Any]]]]
43
+ MESSAGE_CREATE: TypeAlias = RouteCall[MessageParam, Union[list[MessageObject], list[dict[str, Any]]]]
43
44
 
44
45
 
45
46
  class MessageOpParam(TypedDict):
@@ -47,7 +48,7 @@ class MessageOpParam(TypedDict):
47
48
  message_id: str
48
49
 
49
50
 
50
- MESSAGE_GET: TypeAlias = RouteCall[MessageOpParam, Union[MessageObject, Dict[str, Any]]]
51
+ MESSAGE_GET: TypeAlias = RouteCall[MessageOpParam, Union[MessageObject, dict[str, Any]]]
51
52
  MESSAGE_DELETE: TypeAlias = RouteCall[MessageOpParam, None]
52
53
 
53
54
 
@@ -68,14 +69,14 @@ class MessageListParam(TypedDict):
68
69
  order: NotRequired[Order]
69
70
 
70
71
 
71
- MESSAGE_LIST: TypeAlias = RouteCall[MessageListParam, Union[PageDequeResult[MessageObject], Dict[str, Any]]]
72
+ MESSAGE_LIST: TypeAlias = RouteCall[MessageListParam, Union[PageDequeResult[MessageObject], dict[str, Any]]]
72
73
 
73
74
 
74
75
  class ChannelParam(TypedDict):
75
76
  channel_id: str
76
77
 
77
78
 
78
- CHANNEL_GET: TypeAlias = RouteCall[ChannelParam, Union[Channel, Dict[str, Any]]]
79
+ CHANNEL_GET: TypeAlias = RouteCall[ChannelParam, Union[Channel, dict[str, Any]]]
79
80
  CHANNEL_DELETE: TypeAlias = RouteCall[ChannelParam, None]
80
81
 
81
82
 
@@ -84,7 +85,7 @@ class ChannelListParam(TypedDict):
84
85
  next: NotRequired[str]
85
86
 
86
87
 
87
- CHANNEL_LIST: TypeAlias = RouteCall[ChannelListParam, Union[PageResult[Channel], Dict[str, Any]]]
88
+ CHANNEL_LIST: TypeAlias = RouteCall[ChannelListParam, Union[PageResult[Channel], dict[str, Any]]]
88
89
 
89
90
 
90
91
  class ChanneCreateParam(TypedDict):
@@ -92,7 +93,7 @@ class ChanneCreateParam(TypedDict):
92
93
  data: dict
93
94
 
94
95
 
95
- CHANNEL_CREATE: TypeAlias = RouteCall[ChanneCreateParam, Union[Channel, Dict[str, Any]]]
96
+ CHANNEL_CREATE: TypeAlias = RouteCall[ChanneCreateParam, Union[Channel, dict[str, Any]]]
96
97
 
97
98
 
98
99
  class ChanneUpdateParam(TypedDict):
@@ -116,21 +117,21 @@ class UserChannelCreateParam(TypedDict):
116
117
  guild_id: NotRequired[str]
117
118
 
118
119
 
119
- ROUTE_USER_CHANNEL_CREATE: TypeAlias = RouteCall[UserChannelCreateParam, Union[Channel, Dict[str, Any]]]
120
+ ROUTE_USER_CHANNEL_CREATE: TypeAlias = RouteCall[UserChannelCreateParam, Union[Channel, dict[str, Any]]]
120
121
 
121
122
 
122
123
  class GuildGetParam(TypedDict):
123
124
  guild_id: str
124
125
 
125
126
 
126
- GUILD_GET: TypeAlias = RouteCall[GuildGetParam, Union[Guild, Dict[str, Any]]]
127
+ GUILD_GET: TypeAlias = RouteCall[GuildGetParam, Union[Guild, dict[str, Any]]]
127
128
 
128
129
 
129
130
  class GuildListParam(TypedDict):
130
131
  next: NotRequired[str]
131
132
 
132
133
 
133
- GUILD_LIST: TypeAlias = RouteCall[GuildListParam, Union[PageResult[Guild], Dict[str, Any]]]
134
+ GUILD_LIST: TypeAlias = RouteCall[GuildListParam, Union[PageResult[Guild], dict[str, Any]]]
134
135
 
135
136
 
136
137
  class GuildMemberGetParam(TypedDict):
@@ -138,7 +139,7 @@ class GuildMemberGetParam(TypedDict):
138
139
  user_id: str
139
140
 
140
141
 
141
- GUILD_MEMBER_GET: TypeAlias = RouteCall[GuildMemberGetParam, Union[Member, Dict[str, Any]]]
142
+ GUILD_MEMBER_GET: TypeAlias = RouteCall[GuildMemberGetParam, Union[Member, dict[str, Any]]]
142
143
 
143
144
 
144
145
  class GuildXXXListParam(TypedDict):
@@ -146,7 +147,7 @@ class GuildXXXListParam(TypedDict):
146
147
  next: NotRequired[str]
147
148
 
148
149
 
149
- GUILD_MEMBER_LIST: TypeAlias = RouteCall[GuildXXXListParam, Union[PageResult[Member], Dict[str, Any]]]
150
+ GUILD_MEMBER_LIST: TypeAlias = RouteCall[GuildXXXListParam, Union[PageResult[Member], dict[str, Any]]]
150
151
 
151
152
 
152
153
  class GuildMemberKickParam(TypedDict):
@@ -176,7 +177,7 @@ class GuildMemberRoleParam(TypedDict):
176
177
  GUILD_MEMBER_ROLE_SET: TypeAlias = RouteCall[GuildMemberRoleParam, None]
177
178
  GUILD_MEMBER_ROLE_UNSET: TypeAlias = RouteCall[GuildMemberRoleParam, None]
178
179
 
179
- GUILD_ROLE_LIST: TypeAlias = RouteCall[GuildXXXListParam, Union[PageResult[Role], Dict[str, Any]]]
180
+ GUILD_ROLE_LIST: TypeAlias = RouteCall[GuildXXXListParam, Union[PageResult[Role], dict[str, Any]]]
180
181
 
181
182
 
182
183
  class GuildRoleCreateParam(TypedDict):
@@ -184,7 +185,7 @@ class GuildRoleCreateParam(TypedDict):
184
185
  role: dict
185
186
 
186
187
 
187
- GUILD_ROLE_CREATE: TypeAlias = RouteCall[GuildRoleCreateParam, Union[Role, Dict[str, Any]]]
188
+ GUILD_ROLE_CREATE: TypeAlias = RouteCall[GuildRoleCreateParam, Union[Role, dict[str, Any]]]
188
189
 
189
190
 
190
191
  class GuildRoleUpdateParam(TypedDict):
@@ -239,22 +240,22 @@ class ReactionListParam(TypedDict):
239
240
  next: NotRequired[str]
240
241
 
241
242
 
242
- REACTION_LIST: TypeAlias = RouteCall[ReactionListParam, Union[PageResult[User], Dict[str, Any]]]
243
- LOGIN_GET: TypeAlias = RouteCall[Any, Union[Login, Dict[str, Any]]]
243
+ REACTION_LIST: TypeAlias = RouteCall[ReactionListParam, Union[PageResult[User], dict[str, Any]]]
244
+ LOGIN_GET: TypeAlias = RouteCall[Any, Union[Login, dict[str, Any]]]
244
245
 
245
246
 
246
247
  class UserGetParam(TypedDict):
247
248
  user_id: str
248
249
 
249
250
 
250
- USER_GET: TypeAlias = RouteCall[UserGetParam, Union[User, Dict[str, Any]]]
251
+ USER_GET: TypeAlias = RouteCall[UserGetParam, Union[User, dict[str, Any]]]
251
252
 
252
253
 
253
254
  class FriendListParam(TypedDict):
254
255
  next: NotRequired[str]
255
256
 
256
257
 
257
- FRIEND_LIST: TypeAlias = RouteCall[FriendListParam, Union[PageResult[User], Dict[str, Any]]]
258
+ FRIEND_LIST: TypeAlias = RouteCall[FriendListParam, Union[PageResult[User], dict[str, Any]]]
258
259
 
259
260
 
260
261
  class ApproveParam(TypedDict):
@@ -266,11 +267,11 @@ class ApproveParam(TypedDict):
266
267
  APPROVE: TypeAlias = RouteCall[ApproveParam, None]
267
268
 
268
269
 
269
- UPLOAD_CREATE: TypeAlias = RouteCall[FormData, Dict[str, str]]
270
+ UPLOAD_CREATE: TypeAlias = RouteCall[FormData, dict[str, str]]
270
271
 
271
272
 
272
273
  class RouterMixin:
273
- routes: Dict[str, RouteCall[Any, Any]]
274
+ routes: dict[str, RouteCall[Any, Any]]
274
275
 
275
276
  @overload
276
277
  def route(self, path: Literal[Api.MESSAGE_CREATE]) -> Callable[[MESSAGE_CREATE], MESSAGE_CREATE]: ...
@@ -1,40 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
-
5
- from starlette.websockets import WebSocket
6
-
7
- from satori.model import Opcode
8
-
9
-
10
- class WebsocketConnection:
11
- connection: WebSocket
12
-
13
- def __init__(self, connection: WebSocket):
14
- self.connection = connection
15
- self.close_signal: asyncio.Event = asyncio.Event()
16
-
17
- @property
18
- def id(self):
19
- return self.connection.headers["X-Self-ID"]
20
-
21
- @property
22
- def alive(self) -> bool:
23
- return not self.close_signal.is_set()
24
-
25
- async def heartbeat(self):
26
- async for msg in self.connection.iter_json():
27
- if not isinstance(msg, dict) or msg.get("op") != Opcode.PING:
28
- continue
29
- await self.connection.send_json({"op": Opcode.PONG})
30
- else:
31
- await self.connection_closed()
32
-
33
- async def connection_closed(self):
34
- self.close_signal.set()
35
-
36
- async def wait_for_available(self):
37
- return
38
-
39
- async def send(self, payload: dict) -> None:
40
- return await self.connection.send_json(payload)
File without changes
File without changes