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.
Files changed (29) hide show
  1. {satori_python-0.16.4 → satori_python-0.16.5}/PKG-INFO +12 -5
  2. {satori_python-0.16.4 → satori_python-0.16.5}/README.md +11 -4
  3. {satori_python-0.16.4 → satori_python-0.16.5}/pyproject.toml +2 -1
  4. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/__init__.py +1 -1
  5. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/__init__.py +27 -0
  6. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/network/base.py +1 -1
  7. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/element.py +5 -2
  8. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/parser.py +2 -2
  9. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/server/__init__.py +65 -16
  10. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/server/route.py +1 -1
  11. {satori_python-0.16.4 → satori_python-0.16.5}/LICENSE +0 -0
  12. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/account.py +0 -0
  13. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/account.pyi +0 -0
  14. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/config.py +0 -0
  15. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/network/__init__.py +0 -0
  16. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/network/util.py +0 -0
  17. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/network/webhook.py +0 -0
  18. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/network/websocket.py +0 -0
  19. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/client/protocol.py +0 -0
  20. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/const.py +0 -0
  21. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/event.py +0 -0
  22. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/exception.py +0 -0
  23. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/model.py +0 -0
  24. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/server/adapter.py +0 -0
  25. /satori_python-0.16.4/src/satori/server/conection.py → /satori_python-0.16.5/src/satori/server/connection.py +0 -0
  26. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/server/formdata.py +0 -0
  27. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/server/model.py +0 -0
  28. {satori_python-0.16.4 → satori_python-0.16.5}/src/satori/server/utils.py +0 -0
  29. {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.4
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.4"
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]
@@ -41,4 +41,4 @@ from .model import Role as Role
41
41
  from .model import Upload as Upload
42
42
  from .model import User as User
43
43
 
44
- __version__ = "0.16.4"
44
+ __version__ = "0.16.5"
@@ -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:
@@ -8,7 +8,7 @@ from launart import Service
8
8
  from ..config import Config as Config
9
9
 
10
10
  if TYPE_CHECKING:
11
- from .. import Account, App
11
+ from satori.client import Account, App
12
12
 
13
13
  TConfig = TypeVar("TConfig", bound=Config)
14
14
 
@@ -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[int] = None
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[int] = None
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
- FileResponse,
30
- HTMLResponse,
31
- JSONResponse,
32
- PlainTextResponse,
33
- Response,
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 .conection import WebsocketConnection
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 = Starlette(
393
- routes=[
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