tauri-runtime 0.1.0__py3-none-any.whl
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.
- tauri_runtime/__init__.py +37 -0
- tauri_runtime/app.py +587 -0
- tauri_runtime/channel.py +79 -0
- tauri_runtime/cli/__init__.py +75 -0
- tauri_runtime/cli/bundle.py +127 -0
- tauri_runtime/cli/scaffold.py +310 -0
- tauri_runtime/cli/templates.py +92 -0
- tauri_runtime/command.py +206 -0
- tauri_runtime/event.py +35 -0
- tauri_runtime/protocol.py +483 -0
- tauri_runtime/py.typed +0 -0
- tauri_runtime/router.py +147 -0
- tauri_runtime-0.1.0.dist-info/METADATA +95 -0
- tauri_runtime-0.1.0.dist-info/RECORD +18 -0
- tauri_runtime-0.1.0.dist-info/WHEEL +4 -0
- tauri_runtime-0.1.0.dist-info/entry_points.txt +2 -0
- tauri_runtime-0.1.0.dist-info/licenses/LICENSE-APACHE +202 -0
- tauri_runtime-0.1.0.dist-info/licenses/LICENSE-MIT +21 -0
tauri_runtime/command.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""命令处理器:签名解析、参数绑定与校验,以及调用上下文 `Command` 和结构化错误 `TauriError`。
|
|
2
|
+
|
|
3
|
+
注册命令时,`build_handler` 根据参数注解将每个参数归类为:调用上下文(`Command`)、
|
|
4
|
+
应用实例(`App`)、二进制载荷(`Raw`)、通道(`Channel[T]`)或普通 JSON 参数;
|
|
5
|
+
普通参数汇总为每个处理器一个的 pydantic 模型;调用时由 `bind_arguments` 按类别填充实参,
|
|
6
|
+
普通参数经该模型一次性校验。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import inspect
|
|
12
|
+
import json
|
|
13
|
+
import typing
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, ValidationError, create_model
|
|
19
|
+
from pydantic.alias_generators import to_camel
|
|
20
|
+
|
|
21
|
+
from .channel import Channel
|
|
22
|
+
from .protocol import Raw
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TauriError(Exception):
|
|
26
|
+
"""带错误码的结构化错误,在命令处理器中抛出即可让调用以指定错误失败。
|
|
27
|
+
|
|
28
|
+
`code` 会原样透传给前端(`pyInvoke` 拒绝时的错误对象中的 `code`),可用于前端按错误码
|
|
29
|
+
分支处理。处理器抛出的其他异常一律转为 `internal` 错误。
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
code: 错误码,原样透传给前端。
|
|
33
|
+
message: 可读的错误描述。
|
|
34
|
+
data: 附加数据,须可序列化为 JSON;否则发送时会丢弃 `data` 并记录 `[WARN]` 日志。
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
code: 错误码。
|
|
38
|
+
message: 错误描述。
|
|
39
|
+
data: 附加数据。
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
```python
|
|
43
|
+
@app.command
|
|
44
|
+
async def load(path: str) -> str:
|
|
45
|
+
if not path:
|
|
46
|
+
raise TauriError("empty_path", "path must not be empty", {"path": path})
|
|
47
|
+
...
|
|
48
|
+
```
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, code: str, message: str, data: Any = None) -> None:
|
|
52
|
+
super().__init__(message)
|
|
53
|
+
self.code = code
|
|
54
|
+
self.message = message
|
|
55
|
+
self.data = data
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class Command:
|
|
60
|
+
"""单次命令调用的上下文;以 `Command` 注解参数即可注入。
|
|
61
|
+
|
|
62
|
+
Attributes:
|
|
63
|
+
id: 调用 id。
|
|
64
|
+
cmd: 命令名。
|
|
65
|
+
window: 发起调用的 webview 标签。
|
|
66
|
+
cancelled: 调用是否已被取消。同步处理器在取消后仍会继续运行,可轮询此标志提前退出。
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
id: int
|
|
70
|
+
cmd: str
|
|
71
|
+
window: str | None = None
|
|
72
|
+
cancelled: bool = False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
_CONTEXT, _APP, _RAW, _CHANNEL, _ARG = range(5)
|
|
76
|
+
|
|
77
|
+
# 键名规则:形参名经 `to_camel` 得到别名(`file_url_path` → `fileUrlPath`),别名与形参名均可作键;
|
|
78
|
+
# 连续大写的键(如 `fileURLPath`)不会被识别。通道键则经 `to_snake` 映射(`fileURLPath` → `file_url_path`)。
|
|
79
|
+
_MODEL_CONFIG = ConfigDict(
|
|
80
|
+
alias_generator=to_camel, populate_by_name=True, arbitrary_types_allowed=False, protected_namespaces=()
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class Param:
|
|
86
|
+
"""处理器的一个注入型形参;`kind` 为 `_CONTEXT`、`_APP`、`_RAW` 或 `_CHANNEL`。"""
|
|
87
|
+
|
|
88
|
+
name: str
|
|
89
|
+
kind: int
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class Handler:
|
|
94
|
+
"""已注册的命令:命令名、原始函数、注入型形参列表及 JSON 参数的 pydantic 模型。"""
|
|
95
|
+
|
|
96
|
+
name: str
|
|
97
|
+
func: Callable[..., Any]
|
|
98
|
+
is_async: bool
|
|
99
|
+
params: list[Param]
|
|
100
|
+
model: type[BaseModel]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _classify(annotation: Any, app_type: type) -> int:
|
|
104
|
+
"""按注解判定参数类别;`App` 子类同样视为应用实例。"""
|
|
105
|
+
origin = typing.get_origin(annotation) or annotation
|
|
106
|
+
if annotation is Command:
|
|
107
|
+
return _CONTEXT
|
|
108
|
+
if isinstance(annotation, type) and issubclass(annotation, app_type):
|
|
109
|
+
return _APP
|
|
110
|
+
if annotation is Raw:
|
|
111
|
+
return _RAW
|
|
112
|
+
if origin is Channel:
|
|
113
|
+
return _CHANNEL
|
|
114
|
+
return _ARG
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def build_handler(name: str, func: Callable[..., Any], app_type: type) -> Handler:
|
|
118
|
+
"""解析函数签名,生成可供路由与绑定使用的 `Handler`。
|
|
119
|
+
|
|
120
|
+
注入型参数之外的 JSON 参数汇总为一个 pydantic 模型;无注解的参数按 `Any` 处理。
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
name: 对外暴露的命令名。
|
|
124
|
+
func: 命令处理函数,同步或异步均可。
|
|
125
|
+
app_type: `App` 类型;注解为它(或其子类)的参数将注入应用实例。以参数传入是为了避免循环导入。
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
解析后的 `Handler`。
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
TypeError: 某个 JSON 参数的注解无法由 pydantic 校验,或参数名无法作为模型字段(如以 `_` 开头)。
|
|
132
|
+
"""
|
|
133
|
+
hints = typing.get_type_hints(func)
|
|
134
|
+
model_name = f"{name}_args"
|
|
135
|
+
params, fields = [], {}
|
|
136
|
+
for p in inspect.signature(func).parameters.values():
|
|
137
|
+
annotation = hints.get(p.name, Any)
|
|
138
|
+
kind = _classify(annotation, app_type)
|
|
139
|
+
if kind != _ARG:
|
|
140
|
+
params.append(Param(p.name, kind))
|
|
141
|
+
continue
|
|
142
|
+
spec = (annotation, ... if p.default is inspect.Parameter.empty else p.default)
|
|
143
|
+
try:
|
|
144
|
+
ok = p.name in create_model(model_name, __config__=_MODEL_CONFIG, **{p.name: spec}).model_fields
|
|
145
|
+
except Exception as e:
|
|
146
|
+
raise TypeError(f"command '{name}': unsupported annotation for parameter '{p.name}': {e}") from e
|
|
147
|
+
if not ok:
|
|
148
|
+
raise TypeError(f"command '{name}': parameter '{p.name}' cannot be a model field")
|
|
149
|
+
fields[p.name] = spec
|
|
150
|
+
model = create_model(model_name, __config__=_MODEL_CONFIG, **fields)
|
|
151
|
+
return Handler(name, func, inspect.iscoroutinefunction(func), params, model)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def bind_arguments(
|
|
155
|
+
handler: Handler,
|
|
156
|
+
args: Any,
|
|
157
|
+
raw: Raw | None,
|
|
158
|
+
channels: dict[str, Channel[Any]],
|
|
159
|
+
ctx: Command,
|
|
160
|
+
app: Any,
|
|
161
|
+
) -> dict[str, Any]:
|
|
162
|
+
"""为一次调用构造处理器的关键字实参。
|
|
163
|
+
|
|
164
|
+
JSON 参数对象经 `handler.model` 一次性校验,camelCase 别名与 snake_case 形参名均可作键,
|
|
165
|
+
未知键忽略。未提供且有默认值的形参不出现在结果中,交由 Python 使用默认值。
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
handler: 目标处理器。
|
|
169
|
+
args: 请求中的 JSON 参数,须为对象或 `None`。
|
|
170
|
+
raw: 二进制请求体;请求未携带二进制载荷时为 `None`。
|
|
171
|
+
channels: 以 snake_case 参数名为键的通道。
|
|
172
|
+
ctx: 调用上下文。
|
|
173
|
+
app: 应用实例。
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
关键字实参字典。
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
TauriError: 错误码均为 `invalid_args`:参数不是 JSON 对象、缺少通道、期望二进制载荷但未提供,
|
|
180
|
+
或 pydantic 校验失败(缺少必填参数、类型不符等;全部字段错误汇总于 `data`)。
|
|
181
|
+
"""
|
|
182
|
+
if args is None:
|
|
183
|
+
args = {}
|
|
184
|
+
if not isinstance(args, dict):
|
|
185
|
+
raise TauriError("invalid_args", "arguments must be a JSON object")
|
|
186
|
+
kwargs: dict[str, Any] = {}
|
|
187
|
+
for p in handler.params:
|
|
188
|
+
if p.kind == _CONTEXT:
|
|
189
|
+
kwargs[p.name] = ctx
|
|
190
|
+
elif p.kind == _APP:
|
|
191
|
+
kwargs[p.name] = app
|
|
192
|
+
elif p.kind == _RAW:
|
|
193
|
+
if raw is None:
|
|
194
|
+
raise TauriError("invalid_args", f"parameter '{p.name}' expects raw bytes")
|
|
195
|
+
kwargs[p.name] = raw
|
|
196
|
+
elif p.name not in channels:
|
|
197
|
+
raise TauriError("invalid_args", f"missing channel '{p.name}'")
|
|
198
|
+
else:
|
|
199
|
+
kwargs[p.name] = channels[p.name]
|
|
200
|
+
try:
|
|
201
|
+
values = handler.model.model_validate(args)
|
|
202
|
+
except ValidationError as e:
|
|
203
|
+
errors = json.loads(e.json(include_url=False))
|
|
204
|
+
fields = ", ".join(".".join(map(str, err["loc"])) for err in errors)
|
|
205
|
+
raise TauriError("invalid_args", f"invalid arguments: {fields}", errors) from None
|
|
206
|
+
return kwargs | {f: getattr(values, f) for f in values.model_fields_set}
|
tauri_runtime/event.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""事件:前端发来的 `Event` 对象,以及按事件名分发监听器的 `EventBus`。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Event:
|
|
11
|
+
"""前端通过 `pyEmit` 发送给 Python 的事件,作为唯一参数传给 `@app.listen` 监听器。
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
name: 事件名。
|
|
15
|
+
payload: 事件载荷;JSON 值,或二进制载荷对应的 `Raw`。
|
|
16
|
+
window: 发出事件的 webview 标签。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
payload: Any = None
|
|
21
|
+
window: str | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EventBus:
|
|
25
|
+
"""事件名到监听器列表的注册表。"""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._listeners: dict[str, list[Callable[..., Any]]] = {}
|
|
29
|
+
|
|
30
|
+
def add(self, name: str, func: Callable[..., Any]) -> None:
|
|
31
|
+
self._listeners.setdefault(name, []).append(func)
|
|
32
|
+
|
|
33
|
+
def get(self, name: str) -> list[Callable[..., Any]]:
|
|
34
|
+
"""返回监听器列表的副本,便于在分发期间安全地新增监听器。"""
|
|
35
|
+
return list(self._listeners.get(name, ()))
|