ErisPulse 2.5.2.dev3__py3-none-any.whl → 2.5.2.dev4__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.
- ErisPulse/CLI/cli.py +58 -0
- ErisPulse/CLI/cli.pyi +8 -0
- ErisPulse/CLI/commands/init.py +1 -1
- ErisPulse/CLI/commands/list.py +32 -0
- ErisPulse/CLI/commands/list.pyi +7 -0
- ErisPulse/CLI/commands/list_remote.py +1 -1
- ErisPulse/CLI/console.py +1 -0
- ErisPulse/CLI/i18n/locales/en.py +2 -0
- ErisPulse/CLI/i18n/locales/ja.py +2 -0
- ErisPulse/CLI/i18n/locales/ru.py +2 -0
- ErisPulse/CLI/i18n/locales/zh_cn.py +2 -0
- ErisPulse/CLI/i18n/locales/zh_tw.py +2 -0
- ErisPulse/Core/Bases/__init__.py +2 -0
- ErisPulse/Core/Bases/__init__.pyi +1 -0
- ErisPulse/Core/Bases/kv_builder.py +378 -0
- ErisPulse/Core/Bases/kv_builder.pyi +126 -0
- ErisPulse/Core/Bases/module.py +12 -3
- ErisPulse/Core/Bases/module.pyi +11 -3
- ErisPulse/Core/Bases/storage.py +99 -0
- ErisPulse/Core/Bases/storage.pyi +68 -0
- ErisPulse/Core/Event/__init__.py +0 -5
- ErisPulse/Core/Event/base.py +73 -11
- ErisPulse/Core/Event/base.pyi +1 -0
- ErisPulse/Core/Event/command.py +89 -0
- ErisPulse/Core/Event/command.pyi +1 -0
- ErisPulse/Core/Event/wrapper.py +197 -31
- ErisPulse/Core/Event/wrapper.pyi +139 -27
- ErisPulse/Core/__init__.py +2 -0
- ErisPulse/Core/__init__.pyi +1 -1
- ErisPulse/Core/config.py +45 -0
- ErisPulse/Core/config.pyi +29 -0
- ErisPulse/Core/constants.py +95 -93
- ErisPulse/Core/constants.pyi +188 -0
- ErisPulse/Core/i18n/locales/en.py +46 -0
- ErisPulse/Core/i18n/locales/ja.py +46 -0
- ErisPulse/Core/i18n/locales/ru.py +46 -0
- ErisPulse/Core/i18n/locales/zh_cn.py +46 -0
- ErisPulse/Core/i18n/locales/zh_tw.py +46 -0
- ErisPulse/Core/lifecycle.py +19 -1
- ErisPulse/Core/lifecycle.pyi +2 -0
- ErisPulse/Core/router.py +30 -0
- ErisPulse/Core/storage.py +22 -0
- ErisPulse/runtime/__init__.py +11 -0
- ErisPulse/runtime/__init__.pyi +1 -0
- ErisPulse/runtime/exceptions.py +120 -19
- ErisPulse/runtime/exceptions.pyi +26 -1
- ErisPulse/runtime/hints.py +340 -0
- ErisPulse/runtime/hints.pyi +127 -0
- ErisPulse/sdk.py +81 -1
- ErisPulse/sdk.pyi +7 -0
- {erispulse-2.5.2.dev3.dist-info → erispulse-2.5.2.dev4.dist-info}/METADATA +46 -45
- {erispulse-2.5.2.dev3.dist-info → erispulse-2.5.2.dev4.dist-info}/RECORD +55 -51
- {erispulse-2.5.2.dev3.dist-info → erispulse-2.5.2.dev4.dist-info}/WHEEL +0 -0
- {erispulse-2.5.2.dev3.dist-info → erispulse-2.5.2.dev4.dist-info}/entry_points.txt +0 -0
- {erispulse-2.5.2.dev3.dist-info → erispulse-2.5.2.dev4.dist-info}/licenses/LICENSE +0 -0
ErisPulse/CLI/cli.py
CHANGED
|
@@ -149,6 +149,51 @@ class CLI:
|
|
|
149
149
|
)
|
|
150
150
|
)
|
|
151
151
|
|
|
152
|
+
def _check_command_typo(self) -> None:
|
|
153
|
+
"""
|
|
154
|
+
在 argparse 解析之前检查命令拼写
|
|
155
|
+
|
|
156
|
+
argparse 的子命令 choices 验证遇到无效命令时会直接打印错误并退出,
|
|
157
|
+
无法附加自定义提示。因此在此提前拦截,给出"你是不是想用 xxx"的拼写建议。
|
|
158
|
+
"""
|
|
159
|
+
# 所有有效命令(规范名 + 别名),用于检查输入是否有效
|
|
160
|
+
all_valid = set(self.registry.list_all()) | set(
|
|
161
|
+
self.registry.list_aliases().keys()
|
|
162
|
+
)
|
|
163
|
+
# 仅规范命令名作为建议候选(不含短别名/缩写,避免扰民)
|
|
164
|
+
canonical_names = self.registry.list_all()
|
|
165
|
+
|
|
166
|
+
# 从 sys.argv 中找到第一个非选项参数(即命令名)
|
|
167
|
+
cmd = None
|
|
168
|
+
for arg in sys.argv[1:]:
|
|
169
|
+
if not arg.startswith("-"):
|
|
170
|
+
cmd = arg
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
if not cmd or cmd in all_valid:
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
# 命令无效,给出拼写建议(前缀加成确保 ins → install 而非 list)
|
|
177
|
+
from ..runtime.hints import best_match_with_prefix
|
|
178
|
+
|
|
179
|
+
print_banner()
|
|
180
|
+
suggestion = best_match_with_prefix(
|
|
181
|
+
cmd, canonical_names, cutoff=0.5
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
console.print(
|
|
185
|
+
f"[error]{i18n.t('cli.run.unknown_command', command=cmd)}[/]"
|
|
186
|
+
)
|
|
187
|
+
if suggestion:
|
|
188
|
+
console.print(
|
|
189
|
+
f"[hint]{i18n.t('cli.run.did_you_mean', name=suggestion)}[/]"
|
|
190
|
+
)
|
|
191
|
+
console.print()
|
|
192
|
+
console.print(f" [dim]epsdk {suggestion} --help[/]")
|
|
193
|
+
else:
|
|
194
|
+
self.parser.print_help()
|
|
195
|
+
sys.exit(1)
|
|
196
|
+
|
|
152
197
|
def _maybe_show_language_hint(self) -> None:
|
|
153
198
|
"""
|
|
154
199
|
在前几次启动时提醒用户确认语言
|
|
@@ -197,6 +242,9 @@ class CLI:
|
|
|
197
242
|
:raises KeyboardInterrupt: 用户中断时抛出
|
|
198
243
|
:raises Exception: 命令执行失败时抛出
|
|
199
244
|
"""
|
|
245
|
+
# 在 argparse 之前检查命令拼写(argparse 的 choices 验证会直接报错退出)
|
|
246
|
+
self._check_command_typo()
|
|
247
|
+
|
|
200
248
|
args, unknown = self.parser.parse_known_args()
|
|
201
249
|
args._unknown_args = unknown
|
|
202
250
|
|
|
@@ -240,6 +288,16 @@ class CLI:
|
|
|
240
288
|
console.print()
|
|
241
289
|
command.execute(args)
|
|
242
290
|
else:
|
|
291
|
+
# 拼写建议:检查是否输入了与已知命令相似的名称
|
|
292
|
+
from ..runtime.hints import best_match_with_prefix
|
|
293
|
+
|
|
294
|
+
suggestion = best_match_with_prefix(
|
|
295
|
+
args.command, self.registry.list_all(), cutoff=0.5
|
|
296
|
+
)
|
|
297
|
+
if suggestion:
|
|
298
|
+
console.print(
|
|
299
|
+
f"[hint]{i18n.t('cli.run.did_you_mean', name=suggestion)}[/]"
|
|
300
|
+
)
|
|
243
301
|
console.print(
|
|
244
302
|
f"[error]{i18n.t('cli.run.unknown_command', command=args.command)}[/]"
|
|
245
303
|
)
|
ErisPulse/CLI/cli.pyi
CHANGED
|
@@ -58,6 +58,14 @@ class CLI:
|
|
|
58
58
|
打印版本信息
|
|
59
59
|
"""
|
|
60
60
|
...
|
|
61
|
+
def _check_command_typo(self: object) -> None:
|
|
62
|
+
"""
|
|
63
|
+
在 argparse 解析之前检查命令拼写
|
|
64
|
+
|
|
65
|
+
argparse 的子命令 choices 验证遇到无效命令时会直接打印错误并退出,
|
|
66
|
+
无法附加自定义提示。因此在此提前拦截,给出"你是不是想用 xxx"的拼写建议。
|
|
67
|
+
"""
|
|
68
|
+
...
|
|
61
69
|
def _maybe_show_language_hint(self: object) -> None:
|
|
62
70
|
"""
|
|
63
71
|
在前几次启动时提醒用户确认语言
|
ErisPulse/CLI/commands/init.py
CHANGED
ErisPulse/CLI/commands/list.py
CHANGED
|
@@ -104,6 +104,8 @@ class ListCommand(Command):
|
|
|
104
104
|
console.print(
|
|
105
105
|
f"[dim] {i18n.t('cli.list.count_modules', count=count)}[/]"
|
|
106
106
|
)
|
|
107
|
+
# 展示模块注册的脚本入口
|
|
108
|
+
self._print_package_scripts(installed["modules"])
|
|
107
109
|
else:
|
|
108
110
|
console.print(f"[dim] {i18n.t('cli.list.no_modules')}[/]")
|
|
109
111
|
|
|
@@ -162,3 +164,33 @@ class ListCommand(Command):
|
|
|
162
164
|
if adapter_info["package"] == package_name:
|
|
163
165
|
return adapter_info["version"] != current_version
|
|
164
166
|
return False
|
|
167
|
+
|
|
168
|
+
def _print_package_scripts(self, packages: dict) -> None:
|
|
169
|
+
"""
|
|
170
|
+
发现并展示已安装模块包注册的 console_scripts 入口
|
|
171
|
+
|
|
172
|
+
:param packages: [dict] 模块信息字典 {name: {package, version, ...}}
|
|
173
|
+
"""
|
|
174
|
+
import importlib.metadata
|
|
175
|
+
|
|
176
|
+
all_scripts: list[tuple[str, str]] = []
|
|
177
|
+
|
|
178
|
+
for module_name, info in packages.items():
|
|
179
|
+
package_name = info.get("package", "")
|
|
180
|
+
if not package_name:
|
|
181
|
+
continue
|
|
182
|
+
try:
|
|
183
|
+
dist = importlib.metadata.distribution(package_name)
|
|
184
|
+
for ep in dist.entry_points:
|
|
185
|
+
if ep.group == "console_scripts":
|
|
186
|
+
all_scripts.append((module_name, ep.name))
|
|
187
|
+
except Exception:
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
if not all_scripts:
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
console.print()
|
|
194
|
+
console.print(f" [dim]{i18n.t('cli.list.scripts_header')}[/]")
|
|
195
|
+
for module_name, script_name in all_scripts:
|
|
196
|
+
console.print(f" [module]{module_name}[/] [cyan]{script_name}[/]")
|
ErisPulse/CLI/commands/list.pyi
CHANGED
|
@@ -52,3 +52,10 @@ class ListCommand(Command):
|
|
|
52
52
|
:return: [bool] 存在更新版本返回 True,否则返回 False
|
|
53
53
|
"""
|
|
54
54
|
...
|
|
55
|
+
def _print_package_scripts(self: object, packages: dict) -> None:
|
|
56
|
+
"""
|
|
57
|
+
发现并展示已安装模块包注册的 console_scripts 入口
|
|
58
|
+
|
|
59
|
+
:param packages: [dict] 模块信息字典 {name: {package, version, ...}}
|
|
60
|
+
"""
|
|
61
|
+
...
|
ErisPulse/CLI/console.py
CHANGED
ErisPulse/CLI/i18n/locales/en.py
CHANGED
|
@@ -15,6 +15,7 @@ TRANSLATIONS = {
|
|
|
15
15
|
"cli.parser.optionals_title": "Options",
|
|
16
16
|
"cli.run.unknown_args": "Unknown arguments: {args}",
|
|
17
17
|
"cli.run.unknown_command": "Unknown command: {command}",
|
|
18
|
+
"cli.run.did_you_mean": "Did you mean '{name}'?",
|
|
18
19
|
"cli.run.user_interrupted": "Operation interrupted by user",
|
|
19
20
|
"cli.run.exec_error": "Error executing command: {error}",
|
|
20
21
|
"cli.run.version_text": "ErisPulse SDK version: {version}",
|
|
@@ -176,6 +177,7 @@ TRANSLATIONS = {
|
|
|
176
177
|
"cli.list.status_disabled": "Disabled",
|
|
177
178
|
"cli.list.count_modules": "{count} module(s)",
|
|
178
179
|
"cli.list.no_modules": "No matching modules",
|
|
180
|
+
"cli.list.scripts_header": "Module-registered CLI tools:",
|
|
179
181
|
"cli.list.header_adapter": "Adapter Name",
|
|
180
182
|
"cli.list.count_adapters": "{count} adapter(s)",
|
|
181
183
|
"cli.list.no_adapters": "No matching adapters",
|
ErisPulse/CLI/i18n/locales/ja.py
CHANGED
|
@@ -15,6 +15,7 @@ TRANSLATIONS = {
|
|
|
15
15
|
"cli.parser.optionals_title": "オプション",
|
|
16
16
|
"cli.run.unknown_args": "認識できない引数: {args}",
|
|
17
17
|
"cli.run.unknown_command": "未知のコマンド: {command}",
|
|
18
|
+
"cli.run.did_you_mean": "もしかして '{name}' コマンドですか?",
|
|
18
19
|
"cli.run.user_interrupted": "操作がユーザーにより中断されました",
|
|
19
20
|
"cli.run.exec_error": "コマンド実行中にエラーが発生しました: {error}",
|
|
20
21
|
"cli.run.version_text": "ErisPulse SDK バージョン: {version}",
|
|
@@ -176,6 +177,7 @@ TRANSLATIONS = {
|
|
|
176
177
|
"cli.list.status_disabled": "無効",
|
|
177
178
|
"cli.list.count_modules": "{count} 個のモジュール",
|
|
178
179
|
"cli.list.no_modules": "該当するモジュールはありません",
|
|
180
|
+
"cli.list.scripts_header": "モジュール登録のコマンドラインツール:",
|
|
179
181
|
"cli.list.header_adapter": "アダプター名",
|
|
180
182
|
"cli.list.count_adapters": "{count} 個のアダプター",
|
|
181
183
|
"cli.list.no_adapters": "該当するアダプターはありません",
|
ErisPulse/CLI/i18n/locales/ru.py
CHANGED
|
@@ -15,6 +15,7 @@ TRANSLATIONS = {
|
|
|
15
15
|
"cli.parser.optionals_title": "Параметры",
|
|
16
16
|
"cli.run.unknown_args": "Неизвестные аргументы: {args}",
|
|
17
17
|
"cli.run.unknown_command": "Неизвестная команда: {command}",
|
|
18
|
+
"cli.run.did_you_mean": "Возможно, вы имели в виду команду '{name}'?",
|
|
18
19
|
"cli.run.user_interrupted": "Операция прервана пользователем",
|
|
19
20
|
"cli.run.exec_error": "Ошибка при выполнении команды: {error}",
|
|
20
21
|
"cli.run.version_text": "ErisPulse SDK версия: {version}",
|
|
@@ -176,6 +177,7 @@ TRANSLATIONS = {
|
|
|
176
177
|
"cli.list.status_disabled": "Отключен",
|
|
177
178
|
"cli.list.count_modules": "{count} модуля(ей)",
|
|
178
179
|
"cli.list.no_modules": "Нет подходящих модулей",
|
|
180
|
+
"cli.list.scripts_header": "Инструменты командной строки модулей:",
|
|
179
181
|
"cli.list.header_adapter": "Имя адаптера",
|
|
180
182
|
"cli.list.count_adapters": "{count} адаптера(ов)",
|
|
181
183
|
"cli.list.no_adapters": "Нет подходящих адаптеров",
|
|
@@ -15,6 +15,7 @@ TRANSLATIONS = {
|
|
|
15
15
|
"cli.parser.optionals_title": "选项",
|
|
16
16
|
"cli.run.unknown_args": "未识别的参数: {args}",
|
|
17
17
|
"cli.run.unknown_command": "未知命令: {command}",
|
|
18
|
+
"cli.run.did_you_mean": "你是不是想用 '{name}' 命令?",
|
|
18
19
|
"cli.run.user_interrupted": "操作被用户中断",
|
|
19
20
|
"cli.run.exec_error": "执行命令时出错: {error}",
|
|
20
21
|
"cli.run.version_text": "ErisPulse SDK 版本: {version}",
|
|
@@ -176,6 +177,7 @@ TRANSLATIONS = {
|
|
|
176
177
|
"cli.list.status_disabled": "禁用",
|
|
177
178
|
"cli.list.count_modules": "{count} 个模块",
|
|
178
179
|
"cli.list.no_modules": "没有符合条件的模块",
|
|
180
|
+
"cli.list.scripts_header": "模块注册的命令行工具:",
|
|
179
181
|
"cli.list.header_adapter": "适配器名",
|
|
180
182
|
"cli.list.count_adapters": "{count} 个适配器",
|
|
181
183
|
"cli.list.no_adapters": "没有符合条件的适配器",
|
|
@@ -15,6 +15,7 @@ TRANSLATIONS = {
|
|
|
15
15
|
"cli.parser.optionals_title": "選項",
|
|
16
16
|
"cli.run.unknown_args": "無法識別的參數: {args}",
|
|
17
17
|
"cli.run.unknown_command": "未知的命令: {command}",
|
|
18
|
+
"cli.run.did_you_mean": "你是不是想用 '{name}' 指令?",
|
|
18
19
|
"cli.run.user_interrupted": "操作被使用者中斷",
|
|
19
20
|
"cli.run.exec_error": "執行命令時發生錯誤: {error}",
|
|
20
21
|
"cli.run.version_text": "ErisPulse SDK 版本: {version}",
|
|
@@ -176,6 +177,7 @@ TRANSLATIONS = {
|
|
|
176
177
|
"cli.list.status_disabled": "停用",
|
|
177
178
|
"cli.list.count_modules": "{count} 個模組",
|
|
178
179
|
"cli.list.no_modules": "沒有符合條件的模組",
|
|
180
|
+
"cli.list.scripts_header": "模組註冊的命令列工具:",
|
|
179
181
|
"cli.list.header_adapter": "適配器名",
|
|
180
182
|
"cli.list.count_adapters": "{count} 個適配器",
|
|
181
183
|
"cli.list.no_adapters": "沒有符合條件的適配器",
|
ErisPulse/Core/Bases/__init__.py
CHANGED
|
@@ -7,6 +7,7 @@ ErisPulse 基础模块
|
|
|
7
7
|
from .adapter import SendDSL, RequestDSL, BaseAdapter
|
|
8
8
|
from .module import BaseModule
|
|
9
9
|
from .storage import BaseStorage, BaseQueryBuilder
|
|
10
|
+
from .kv_builder import KVQueryBuilder
|
|
10
11
|
from .errors import (
|
|
11
12
|
ErisPulseError,
|
|
12
13
|
ClientError,
|
|
@@ -27,6 +28,7 @@ __all__ = [
|
|
|
27
28
|
"BaseModule",
|
|
28
29
|
"BaseStorage",
|
|
29
30
|
"BaseQueryBuilder",
|
|
31
|
+
"KVQueryBuilder",
|
|
30
32
|
"HttpRequest",
|
|
31
33
|
"WebSocketConnection",
|
|
32
34
|
"WebSocketConnectionBase",
|
|
@@ -13,6 +13,7 @@ ErisPulse 基础模块
|
|
|
13
13
|
from .adapter import SendDSL, RequestDSL, BaseAdapter
|
|
14
14
|
from .module import BaseModule
|
|
15
15
|
from .storage import BaseStorage, BaseQueryBuilder
|
|
16
|
+
from .kv_builder import KVQueryBuilder
|
|
16
17
|
from .errors import ErisPulseError, ClientError, ClientConnectionError, ClientTimeoutError, HTTPStatusError, WebSocketError, WebSocketDisconnect
|
|
17
18
|
from .websocket import WebSocketConnectionBase, WSMessage
|
|
18
19
|
from .router import HttpRequest, WebSocketConnection
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ErisPulse KV 查询构建器
|
|
3
|
+
|
|
4
|
+
将链式 SQL 操作(Table/Insert/Select/Where 等)映射为 KV 键前缀操作。
|
|
5
|
+
任何实现了 BaseStorage KV 接口的后端(Redis、内存字典等)均可使用。
|
|
6
|
+
|
|
7
|
+
键命名规则:
|
|
8
|
+
_table:{table_name}:schema — 表结构定义(列名 → 类型)
|
|
9
|
+
_table:{table_name}:next_id — 自增 ID 计数器
|
|
10
|
+
_table:{table_name}:data:{id} — 行数据(JSON 序列化)
|
|
11
|
+
|
|
12
|
+
使用方式:
|
|
13
|
+
>>> storage = MyKVStorage()
|
|
14
|
+
>>> qb = KVQueryBuilder(storage, "users")
|
|
15
|
+
>>> qb.Insert({"name": "Alice", "age": 30}).Execute()
|
|
16
|
+
|
|
17
|
+
{!--< tips >!--}
|
|
18
|
+
1. 查询性能取决于 get_all_keys() 的效率
|
|
19
|
+
2. WHERE 条件在 Python 内存中过滤,不适合百万级数据
|
|
20
|
+
3. 适合中小规模的结构化数据存储
|
|
21
|
+
{!--< /tips >!--}
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from .storage import BaseQueryBuilder
|
|
28
|
+
|
|
29
|
+
_TABLE_PREFIX = "__erispulse_sql__"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class KVQueryBuilder(BaseQueryBuilder):
|
|
33
|
+
"""
|
|
34
|
+
基于 KV 存储的查询构建器
|
|
35
|
+
|
|
36
|
+
将 Insert/Select/Update/Delete/Where/OrderBy/Limit 等链式操作
|
|
37
|
+
映射为对 BaseStorage KV 接口的调用。
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, storage, table: str):
|
|
41
|
+
super().__init__(storage, table)
|
|
42
|
+
self._table_prefix = f"{_TABLE_PREFIX}:{table}"
|
|
43
|
+
self._bound_clauses: list[str] | None = None
|
|
44
|
+
|
|
45
|
+
# ---- key helpers ----
|
|
46
|
+
|
|
47
|
+
def _schema_key(self) -> str:
|
|
48
|
+
return f"{self._table_prefix}:schema"
|
|
49
|
+
|
|
50
|
+
def _next_id_key(self) -> str:
|
|
51
|
+
return f"{self._table_prefix}:next_id"
|
|
52
|
+
|
|
53
|
+
def _row_key(self, row_id: int) -> str:
|
|
54
|
+
return f"{self._table_prefix}:data:{row_id}"
|
|
55
|
+
|
|
56
|
+
def _row_prefix(self) -> str:
|
|
57
|
+
return f"{self._table_prefix}:data:"
|
|
58
|
+
|
|
59
|
+
# ---- ID management ----
|
|
60
|
+
|
|
61
|
+
def _get_next_id(self) -> int:
|
|
62
|
+
"""获取并递增自增 ID"""
|
|
63
|
+
current = self._storage.get(self._next_id_key(), 1)
|
|
64
|
+
next_id = int(current)
|
|
65
|
+
self._storage.set(self._next_id_key(), next_id + 1)
|
|
66
|
+
return next_id
|
|
67
|
+
|
|
68
|
+
def _set_next_id(self, value: int) -> None:
|
|
69
|
+
self._storage.set(self._next_id_key(), value)
|
|
70
|
+
|
|
71
|
+
# ---- row scan ----
|
|
72
|
+
|
|
73
|
+
def _scan_rows(self) -> list[tuple[int, dict]]:
|
|
74
|
+
"""扫描所有行,返回 [(row_id, row_data), ...]"""
|
|
75
|
+
all_keys = self._storage.get_all_keys()
|
|
76
|
+
rows = []
|
|
77
|
+
for key in all_keys:
|
|
78
|
+
if key.startswith(self._row_prefix()):
|
|
79
|
+
try:
|
|
80
|
+
row_id = int(key.rsplit(":", 1)[-1])
|
|
81
|
+
except (ValueError, IndexError):
|
|
82
|
+
continue
|
|
83
|
+
value = self._storage.get(key)
|
|
84
|
+
if isinstance(value, str):
|
|
85
|
+
try:
|
|
86
|
+
value = json.loads(value)
|
|
87
|
+
except json.JSONDecodeError:
|
|
88
|
+
# 兼容非 JSON 值
|
|
89
|
+
pass
|
|
90
|
+
if isinstance(value, dict):
|
|
91
|
+
rows.append((row_id, value))
|
|
92
|
+
return rows
|
|
93
|
+
|
|
94
|
+
# ---- where matching ----
|
|
95
|
+
|
|
96
|
+
def _match_row(self, row: dict) -> bool:
|
|
97
|
+
"""检查行是否满足所有 WHERE 条件"""
|
|
98
|
+
if self._bound_clauses is None:
|
|
99
|
+
self._bind_clauses()
|
|
100
|
+
for clause in self._bound_clauses:
|
|
101
|
+
if not self._eval_clause(row, clause):
|
|
102
|
+
return False
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
def _bind_clauses(self):
|
|
106
|
+
"""将 WHERE 子句中的 ? 替换为实际参数(只执行一次)"""
|
|
107
|
+
self._bound_clauses = []
|
|
108
|
+
params = list(self._where_params)
|
|
109
|
+
for clause in self._where_clauses:
|
|
110
|
+
if "?" in clause and params:
|
|
111
|
+
val = params.pop(0)
|
|
112
|
+
if isinstance(val, str):
|
|
113
|
+
val = f"'{val}'"
|
|
114
|
+
clause = clause.replace("?", str(val), 1)
|
|
115
|
+
self._bound_clauses.append(clause)
|
|
116
|
+
|
|
117
|
+
def _eval_clause(self, row: dict, clause: str) -> bool:
|
|
118
|
+
"""评估单条已绑定的 WHERE 子句
|
|
119
|
+
|
|
120
|
+
clause 中的占位符 `?` 已在 `_bind_clauses` 中替换为实际值。
|
|
121
|
+
支持的操作符: =, !=, >, >=, <, <=, LIKE
|
|
122
|
+
"""
|
|
123
|
+
parts = clause.strip().split(None, 2)
|
|
124
|
+
if len(parts) < 3:
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
column, op, expected = parts[0], parts[1], parts[2]
|
|
128
|
+
|
|
129
|
+
# 去掉引号
|
|
130
|
+
expected = expected.strip("'\"")
|
|
131
|
+
|
|
132
|
+
actual = row.get(column)
|
|
133
|
+
|
|
134
|
+
# 类型转换
|
|
135
|
+
try:
|
|
136
|
+
expected = self._coerce_value(expected, actual)
|
|
137
|
+
except (ValueError, TypeError):
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
if op == "=":
|
|
141
|
+
return actual == expected
|
|
142
|
+
elif op == "!=":
|
|
143
|
+
return actual != expected
|
|
144
|
+
elif op == ">":
|
|
145
|
+
return self._safe_cmp(actual, expected) > 0
|
|
146
|
+
elif op == ">=":
|
|
147
|
+
return self._safe_cmp(actual, expected) >= 0
|
|
148
|
+
elif op == "<":
|
|
149
|
+
return self._safe_cmp(actual, expected) < 0
|
|
150
|
+
elif op == "<=":
|
|
151
|
+
return self._safe_cmp(actual, expected) <= 0
|
|
152
|
+
elif op.upper() == "LIKE":
|
|
153
|
+
pattern = str(expected).replace("%", ".*").replace("_", ".")
|
|
154
|
+
import re
|
|
155
|
+
return bool(re.match(pattern, str(actual), re.IGNORECASE))
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _coerce_value(expected: Any, actual: Any) -> Any:
|
|
160
|
+
"""根据 actual 的类型,将 expected 转换为同类型"""
|
|
161
|
+
if expected is None or actual is None:
|
|
162
|
+
return expected
|
|
163
|
+
if isinstance(actual, bool):
|
|
164
|
+
return str(expected).lower() in ("true", "1", "yes")
|
|
165
|
+
if isinstance(actual, int):
|
|
166
|
+
return int(expected)
|
|
167
|
+
if isinstance(actual, float):
|
|
168
|
+
return float(expected)
|
|
169
|
+
return str(expected)
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _safe_cmp(a: Any, b: Any) -> int:
|
|
173
|
+
"""安全比较,处理 None"""
|
|
174
|
+
if a is None and b is None:
|
|
175
|
+
return 0
|
|
176
|
+
if a is None:
|
|
177
|
+
return -1
|
|
178
|
+
if b is None:
|
|
179
|
+
return 1
|
|
180
|
+
try:
|
|
181
|
+
if a < b:
|
|
182
|
+
return -1
|
|
183
|
+
elif a > b:
|
|
184
|
+
return 1
|
|
185
|
+
return 0
|
|
186
|
+
except TypeError:
|
|
187
|
+
return -1 if str(a) < str(b) else 1
|
|
188
|
+
|
|
189
|
+
# ---- Execute ----
|
|
190
|
+
|
|
191
|
+
def Execute(self) -> list[tuple] | int:
|
|
192
|
+
if self._operation == "insert":
|
|
193
|
+
return self._exec_insert()
|
|
194
|
+
elif self._operation == "insert_multi":
|
|
195
|
+
return self._exec_insert_multi()
|
|
196
|
+
elif self._operation == "select":
|
|
197
|
+
return self._exec_select()
|
|
198
|
+
elif self._operation == "update":
|
|
199
|
+
return self._exec_update()
|
|
200
|
+
elif self._operation == "delete":
|
|
201
|
+
return self._exec_delete()
|
|
202
|
+
raise ValueError(f"Unknown operation: {self._operation}")
|
|
203
|
+
|
|
204
|
+
def _exec_insert(self) -> int:
|
|
205
|
+
if not isinstance(self._data, dict):
|
|
206
|
+
raise ValueError("Insert 操作需要字典数据")
|
|
207
|
+
row_id = self._get_next_id()
|
|
208
|
+
self._storage.set(self._row_key(row_id), json.dumps(self._data, ensure_ascii=False))
|
|
209
|
+
return 1
|
|
210
|
+
|
|
211
|
+
def _exec_insert_multi(self) -> int:
|
|
212
|
+
if not isinstance(self._data, list):
|
|
213
|
+
raise ValueError("InsertMulti 操作需要列表数据")
|
|
214
|
+
count = 0
|
|
215
|
+
for row in self._data:
|
|
216
|
+
row_id = self._get_next_id()
|
|
217
|
+
self._storage.set(self._row_key(row_id), json.dumps(row, ensure_ascii=False))
|
|
218
|
+
count += 1
|
|
219
|
+
return count
|
|
220
|
+
|
|
221
|
+
def _exec_select(self) -> list[tuple]:
|
|
222
|
+
rows = self._scan_rows()
|
|
223
|
+
# filter
|
|
224
|
+
if self._where_clauses:
|
|
225
|
+
rows = [(rid, r) for rid, r in rows if self._match_row(r)]
|
|
226
|
+
# order
|
|
227
|
+
if self._order_by:
|
|
228
|
+
for col, desc in reversed(self._order_by):
|
|
229
|
+
try:
|
|
230
|
+
rows.sort(key=lambda x: x[1].get(col, ""), reverse=desc)
|
|
231
|
+
except TypeError:
|
|
232
|
+
rows.sort(key=lambda x: str(x[1].get(col, "")), reverse=desc)
|
|
233
|
+
# offset
|
|
234
|
+
if self._offset:
|
|
235
|
+
rows = rows[self._offset:]
|
|
236
|
+
# limit
|
|
237
|
+
if self._limit is not None:
|
|
238
|
+
rows = rows[:self._limit]
|
|
239
|
+
# project
|
|
240
|
+
if self._columns:
|
|
241
|
+
return [tuple(r.get(c) for c in self._columns) for _, r in rows]
|
|
242
|
+
else:
|
|
243
|
+
return [tuple(r.values()) for _, r in rows]
|
|
244
|
+
|
|
245
|
+
def _exec_update(self) -> int:
|
|
246
|
+
if not isinstance(self._data, dict):
|
|
247
|
+
raise ValueError("Update 操作需要字典数据")
|
|
248
|
+
count = 0
|
|
249
|
+
for row_id, row in self._scan_rows():
|
|
250
|
+
if self._match_row(row):
|
|
251
|
+
row.update(self._data)
|
|
252
|
+
self._storage.set(self._row_key(row_id), json.dumps(row, ensure_ascii=False))
|
|
253
|
+
count += 1
|
|
254
|
+
return count
|
|
255
|
+
|
|
256
|
+
def _exec_delete(self) -> int:
|
|
257
|
+
count = 0
|
|
258
|
+
for row_id, row in self._scan_rows():
|
|
259
|
+
if self._match_row(row):
|
|
260
|
+
self._storage.delete(self._row_key(row_id))
|
|
261
|
+
count += 1
|
|
262
|
+
return count
|
|
263
|
+
|
|
264
|
+
# ---- ExecuteOne / Count / Exists ----
|
|
265
|
+
|
|
266
|
+
def ExecuteOne(self) -> tuple | None:
|
|
267
|
+
old_limit = self._limit
|
|
268
|
+
self._limit = 1
|
|
269
|
+
try:
|
|
270
|
+
rows = self._exec_select()
|
|
271
|
+
return rows[0] if rows else None
|
|
272
|
+
finally:
|
|
273
|
+
self._limit = old_limit
|
|
274
|
+
|
|
275
|
+
def Count(self) -> int:
|
|
276
|
+
rows = self._scan_rows()
|
|
277
|
+
if self._where_clauses:
|
|
278
|
+
rows = [(rid, r) for rid, r in rows if self._match_row(r)]
|
|
279
|
+
return len(rows)
|
|
280
|
+
|
|
281
|
+
def Exists(self) -> bool:
|
|
282
|
+
return self.Count() > 0
|
|
283
|
+
|
|
284
|
+
# ==================== 异步接口 ====================
|
|
285
|
+
|
|
286
|
+
async def _ascan_rows(self) -> list[tuple[int, dict]]:
|
|
287
|
+
"""异步扫描所有行"""
|
|
288
|
+
all_keys = await self._storage.aget_all_keys()
|
|
289
|
+
rows = []
|
|
290
|
+
for key in all_keys:
|
|
291
|
+
if key.startswith(self._row_prefix()):
|
|
292
|
+
try:
|
|
293
|
+
row_id = int(key.rsplit(":", 1)[-1])
|
|
294
|
+
except (ValueError, IndexError):
|
|
295
|
+
continue
|
|
296
|
+
value = await self._storage.aget(key)
|
|
297
|
+
if isinstance(value, str):
|
|
298
|
+
try:
|
|
299
|
+
value = json.loads(value)
|
|
300
|
+
except json.JSONDecodeError:
|
|
301
|
+
pass
|
|
302
|
+
if isinstance(value, dict):
|
|
303
|
+
rows.append((row_id, value))
|
|
304
|
+
return rows
|
|
305
|
+
|
|
306
|
+
async def aExecute(self) -> list[tuple] | int:
|
|
307
|
+
"""异步执行查询"""
|
|
308
|
+
if self._operation == "insert":
|
|
309
|
+
if not isinstance(self._data, dict):
|
|
310
|
+
raise ValueError("Insert 操作需要字典数据")
|
|
311
|
+
row_id = self._get_next_id()
|
|
312
|
+
await self._storage.aset(self._row_key(row_id), json.dumps(self._data, ensure_ascii=False))
|
|
313
|
+
return 1
|
|
314
|
+
elif self._operation == "insert_multi":
|
|
315
|
+
if not isinstance(self._data, list):
|
|
316
|
+
raise ValueError("InsertMulti 操作需要列表数据")
|
|
317
|
+
count = 0
|
|
318
|
+
for row in self._data:
|
|
319
|
+
row_id = self._get_next_id()
|
|
320
|
+
await self._storage.aset(self._row_key(row_id), json.dumps(row, ensure_ascii=False))
|
|
321
|
+
count += 1
|
|
322
|
+
return count
|
|
323
|
+
elif self._operation == "select":
|
|
324
|
+
return await self._aexec_select()
|
|
325
|
+
elif self._operation == "update":
|
|
326
|
+
if not isinstance(self._data, dict):
|
|
327
|
+
raise ValueError("Update 操作需要字典数据")
|
|
328
|
+
count = 0
|
|
329
|
+
for row_id, row in await self._ascan_rows():
|
|
330
|
+
if self._match_row(row):
|
|
331
|
+
row.update(self._data)
|
|
332
|
+
await self._storage.aset(self._row_key(row_id), json.dumps(row, ensure_ascii=False))
|
|
333
|
+
count += 1
|
|
334
|
+
return count
|
|
335
|
+
elif self._operation == "delete":
|
|
336
|
+
count = 0
|
|
337
|
+
for row_id, row in await self._ascan_rows():
|
|
338
|
+
if self._match_row(row):
|
|
339
|
+
await self._storage.adelete(self._row_key(row_id))
|
|
340
|
+
count += 1
|
|
341
|
+
return count
|
|
342
|
+
raise ValueError(f"Unknown operation: {self._operation}")
|
|
343
|
+
|
|
344
|
+
async def _aexec_select(self) -> list[tuple]:
|
|
345
|
+
rows = await self._ascan_rows()
|
|
346
|
+
if self._where_clauses:
|
|
347
|
+
rows = [(rid, r) for rid, r in rows if self._match_row(r)]
|
|
348
|
+
if self._order_by:
|
|
349
|
+
for col, desc in reversed(self._order_by):
|
|
350
|
+
try:
|
|
351
|
+
rows.sort(key=lambda x: x[1].get(col, ""), reverse=desc)
|
|
352
|
+
except TypeError:
|
|
353
|
+
rows.sort(key=lambda x: str(x[1].get(col, "")), reverse=desc)
|
|
354
|
+
if self._offset:
|
|
355
|
+
rows = rows[self._offset:]
|
|
356
|
+
if self._limit is not None:
|
|
357
|
+
rows = rows[:self._limit]
|
|
358
|
+
if self._columns:
|
|
359
|
+
return [tuple(r.get(c) for c in self._columns) for _, r in rows]
|
|
360
|
+
return [tuple(r.values()) for _, r in rows]
|
|
361
|
+
|
|
362
|
+
async def aExecuteOne(self) -> tuple | None:
|
|
363
|
+
old_limit = self._limit
|
|
364
|
+
self._limit = 1
|
|
365
|
+
try:
|
|
366
|
+
rows = await self._aexec_select()
|
|
367
|
+
return rows[0] if rows else None
|
|
368
|
+
finally:
|
|
369
|
+
self._limit = old_limit
|
|
370
|
+
|
|
371
|
+
async def aCount(self) -> int:
|
|
372
|
+
rows = await self._ascan_rows()
|
|
373
|
+
if self._where_clauses:
|
|
374
|
+
rows = [(rid, r) for rid, r in rows if self._match_row(r)]
|
|
375
|
+
return len(rows)
|
|
376
|
+
|
|
377
|
+
async def aExists(self) -> bool:
|
|
378
|
+
return await self.aCount() > 0
|