ErisPulse 2.3.3.dev0__py3-none-any.whl → 2.3.4.dev2__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/__init__.py +11 -0
- ErisPulse/CLI/__init__.pyi +13 -0
- ErisPulse/CLI/base.py +52 -0
- ErisPulse/CLI/base.pyi +50 -0
- ErisPulse/CLI/cli.py +224 -0
- ErisPulse/CLI/cli.pyi +80 -0
- ErisPulse/CLI/commands/__init__.py +6 -0
- ErisPulse/CLI/commands/__init__.pyi +12 -0
- ErisPulse/CLI/commands/init.py +395 -0
- ErisPulse/CLI/commands/init.pyi +70 -0
- ErisPulse/CLI/commands/install.py +302 -0
- ErisPulse/CLI/commands/install.pyi +58 -0
- ErisPulse/CLI/commands/list.py +160 -0
- ErisPulse/CLI/commands/list.pyi +44 -0
- ErisPulse/CLI/commands/list_remote.py +123 -0
- ErisPulse/CLI/commands/list_remote.pyi +35 -0
- ErisPulse/CLI/commands/run.py +108 -0
- ErisPulse/CLI/commands/run.pyi +39 -0
- ErisPulse/CLI/commands/self_update.py +232 -0
- ErisPulse/CLI/commands/self_update.pyi +47 -0
- ErisPulse/CLI/commands/uninstall.py +32 -0
- ErisPulse/CLI/commands/uninstall.pyi +24 -0
- ErisPulse/CLI/commands/upgrade.py +56 -0
- ErisPulse/CLI/commands/upgrade.pyi +25 -0
- ErisPulse/CLI/console.pyi +20 -0
- ErisPulse/CLI/registry.py +112 -0
- ErisPulse/CLI/registry.pyi +99 -0
- ErisPulse/{utils → CLI/utils}/__init__.py +2 -6
- ErisPulse/CLI/utils/__init__.pyi +14 -0
- ErisPulse/{utils → CLI/utils}/package_manager.py +146 -20
- ErisPulse/CLI/utils/package_manager.pyi +241 -0
- ErisPulse/{utils → CLI/utils}/reload_handler.py +7 -8
- ErisPulse/CLI/utils/reload_handler.pyi +64 -0
- ErisPulse/Core/Bases/__init__.pyi +14 -0
- ErisPulse/Core/Bases/adapter.py +13 -1
- ErisPulse/Core/Bases/adapter.pyi +140 -0
- ErisPulse/Core/Bases/manager.py +136 -0
- ErisPulse/Core/Bases/manager.pyi +108 -0
- ErisPulse/Core/Bases/module.py +53 -1
- ErisPulse/Core/Bases/module.pyi +95 -0
- ErisPulse/Core/Event/__init__.pyi +26 -0
- ErisPulse/Core/Event/base.pyi +62 -0
- ErisPulse/Core/Event/command.py +6 -1
- ErisPulse/Core/Event/command.pyi +113 -0
- ErisPulse/Core/Event/exceptions.pyi +43 -0
- ErisPulse/Core/Event/message.pyi +93 -0
- ErisPulse/Core/Event/meta.pyi +92 -0
- ErisPulse/Core/Event/notice.pyi +108 -0
- ErisPulse/Core/Event/request.pyi +76 -0
- ErisPulse/Core/Event/wrapper.py +2 -3
- ErisPulse/Core/Event/wrapper.pyi +403 -0
- ErisPulse/Core/__init__.py +16 -13
- ErisPulse/Core/__init__.pyi +16 -0
- ErisPulse/Core/_self_config.py +1 -1
- ErisPulse/Core/_self_config.pyi +72 -0
- ErisPulse/Core/adapter.py +70 -10
- ErisPulse/Core/adapter.pyi +246 -0
- ErisPulse/Core/config.pyi +70 -0
- ErisPulse/Core/exceptions.py +4 -2
- ErisPulse/Core/exceptions.pyi +60 -0
- ErisPulse/Core/lifecycle.py +15 -1
- ErisPulse/Core/lifecycle.pyi +92 -0
- ErisPulse/Core/logger.py +21 -15
- ErisPulse/Core/logger.pyi +169 -0
- ErisPulse/Core/module.py +57 -9
- ErisPulse/Core/module.pyi +189 -0
- ErisPulse/Core/router.py +13 -5
- ErisPulse/Core/router.pyi +120 -0
- ErisPulse/Core/storage.py +94 -256
- ErisPulse/Core/storage.pyi +220 -0
- ErisPulse/__init__.py +35 -1236
- ErisPulse/__init__.pyi +22 -0
- ErisPulse/__main__.py +1 -1
- ErisPulse/__main__.pyi +24 -0
- ErisPulse/loaders/__init__.py +22 -0
- ErisPulse/loaders/__init__.pyi +21 -0
- ErisPulse/loaders/adapter_loader.py +187 -0
- ErisPulse/loaders/adapter_loader.pyi +82 -0
- ErisPulse/loaders/base_loader.py +162 -0
- ErisPulse/loaders/base_loader.pyi +23 -0
- ErisPulse/loaders/initializer.py +150 -0
- ErisPulse/loaders/initializer.pyi +60 -0
- ErisPulse/loaders/module_loader.py +618 -0
- ErisPulse/loaders/module_loader.pyi +179 -0
- ErisPulse/loaders/strategy.py +129 -0
- ErisPulse/loaders/strategy.pyi +90 -0
- ErisPulse/sdk.py +435 -0
- ErisPulse/sdk.pyi +158 -0
- {erispulse-2.3.3.dev0.dist-info → erispulse-2.3.4.dev2.dist-info}/METADATA +6 -20
- erispulse-2.3.4.dev2.dist-info/RECORD +103 -0
- {erispulse-2.3.3.dev0.dist-info → erispulse-2.3.4.dev2.dist-info}/licenses/LICENSE +3 -3
- ErisPulse/Core/ux.py +0 -635
- ErisPulse/utils/cli.py +0 -1097
- erispulse-2.3.3.dev0.dist-info/RECORD +0 -35
- /ErisPulse/{utils → CLI}/console.py +0 -0
- {erispulse-2.3.3.dev0.dist-info → erispulse-2.3.4.dev2.dist-info}/WHEEL +0 -0
- {erispulse-2.3.3.dev0.dist-info → erispulse-2.3.4.dev2.dist-info}/entry_points.txt +0 -0
ErisPulse/Core/router.py
CHANGED
|
@@ -235,13 +235,20 @@ class RouterManager:
|
|
|
235
235
|
try:
|
|
236
236
|
full_path = f"/{module_name}{path}"
|
|
237
237
|
|
|
238
|
-
#
|
|
239
|
-
if full_path in self.
|
|
240
|
-
self.app.remove_api_websocket_route(full_path) # type: ignore || 原因:实际上,FastAPI的API提供了remove_api_websocket_route方法
|
|
238
|
+
# 检查 WebSocket 路由是否存在于我们的内部记录中
|
|
239
|
+
if full_path in self._websocket_routes.get(module_name, {}):
|
|
241
240
|
display_url = self._format_display_url(f"{self.base_url}{full_path}")
|
|
242
241
|
logger.info(f"注销WebSocket: {display_url}")
|
|
243
242
|
del self._websocket_routes[module_name][full_path]
|
|
243
|
+
|
|
244
|
+
# 从 FastAPI 路由列表中移除对应的 WebSocket 路由
|
|
245
|
+
# FastAPI 的 WebSocket 路由有 websocket_endpoint 属性
|
|
246
|
+
self.app.router.routes = [
|
|
247
|
+
route for route in self.app.router.routes
|
|
248
|
+
if not (hasattr(route, 'path') and route.path == full_path)
|
|
249
|
+
]
|
|
244
250
|
return True
|
|
251
|
+
|
|
245
252
|
display_url = self._format_display_url(f"{self.base_url}{full_path}")
|
|
246
253
|
logger.error(f"注销WebSocket失败: 路径 {display_url} 不存在")
|
|
247
254
|
return False
|
|
@@ -339,8 +346,9 @@ class RouterManager:
|
|
|
339
346
|
if "0.0.0.0" in url:
|
|
340
347
|
display_url = url.replace("0.0.0.0", "127.0.0.1")
|
|
341
348
|
return f"{url} (可访问: {display_url})"
|
|
342
|
-
elif "::" in url:
|
|
343
|
-
|
|
349
|
+
elif "[::]" in url:
|
|
350
|
+
# IPv6 回环地址需要替换括号
|
|
351
|
+
display_url = url.replace("[::]", "localhost")
|
|
344
352
|
return f"{url} (可访问: {display_url})"
|
|
345
353
|
return url
|
|
346
354
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# type: ignore
|
|
2
|
+
#
|
|
3
|
+
# Auto-generated type stub for router.py
|
|
4
|
+
# DO NOT EDIT MANUALLY - Generated by generate-type-stubs.py
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
ErisPulse 路由系统
|
|
9
|
+
|
|
10
|
+
提供统一的HTTP和WebSocket路由管理,支持多适配器路由注册和生命周期管理。
|
|
11
|
+
|
|
12
|
+
{!--< tips >!--}
|
|
13
|
+
1. 适配器只需注册路由,无需自行管理服务器
|
|
14
|
+
2. WebSocket支持自定义认证逻辑
|
|
15
|
+
{!--< /tips >!--}
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
19
|
+
from fastapi.routing import APIRoute
|
|
20
|
+
from typing import Dict, List, Optional, Callable, Any, Awaitable, Tuple
|
|
21
|
+
from collections import defaultdict
|
|
22
|
+
from .logger import logger
|
|
23
|
+
from .lifecycle import lifecycle
|
|
24
|
+
import asyncio
|
|
25
|
+
from hypercorn.config import Config
|
|
26
|
+
from hypercorn.asyncio import serve
|
|
27
|
+
|
|
28
|
+
class RouterManager:
|
|
29
|
+
"""
|
|
30
|
+
路由管理器
|
|
31
|
+
|
|
32
|
+
{!--< tips >!--}
|
|
33
|
+
核心功能:
|
|
34
|
+
- HTTP/WebSocket路由注册
|
|
35
|
+
- 生命周期管理
|
|
36
|
+
- 统一错误处理
|
|
37
|
+
{!--< /tips >!--}
|
|
38
|
+
"""
|
|
39
|
+
def __init__(self: None) -> ...:
|
|
40
|
+
"""
|
|
41
|
+
初始化路由管理器
|
|
42
|
+
|
|
43
|
+
{!--< tips >!--}
|
|
44
|
+
会自动创建FastAPI实例并设置核心路由
|
|
45
|
+
{!--< /tips >!--}
|
|
46
|
+
"""
|
|
47
|
+
...
|
|
48
|
+
def register_http_route(self: object, module_name: str, path: str, handler: Callable, methods: List[str] = ...) -> None:
|
|
49
|
+
"""
|
|
50
|
+
注册HTTP路由
|
|
51
|
+
|
|
52
|
+
:param module_name: str 模块名称
|
|
53
|
+
:param path: str 路由路径
|
|
54
|
+
:param handler: Callable 处理函数
|
|
55
|
+
:param methods: List[str] HTTP方法列表(默认["POST"])
|
|
56
|
+
|
|
57
|
+
:raises ValueError: 当路径已注册时抛出
|
|
58
|
+
"""
|
|
59
|
+
...
|
|
60
|
+
def register_webhook(self: object, *args: ..., **kwargs: ...) -> None:
|
|
61
|
+
"""
|
|
62
|
+
兼容性方法:注册HTTP路由(适配器旧接口)
|
|
63
|
+
"""
|
|
64
|
+
...
|
|
65
|
+
def unregister_http_route(self: object, module_name: str, path: str) -> bool:
|
|
66
|
+
"""
|
|
67
|
+
取消注册HTTP路由
|
|
68
|
+
|
|
69
|
+
:param module_name: 模块名称
|
|
70
|
+
:param path: 路由路径
|
|
71
|
+
|
|
72
|
+
:return: Bool
|
|
73
|
+
"""
|
|
74
|
+
...
|
|
75
|
+
def register_websocket(self: object, module_name: str, path: str, handler: Callable[([WebSocket], Awaitable[Any])], auth_handler: Optional[Callable[([WebSocket], Awaitable[bool])]] = ...) -> None:
|
|
76
|
+
"""
|
|
77
|
+
注册WebSocket路由
|
|
78
|
+
|
|
79
|
+
:param module_name: str 模块名称
|
|
80
|
+
:param path: str WebSocket路径
|
|
81
|
+
:param handler: Callable[[WebSocket], Awaitable[Any]] 主处理函数
|
|
82
|
+
:param auth_handler: Optional[Callable[[WebSocket], Awaitable[bool]]] 认证函数
|
|
83
|
+
|
|
84
|
+
:raises ValueError: 当路径已注册时抛出
|
|
85
|
+
"""
|
|
86
|
+
...
|
|
87
|
+
def unregister_websocket(self: object, module_name: str, path: str) -> bool:
|
|
88
|
+
...
|
|
89
|
+
def get_app(self: object) -> FastAPI:
|
|
90
|
+
"""
|
|
91
|
+
获取FastAPI应用实例
|
|
92
|
+
|
|
93
|
+
:return: FastAPI应用实例
|
|
94
|
+
"""
|
|
95
|
+
...
|
|
96
|
+
async def start(self: object, host: str = ..., port: int = ..., ssl_certfile: Optional[str] = ..., ssl_keyfile: Optional[str] = ...) -> None:
|
|
97
|
+
"""
|
|
98
|
+
启动路由服务器
|
|
99
|
+
|
|
100
|
+
:param host: str 监听地址(默认"0.0.0.0")
|
|
101
|
+
:param port: int 监听端口(默认8000)
|
|
102
|
+
:param ssl_certfile: Optional[str] SSL证书路径
|
|
103
|
+
:param ssl_keyfile: Optional[str] SSL密钥路径
|
|
104
|
+
|
|
105
|
+
:raises RuntimeError: 当服务器已在运行时抛出
|
|
106
|
+
"""
|
|
107
|
+
...
|
|
108
|
+
async def stop(self: object) -> None:
|
|
109
|
+
"""
|
|
110
|
+
停止服务器
|
|
111
|
+
"""
|
|
112
|
+
...
|
|
113
|
+
def _format_display_url(self: object, url: str) -> str:
|
|
114
|
+
"""
|
|
115
|
+
格式化URL显示,将回环地址转换为更友好的格式
|
|
116
|
+
|
|
117
|
+
:param url: 原始URL
|
|
118
|
+
:return: 格式化后的URL
|
|
119
|
+
"""
|
|
120
|
+
...
|
ErisPulse/Core/storage.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
ErisPulse 存储管理模块
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
提供键值存储和事务支持,用于管理框架运行时数据。
|
|
5
5
|
基于SQLite实现持久化存储,支持复杂数据类型和原子操作。
|
|
6
6
|
|
|
7
7
|
支持两种数据库模式:
|
|
@@ -17,23 +17,21 @@ use_global_db = true
|
|
|
17
17
|
{!--< tips >!--}
|
|
18
18
|
1. 支持JSON序列化存储复杂数据类型
|
|
19
19
|
2. 提供事务支持确保数据一致性
|
|
20
|
-
3. 自动快照功能防止数据丢失
|
|
21
20
|
{!--< /tips >!--}
|
|
22
21
|
"""
|
|
23
22
|
|
|
24
23
|
import os
|
|
25
24
|
import json
|
|
26
25
|
import sqlite3
|
|
27
|
-
import
|
|
28
|
-
import
|
|
29
|
-
from
|
|
30
|
-
from typing import List, Dict, Optional, Any, Tuple, Type
|
|
26
|
+
import threading
|
|
27
|
+
from typing import List, Dict, Optional, Any, Type
|
|
28
|
+
from contextlib import contextmanager
|
|
31
29
|
|
|
32
30
|
class StorageManager:
|
|
33
31
|
"""
|
|
34
32
|
存储管理器
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
单例模式实现,提供键值存储的增删改查和事务管理
|
|
37
35
|
|
|
38
36
|
支持两种数据库模式:
|
|
39
37
|
1. 项目数据库(默认):位于项目目录下的 config/config.db
|
|
@@ -48,7 +46,6 @@ class StorageManager:
|
|
|
48
46
|
{!--< tips >!--}
|
|
49
47
|
1. 使用get/set方法操作存储项
|
|
50
48
|
2. 使用transaction上下文管理事务
|
|
51
|
-
3. 使用snapshot/restore管理数据快照
|
|
52
49
|
{!--< /tips >!--}
|
|
53
50
|
"""
|
|
54
51
|
|
|
@@ -57,7 +54,8 @@ class StorageManager:
|
|
|
57
54
|
DEFAULT_PROJECT_DB_PATH = os.path.join(os.getcwd(), "config", "config.db")
|
|
58
55
|
# 包内全局数据库路径
|
|
59
56
|
GLOBAL_DB_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../data/config.db"))
|
|
60
|
-
|
|
57
|
+
# 线程本地存储,用于跟踪活动事务的连接
|
|
58
|
+
_local = threading.local()
|
|
61
59
|
|
|
62
60
|
def __new__(cls, *args, **kwargs):
|
|
63
61
|
if not cls._instance:
|
|
@@ -73,20 +71,41 @@ class StorageManager:
|
|
|
73
71
|
self._ensure_directories()
|
|
74
72
|
|
|
75
73
|
# 根据配置决定使用哪个数据库
|
|
76
|
-
from .
|
|
77
|
-
|
|
74
|
+
from ._self_config import get_storage_config
|
|
75
|
+
storage_config = get_storage_config()
|
|
76
|
+
|
|
77
|
+
use_global_db = storage_config.get("use_global_db", False)
|
|
78
78
|
|
|
79
79
|
if use_global_db and os.path.exists(self.GLOBAL_DB_PATH):
|
|
80
80
|
self.db_path = self.GLOBAL_DB_PATH
|
|
81
81
|
else:
|
|
82
82
|
self.db_path = self.DEFAULT_PROJECT_DB_PATH
|
|
83
83
|
|
|
84
|
-
self._last_snapshot_time = time.time()
|
|
85
|
-
self._snapshot_interval = 3600
|
|
86
|
-
|
|
87
84
|
self._init_db()
|
|
88
85
|
self._initialized = True
|
|
89
86
|
|
|
87
|
+
@contextmanager
|
|
88
|
+
def _get_connection(self):
|
|
89
|
+
"""
|
|
90
|
+
获取数据库连接(支持事务)
|
|
91
|
+
|
|
92
|
+
如果在事务中,返回事务的连接
|
|
93
|
+
否则创建新连接
|
|
94
|
+
"""
|
|
95
|
+
# 检查是否在线程本地存储中有活动事务连接
|
|
96
|
+
if hasattr(self._local, 'transaction_conn') and self._local.transaction_conn is not None:
|
|
97
|
+
conn = self._local.transaction_conn
|
|
98
|
+
should_close = False
|
|
99
|
+
else:
|
|
100
|
+
conn = sqlite3.connect(self.db_path)
|
|
101
|
+
should_close = True
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
yield conn
|
|
105
|
+
finally:
|
|
106
|
+
if should_close:
|
|
107
|
+
conn.close()
|
|
108
|
+
|
|
90
109
|
def _ensure_directories(self) -> None:
|
|
91
110
|
"""
|
|
92
111
|
确保必要的目录存在
|
|
@@ -96,12 +115,6 @@ class StorageManager:
|
|
|
96
115
|
os.makedirs(os.path.dirname(self.DEFAULT_PROJECT_DB_PATH), exist_ok=True)
|
|
97
116
|
except Exception:
|
|
98
117
|
pass # 如果无法创建项目目录,则跳过
|
|
99
|
-
|
|
100
|
-
# 确保快照目录存在
|
|
101
|
-
try:
|
|
102
|
-
os.makedirs(self.SNAPSHOT_DIR, exist_ok=True)
|
|
103
|
-
except Exception:
|
|
104
|
-
pass # 如果无法创建快照目录,则跳过
|
|
105
118
|
|
|
106
119
|
def _init_db(self) -> None:
|
|
107
120
|
"""
|
|
@@ -140,10 +153,6 @@ class StorageManager:
|
|
|
140
153
|
except Exception as e:
|
|
141
154
|
logger.error(f"初始化数据库时发生未知错误: {e}")
|
|
142
155
|
raise
|
|
143
|
-
|
|
144
|
-
# 初始化自动快照调度器
|
|
145
|
-
self._last_snapshot_time = time.time() # 初始化为当前时间
|
|
146
|
-
self._snapshot_interval = 3600 # 默认每小时自动快照
|
|
147
156
|
|
|
148
157
|
def get(self, key: str, default: Any = None) -> Any:
|
|
149
158
|
"""
|
|
@@ -226,15 +235,14 @@ class StorageManager:
|
|
|
226
235
|
return False
|
|
227
236
|
|
|
228
237
|
try:
|
|
229
|
-
serialized_value = json.dumps(value)
|
|
230
|
-
with self.
|
|
231
|
-
conn = sqlite3.connect(self.db_path)
|
|
238
|
+
serialized_value = json.dumps(value)
|
|
239
|
+
with self._get_connection() as conn:
|
|
232
240
|
cursor = conn.cursor()
|
|
233
241
|
cursor.execute("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", (key, serialized_value))
|
|
234
|
-
|
|
235
|
-
|
|
242
|
+
# 如果不在事务中,提交更改
|
|
243
|
+
if not (hasattr(self._local, 'transaction_conn') and self._local.transaction_conn is not None):
|
|
244
|
+
conn.commit()
|
|
236
245
|
|
|
237
|
-
self._check_auto_snapshot()
|
|
238
246
|
return True
|
|
239
247
|
except Exception as e:
|
|
240
248
|
from .logger import logger
|
|
@@ -260,17 +268,16 @@ class StorageManager:
|
|
|
260
268
|
return False
|
|
261
269
|
|
|
262
270
|
try:
|
|
263
|
-
with self.
|
|
264
|
-
conn = sqlite3.connect(self.db_path)
|
|
271
|
+
with self._get_connection() as conn:
|
|
265
272
|
cursor = conn.cursor()
|
|
266
273
|
for key, value in items.items():
|
|
267
|
-
serialized_value = json.dumps(value)
|
|
274
|
+
serialized_value = json.dumps(value)
|
|
268
275
|
cursor.execute("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)",
|
|
269
276
|
(key, serialized_value))
|
|
270
|
-
|
|
271
|
-
|
|
277
|
+
# 如果不在事务中,提交更改
|
|
278
|
+
if not (hasattr(self._local, 'transaction_conn') and self._local.transaction_conn is not None):
|
|
279
|
+
conn.commit()
|
|
272
280
|
|
|
273
|
-
self._check_auto_snapshot()
|
|
274
281
|
return True
|
|
275
282
|
except Exception:
|
|
276
283
|
return False
|
|
@@ -316,14 +323,13 @@ class StorageManager:
|
|
|
316
323
|
return False
|
|
317
324
|
|
|
318
325
|
try:
|
|
319
|
-
with self.
|
|
320
|
-
conn = sqlite3.connect(self.db_path)
|
|
326
|
+
with self._get_connection() as conn:
|
|
321
327
|
cursor = conn.cursor()
|
|
322
328
|
cursor.execute("DELETE FROM config WHERE key = ?", (key,))
|
|
323
|
-
|
|
324
|
-
|
|
329
|
+
# 如果不在事务中,提交更改
|
|
330
|
+
if not (hasattr(self._local, 'transaction_conn') and self._local.transaction_conn is not None):
|
|
331
|
+
conn.commit()
|
|
325
332
|
|
|
326
|
-
self._check_auto_snapshot()
|
|
327
333
|
return True
|
|
328
334
|
except Exception:
|
|
329
335
|
return False
|
|
@@ -343,14 +349,13 @@ class StorageManager:
|
|
|
343
349
|
return False
|
|
344
350
|
|
|
345
351
|
try:
|
|
346
|
-
with self.
|
|
347
|
-
conn = sqlite3.connect(self.db_path)
|
|
352
|
+
with self._get_connection() as conn:
|
|
348
353
|
cursor = conn.cursor()
|
|
349
354
|
cursor.executemany("DELETE FROM config WHERE key = ?", [(k,) for k in keys])
|
|
350
|
-
|
|
351
|
-
|
|
355
|
+
# 如果不在事务中,提交更改
|
|
356
|
+
if not (hasattr(self._local, 'transaction_conn') and self._local.transaction_conn is not None):
|
|
357
|
+
conn.commit()
|
|
352
358
|
|
|
353
|
-
self._check_auto_snapshot()
|
|
354
359
|
return True
|
|
355
360
|
except Exception:
|
|
356
361
|
return False
|
|
@@ -374,8 +379,12 @@ class StorageManager:
|
|
|
374
379
|
cursor = conn.cursor()
|
|
375
380
|
placeholders = ','.join(['?'] * len(keys))
|
|
376
381
|
cursor.execute(f"SELECT key, value FROM config WHERE key IN ({placeholders})", keys)
|
|
377
|
-
results = {
|
|
378
|
-
|
|
382
|
+
results = {}
|
|
383
|
+
for row in cursor.fetchall():
|
|
384
|
+
try:
|
|
385
|
+
results[row[0]] = json.loads(row[1])
|
|
386
|
+
except json.JSONDecodeError:
|
|
387
|
+
results[row[0]] = row[1]
|
|
379
388
|
conn.close()
|
|
380
389
|
return results
|
|
381
390
|
except Exception as e:
|
|
@@ -403,6 +412,15 @@ class StorageManager:
|
|
|
403
412
|
def __exit__(self, *args):
|
|
404
413
|
pass
|
|
405
414
|
return EmptyTransaction()
|
|
415
|
+
|
|
416
|
+
# 如果已经在事务中(嵌套事务),返回一个空事务,复用现有连接
|
|
417
|
+
if hasattr(self._local, 'transaction_conn') and self._local.transaction_conn is not None:
|
|
418
|
+
class NestedTransaction:
|
|
419
|
+
def __enter__(self):
|
|
420
|
+
return self
|
|
421
|
+
def __exit__(self, *args):
|
|
422
|
+
pass
|
|
423
|
+
return NestedTransaction()
|
|
406
424
|
|
|
407
425
|
return self._Transaction(self)
|
|
408
426
|
|
|
@@ -426,12 +444,18 @@ class StorageManager:
|
|
|
426
444
|
self.conn = sqlite3.connect(self.storage_manager.db_path)
|
|
427
445
|
self.cursor = self.conn.cursor()
|
|
428
446
|
self.cursor.execute("BEGIN TRANSACTION")
|
|
447
|
+
# 将连接存储到线程本地存储,供其他方法复用
|
|
448
|
+
self.storage_manager._local.transaction_conn = self.conn
|
|
429
449
|
return self
|
|
430
450
|
|
|
431
451
|
def __exit__(self, exc_type: Type[Exception], exc_val: Exception, exc_tb: Any) -> None:
|
|
432
452
|
"""
|
|
433
453
|
退出事务上下文
|
|
434
454
|
"""
|
|
455
|
+
# 清除线程本地存储中的连接引用
|
|
456
|
+
if hasattr(self.storage_manager._local, 'transaction_conn'):
|
|
457
|
+
self.storage_manager._local.transaction_conn = None
|
|
458
|
+
|
|
435
459
|
if self.conn is not None:
|
|
436
460
|
try:
|
|
437
461
|
if exc_type is None:
|
|
@@ -446,58 +470,6 @@ class StorageManager:
|
|
|
446
470
|
if hasattr(self.conn, 'close'):
|
|
447
471
|
self.conn.close()
|
|
448
472
|
|
|
449
|
-
def _check_auto_snapshot(self) -> None:
|
|
450
|
-
"""
|
|
451
|
-
{!--< internal-use >!--}
|
|
452
|
-
检查并执行自动快照
|
|
453
|
-
"""
|
|
454
|
-
# 避免在初始化过程中调用此方法导致问题
|
|
455
|
-
if not hasattr(self, '_initialized') or not self._initialized:
|
|
456
|
-
return
|
|
457
|
-
|
|
458
|
-
from .logger import logger
|
|
459
|
-
|
|
460
|
-
if not hasattr(self, '_last_snapshot_time') or self._last_snapshot_time is None:
|
|
461
|
-
self._last_snapshot_time = time.time()
|
|
462
|
-
|
|
463
|
-
if not hasattr(self, '_snapshot_interval') or self._snapshot_interval is None:
|
|
464
|
-
self._snapshot_interval = 3600
|
|
465
|
-
|
|
466
|
-
current_time = time.time()
|
|
467
|
-
|
|
468
|
-
try:
|
|
469
|
-
time_diff = current_time - self._last_snapshot_time
|
|
470
|
-
if not isinstance(time_diff, (int, float)):
|
|
471
|
-
raise ValueError("时间差应为数值类型")
|
|
472
|
-
|
|
473
|
-
if not isinstance(self._snapshot_interval, (int, float)):
|
|
474
|
-
raise ValueError("快照间隔应为数值类型")
|
|
475
|
-
|
|
476
|
-
if time_diff > self._snapshot_interval:
|
|
477
|
-
self._last_snapshot_time = current_time
|
|
478
|
-
self.snapshot(f"auto_{datetime.now().strftime('%Y%m%d_%H%M%S')}")
|
|
479
|
-
|
|
480
|
-
except Exception as e:
|
|
481
|
-
logger.error(f"自动快照检查失败: {e}")
|
|
482
|
-
self._last_snapshot_time = current_time
|
|
483
|
-
self._snapshot_interval = 3600
|
|
484
|
-
|
|
485
|
-
def set_snapshot_interval(self, seconds: int) -> None:
|
|
486
|
-
"""
|
|
487
|
-
设置自动快照间隔
|
|
488
|
-
|
|
489
|
-
:param seconds: 间隔秒数
|
|
490
|
-
|
|
491
|
-
:example:
|
|
492
|
-
>>> # 每30分钟自动快照
|
|
493
|
-
>>> storage.set_snapshot_interval(1800)
|
|
494
|
-
"""
|
|
495
|
-
# 避免在初始化过程中调用此方法导致问题
|
|
496
|
-
if not hasattr(self, '_initialized') or not self._initialized:
|
|
497
|
-
return
|
|
498
|
-
|
|
499
|
-
self._snapshot_interval = seconds
|
|
500
|
-
|
|
501
473
|
def clear(self) -> bool:
|
|
502
474
|
"""
|
|
503
475
|
清空所有存储项
|
|
@@ -512,11 +484,13 @@ class StorageManager:
|
|
|
512
484
|
return False
|
|
513
485
|
|
|
514
486
|
try:
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
487
|
+
with self._get_connection() as conn:
|
|
488
|
+
cursor = conn.cursor()
|
|
489
|
+
cursor.execute("DELETE FROM config")
|
|
490
|
+
# 如果不在事务中,提交更改
|
|
491
|
+
if not (hasattr(self._local, 'transaction_conn') and self._local.transaction_conn is not None):
|
|
492
|
+
conn.commit()
|
|
493
|
+
|
|
520
494
|
return True
|
|
521
495
|
except Exception:
|
|
522
496
|
return False
|
|
@@ -540,11 +514,24 @@ class StorageManager:
|
|
|
540
514
|
# 避免在初始化过程中调用此方法导致问题
|
|
541
515
|
if not hasattr(self, '_initialized') or not self._initialized:
|
|
542
516
|
raise AttributeError(f"存储尚未初始化完成: {key}")
|
|
543
|
-
|
|
517
|
+
|
|
518
|
+
# 检查键是否存在
|
|
544
519
|
try:
|
|
545
|
-
|
|
520
|
+
with sqlite3.connect(self.db_path) as conn:
|
|
521
|
+
cursor = conn.cursor()
|
|
522
|
+
cursor.execute("SELECT value FROM config WHERE key = ?", (key,))
|
|
523
|
+
result = cursor.fetchone()
|
|
546
524
|
except Exception:
|
|
547
525
|
raise AttributeError(f"存储项 {key} 不存在或访问出错")
|
|
526
|
+
|
|
527
|
+
if result is None:
|
|
528
|
+
raise AttributeError(f"存储项 {key} 不存在")
|
|
529
|
+
|
|
530
|
+
# 解析并返回值
|
|
531
|
+
try:
|
|
532
|
+
return json.loads(result[0])
|
|
533
|
+
except json.JSONDecodeError:
|
|
534
|
+
return result[0]
|
|
548
535
|
|
|
549
536
|
def __setattr__(self, key: str, value: Any) -> None:
|
|
550
537
|
"""
|
|
@@ -572,157 +559,8 @@ class StorageManager:
|
|
|
572
559
|
from .logger import logger
|
|
573
560
|
logger.error(f"设置存储项 {key} 失败: {e}")
|
|
574
561
|
|
|
575
|
-
def snapshot(self, name: Optional[str] = None) -> str:
|
|
576
|
-
"""
|
|
577
|
-
创建数据库快照
|
|
578
|
-
|
|
579
|
-
:param name: 快照名称(可选)
|
|
580
|
-
:return: 快照文件路径
|
|
581
|
-
|
|
582
|
-
:example:
|
|
583
|
-
>>> # 创建命名快照
|
|
584
|
-
>>> snapshot_path = storage.snapshot("before_update")
|
|
585
|
-
>>> # 创建时间戳快照
|
|
586
|
-
>>> snapshot_path = storage.snapshot()
|
|
587
|
-
"""
|
|
588
|
-
# 避免在初始化过程中调用此方法导致问题
|
|
589
|
-
if not hasattr(self, '_initialized') or not self._initialized:
|
|
590
|
-
raise RuntimeError("存储尚未初始化完成")
|
|
591
|
-
|
|
592
|
-
if not name:
|
|
593
|
-
name = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
594
|
-
snapshot_path = os.path.join(self.SNAPSHOT_DIR, f"{name}.db")
|
|
595
|
-
|
|
596
|
-
try:
|
|
597
|
-
# 快照目录
|
|
598
|
-
os.makedirs(self.SNAPSHOT_DIR, exist_ok=True)
|
|
599
|
-
|
|
600
|
-
# 安全关闭连接
|
|
601
|
-
if hasattr(self, "_conn") and self._conn is not None:
|
|
602
|
-
try:
|
|
603
|
-
self._conn.close()
|
|
604
|
-
except Exception as e:
|
|
605
|
-
from .logger import logger
|
|
606
|
-
logger.warning(f"关闭数据库连接时出错: {e}")
|
|
607
|
-
|
|
608
|
-
# 创建快照
|
|
609
|
-
shutil.copy2(self.db_path, snapshot_path)
|
|
610
|
-
from .logger import logger
|
|
611
|
-
logger.info(f"数据库快照已创建: {snapshot_path}")
|
|
612
|
-
return snapshot_path
|
|
613
|
-
except Exception as e:
|
|
614
|
-
from .logger import logger
|
|
615
|
-
logger.error(f"创建快照失败: {e}")
|
|
616
|
-
raise
|
|
617
|
-
|
|
618
|
-
def restore(self, snapshot_name: str) -> bool:
|
|
619
|
-
"""
|
|
620
|
-
从快照恢复数据库
|
|
621
|
-
|
|
622
|
-
:param snapshot_name: 快照名称或路径
|
|
623
|
-
:return: 恢复是否成功
|
|
624
|
-
|
|
625
|
-
:example:
|
|
626
|
-
>>> storage.restore("before_update")
|
|
627
|
-
"""
|
|
628
|
-
# 避免在初始化过程中调用此方法导致问题
|
|
629
|
-
if not hasattr(self, '_initialized') or not self._initialized:
|
|
630
|
-
return False
|
|
631
|
-
|
|
632
|
-
snapshot_path = os.path.join(self.SNAPSHOT_DIR, f"{snapshot_name}.db") \
|
|
633
|
-
if not snapshot_name.endswith('.db') else snapshot_name
|
|
634
|
-
|
|
635
|
-
if not os.path.exists(snapshot_path):
|
|
636
|
-
from .logger import logger
|
|
637
|
-
logger.error(f"快照文件不存在: {snapshot_path}")
|
|
638
|
-
return False
|
|
639
|
-
|
|
640
|
-
try:
|
|
641
|
-
# 安全关闭连接
|
|
642
|
-
if hasattr(self, "_conn") and self._conn is not None:
|
|
643
|
-
try:
|
|
644
|
-
self._conn.close()
|
|
645
|
-
except Exception as e:
|
|
646
|
-
from .logger import logger
|
|
647
|
-
logger.warning(f"关闭数据库连接时出错: {e}")
|
|
648
|
-
|
|
649
|
-
# 执行恢复操作
|
|
650
|
-
shutil.copy2(snapshot_path, self.db_path)
|
|
651
|
-
self._init_db() # 恢复后重新初始化数据库连接
|
|
652
|
-
from .logger import logger
|
|
653
|
-
logger.info(f"数据库已从快照恢复: {snapshot_path}")
|
|
654
|
-
return True
|
|
655
|
-
except Exception as e:
|
|
656
|
-
from .logger import logger
|
|
657
|
-
logger.error(f"恢复快照失败: {e}")
|
|
658
|
-
return False
|
|
659
|
-
|
|
660
|
-
def list_snapshots(self) -> List[Tuple[str, datetime, int]]:
|
|
661
|
-
"""
|
|
662
|
-
列出所有可用的快照
|
|
663
|
-
|
|
664
|
-
:return: 快照信息列表(名称, 创建时间, 大小)
|
|
665
|
-
|
|
666
|
-
:example:
|
|
667
|
-
>>> for name, date, size in storage.list_snapshots():
|
|
668
|
-
>>> print(f"{name} - {date} ({size} bytes)")
|
|
669
|
-
"""
|
|
670
|
-
# 避免在初始化过程中调用此方法导致问题
|
|
671
|
-
if not hasattr(self, '_initialized') or not self._initialized:
|
|
672
|
-
return []
|
|
673
|
-
|
|
674
|
-
try:
|
|
675
|
-
snapshots = []
|
|
676
|
-
for f in os.listdir(self.SNAPSHOT_DIR):
|
|
677
|
-
if f.endswith('.db'):
|
|
678
|
-
path = os.path.join(self.SNAPSHOT_DIR, f)
|
|
679
|
-
stat = os.stat(path)
|
|
680
|
-
snapshots.append((
|
|
681
|
-
f[:-3], # 去掉.db后缀
|
|
682
|
-
datetime.fromtimestamp(stat.st_ctime),
|
|
683
|
-
stat.st_size
|
|
684
|
-
))
|
|
685
|
-
return sorted(snapshots, key=lambda x: x[1], reverse=True)
|
|
686
|
-
except Exception as e:
|
|
687
|
-
from .logger import logger
|
|
688
|
-
logger.error(f"列出快照时发生错误: {e}")
|
|
689
|
-
return []
|
|
690
|
-
|
|
691
|
-
def delete_snapshot(self, snapshot_name: str) -> bool:
|
|
692
|
-
"""
|
|
693
|
-
删除指定的快照
|
|
694
|
-
|
|
695
|
-
:param snapshot_name: 快照名称
|
|
696
|
-
:return: 删除是否成功
|
|
697
|
-
|
|
698
|
-
:example:
|
|
699
|
-
>>> storage.delete_snapshot("old_backup")
|
|
700
|
-
"""
|
|
701
|
-
# 避免在初始化过程中调用此方法导致问题
|
|
702
|
-
if not hasattr(self, '_initialized') or not self._initialized:
|
|
703
|
-
return False
|
|
704
|
-
|
|
705
|
-
snapshot_path = os.path.join(self.SNAPSHOT_DIR, f"{snapshot_name}.db") \
|
|
706
|
-
if not snapshot_name.endswith('.db') else snapshot_name
|
|
707
|
-
|
|
708
|
-
if not os.path.exists(snapshot_path):
|
|
709
|
-
from .logger import logger
|
|
710
|
-
logger.error(f"快照文件不存在: {snapshot_path}")
|
|
711
|
-
return False
|
|
712
|
-
|
|
713
|
-
try:
|
|
714
|
-
os.remove(snapshot_path)
|
|
715
|
-
from .logger import logger
|
|
716
|
-
logger.info(f"快照已删除: {snapshot_path}")
|
|
717
|
-
return True
|
|
718
|
-
except Exception as e:
|
|
719
|
-
from .logger import logger
|
|
720
|
-
logger.error(f"删除快照失败: {e}")
|
|
721
|
-
return False
|
|
722
|
-
|
|
723
562
|
storage = StorageManager()
|
|
724
563
|
|
|
725
564
|
__all__ = [
|
|
726
565
|
"storage"
|
|
727
566
|
]
|
|
728
|
-
|