satori-python 0.11.1__tar.gz → 0.11.3__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.
- {satori_python-0.11.1 → satori_python-0.11.3}/PKG-INFO +1 -1
- {satori_python-0.11.1 → satori_python-0.11.3}/pyproject.toml +1 -1
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/__init__.py +1 -1
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/client/__init__.py +23 -2
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/element.py +81 -32
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/server/__init__.py +24 -3
- {satori_python-0.11.1 → satori_python-0.11.3}/LICENSE +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/README.md +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/client/account.py +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/client/account.pyi +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/client/network/__init__.py +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/client/network/base.py +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/client/network/webhook.py +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/client/network/websocket.py +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/client/session.py +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/config.py +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/const.py +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/event.py +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/exception.py +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/model.py +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/parser.py +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/server/adapter.py +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/server/conection.py +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/server/model.py +0 -0
- {satori_python-0.11.1 → satori_python-0.11.3}/src/satori/server/route.py +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import functools
|
|
4
5
|
import signal
|
|
6
|
+
import threading
|
|
5
7
|
from functools import wraps
|
|
6
8
|
from typing import Any, Awaitable, Callable, Iterable, Literal, TypeVar, overload
|
|
7
9
|
|
|
@@ -211,8 +213,27 @@ class App(Service):
|
|
|
211
213
|
manager.add_component(self)
|
|
212
214
|
manager.launch_blocking(loop=loop, stop_signal=stop_signal)
|
|
213
215
|
|
|
214
|
-
async def run_async(
|
|
216
|
+
async def run_async(
|
|
217
|
+
self,
|
|
218
|
+
manager: Launart | None = None,
|
|
219
|
+
stop_signal: Iterable[signal.Signals] = (signal.SIGINT,),
|
|
220
|
+
):
|
|
215
221
|
if manager is None:
|
|
216
222
|
manager = it(Launart)
|
|
217
223
|
manager.add_component(self)
|
|
218
|
-
|
|
224
|
+
handled_signals: dict[signal.Signals, Any] = {}
|
|
225
|
+
launch_task = asyncio.create_task(manager.launch(), name="amnesia-launch")
|
|
226
|
+
signal_handler = functools.partial(manager._on_sys_signal, main_task=launch_task)
|
|
227
|
+
if threading.current_thread() is threading.main_thread(): # pragma: worst case
|
|
228
|
+
try:
|
|
229
|
+
for sig in stop_signal:
|
|
230
|
+
handled_signals[sig] = signal.getsignal(sig)
|
|
231
|
+
signal.signal(sig, signal_handler)
|
|
232
|
+
except ValueError: # pragma: no cover
|
|
233
|
+
# `signal.signal` may throw if `threading.main_thread` does
|
|
234
|
+
# not support signals
|
|
235
|
+
handled_signals.clear()
|
|
236
|
+
await launch_task
|
|
237
|
+
for sig, handler in handled_signals.items():
|
|
238
|
+
if signal.getsignal(sig) is signal_handler:
|
|
239
|
+
signal.signal(sig, handler)
|
|
@@ -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, Dict, List, Optional, TypeVar, Union, get_args
|
|
5
|
+
from typing import Any, ClassVar, Dict, List, Optional, Tuple, TypeVar, Union, get_args
|
|
6
6
|
from typing_extensions import override
|
|
7
7
|
|
|
8
8
|
from .parser import Element as RawElement
|
|
@@ -11,15 +11,23 @@ from .parser import escape, param_case
|
|
|
11
11
|
TE = TypeVar("TE", bound="Element")
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
@dataclass
|
|
14
|
+
@dataclass(repr=False)
|
|
15
15
|
class Element:
|
|
16
|
-
_attrs: Dict[str, Any] = field(init=False, default_factory=dict
|
|
16
|
+
_attrs: Dict[str, Any] = field(init=False, default_factory=dict)
|
|
17
17
|
_children: List["Element"] = field(init=False, default_factory=list)
|
|
18
18
|
|
|
19
|
+
__names__: ClassVar[Tuple[str, ...]]
|
|
20
|
+
|
|
19
21
|
@property
|
|
20
22
|
def tag(self) -> str:
|
|
21
23
|
return self.__class__.__name__.lower()
|
|
22
24
|
|
|
25
|
+
@classmethod
|
|
26
|
+
def unpack(cls, attrs: Dict[str, Any]):
|
|
27
|
+
obj = cls(**{k: v for k, v in attrs.items() if k in cls.__names__})
|
|
28
|
+
obj._attrs.update({k: v for k, v in attrs.items() if k not in cls.__names__})
|
|
29
|
+
return obj
|
|
30
|
+
|
|
23
31
|
def __post_init__(self):
|
|
24
32
|
for f in fields(self):
|
|
25
33
|
if f.name in ("_attrs", "_children"):
|
|
@@ -62,6 +70,13 @@ class Element:
|
|
|
62
70
|
def __str__(self) -> str:
|
|
63
71
|
return self.dumps()
|
|
64
72
|
|
|
73
|
+
def __repr__(self) -> str:
|
|
74
|
+
args = {**self._attrs}
|
|
75
|
+
elem = f"{self.__class__.__name__}(" + ", ".join(f"{k}={v!r}" for k, v in args.items())
|
|
76
|
+
if self._children:
|
|
77
|
+
elem += ", { " + ", ".join(repr(i) for i in self._children) + " }"
|
|
78
|
+
return elem + ")"
|
|
79
|
+
|
|
65
80
|
def __call__(self, *content: Union[str, "Element"]):
|
|
66
81
|
self._children.extend(Text(i) if isinstance(i, str) else i for i in content)
|
|
67
82
|
self.__post_call__()
|
|
@@ -69,8 +84,11 @@ class Element:
|
|
|
69
84
|
|
|
70
85
|
def __post_call__(self): ...
|
|
71
86
|
|
|
87
|
+
def __getitem__(self, key: str) -> Any:
|
|
88
|
+
return self._attrs[key]
|
|
72
89
|
|
|
73
|
-
|
|
90
|
+
|
|
91
|
+
@dataclass(repr=False)
|
|
74
92
|
class Text(Element):
|
|
75
93
|
"""一段纯文本。"""
|
|
76
94
|
|
|
@@ -80,8 +98,10 @@ class Text(Element):
|
|
|
80
98
|
def dumps(self, strip: bool = False) -> str:
|
|
81
99
|
return self.text if strip else escape(self.text)
|
|
82
100
|
|
|
101
|
+
__names__ = ("text",)
|
|
83
102
|
|
|
84
|
-
|
|
103
|
+
|
|
104
|
+
@dataclass(repr=False)
|
|
85
105
|
class At(Element):
|
|
86
106
|
"""<at> 元素用于提及某个或某些用户。"""
|
|
87
107
|
|
|
@@ -101,8 +121,10 @@ class At(Element):
|
|
|
101
121
|
def all(here: bool = False) -> "At":
|
|
102
122
|
return At(type="here" if here else "all")
|
|
103
123
|
|
|
124
|
+
__names__ = ("id", "name", "role", "type")
|
|
104
125
|
|
|
105
|
-
|
|
126
|
+
|
|
127
|
+
@dataclass(repr=False)
|
|
106
128
|
class Sharp(Element):
|
|
107
129
|
"""<sharp> 元素用于提及某个频道。"""
|
|
108
130
|
|
|
@@ -114,7 +136,9 @@ class Sharp(Element):
|
|
|
114
136
|
class Link(Element):
|
|
115
137
|
"""<a> 元素用于显示一个链接。"""
|
|
116
138
|
|
|
117
|
-
|
|
139
|
+
href: str
|
|
140
|
+
|
|
141
|
+
__names__ = ("href",)
|
|
118
142
|
|
|
119
143
|
def __post_call__(self):
|
|
120
144
|
if not self._children:
|
|
@@ -128,18 +152,20 @@ class Link(Element):
|
|
|
128
152
|
def tag(self) -> str:
|
|
129
153
|
return "a"
|
|
130
154
|
|
|
131
|
-
@
|
|
132
|
-
def
|
|
133
|
-
return
|
|
155
|
+
@property
|
|
156
|
+
def url(self) -> str:
|
|
157
|
+
return self.href
|
|
134
158
|
|
|
135
159
|
|
|
136
|
-
@dataclass
|
|
160
|
+
@dataclass(repr=False)
|
|
137
161
|
class Resource(Element):
|
|
138
162
|
src: str
|
|
139
163
|
extra: InitVar[Optional[Dict[str, Any]]] = None
|
|
140
164
|
cache: Optional[bool] = None
|
|
141
165
|
timeout: Optional[str] = None
|
|
142
166
|
|
|
167
|
+
__names__ = ("src",)
|
|
168
|
+
|
|
143
169
|
@classmethod
|
|
144
170
|
def of(
|
|
145
171
|
cls,
|
|
@@ -173,44 +199,58 @@ class Resource(Element):
|
|
|
173
199
|
self._attrs.update(extra)
|
|
174
200
|
|
|
175
201
|
|
|
176
|
-
@dataclass
|
|
202
|
+
@dataclass(repr=False)
|
|
177
203
|
class Image(Resource):
|
|
178
204
|
"""<img> 元素用于表示图片。"""
|
|
179
205
|
|
|
180
206
|
width: Optional[int] = None
|
|
181
207
|
height: Optional[int] = None
|
|
182
208
|
|
|
209
|
+
__names__ = ("src", "width", "height")
|
|
210
|
+
|
|
183
211
|
@property
|
|
184
212
|
@override
|
|
185
213
|
def tag(self) -> str:
|
|
186
214
|
return "img"
|
|
187
215
|
|
|
188
216
|
|
|
189
|
-
@dataclass
|
|
217
|
+
@dataclass(repr=False)
|
|
190
218
|
class Audio(Resource):
|
|
191
219
|
"""<audio> 元素用于表示语音。"""
|
|
192
220
|
|
|
193
|
-
|
|
221
|
+
duration: Optional[int] = None
|
|
222
|
+
poster: Optional[str] = None
|
|
194
223
|
|
|
224
|
+
__names__ = ("src", "duration", "poster")
|
|
195
225
|
|
|
196
|
-
|
|
226
|
+
|
|
227
|
+
@dataclass(repr=False)
|
|
197
228
|
class Video(Resource):
|
|
198
229
|
"""<video> 元素用于表示视频。"""
|
|
199
230
|
|
|
200
|
-
|
|
231
|
+
width: Optional[int] = None
|
|
232
|
+
height: Optional[int] = None
|
|
233
|
+
duration: Optional[int] = None
|
|
234
|
+
poster: Optional[str] = None
|
|
201
235
|
|
|
236
|
+
__names__ = ("src", "width", "height", "duration", "poster")
|
|
202
237
|
|
|
203
|
-
|
|
238
|
+
|
|
239
|
+
@dataclass(repr=False)
|
|
204
240
|
class File(Resource):
|
|
205
241
|
"""<file> 元素用于表示文件。"""
|
|
206
242
|
|
|
207
|
-
|
|
243
|
+
poster: Optional[str] = None
|
|
208
244
|
|
|
245
|
+
__names__ = ("src", "poster")
|
|
209
246
|
|
|
210
|
-
|
|
247
|
+
|
|
248
|
+
@dataclass(init=False, repr=False)
|
|
211
249
|
class Style(Element):
|
|
212
250
|
"""样式元素的基类。"""
|
|
213
251
|
|
|
252
|
+
__names__ = ()
|
|
253
|
+
|
|
214
254
|
def __init__(self, *text: Union[str, Text, "Style"]):
|
|
215
255
|
super().__init__()
|
|
216
256
|
self.__call__(*text)
|
|
@@ -311,13 +351,15 @@ class Paragraph(Style):
|
|
|
311
351
|
return "p"
|
|
312
352
|
|
|
313
353
|
|
|
314
|
-
@dataclass(init=False)
|
|
354
|
+
@dataclass(init=False, repr=False)
|
|
315
355
|
class Message(Element):
|
|
316
356
|
"""<message> 元素的基本用法是表示一条消息。
|
|
317
357
|
|
|
318
358
|
子元素对应于消息的内容。如果其没有子元素,则消息不会被发送。
|
|
319
359
|
"""
|
|
320
360
|
|
|
361
|
+
__names__ = ("id", "forward")
|
|
362
|
+
|
|
321
363
|
id: Optional[str]
|
|
322
364
|
forward: Optional[bool]
|
|
323
365
|
|
|
@@ -333,7 +375,6 @@ class Message(Element):
|
|
|
333
375
|
self.__call__(*content or [])
|
|
334
376
|
|
|
335
377
|
|
|
336
|
-
@dataclass
|
|
337
378
|
class Quote(Message):
|
|
338
379
|
"""<quote> 元素用于表示对消息引用。
|
|
339
380
|
|
|
@@ -343,16 +384,18 @@ class Quote(Message):
|
|
|
343
384
|
pass
|
|
344
385
|
|
|
345
386
|
|
|
346
|
-
@dataclass
|
|
387
|
+
@dataclass(repr=False)
|
|
347
388
|
class Author(Element):
|
|
348
389
|
"""<author> 元素用于表示消息的作者。它的子元素会被渲染为作者的名字。"""
|
|
349
390
|
|
|
350
391
|
id: str
|
|
351
|
-
|
|
392
|
+
name: Optional[str] = None
|
|
352
393
|
avatar: Optional[str] = None
|
|
353
394
|
|
|
395
|
+
__names__ = ("id", "name", "avatar")
|
|
354
396
|
|
|
355
|
-
|
|
397
|
+
|
|
398
|
+
@dataclass(repr=False)
|
|
356
399
|
class Button(Element):
|
|
357
400
|
"""<button> 元素用于表示一个按钮。它的子元素会被渲染为按钮的文本。"""
|
|
358
401
|
|
|
@@ -362,6 +405,8 @@ class Button(Element):
|
|
|
362
405
|
text: Optional[str] = None
|
|
363
406
|
theme: Optional[str] = None
|
|
364
407
|
|
|
408
|
+
__names__ = ("type", "id", "href", "text", "theme")
|
|
409
|
+
|
|
365
410
|
@classmethod
|
|
366
411
|
def action(cls, button_id: str, theme: Optional[str] = None):
|
|
367
412
|
return Button("action", id=button_id, theme=theme)
|
|
@@ -395,12 +440,14 @@ class Button(Element):
|
|
|
395
440
|
raise ValueError("Button can only have one Text child")
|
|
396
441
|
|
|
397
442
|
|
|
398
|
-
@dataclass(init=False)
|
|
443
|
+
@dataclass(init=False, repr=False)
|
|
399
444
|
class Custom(Element):
|
|
400
445
|
"""自定义元素用于构造标准元素以外的元素"""
|
|
401
446
|
|
|
402
447
|
type: str
|
|
403
448
|
|
|
449
|
+
__names__ = ()
|
|
450
|
+
|
|
404
451
|
def __init__(
|
|
405
452
|
self,
|
|
406
453
|
type: str,
|
|
@@ -424,12 +471,14 @@ class Custom(Element):
|
|
|
424
471
|
return self.type
|
|
425
472
|
|
|
426
473
|
|
|
427
|
-
@dataclass
|
|
474
|
+
@dataclass(repr=False)
|
|
428
475
|
class Raw(Element):
|
|
429
476
|
"""Raw 元素表示原始文本"""
|
|
430
477
|
|
|
431
478
|
content: str
|
|
432
479
|
|
|
480
|
+
__names__ = ()
|
|
481
|
+
|
|
433
482
|
@override
|
|
434
483
|
def dumps(self, strip: bool = False):
|
|
435
484
|
return self.content if strip else escape(self.content)
|
|
@@ -480,25 +529,25 @@ def transform(elements: List[RawElement]) -> List[Element]:
|
|
|
480
529
|
tag = elem.tag()
|
|
481
530
|
if tag in ELEMENT_TYPE_MAP:
|
|
482
531
|
seg_cls = ELEMENT_TYPE_MAP[tag]
|
|
483
|
-
msg.append(seg_cls(
|
|
532
|
+
msg.append(seg_cls.unpack(elem.attrs))
|
|
484
533
|
elif tag in ("a", "link"):
|
|
485
|
-
link = Link(elem.attrs
|
|
534
|
+
link = Link.unpack(elem.attrs)
|
|
486
535
|
if elem.children:
|
|
487
536
|
link(*transform(elem.children))
|
|
488
537
|
msg.append(link)
|
|
489
538
|
elif tag == "button":
|
|
490
|
-
button = Button(
|
|
539
|
+
button = Button.unpack(elem.attrs)
|
|
491
540
|
if elem.children:
|
|
492
541
|
button(*transform(elem.children))
|
|
493
542
|
elif tag in STYLE_TYPE_MAP:
|
|
494
543
|
seg_cls = STYLE_TYPE_MAP[tag]
|
|
495
|
-
msg.append(seg_cls()(*transform(elem.children)))
|
|
544
|
+
msg.append(seg_cls.unpack(elem.attrs)(*transform(elem.children)))
|
|
496
545
|
elif tag in ("br", "newline"):
|
|
497
546
|
msg.append(Br())
|
|
498
547
|
elif tag == "message":
|
|
499
|
-
msg.append(Message(
|
|
548
|
+
msg.append(Message.unpack(elem.attrs)(*transform(elem.children)))
|
|
500
549
|
elif tag == "quote":
|
|
501
|
-
msg.append(Quote(
|
|
550
|
+
msg.append(Quote.unpack(elem.attrs)(*transform(elem.children)))
|
|
502
551
|
else:
|
|
503
552
|
msg.append(Custom(elem.type, elem.attrs)(*transform(elem.children)))
|
|
504
553
|
return msg
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import functools
|
|
4
5
|
import signal
|
|
6
|
+
import threading
|
|
5
7
|
from contextlib import suppress
|
|
6
8
|
from traceback import print_exc
|
|
7
|
-
from typing import Callable, Iterable, Literal, cast, overload
|
|
9
|
+
from typing import Any, Callable, Iterable, Literal, cast, overload
|
|
8
10
|
|
|
9
11
|
import aiohttp
|
|
10
12
|
from creart import it
|
|
@@ -383,8 +385,27 @@ class Server(Service):
|
|
|
383
385
|
manager.add_component(self)
|
|
384
386
|
manager.launch_blocking(loop=loop, stop_signal=stop_signal)
|
|
385
387
|
|
|
386
|
-
async def run_async(
|
|
388
|
+
async def run_async(
|
|
389
|
+
self,
|
|
390
|
+
manager: Launart | None = None,
|
|
391
|
+
stop_signal: Iterable[signal.Signals] = (signal.SIGINT,),
|
|
392
|
+
):
|
|
387
393
|
if manager is None:
|
|
388
394
|
manager = it(Launart)
|
|
389
395
|
manager.add_component(self)
|
|
390
|
-
|
|
396
|
+
handled_signals: dict[signal.Signals, Any] = {}
|
|
397
|
+
launch_task = asyncio.create_task(manager.launch(), name="amnesia-launch")
|
|
398
|
+
signal_handler = functools.partial(manager._on_sys_signal, main_task=launch_task)
|
|
399
|
+
if threading.current_thread() is threading.main_thread(): # pragma: worst case
|
|
400
|
+
try:
|
|
401
|
+
for sig in stop_signal:
|
|
402
|
+
handled_signals[sig] = signal.getsignal(sig)
|
|
403
|
+
signal.signal(sig, signal_handler)
|
|
404
|
+
except ValueError: # pragma: no cover
|
|
405
|
+
# `signal.signal` may throw if `threading.main_thread` does
|
|
406
|
+
# not support signals
|
|
407
|
+
handled_signals.clear()
|
|
408
|
+
await launch_task
|
|
409
|
+
for sig, handler in handled_signals.items():
|
|
410
|
+
if signal.getsignal(sig) is signal_handler:
|
|
411
|
+
signal.signal(sig, handler)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|