sdpy-kit 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.
kit/db/models/base.py ADDED
@@ -0,0 +1,56 @@
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
+ """SQLModel 基础模型混入类
22
+
23
+ 提供 TimestampMixin(时间戳)、SoftDeleteMixin(软删除)、AuditMixin(审计字段)。
24
+ default_factory 统一使用 datetime.now(UTC),避免废弃的 utcnow()。
25
+ """
26
+
27
+ from datetime import UTC, datetime
28
+ from typing import Optional
29
+
30
+ from sqlmodel import Field, SQLModel
31
+
32
+
33
+ def _utcnow() -> datetime:
34
+ """获取当前 UTC 时间"""
35
+ return datetime.now(UTC)
36
+
37
+
38
+ class TimestampMixin(SQLModel):
39
+ """时间戳混入:自动添加 created_at 和 updated_at 字段"""
40
+
41
+ created_at: datetime = Field(default_factory=_utcnow, nullable=False)
42
+ updated_at: Optional[datetime] = Field(default=None, nullable=True)
43
+
44
+
45
+ class SoftDeleteMixin(SQLModel):
46
+ """软删除混入:添加 is_deleted 和 deleted_at 字段"""
47
+
48
+ is_deleted: bool = Field(default=False, nullable=False)
49
+ deleted_at: Optional[datetime] = Field(default=None, nullable=True)
50
+
51
+
52
+ class AuditMixin(TimestampMixin):
53
+ """审计混入:在 TimestampMixin 基础上添加操作人字段"""
54
+
55
+ created_by: Optional[str] = Field(default=None, max_length=100)
56
+ updated_by: Optional[str] = Field(default=None, max_length=100)
kit/db/mysql_kit.py ADDED
@@ -0,0 +1,89 @@
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
+ from __future__ import annotations
21
+
22
+ # date: 2026-08-22
23
+ """MySQL 数据库工具类
24
+
25
+ 基于 kit.db 统一数据库层封装,提供开箱即用的服务方法。
26
+ 业务代码无需管理 session/transaction,直接调用即可。
27
+
28
+ 本类继承 BaseDbKit,复用全部共享实现(连接管理 / ORM / 原生 SQL / 事务 /
29
+ 表操作),仅声明 DB_TYPE 并覆写 MySQL 专属的 get_table_info。
30
+
31
+ 使用方式::
32
+
33
+ # 单条操作(自动管理事务)
34
+ user = MySqlKit.create(UserModel, {"name": "张三", "email": "zs@example.com"})
35
+ users = MySqlKit.list(UserModel, skip=0, limit=10)
36
+ MySqlKit.update(UserModel, user.id, {"name": "李四"})
37
+
38
+ # 原生 SQL 操作
39
+ rows = MySqlKit.query("SELECT * FROM users WHERE id = :id", {"id": 1})
40
+
41
+ # 多操作事务
42
+ with MySqlKit.transaction() as session:
43
+ crud = CRUDBase(UserModel)
44
+ crud.create(session, {"name": "test"})
45
+ # 退出 with 时统一提交
46
+ """
47
+
48
+ from kit.config.log_config import get_default_logger
49
+ from kit.db.base_db_kit import BaseDbKit
50
+ from kit.db.engine import MYSQL
51
+
52
+ logger = get_default_logger(__name__)
53
+
54
+
55
+ class MySqlKit(BaseDbKit):
56
+ """MySQL 数据库工具类
57
+
58
+ 基于 kit.db 统一数据库层,封装为业务可直调的服务方法。
59
+ 所有 ORM 方法内部使用 session_scope 管理事务(自动提交/回滚),
60
+ 业务代码无需手动管理 session。
61
+
62
+ 对于多操作事务场景,请使用 transaction() 上下文管理器。
63
+ """
64
+
65
+ DB_TYPE = MYSQL
66
+
67
+ @classmethod
68
+ def get_table_info(cls, table_name: str) -> list[dict[str, object]]:
69
+ """获取表的列信息
70
+
71
+ Args:
72
+ table_name: 表名
73
+
74
+ Returns:
75
+ 列信息字典列表(column_name, data_type, is_nullable 等)
76
+ """
77
+ sql = """
78
+ SELECT
79
+ COLUMN_NAME as column_name,
80
+ DATA_TYPE as data_type,
81
+ IS_NULLABLE as is_nullable,
82
+ COLUMN_DEFAULT as column_default,
83
+ COLUMN_COMMENT as column_comment
84
+ FROM INFORMATION_SCHEMA.COLUMNS
85
+ WHERE TABLE_NAME = :table_name
86
+ AND TABLE_SCHEMA = DATABASE()
87
+ ORDER BY ORDINAL_POSITION
88
+ """
89
+ return cls.query(sql, {"table_name": table_name})
kit/db/pg_kit.py ADDED
@@ -0,0 +1,87 @@
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
+ from __future__ import annotations
21
+
22
+ # date: 2026-08-22
23
+ """PostgreSQL 数据库工具类
24
+
25
+ 基于 kit.db 统一数据库层封装,提供开箱即用的服务方法。
26
+ 业务代码无需管理 session/transaction,直接调用即可。
27
+
28
+ 本类继承 BaseDbKit,复用全部共享实现(连接管理 / ORM / 原生 SQL / 事务 /
29
+ 表操作),仅声明 DB_TYPE 并覆写 PostgreSQL 专属的 get_table_info。
30
+
31
+ 使用方式::
32
+
33
+ # 单条操作(自动管理事务)
34
+ user = PgKit.create(UserModel, {"name": "张三", "email": "zs@example.com"})
35
+ users = PgKit.list(UserModel, skip=0, limit=10)
36
+ PgKit.update(UserModel, user.id, {"name": "李四"})
37
+
38
+ # 原生 SQL 操作
39
+ rows = PgKit.query("SELECT * FROM users WHERE active = :active", {"active": True})
40
+
41
+ # 多操作事务
42
+ with PgKit.transaction() as session:
43
+ crud = CRUDBase(UserModel)
44
+ crud.create(session, {"name": "test"})
45
+ # 退出 with 时统一提交
46
+ """
47
+
48
+ from kit.config.log_config import get_default_logger
49
+ from kit.db.base_db_kit import BaseDbKit
50
+ from kit.db.engine import POSTGRESQL
51
+
52
+ logger = get_default_logger(__name__)
53
+
54
+
55
+ class PgKit(BaseDbKit):
56
+ """PostgreSQL 数据库工具类
57
+
58
+ 基于 kit.db 统一数据库层,封装为业务可直调的服务方法。
59
+ 所有 ORM 方法内部使用 session_scope 管理事务(自动提交/回滚),
60
+ 业务代码无需手动管理 session。
61
+
62
+ 对于多操作事务场景,请使用 transaction() 上下文管理器。
63
+ """
64
+
65
+ DB_TYPE = POSTGRESQL
66
+
67
+ @classmethod
68
+ def get_table_info(cls, table_name: str) -> list[dict[str, object]]:
69
+ """获取表的列信息
70
+
71
+ Args:
72
+ table_name: 表名
73
+
74
+ Returns:
75
+ 列信息字典列表(column_name, data_type, is_nullable, column_default)
76
+ """
77
+ sql = """
78
+ SELECT
79
+ column_name,
80
+ data_type,
81
+ is_nullable,
82
+ column_default
83
+ FROM information_schema.columns
84
+ WHERE table_name = :table_name
85
+ ORDER BY ordinal_position
86
+ """
87
+ return cls.query(sql, {"table_name": table_name})
kit/db/session.py ADDED
@@ -0,0 +1,85 @@
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
+ 提供唯一的会话入口 session_scope,自动管理提交/回滚/关闭。
24
+ CRUD 操作不应在内部调用 commit/rollback,由 session_scope 统一管理事务边界。
25
+ """
26
+
27
+ from contextlib import contextmanager
28
+ from typing import Generator
29
+
30
+ from sqlmodel import Session
31
+
32
+ from kit.config.log_config import get_default_logger
33
+ from kit.db.engine import get_engine
34
+
35
+ logger = get_default_logger(__name__)
36
+
37
+
38
+ @contextmanager
39
+ def session_scope(
40
+ db_type: str | None = None,
41
+ *,
42
+ autocommit: bool = True,
43
+ ) -> Generator[Session, None, None]:
44
+ """会话作用域上下文管理器(唯一的会话入口)
45
+
46
+ 正常退出时自动提交(autocommit=True),异常时自动回滚,最终关闭会话。
47
+
48
+ Args:
49
+ db_type: 数据库类型(postgresql/mysql/sqlite),None 时使用配置默认值
50
+ autocommit: 是否在正常退出时自动提交事务
51
+
52
+ Yields:
53
+ SQLModel Session 实例
54
+
55
+ 用法::
56
+
57
+ with session_scope() as session:
58
+ user = User(name="test")
59
+ session.add(user)
60
+ # 退出时自动提交
61
+ """
62
+ engine = get_engine(db_type)
63
+ session = Session(engine)
64
+ db_name = db_type or "默认"
65
+
66
+ try:
67
+ yield session
68
+ if autocommit:
69
+ session.commit()
70
+ logger.info(f"✅ [{db_name}] 会话事务已提交")
71
+ except Exception as e:
72
+ session.rollback()
73
+ logger.warning(f"⚠️ [{db_name}] 会话事务已回滚,原因: {e}")
74
+ raise
75
+ finally:
76
+ session.close()
77
+
78
+
79
+ def get_session(db_type: str | None = None) -> Session:
80
+ """获取裸 Session(调用者需自行管理生命周期)
81
+
82
+ 建议优先使用 session_scope 上下文管理器。
83
+ """
84
+ engine = get_engine(db_type)
85
+ return Session(engine)
kit/db/sqlite_kit.py ADDED
@@ -0,0 +1,284 @@
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
+ from __future__ import annotations
21
+
22
+ # date: 2026-08-22
23
+ """SQLite 数据库工具类
24
+
25
+ 基于 kit.db 统一数据库层封装,提供开箱即用的服务方法。
26
+ 业务代码无需管理 session/transaction,直接调用即可。
27
+
28
+ 本类继承 BaseDbKit,复用全部共享实现(连接管理 / ORM / 原生 SQL / 事务 /
29
+ 表操作),并额外提供 SQLite 专属的向量操作(基于 sqlite-vector 扩展)。
30
+
31
+ 使用方式::
32
+
33
+ # 单条操作(自动管理事务)
34
+ user = SQLiteKit.create(UserModel, {"name": "张三"})
35
+ users = SQLiteKit.list(UserModel, skip=0, limit=10)
36
+ SQLiteKit.update(UserModel, user.id, {"name": "李四"})
37
+
38
+ # 向量操作
39
+ SQLiteKit.create_vector_table("images", dims=384)
40
+ SQLiteKit.insert_vectors("images", [[0.1, 0.2, ...], ...])
41
+ rows = SQLiteKit.search_vectors("images", [0.1, 0.2, ...], top_k=5)
42
+ """
43
+
44
+ import numpy as np
45
+ from sqlalchemy import text
46
+
47
+ from kit.config.log_config import get_default_logger
48
+ from kit.db.base_db_kit import BaseDbKit
49
+ from kit.db.engine import SQLITE, get_engine
50
+ from kit.db.session import session_scope
51
+
52
+ logger = get_default_logger(__name__)
53
+
54
+
55
+ class SQLiteKit(BaseDbKit):
56
+ """SQLite 数据库工具类
57
+
58
+ 基于 kit.db 统一数据库层,封装为业务可直调的服务方法。
59
+ 所有 ORM 方法内部使用 session_scope 管理事务(自动提交/回滚),
60
+ 业务代码无需手动管理 session。
61
+
62
+ 对于多操作事务场景,请使用 transaction() 上下文管理器。
63
+ """
64
+
65
+ DB_TYPE = SQLITE
66
+
67
+ @classmethod
68
+ def raw_connection(cls):
69
+ """获取原生 DBAPI 连接(sqlite-vector 扩展已由引擎自动加载)
70
+
71
+ 用于执行 sqlite-vector 特殊函数(vector_init / vector_quantize /
72
+ vector_quantize_preload 等)与静态 DDL。调用方负责关闭连接。
73
+ """
74
+ return get_engine(cls.DB_TYPE).raw_connection()
75
+
76
+ @classmethod
77
+ def create_raw_table(
78
+ cls, table_name: str, columns: dict[str, str], if_not_exists: bool = True
79
+ ) -> None:
80
+ """根据「列名 → 列定义」字典创建普通表
81
+
82
+ Args:
83
+ table_name: 表名
84
+ columns: 列定义字典,如 {"id": "TEXT PRIMARY KEY", "name": "TEXT NOT NULL"}
85
+ if_not_exists: 是否附加 IF NOT EXISTS(默认 True)
86
+ """
87
+ column_defs = ", ".join(f"{name} {definition}" for name, definition in columns.items())
88
+ if_not_exists_sql = "IF NOT EXISTS " if if_not_exists else ""
89
+ sql = f"CREATE TABLE {if_not_exists_sql}{table_name} ({column_defs})"
90
+ with get_engine(cls.DB_TYPE).connect() as conn:
91
+ conn.execute(text(sql))
92
+ conn.commit()
93
+ logger.info(f"✅ 创建表 {table_name}")
94
+
95
+ @classmethod
96
+ def get_table_info(cls, table_name: str) -> list[dict[str, object]]:
97
+ """获取表的列信息
98
+
99
+ Args:
100
+ table_name: 表名
101
+
102
+ Returns:
103
+ 列信息字典列表(column_name, data_type, is_nullable, column_default)
104
+ """
105
+ sql = """
106
+ SELECT
107
+ name as column_name,
108
+ type as data_type,
109
+ CASE WHEN "notnull" = 0 THEN 'YES' ELSE 'NO' END as is_nullable,
110
+ dflt_value as column_default
111
+ FROM pragma_table_info(:table_name)
112
+ ORDER BY cid
113
+ """
114
+ return cls.query(sql, {"table_name": table_name})
115
+
116
+ # ==================== 向量操作 ====================
117
+
118
+ @classmethod
119
+ def create_vector_table(
120
+ cls,
121
+ table_name: str,
122
+ id_col: str = "id",
123
+ dims: int = 384,
124
+ extra_cols: list[dict[str, str]] | None = None,
125
+ ) -> None:
126
+ """创建包含向量列的表
127
+
128
+ Args:
129
+ table_name: 表名
130
+ id_col: 主键列名(INTEGER PRIMARY KEY)
131
+ dims: 向量维度(默认 384)
132
+ extra_cols: 额外列定义,每项为 {"name": str, "type": str},
133
+ 如 [{"name": "label", "type": "TEXT"}]
134
+
135
+ 表结构示例::
136
+
137
+ CREATE TABLE images (
138
+ id INTEGER PRIMARY KEY,
139
+ embedding BLOB, -- FLOAT32 向量
140
+ label TEXT
141
+ );
142
+ """
143
+ extra_defs = ""
144
+ if extra_cols:
145
+ extra_defs = ", " + ", ".join(
146
+ f"{c['name']} {c['type']}" for c in extra_cols
147
+ )
148
+
149
+ sql = f"""
150
+ CREATE TABLE IF NOT EXISTS {table_name} (
151
+ {id_col} INTEGER PRIMARY KEY,
152
+ embedding BLOB{extra_defs}
153
+ )
154
+ """
155
+ with session_scope(cls.DB_TYPE, autocommit=False) as session:
156
+ session.execute(text(sql))
157
+ logger.info(f"✅ 创建向量表 {table_name}(维度={dims})")
158
+
159
+ @classmethod
160
+ def insert_vectors(
161
+ cls,
162
+ table_name: str,
163
+ vectors: list[list[float]],
164
+ ids: list[int] | None = None,
165
+ extra_data: list[dict[str, object]] | None = None,
166
+ ) -> int:
167
+ """批量插入向量数据
168
+
169
+ Args:
170
+ table_name: 表名
171
+ vectors: 向量列表,每个向量为 float 列表
172
+ ids: 可选的 ID 列表(长度需与 vectors 一致),None 时自动递增
173
+ extra_data: 额外列数据列表(长度需与 vectors 一致)
174
+
175
+ Returns:
176
+ 插入的行数
177
+ """
178
+ if ids is not None and len(ids) != len(vectors):
179
+ raise ValueError("ids 长度必须与 vectors 一致")
180
+ if extra_data is not None and len(extra_data) != len(vectors):
181
+ raise ValueError("extra_data 长度必须与 vectors 一致")
182
+
183
+ engine = get_engine(cls.DB_TYPE)
184
+ with engine.connect() as conn:
185
+ for i, vec in enumerate(vectors):
186
+ vec_bytes = np.array(vec, dtype=np.float32).tobytes()
187
+ if ids is not None:
188
+ _id = ids[i]
189
+ sql = f"INSERT INTO {table_name} (id, embedding) VALUES (:id, :vec)"
190
+ params: dict[str, object] = {"id": _id, "vec": vec_bytes}
191
+ else:
192
+ sql = f"INSERT INTO {table_name} (embedding) VALUES (:vec)"
193
+ params = {"vec": vec_bytes}
194
+
195
+ if extra_data is not None:
196
+ col_names = ", ".join(k for k in extra_data[i].keys())
197
+ col_vals = ", ".join(f":{k}" for k in extra_data[i].keys())
198
+ if ids is not None:
199
+ sql = (
200
+ f"INSERT INTO {table_name} "
201
+ f"(id, embedding, {col_names}) VALUES (:id, :vec, {col_vals})" # noqa: E501
202
+ )
203
+ params.update(extra_data[i])
204
+ else:
205
+ sql = (
206
+ f"INSERT INTO {table_name} "
207
+ f"(embedding, {col_names}) VALUES (:vec, {col_vals})"
208
+ )
209
+ params = {"vec": vec_bytes, **extra_data[i]}
210
+
211
+ conn.execute(text(sql), params)
212
+ conn.commit()
213
+ logger.info(f"✅ 插入 {len(vectors)} 条向量到 {table_name}")
214
+ return len(vectors)
215
+
216
+ @classmethod
217
+ def search_vectors(
218
+ cls,
219
+ table_name: str,
220
+ query_vector: list[float],
221
+ top_k: int = 10,
222
+ distance_type: str = "COSINE",
223
+ filter_sql: str | None = None,
224
+ ) -> list[dict[str, object]]:
225
+ """搜索最近邻向量
226
+
227
+ Args:
228
+ table_name: 表名
229
+ query_vector: 查询向量(float 列表)
230
+ top_k: 返回结果数
231
+ distance_type: 距离类型(COSINE / L2 / L1 / DOT / SQUARED_L2)
232
+ filter_sql: 可选的过滤条件 WHERE 子句(不含 WHERE 关键字),
233
+ 如 "label = 'cat'"
234
+
235
+ Returns:
236
+ 按距离升序排列的结果列表,每项为 {"id": int, "distance": float, ...额外列}
237
+ """
238
+ engine = get_engine(cls.DB_TYPE)
239
+ query_bytes = np.array(query_vector, dtype=np.float32).tobytes()
240
+
241
+ with engine.connect() as conn:
242
+ # 初始化向量索引
243
+ conn.execute(
244
+ text(
245
+ "SELECT vector_init("
246
+ f"'{table_name}', 'embedding', 'type=FLOAT32,dimension={len(query_vector)}')" # noqa: E501
247
+ )
248
+ )
249
+
250
+ # 量化
251
+ conn.execute(text(f"SELECT vector_quantize('{table_name}', 'embedding')"))
252
+
253
+ where_clause = f"WHERE {filter_sql}" if filter_sql else ""
254
+
255
+ sql = f"""
256
+ SELECT e.id, v.distance
257
+ FROM {table_name} AS e
258
+ JOIN vector_quantize_scan('{table_name}', 'embedding', :query, {top_k}) AS v
259
+ ON e.id = v.rowid
260
+ {where_clause}
261
+ ORDER BY v.distance ASC
262
+ """
263
+ result = conn.execute(text(sql), {"query": query_bytes})
264
+ rows = result.fetchall()
265
+ columns = result.keys()
266
+ data = [dict(zip(columns, row)) for row in rows]
267
+ logger.info(f"🔍 向量搜索 {table_name} → 返回 {len(data)} 条")
268
+ return data
269
+
270
+ @classmethod
271
+ def vector_version(cls) -> str:
272
+ """获取 sqlite-vector 扩展版本
273
+
274
+ Returns:
275
+ 版本号字符串,加载失败时返回 "unavailable"
276
+ """
277
+ try:
278
+ engine = get_engine(cls.DB_TYPE)
279
+ with engine.connect() as conn:
280
+ row = conn.execute(text("SELECT vector_version()")).fetchone()
281
+ return str(row[0]) if row else "unknown"
282
+ except Exception as e:
283
+ logger.warning(f"⚠️ 获取 vector_version 失败: {e}")
284
+ return "unavailable"