sdpy-kit 0.1.0__tar.gz → 0.1.2__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.
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/PKG-INFO +8 -1
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/__init__.py +68 -68
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/config/kit_config.py +0 -6
- sdpy_kit-0.1.2/kit/db/query.py +338 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/db/sqlite_kit.py +39 -1
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/pyproject.toml +10 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/.gitignore +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/README.md +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/ai/__init__.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/ai/base_service.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/ai/chat_service.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/ai/embedding_service.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/ai/exceptions.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/ai/failover.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/ai/image_service.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/ai/model_builder.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/ai/model_client.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/ai/pool.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/ai/profiles.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/ai/rerank_service.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/ai/runner.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/ai/types.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/common_kit.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/config/__init__.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/config/log_config.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/db/__init__.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/db/base_db_kit.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/db/crud.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/db/engine.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/db/exceptions.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/db/models/__init__.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/db/models/base.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/db/mysql_kit.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/db/pg_kit.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/db/session.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/db/transaction.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/doc_kit.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/file_kit.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/mcp_kit.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/mineru_kit.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/redis_kit.py +0 -0
- {sdpy_kit-0.1.0 → sdpy_kit-0.1.2}/kit/zvec_kit.py +0 -0
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sdpy-kit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: SDPY Kit——统一数据库访问层(PG/MySQL/SQLite)与 AI/MCP/Redis/文件/文档/向量工具集
|
|
5
5
|
Author-email: Clark <changhongyuan@126.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
|
+
Keywords: ai,kit,mcp,mysql,postgresql,redis,sdpy,sqlalchemy,sqlite,sqlmodel,向量检索,工具集,开发工具,数据库
|
|
7
8
|
Classifier: Development Status :: 4 - Beta
|
|
8
9
|
Classifier: Intended Audience :: Developers
|
|
9
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Natural Language :: Chinese (Simplified)
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
10
13
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
12
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Topic :: Database
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
19
|
Requires-Python: >=3.13
|
|
14
20
|
Requires-Dist: fastmcp>=4.0.3
|
|
15
21
|
Requires-Dist: firecrawl-anydoc>=0.2.4
|
|
16
22
|
Requires-Dist: httpx>=0.28.1
|
|
17
23
|
Requires-Dist: markdown-it-py>=3.0
|
|
24
|
+
Requires-Dist: numpy>=2.0
|
|
18
25
|
Requires-Dist: pimd[pdfa]>=2.2.5
|
|
19
26
|
Requires-Dist: psycopg2-binary>=2.9.12
|
|
20
27
|
Requires-Dist: pydantic-settings>=2.0
|
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
|
|
3
|
-
|
|
4
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
you may not use this file except in compliance with the License.
|
|
6
|
-
You may obtain a copy of the License at
|
|
7
|
-
|
|
8
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
|
|
10
|
-
Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
See the License for the specific language governing permissions and
|
|
14
|
-
limitations under the License.
|
|
15
|
-
|
|
16
|
-
Project: sdpy
|
|
17
|
-
Author: Clark Chang
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
# date: 2026-08-23
|
|
21
|
-
"""Kit 模块——统一数据库访问层 + 工具集
|
|
22
|
-
|
|
23
|
-
数据库访问通过 kit.db 子包提供(基于 SQLModel)。
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
__version__ = "0.1.
|
|
27
|
-
|
|
28
|
-
from kit.db import (
|
|
29
|
-
MYSQL,
|
|
30
|
-
POSTGRESQL,
|
|
31
|
-
SQLITE,
|
|
32
|
-
CRUDBase,
|
|
33
|
-
DBError,
|
|
34
|
-
NotFoundError,
|
|
35
|
-
close_all_engines,
|
|
36
|
-
close_engine,
|
|
37
|
-
create_tables,
|
|
38
|
-
get_engine,
|
|
39
|
-
get_session,
|
|
40
|
-
list_tables,
|
|
41
|
-
session_scope,
|
|
42
|
-
table_exists,
|
|
43
|
-
transactional,
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
__all__ = [
|
|
47
|
-
# 引擎管理
|
|
48
|
-
"get_engine",
|
|
49
|
-
"create_tables",
|
|
50
|
-
"list_tables",
|
|
51
|
-
"table_exists",
|
|
52
|
-
"close_all_engines",
|
|
53
|
-
"close_engine",
|
|
54
|
-
# 常量
|
|
55
|
-
"POSTGRESQL",
|
|
56
|
-
"MYSQL",
|
|
57
|
-
"SQLITE",
|
|
58
|
-
# 会话
|
|
59
|
-
"session_scope",
|
|
60
|
-
"get_session",
|
|
61
|
-
# 异常
|
|
62
|
-
"DBError",
|
|
63
|
-
"NotFoundError",
|
|
64
|
-
# CRUD
|
|
65
|
-
"CRUDBase",
|
|
66
|
-
# 事务
|
|
67
|
-
"transactional",
|
|
68
|
-
]
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Project: sdpy
|
|
17
|
+
Author: Clark Chang
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# date: 2026-08-23
|
|
21
|
+
"""Kit 模块——统一数据库访问层 + 工具集
|
|
22
|
+
|
|
23
|
+
数据库访问通过 kit.db 子包提供(基于 SQLModel)。
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.2"
|
|
27
|
+
|
|
28
|
+
from kit.db import (
|
|
29
|
+
MYSQL,
|
|
30
|
+
POSTGRESQL,
|
|
31
|
+
SQLITE,
|
|
32
|
+
CRUDBase,
|
|
33
|
+
DBError,
|
|
34
|
+
NotFoundError,
|
|
35
|
+
close_all_engines,
|
|
36
|
+
close_engine,
|
|
37
|
+
create_tables,
|
|
38
|
+
get_engine,
|
|
39
|
+
get_session,
|
|
40
|
+
list_tables,
|
|
41
|
+
session_scope,
|
|
42
|
+
table_exists,
|
|
43
|
+
transactional,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
# 引擎管理
|
|
48
|
+
"get_engine",
|
|
49
|
+
"create_tables",
|
|
50
|
+
"list_tables",
|
|
51
|
+
"table_exists",
|
|
52
|
+
"close_all_engines",
|
|
53
|
+
"close_engine",
|
|
54
|
+
# 常量
|
|
55
|
+
"POSTGRESQL",
|
|
56
|
+
"MYSQL",
|
|
57
|
+
"SQLITE",
|
|
58
|
+
# 会话
|
|
59
|
+
"session_scope",
|
|
60
|
+
"get_session",
|
|
61
|
+
# 异常
|
|
62
|
+
"DBError",
|
|
63
|
+
"NotFoundError",
|
|
64
|
+
# CRUD
|
|
65
|
+
"CRUDBase",
|
|
66
|
+
# 事务
|
|
67
|
+
"transactional",
|
|
68
|
+
]
|
|
@@ -28,12 +28,6 @@ from pydantic_settings import BaseSettings
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class Settings(BaseSettings):
|
|
31
|
-
APP_NAME: str = Field(default="SDPY App")
|
|
32
|
-
VERSION: str = Field(default="0.1.0")
|
|
33
|
-
DEBUG: bool = Field(default=True)
|
|
34
|
-
HOST: str = Field(default="0.0.0.0")
|
|
35
|
-
PORT: int = Field(default=8000)
|
|
36
|
-
|
|
37
31
|
LOG_LEVEL: str = Field(default="INFO")
|
|
38
32
|
LOG_TO_CONSOLE: bool = Field(default=True)
|
|
39
33
|
LOG_TO_FILE: bool = Field(default=False)
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Project: sdpy
|
|
17
|
+
Author: Clark Chang
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# date: 2025-04-19
|
|
21
|
+
"""查询构建器(不可变)+ 线程安全查询缓存
|
|
22
|
+
|
|
23
|
+
QueryBuilder 采用不可变设计:每个链式方法返回新实例,避免状态副作用。
|
|
24
|
+
count() 从原始模型构建独立计数查询,仅复制 WHERE 条件,不受 limit/offset 影响。
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import functools
|
|
28
|
+
import threading
|
|
29
|
+
import time
|
|
30
|
+
from collections.abc import Callable
|
|
31
|
+
from datetime import UTC, datetime, timedelta
|
|
32
|
+
from typing import Any, TypeVar
|
|
33
|
+
|
|
34
|
+
from sqlalchemy import func
|
|
35
|
+
from sqlalchemy.orm import load_only
|
|
36
|
+
from sqlmodel import Session, SQLModel, select
|
|
37
|
+
|
|
38
|
+
from kit.config.log_config import get_default_logger
|
|
39
|
+
|
|
40
|
+
logger = get_default_logger(__name__)
|
|
41
|
+
|
|
42
|
+
T = TypeVar("T", bound=SQLModel)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class QueryBuilder:
|
|
46
|
+
"""不可变链式查询构建器
|
|
47
|
+
|
|
48
|
+
每个链式方法返回新的 QueryBuilder 实例,避免修改原始 builder 的状态。
|
|
49
|
+
这允许同一个 builder 安全复用于不同查询。
|
|
50
|
+
|
|
51
|
+
用法::
|
|
52
|
+
|
|
53
|
+
qb = QueryBuilder(User, session)
|
|
54
|
+
active = qb.filter(User.is_active == True).all()
|
|
55
|
+
# qb 的状态未被修改,可继续复用
|
|
56
|
+
young = qb.filter(User.age < 30).all()
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, model: type[T], session: Session):
|
|
60
|
+
self._model = model
|
|
61
|
+
self._session = session
|
|
62
|
+
self._statement = select(model)
|
|
63
|
+
|
|
64
|
+
def _clone(self) -> "QueryBuilder[T]":
|
|
65
|
+
"""创建当前 builder 的浅拷贝(SQLAlchemy Select 不可变,直接引用即可)"""
|
|
66
|
+
new = QueryBuilder.__new__(QueryBuilder)
|
|
67
|
+
new._model = self._model
|
|
68
|
+
new._session = self._session
|
|
69
|
+
new._statement = self._statement
|
|
70
|
+
return new
|
|
71
|
+
|
|
72
|
+
def filter(self, *conditions) -> "QueryBuilder[T]":
|
|
73
|
+
"""添加过滤条件"""
|
|
74
|
+
builder = self._clone()
|
|
75
|
+
if conditions:
|
|
76
|
+
builder._statement = builder._statement.where(*conditions)
|
|
77
|
+
return builder
|
|
78
|
+
|
|
79
|
+
def filter_by(self, **kwargs) -> "QueryBuilder[T]":
|
|
80
|
+
"""按字段值过滤(等值条件)"""
|
|
81
|
+
conditions = [
|
|
82
|
+
getattr(self._model, key) == value for key, value in kwargs.items()
|
|
83
|
+
]
|
|
84
|
+
return self.filter(*conditions)
|
|
85
|
+
|
|
86
|
+
def join(self, target, onclause=None, isouter: bool = False) -> "QueryBuilder[T]":
|
|
87
|
+
"""添加 JOIN"""
|
|
88
|
+
builder = self._clone()
|
|
89
|
+
builder._statement = (
|
|
90
|
+
builder._statement.join(target, onclause, isouter=isouter)
|
|
91
|
+
if onclause
|
|
92
|
+
else builder._statement.join(target, isouter=isouter)
|
|
93
|
+
)
|
|
94
|
+
return builder
|
|
95
|
+
|
|
96
|
+
def order_by(self, *columns) -> "QueryBuilder[T]":
|
|
97
|
+
"""排序"""
|
|
98
|
+
builder = self._clone()
|
|
99
|
+
builder._statement = builder._statement.order_by(*columns)
|
|
100
|
+
return builder
|
|
101
|
+
|
|
102
|
+
def limit(self, limit: int) -> "QueryBuilder[T]":
|
|
103
|
+
"""限制返回行数"""
|
|
104
|
+
builder = self._clone()
|
|
105
|
+
builder._statement = builder._statement.limit(limit)
|
|
106
|
+
return builder
|
|
107
|
+
|
|
108
|
+
def offset(self, offset: int) -> "QueryBuilder[T]":
|
|
109
|
+
"""跳过前 N 行"""
|
|
110
|
+
builder = self._clone()
|
|
111
|
+
builder._statement = builder._statement.offset(offset)
|
|
112
|
+
return builder
|
|
113
|
+
|
|
114
|
+
def distinct(self) -> "QueryBuilder[T]":
|
|
115
|
+
"""去重"""
|
|
116
|
+
builder = self._clone()
|
|
117
|
+
builder._statement = builder._statement.distinct()
|
|
118
|
+
return builder
|
|
119
|
+
|
|
120
|
+
def only(self, *field_names: str) -> "QueryBuilder[T]":
|
|
121
|
+
"""只加载指定字段"""
|
|
122
|
+
builder = self._clone()
|
|
123
|
+
columns = [getattr(self._model, name) for name in field_names]
|
|
124
|
+
builder._statement = builder._statement.options(load_only(*columns))
|
|
125
|
+
return builder
|
|
126
|
+
|
|
127
|
+
# ==================== 执行方法 ====================
|
|
128
|
+
|
|
129
|
+
def all(self) -> list[T]:
|
|
130
|
+
"""获取所有记录"""
|
|
131
|
+
return list(self._session.exec(self._statement).all())
|
|
132
|
+
|
|
133
|
+
def first(self) -> T | None:
|
|
134
|
+
"""获取第一条记录"""
|
|
135
|
+
return self._session.exec(self._statement.limit(1)).first()
|
|
136
|
+
|
|
137
|
+
def count(self) -> int:
|
|
138
|
+
"""获取记录数(不受 limit/offset 影响)"""
|
|
139
|
+
# 从原始模型构建计数查询,仅保留 WHERE 条件,忽略 limit/offset
|
|
140
|
+
count_stmt = select(func.count()).select_from(self._model)
|
|
141
|
+
whereclause = self._statement.whereclause
|
|
142
|
+
if whereclause is not None:
|
|
143
|
+
count_stmt = count_stmt.where(whereclause)
|
|
144
|
+
return self._session.exec(count_stmt).one()
|
|
145
|
+
|
|
146
|
+
def exists(self) -> bool:
|
|
147
|
+
"""检查是否存在符合条件的记录"""
|
|
148
|
+
from sqlalchemy import exists as sa_exists
|
|
149
|
+
|
|
150
|
+
exists_stmt = select(sa_exists().where(self._statement.whereclause))
|
|
151
|
+
return bool(self._session.exec(exists_stmt).one())
|
|
152
|
+
|
|
153
|
+
def paginate(self, page: int = 1, per_page: int = 20) -> dict[str, Any]:
|
|
154
|
+
"""分页查询
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
page: 页码(从 1 开始)
|
|
158
|
+
per_page: 每页条数
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
包含 items/total/page/per_page/pages/has_prev/has_next 的字典
|
|
162
|
+
"""
|
|
163
|
+
offset = (page - 1) * per_page
|
|
164
|
+
total = self.count()
|
|
165
|
+
|
|
166
|
+
paginated = self._statement.offset(offset).limit(per_page)
|
|
167
|
+
items = list(self._session.exec(paginated).all())
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
"items": items,
|
|
171
|
+
"total": total,
|
|
172
|
+
"page": page,
|
|
173
|
+
"per_page": per_page,
|
|
174
|
+
"pages": (total + per_page - 1) // per_page if per_page > 0 else 0,
|
|
175
|
+
"has_prev": page > 1,
|
|
176
|
+
"has_next": page * per_page < total,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class QueryCache:
|
|
181
|
+
"""线程安全的查询缓存(TTL + LRU 淘汰)"""
|
|
182
|
+
|
|
183
|
+
def __init__(self, default_ttl: int = 300, maxsize: int = 1000):
|
|
184
|
+
self._cache: dict[str, dict[str, Any]] = {}
|
|
185
|
+
self._default_ttl = default_ttl
|
|
186
|
+
self._maxsize = maxsize
|
|
187
|
+
self._lock = threading.RLock()
|
|
188
|
+
|
|
189
|
+
def get(self, key: str) -> Any | None:
|
|
190
|
+
"""获取缓存值"""
|
|
191
|
+
with self._lock:
|
|
192
|
+
if key not in self._cache:
|
|
193
|
+
return None
|
|
194
|
+
item = self._cache[key]
|
|
195
|
+
if item["expires_at"] > datetime.now(UTC):
|
|
196
|
+
return item["value"]
|
|
197
|
+
del self._cache[key]
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
def set(self, key: str, value: Any, ttl: int | None = None) -> None:
|
|
201
|
+
"""设置缓存值"""
|
|
202
|
+
with self._lock:
|
|
203
|
+
# 达到上限时清理最早的条目
|
|
204
|
+
if len(self._cache) >= self._maxsize:
|
|
205
|
+
oldest = min(self._cache, key=lambda k: self._cache[k]["expires_at"])
|
|
206
|
+
del self._cache[oldest]
|
|
207
|
+
self._cache[key] = {
|
|
208
|
+
"value": value,
|
|
209
|
+
"expires_at": datetime.now(UTC)
|
|
210
|
+
+ timedelta(seconds=ttl or self._default_ttl),
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
def delete(self, key: str) -> None:
|
|
214
|
+
"""删除缓存"""
|
|
215
|
+
with self._lock:
|
|
216
|
+
self._cache.pop(key, None)
|
|
217
|
+
|
|
218
|
+
def clear(self) -> None:
|
|
219
|
+
"""清空缓存"""
|
|
220
|
+
with self._lock:
|
|
221
|
+
self._cache.clear()
|
|
222
|
+
|
|
223
|
+
def clean_expired(self) -> int:
|
|
224
|
+
"""清理过期缓存,返回清理的条目数"""
|
|
225
|
+
with self._lock:
|
|
226
|
+
now = datetime.now(UTC)
|
|
227
|
+
expired = [k for k, v in self._cache.items() if v["expires_at"] <= now]
|
|
228
|
+
for k in expired:
|
|
229
|
+
del self._cache[k]
|
|
230
|
+
return len(expired)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class QueryOptimizer:
|
|
234
|
+
"""查询优化器:分析执行计划、建议索引、监控性能"""
|
|
235
|
+
|
|
236
|
+
def __init__(self, session: Session):
|
|
237
|
+
self.session = session
|
|
238
|
+
self._stats: list[dict[str, Any]] = []
|
|
239
|
+
|
|
240
|
+
def explain(self, statement) -> dict[str, Any]:
|
|
241
|
+
"""分析查询执行计划"""
|
|
242
|
+
from sqlalchemy import text
|
|
243
|
+
|
|
244
|
+
dialect = self.session.get_bind().dialect.name
|
|
245
|
+
compiled = statement.compile(self.session.get_bind())
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
if dialect == "postgresql":
|
|
249
|
+
result = self.session.execute(text(f"EXPLAIN (FORMAT JSON) {compiled}"))
|
|
250
|
+
return {"dialect": dialect, "plan": result.scalar()}
|
|
251
|
+
elif dialect == "mysql":
|
|
252
|
+
result = self.session.execute(text(f"EXPLAIN FORMAT=JSON {compiled}"))
|
|
253
|
+
return {"dialect": dialect, "plan": result.scalar()}
|
|
254
|
+
elif dialect == "sqlite":
|
|
255
|
+
result = self.session.execute(text(f"EXPLAIN QUERY PLAN {compiled}"))
|
|
256
|
+
return {
|
|
257
|
+
"dialect": dialect,
|
|
258
|
+
"plan": [dict(row._mapping) for row in result.fetchall()],
|
|
259
|
+
}
|
|
260
|
+
else:
|
|
261
|
+
return {"dialect": dialect, "plan": None, "error": "不支持的数据库"}
|
|
262
|
+
except Exception as e:
|
|
263
|
+
logger.error(f"❌ 查询分析失败: {e}")
|
|
264
|
+
return {"error": str(e)}
|
|
265
|
+
|
|
266
|
+
def suggest_indexes(
|
|
267
|
+
self, model: type[SQLModel], query_fields: list[str]
|
|
268
|
+
) -> list[str]:
|
|
269
|
+
"""建议索引"""
|
|
270
|
+
table_name = model.__tablename__
|
|
271
|
+
suggestions = [
|
|
272
|
+
f"CREATE INDEX idx_{table_name}_{field} ON {table_name}({field});"
|
|
273
|
+
for field in query_fields
|
|
274
|
+
]
|
|
275
|
+
if len(query_fields) > 1:
|
|
276
|
+
composite = "_".join(query_fields[:3])
|
|
277
|
+
suggestions.append(
|
|
278
|
+
f"CREATE INDEX idx_{table_name}_{composite} "
|
|
279
|
+
f"ON {table_name}({', '.join(query_fields[:3])});"
|
|
280
|
+
)
|
|
281
|
+
return suggestions
|
|
282
|
+
|
|
283
|
+
def measure(self, statement, params: dict | None = None) -> dict[str, Any]:
|
|
284
|
+
"""测量查询性能"""
|
|
285
|
+
start = time.time()
|
|
286
|
+
try:
|
|
287
|
+
result = self.session.execute(statement, params or {})
|
|
288
|
+
rows = result.fetchall()
|
|
289
|
+
elapsed = time.time() - start
|
|
290
|
+
|
|
291
|
+
stats: dict[str, Any] = {
|
|
292
|
+
"execution_time": elapsed,
|
|
293
|
+
"row_count": len(rows),
|
|
294
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
295
|
+
}
|
|
296
|
+
if elapsed > 1.0:
|
|
297
|
+
stats["warning"] = "慢查询"
|
|
298
|
+
logger.warning(f"⚠️ 慢查询: {elapsed:.2f}s, 返回 {len(rows)} 行")
|
|
299
|
+
self._stats.append(stats)
|
|
300
|
+
return stats
|
|
301
|
+
except Exception as e:
|
|
302
|
+
logger.error(f"❌ 性能测量失败: {e}")
|
|
303
|
+
return {"error": str(e)}
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def stats(self) -> list[dict[str, Any]]:
|
|
307
|
+
"""获取查询统计信息"""
|
|
308
|
+
return list(self._stats)
|
|
309
|
+
|
|
310
|
+
def clear_stats(self) -> None:
|
|
311
|
+
"""清空统计信息"""
|
|
312
|
+
self._stats.clear()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def cached_query(ttl: int = 300, maxsize: int = 1000) -> Callable:
|
|
316
|
+
"""查询缓存装饰器
|
|
317
|
+
|
|
318
|
+
注意:被装饰的函数不应将 Session 作为参数,否则每次缓存键不同。
|
|
319
|
+
"""
|
|
320
|
+
cache = QueryCache(default_ttl=ttl, maxsize=maxsize)
|
|
321
|
+
|
|
322
|
+
def decorator(func: Callable) -> Callable:
|
|
323
|
+
@functools.wraps(func)
|
|
324
|
+
def wrapper(*args, **kwargs):
|
|
325
|
+
cache_key = f"{func.__name__}:{repr(args)}:{repr(kwargs)}"
|
|
326
|
+
cached = cache.get(cache_key)
|
|
327
|
+
if cached is not None:
|
|
328
|
+
logger.debug(f"✅ 缓存命中: {func.__name__}")
|
|
329
|
+
return cached
|
|
330
|
+
result = func(*args, **kwargs)
|
|
331
|
+
cache.set(cache_key, result, ttl)
|
|
332
|
+
return result
|
|
333
|
+
|
|
334
|
+
# 暴露缓存实例供外部管理
|
|
335
|
+
wrapper._cache = cache # type: ignore
|
|
336
|
+
return wrapper
|
|
337
|
+
|
|
338
|
+
return decorator
|
|
@@ -84,7 +84,9 @@ class SQLiteKit(BaseDbKit):
|
|
|
84
84
|
columns: 列定义字典,如 {"id": "TEXT PRIMARY KEY", "name": "TEXT NOT NULL"}
|
|
85
85
|
if_not_exists: 是否附加 IF NOT EXISTS(默认 True)
|
|
86
86
|
"""
|
|
87
|
-
column_defs = ", ".join(
|
|
87
|
+
column_defs = ", ".join(
|
|
88
|
+
f"{name} {definition}" for name, definition in columns.items()
|
|
89
|
+
)
|
|
88
90
|
if_not_exists_sql = "IF NOT EXISTS " if if_not_exists else ""
|
|
89
91
|
sql = f"CREATE TABLE {if_not_exists_sql}{table_name} ({column_defs})"
|
|
90
92
|
with get_engine(cls.DB_TYPE).connect() as conn:
|
|
@@ -92,6 +94,42 @@ class SQLiteKit(BaseDbKit):
|
|
|
92
94
|
conn.commit()
|
|
93
95
|
logger.info(f"✅ 创建表 {table_name}")
|
|
94
96
|
|
|
97
|
+
@classmethod
|
|
98
|
+
def raw_insert(cls, table_name: str, data: dict[str, object]) -> int:
|
|
99
|
+
"""向指定表插入一行数据(键为列名)
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
table_name: 表名
|
|
103
|
+
data: 列名 → 值 字典
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
受影响的行数
|
|
107
|
+
"""
|
|
108
|
+
columns = ", ".join(data.keys())
|
|
109
|
+
placeholders = ", ".join(f":{name}" for name in data)
|
|
110
|
+
sql = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})"
|
|
111
|
+
return cls.execute(sql, dict(data))
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def raw_get_by(
|
|
115
|
+
cls, table_name: str, column: str, value: object
|
|
116
|
+
) -> dict[str, object] | None:
|
|
117
|
+
"""按单列等值查询一行
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
table_name: 表名
|
|
121
|
+
column: 列名
|
|
122
|
+
value: 查询值
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
匹配的第一行(字典);无匹配返回 None
|
|
126
|
+
"""
|
|
127
|
+
rows = cls.query(
|
|
128
|
+
f"SELECT * FROM {table_name} WHERE {column} = :value LIMIT 1",
|
|
129
|
+
{"value": value},
|
|
130
|
+
)
|
|
131
|
+
return rows[0] if rows else None
|
|
132
|
+
|
|
95
133
|
@classmethod
|
|
96
134
|
def get_table_info(cls, table_name: str) -> list[dict[str, object]]:
|
|
97
135
|
"""获取表的列信息
|
|
@@ -11,17 +11,27 @@ authors = [
|
|
|
11
11
|
{ name = "Clark", email = "changhongyuan@126.com" }
|
|
12
12
|
]
|
|
13
13
|
requires-python = ">=3.13"
|
|
14
|
+
keywords = [
|
|
15
|
+
"sdpy", "kit", "工具集", "数据库", "postgresql", "mysql", "sqlite",
|
|
16
|
+
"sqlmodel", "sqlalchemy", "ai", "mcp", "redis", "向量检索", "开发工具",
|
|
17
|
+
]
|
|
14
18
|
classifiers = [
|
|
15
19
|
"Development Status :: 4 - Beta",
|
|
16
20
|
"Intended Audience :: Developers",
|
|
17
21
|
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Natural Language :: Chinese (Simplified)",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
18
24
|
"Programming Language :: Python :: 3 :: Only",
|
|
19
25
|
"Programming Language :: Python :: 3.13",
|
|
20
26
|
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Topic :: Database",
|
|
28
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
29
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
30
|
]
|
|
22
31
|
dependencies = [
|
|
23
32
|
"pydantic>=2.0",
|
|
24
33
|
"pydantic-settings>=2.0",
|
|
34
|
+
"numpy>=2.0",
|
|
25
35
|
"sqlalchemy~=2.0.51",
|
|
26
36
|
"sqlmodel>=0.0.39",
|
|
27
37
|
"pymysql>=1.2.0",
|
|
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
|
|
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
|