satori-python 0.16.4__tar.gz → 0.16.5__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.16.4 → satori_python-0.16.5}/PKG-INFO +12 -5
- {satori_python-0.16.4 → satori_python-0.16.5}/README.md +11 -4
- {satori_python-0.16.4 → satori_python-0.16.5}/pyproject.toml +2 -1
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/__init__.py +1 -1
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/__init__.py +27 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/network/base.py +1 -1
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/element.py +5 -2
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/parser.py +2 -2
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/server/__init__.py +65 -16
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/server/route.py +1 -1
- {satori_python-0.16.4 → satori_python-0.16.5}/LICENSE +0 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/account.py +0 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/account.pyi +0 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/config.py +0 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/network/__init__.py +0 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/network/util.py +0 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/network/webhook.py +0 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/network/websocket.py +0 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/protocol.py +0 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/const.py +0 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/event.py +0 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/exception.py +0 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/model.py +0 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/server/adapter.py +0 -0
- /satori_python-0.16.4/src/satori/server/conection.py → /satori_python-0.16.5/src/satori/server/connection.py +0 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/server/formdata.py +0 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/server/model.py +0 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/server/utils.py +0 -0
- {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: satori-python
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.5
|
|
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>
|
|
@@ -77,10 +77,17 @@ pip install satori-python-server
|
|
|
77
77
|
|
|
78
78
|
### 官方适配器
|
|
79
79
|
|
|
80
|
-
| 适配器 | 安装 |
|
|
81
|
-
|
|
82
|
-
| Satori | `pip install satori-python-adapter-satori` |
|
|
83
|
-
| OneBot V11 | `pip install satori-python-adapter-onebot11` |
|
|
80
|
+
| 适配器 | 安装 | 路径 |
|
|
81
|
+
|------------|----------------------------------------------|--------------------------------------------------------------------|
|
|
82
|
+
| Satori | `pip install satori-python-adapter-satori` | satori.adapters.satori |
|
|
83
|
+
| OneBot V11 | `pip install satori-python-adapter-onebot11` | satori.adapters.onebot11.forward, satori.adapters.onebot11.reverse |
|
|
84
|
+
| Console | `pip install satori-python-adapter-console` | satori.adapters.console |
|
|
85
|
+
|
|
86
|
+
### 社区适配器
|
|
87
|
+
|
|
88
|
+
| 适配器 | 安装 | 路径 |
|
|
89
|
+
|-------------------|-----------------------|--------------|
|
|
90
|
+
| nekobox(Lagrange) | `pip install nekobox` | nekobox.main |
|
|
84
91
|
|
|
85
92
|
## 使用
|
|
86
93
|
|
|
@@ -47,10 +47,17 @@ pip install satori-python-server
|
|
|
47
47
|
|
|
48
48
|
### 官方适配器
|
|
49
49
|
|
|
50
|
-
| 适配器 | 安装 |
|
|
51
|
-
|
|
52
|
-
| Satori | `pip install satori-python-adapter-satori` |
|
|
53
|
-
| OneBot V11 | `pip install satori-python-adapter-onebot11` |
|
|
50
|
+
| 适配器 | 安装 | 路径 |
|
|
51
|
+
|------------|----------------------------------------------|--------------------------------------------------------------------|
|
|
52
|
+
| Satori | `pip install satori-python-adapter-satori` | satori.adapters.satori |
|
|
53
|
+
| OneBot V11 | `pip install satori-python-adapter-onebot11` | satori.adapters.onebot11.forward, satori.adapters.onebot11.reverse |
|
|
54
|
+
| Console | `pip install satori-python-adapter-console` | satori.adapters.console |
|
|
55
|
+
|
|
56
|
+
### 社区适配器
|
|
57
|
+
|
|
58
|
+
| 适配器 | 安装 | 路径 |
|
|
59
|
+
|-------------------|-----------------------|--------------|
|
|
60
|
+
| nekobox(Lagrange) | `pip install nekobox` | nekobox.main |
|
|
54
61
|
|
|
55
62
|
## 使用
|
|
56
63
|
|
|
@@ -29,7 +29,7 @@ classifiers = [
|
|
|
29
29
|
"Programming Language :: Python :: 3.12",
|
|
30
30
|
"Operating System :: OS Independent",
|
|
31
31
|
]
|
|
32
|
-
version = "0.16.
|
|
32
|
+
version = "0.16.5"
|
|
33
33
|
|
|
34
34
|
[project.license]
|
|
35
35
|
text = "MIT"
|
|
@@ -54,6 +54,7 @@ dev = [
|
|
|
54
54
|
"fix-future-annotations>=0.5.0",
|
|
55
55
|
"mina-build<0.6,>=0.5.1",
|
|
56
56
|
"pdm-mina>=0.3.2",
|
|
57
|
+
"nonechat<0.7.0,>=0.6.0",
|
|
57
58
|
]
|
|
58
59
|
|
|
59
60
|
[tool.pdm.build]
|
|
@@ -35,6 +35,28 @@ MAPPING: dict[type[Config], type[BaseNetwork]] = {
|
|
|
35
35
|
WebsocketsInfo: WsNetwork,
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
_app: App | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_accounts() -> dict[str, Account]:
|
|
42
|
+
if _app is None:
|
|
43
|
+
raise RuntimeError("App instance is not initialized.")
|
|
44
|
+
return _app.accounts
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_app() -> App:
|
|
48
|
+
"""Get the current App instance."""
|
|
49
|
+
if _app is None:
|
|
50
|
+
raise RuntimeError("App instance is not initialized.")
|
|
51
|
+
return _app
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_account(self_id: str) -> Account:
|
|
55
|
+
"""Get an account by its self_id."""
|
|
56
|
+
if _app is None:
|
|
57
|
+
raise RuntimeError("App instance is not initialized.")
|
|
58
|
+
return _app.get_account(self_id)
|
|
59
|
+
|
|
38
60
|
|
|
39
61
|
class App(Service):
|
|
40
62
|
id = "satori-python.client"
|
|
@@ -51,6 +73,10 @@ class App(Service):
|
|
|
51
73
|
MAPPING[tc] = tn
|
|
52
74
|
|
|
53
75
|
def __init__(self, *configs: Config, default_api_cls: type[ApiProtocol] = ApiProtocol):
|
|
76
|
+
global _app
|
|
77
|
+
|
|
78
|
+
if _app is not None:
|
|
79
|
+
raise RuntimeError("App instance already exists. Only one App instance is allowed.")
|
|
54
80
|
self.accounts = {}
|
|
55
81
|
self.connections = []
|
|
56
82
|
self.event_callbacks = []
|
|
@@ -59,6 +85,7 @@ class App(Service):
|
|
|
59
85
|
for config in configs:
|
|
60
86
|
self.apply(config)
|
|
61
87
|
self.default_api_cls = default_api_cls
|
|
88
|
+
_app = self
|
|
62
89
|
|
|
63
90
|
def apply(self, config: Config):
|
|
64
91
|
try:
|
|
@@ -180,6 +180,7 @@ class Resource(Element):
|
|
|
180
180
|
raw: Optional[Union[bytes, BytesIO]] = None,
|
|
181
181
|
mime: Optional[str] = None,
|
|
182
182
|
name: Optional[str] = None,
|
|
183
|
+
duration: Optional[float] = None,
|
|
183
184
|
poster: Optional[str] = None,
|
|
184
185
|
extra: Optional[dict[str, Any]] = None,
|
|
185
186
|
cache: Optional[bool] = None,
|
|
@@ -198,6 +199,8 @@ class Resource(Element):
|
|
|
198
199
|
raise ValueError(f"{cls} need at least one of url, path and raw")
|
|
199
200
|
if name is not None:
|
|
200
201
|
data["title"] = name
|
|
202
|
+
if duration is not None and cls is Audio:
|
|
203
|
+
data["duration"] = duration
|
|
201
204
|
if poster is not None and cls in (Video, Audio, File):
|
|
202
205
|
data["poster"] = poster
|
|
203
206
|
if cache is not None:
|
|
@@ -231,7 +234,7 @@ class Image(Resource):
|
|
|
231
234
|
class Audio(Resource):
|
|
232
235
|
"""<audio> 元素用于表示语音。"""
|
|
233
236
|
|
|
234
|
-
duration: Optional[
|
|
237
|
+
duration: Optional[float] = None
|
|
235
238
|
poster: Optional[str] = None
|
|
236
239
|
|
|
237
240
|
__names__ = ("src", "title", "duration", "poster")
|
|
@@ -243,7 +246,7 @@ class Video(Resource):
|
|
|
243
246
|
|
|
244
247
|
width: Optional[int] = None
|
|
245
248
|
height: Optional[int] = None
|
|
246
|
-
duration: Optional[
|
|
249
|
+
duration: Optional[float] = None
|
|
247
250
|
poster: Optional[str] = None
|
|
248
251
|
|
|
249
252
|
__names__ = ("src", "title", "width", "height", "duration", "poster")
|
|
@@ -358,9 +358,9 @@ def parse(src: str, context: Optional[dict] = None):
|
|
|
358
358
|
def parse_content(source: str, _start: bool, _end: bool):
|
|
359
359
|
source = unescape(source)
|
|
360
360
|
if _start:
|
|
361
|
-
source = re.sub(r"^\s*\n\s*", "", source, re.MULTILINE)
|
|
361
|
+
source = re.sub(r"^\s*\n\s*", "", source, flags=re.MULTILINE)
|
|
362
362
|
if _end:
|
|
363
|
-
source = re.sub(r"\s*\n\s*$", "", source, re.MULTILINE)
|
|
363
|
+
source = re.sub(r"\s*\n\s*$", "", source, flags=re.MULTILINE)
|
|
364
364
|
push_text(source)
|
|
365
365
|
|
|
366
366
|
tag_pat = tag_pat2 if context is not None else tag_pat1
|
|
@@ -8,31 +8,29 @@ import secrets
|
|
|
8
8
|
import signal
|
|
9
9
|
import threading
|
|
10
10
|
import urllib.parse
|
|
11
|
-
from collections.abc import Iterable
|
|
11
|
+
from collections.abc import Awaitable, Iterable
|
|
12
12
|
from contextlib import suppress
|
|
13
13
|
from itertools import chain
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from tempfile import TemporaryDirectory
|
|
16
16
|
from traceback import print_exc
|
|
17
|
-
from typing import Any
|
|
17
|
+
from typing import Any, Callable, TypeVar
|
|
18
18
|
|
|
19
19
|
import aiohttp
|
|
20
20
|
from creart import it
|
|
21
21
|
from graia.amnesia.builtins.aiohttp import AiohttpClientService
|
|
22
|
-
from graia.amnesia.builtins.asgi import UvicornASGIService
|
|
22
|
+
from graia.amnesia.builtins.asgi import UvicornASGIService, asgitypes
|
|
23
23
|
from launart import Launart, Service, any_completed
|
|
24
24
|
from loguru import logger
|
|
25
25
|
from starlette.applications import Starlette
|
|
26
26
|
from starlette.datastructures import FormData as FormData
|
|
27
27
|
from starlette.requests import Request as StarletteRequest
|
|
28
|
-
from starlette.responses import
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
StreamingResponse,
|
|
35
|
-
)
|
|
28
|
+
from starlette.responses import FileResponse as FileResponse
|
|
29
|
+
from starlette.responses import HTMLResponse as HTMLResponse
|
|
30
|
+
from starlette.responses import JSONResponse as JSONResponse
|
|
31
|
+
from starlette.responses import PlainTextResponse as PlainTextResponse
|
|
32
|
+
from starlette.responses import Response as Response
|
|
33
|
+
from starlette.responses import StreamingResponse as StreamingResponse
|
|
36
34
|
from starlette.routing import Route, WebSocketRoute
|
|
37
35
|
from starlette.staticfiles import StaticFiles
|
|
38
36
|
from starlette.websockets import WebSocket, WebSocketDisconnect
|
|
@@ -43,7 +41,7 @@ from satori.model import Event, Meta, ModelBase, Opcode
|
|
|
43
41
|
|
|
44
42
|
from .. import EventType
|
|
45
43
|
from .adapter import Adapter as Adapter
|
|
46
|
-
from .
|
|
44
|
+
from .connection import WebsocketConnection
|
|
47
45
|
from .formdata import parse_content_disposition as parse_content_disposition
|
|
48
46
|
from .model import Provider as Provider
|
|
49
47
|
from .model import Request as Request
|
|
@@ -53,6 +51,10 @@ from .route import RouteCall as RouteCall
|
|
|
53
51
|
from .route import RouterMixin as RouterMixin
|
|
54
52
|
from .utils import Deque
|
|
55
53
|
|
|
54
|
+
_T_endpoint = TypeVar("_T_endpoint", bound=Callable[[StarletteRequest], Awaitable[Response] | Response])
|
|
55
|
+
_T_ws_endpoint = TypeVar("_T_ws_endpoint", bound=Callable[[WebSocket], Awaitable[None]])
|
|
56
|
+
StarletteResponse = Response
|
|
57
|
+
|
|
56
58
|
|
|
57
59
|
async def _request_handler(
|
|
58
60
|
action: str, request: StarletteRequest, func: RouteCall, platform: str, self_id: str
|
|
@@ -134,8 +136,55 @@ class Server(Service, RouterMixin):
|
|
|
134
136
|
self.stream_threshold = stream_threshold
|
|
135
137
|
self.stream_chunk_size = stream_chunk_size
|
|
136
138
|
self.resources: dict[str, Path] = {}
|
|
139
|
+
self.app = Starlette()
|
|
137
140
|
super().__init__()
|
|
138
141
|
|
|
142
|
+
def replace_app(self, app: asgitypes.ASGI3Application):
|
|
143
|
+
"""替换当前的 Starlette 应用"""
|
|
144
|
+
self.app = app
|
|
145
|
+
|
|
146
|
+
def asgi_route(
|
|
147
|
+
self,
|
|
148
|
+
path: str,
|
|
149
|
+
methods: list[str] | None = None,
|
|
150
|
+
name: str | None = None,
|
|
151
|
+
include_in_schema: bool = True,
|
|
152
|
+
) -> Callable[[_T_endpoint], _T_endpoint]:
|
|
153
|
+
"""注册一个 ASGI 路由
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
path (str): 路由路径
|
|
157
|
+
methods (list[str], optional): 支持的 HTTP 方法,默认为 None,表示 ["GET"]
|
|
158
|
+
name (str, optional): 路由名称,默认为 None
|
|
159
|
+
include_in_schema (bool, optional): 是否包含在 OpenAPI 文档中,默认为 True
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def wrapper(endpoint: _T_endpoint, /) -> _T_endpoint:
|
|
163
|
+
self.app.add_route(
|
|
164
|
+
path, endpoint, methods=methods, name=name, include_in_schema=include_in_schema
|
|
165
|
+
)
|
|
166
|
+
return endpoint
|
|
167
|
+
|
|
168
|
+
return wrapper
|
|
169
|
+
|
|
170
|
+
def asgi_websocket_route(
|
|
171
|
+
self,
|
|
172
|
+
path: str,
|
|
173
|
+
name: str | None = None,
|
|
174
|
+
) -> Callable[[_T_ws_endpoint], _T_ws_endpoint]:
|
|
175
|
+
"""注册一个 ASGI WebSocket 路由
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
path (str): 路由路径
|
|
179
|
+
name (str, optional): 路由名称,默认为 None
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def wrapper(endpoint: _T_ws_endpoint, /) -> _T_ws_endpoint:
|
|
183
|
+
self.app.add_websocket_route(path, endpoint, name=name)
|
|
184
|
+
return endpoint
|
|
185
|
+
|
|
186
|
+
return wrapper
|
|
187
|
+
|
|
139
188
|
@property
|
|
140
189
|
def url_base(self):
|
|
141
190
|
return f"http://{self.host}:{self.port}{self.path}/{self.version}"
|
|
@@ -389,8 +438,8 @@ class Server(Service, RouterMixin):
|
|
|
389
438
|
|
|
390
439
|
async with self.stage("preparing"):
|
|
391
440
|
asgi_service = manager.get_component(UvicornASGIService)
|
|
392
|
-
app
|
|
393
|
-
|
|
441
|
+
self.app.routes.extend(
|
|
442
|
+
[
|
|
394
443
|
*chain.from_iterable(ada.get_routes() for ada in self._adapters),
|
|
395
444
|
WebSocketRoute(f"{self.path}/{self.version}/events", self.websocket_server_handler),
|
|
396
445
|
Route(
|
|
@@ -421,8 +470,8 @@ class Server(Service, RouterMixin):
|
|
|
421
470
|
]
|
|
422
471
|
)
|
|
423
472
|
for path, file in self.resources.items():
|
|
424
|
-
app.mount(path, StaticFiles(directory=file.parent, html=file.suffix == ".html"))
|
|
425
|
-
asgi_service.middleware.mounts[""] = app # type: ignore
|
|
473
|
+
self.app.mount(path, StaticFiles(directory=file.parent, html=file.suffix == ".html"))
|
|
474
|
+
asgi_service.middleware.mounts[""] = self.app # type: ignore
|
|
426
475
|
|
|
427
476
|
async def event_task(_provider: Provider):
|
|
428
477
|
async for event in _provider.publisher():
|
|
@@ -402,7 +402,7 @@ class RouterMixin:
|
|
|
402
402
|
def route(self, path: str) -> Callable[[INTERAL], INTERAL]: ...
|
|
403
403
|
|
|
404
404
|
def route(self, path: Union[str, Api]) -> Callable[[RouteCall], RouteCall]:
|
|
405
|
-
"""
|
|
405
|
+
"""注册一个 Satori 路由
|
|
406
406
|
|
|
407
407
|
Args:
|
|
408
408
|
path (str | Api): 路由路径;若 path 不属于 Api,则会被认为是内部接口
|
|
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
|