sdpy-kit 0.2.4__tar.gz → 0.2.5__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.2.4 → sdpy_kit-0.2.5}/PKG-INFO +29 -16
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/README.md +18 -5
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/__init__.py +4 -1
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/db/__init__.py +4 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/db/base_db_kit.py +132 -21
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/pyproject.toml +10 -10
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/.gitignore +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/ai/__init__.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/ai/base_service.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/ai/chat_service.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/ai/embedding_service.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/ai/exceptions.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/ai/failover.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/ai/image_service.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/ai/model_builder.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/ai/model_client.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/ai/pool.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/ai/profiles.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/ai/rerank_service.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/ai/runner.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/ai/types.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/anydoc_kit.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/common_kit.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/config/__init__.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/config/kit_config.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/config/log_config.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/db/crud.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/db/engine.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/db/exceptions.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/db/models/__init__.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/db/models/base.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/db/mysql_kit.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/db/pg_kit.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/db/query.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/db/session.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/db/sqlite_kit.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/db/transaction.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/docx_kit.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/file_kit.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/marker_kit.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/mcp_kit.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/md2doc_kit.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/mineru_kit.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/redis_kit.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/wx_article_kit.py +0 -0
- {sdpy_kit-0.2.4 → sdpy_kit-0.2.5}/kit/zvec_kit.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sdpy-kit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: SDPY Kit——统一数据库访问层(PG/MySQL/SQLite)与 AI/MCP/Redis/文件/文档/向量工具集
|
|
5
5
|
Author-email: Clark <changhongyuan@126.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -18,38 +18,38 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
18
18
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
19
|
Requires-Python: >=3.13
|
|
20
20
|
Requires-Dist: pydantic-settings>=2.15.0
|
|
21
|
-
Requires-Dist: pydantic>=2.
|
|
21
|
+
Requires-Dist: pydantic>=2.14.1
|
|
22
22
|
Requires-Dist: sqlalchemy~=2.0.54
|
|
23
|
-
Requires-Dist: sqlmodel>=0.0.
|
|
23
|
+
Requires-Dist: sqlmodel>=0.0.48
|
|
24
24
|
Provides-Extra: ai
|
|
25
25
|
Requires-Dist: httpx>=0.28.1; extra == 'ai'
|
|
26
|
-
Requires-Dist: python-dotenv>=1.2.
|
|
26
|
+
Requires-Dist: python-dotenv>=1.2.4; extra == 'ai'
|
|
27
27
|
Provides-Extra: all
|
|
28
28
|
Requires-Dist: docxtpl>=0.20.2; extra == 'all'
|
|
29
|
-
Requires-Dist: fastmcp>=4.0
|
|
29
|
+
Requires-Dist: fastmcp>=4.1.0; extra == 'all'
|
|
30
30
|
Requires-Dist: firecrawl-anydoc>=0.2.4; extra == 'all'
|
|
31
31
|
Requires-Dist: httpx>=0.28.1; extra == 'all'
|
|
32
32
|
Requires-Dist: markdown-it-py>=4.2.0; extra == 'all'
|
|
33
33
|
Requires-Dist: marker-pdf[full]>=2.0.0; extra == 'all'
|
|
34
|
-
Requires-Dist: numpy>=2.5.
|
|
35
|
-
Requires-Dist: pikepdf>=10.
|
|
34
|
+
Requires-Dist: numpy>=2.5.4; extra == 'all'
|
|
35
|
+
Requires-Dist: pikepdf>=10.17.0; extra == 'all'
|
|
36
36
|
Requires-Dist: pimd[pdfa]>=2.2.5; extra == 'all'
|
|
37
37
|
Requires-Dist: psycopg2-binary>=2.9.13; extra == 'all'
|
|
38
38
|
Requires-Dist: pymysql>=1.2.3; extra == 'all'
|
|
39
|
-
Requires-Dist: python-dotenv>=1.2.
|
|
39
|
+
Requires-Dist: python-dotenv>=1.2.4; extra == 'all'
|
|
40
40
|
Requires-Dist: redis>=8.1.0; extra == 'all'
|
|
41
41
|
Requires-Dist: requests>=2.34.2; extra == 'all'
|
|
42
42
|
Requires-Dist: sqliteai-vector>=1.1.2; extra == 'all'
|
|
43
43
|
Requires-Dist: zvec>=0.7.0; extra == 'all'
|
|
44
44
|
Provides-Extra: anydoc
|
|
45
45
|
Requires-Dist: firecrawl-anydoc>=0.2.4; extra == 'anydoc'
|
|
46
|
-
Requires-Dist: pikepdf>=10.
|
|
46
|
+
Requires-Dist: pikepdf>=10.17.0; extra == 'anydoc'
|
|
47
47
|
Provides-Extra: docx
|
|
48
48
|
Requires-Dist: docxtpl>=0.20.2; extra == 'docx'
|
|
49
49
|
Provides-Extra: marker
|
|
50
50
|
Requires-Dist: marker-pdf[full]>=2.0.0; extra == 'marker'
|
|
51
51
|
Provides-Extra: mcp
|
|
52
|
-
Requires-Dist: fastmcp>=4.0
|
|
52
|
+
Requires-Dist: fastmcp>=4.1.0; extra == 'mcp'
|
|
53
53
|
Provides-Extra: md2doc
|
|
54
54
|
Requires-Dist: markdown-it-py>=4.2.0; extra == 'md2doc'
|
|
55
55
|
Requires-Dist: pimd[pdfa]>=2.2.5; extra == 'md2doc'
|
|
@@ -62,7 +62,7 @@ Requires-Dist: psycopg2-binary>=2.9.13; extra == 'pg'
|
|
|
62
62
|
Provides-Extra: redis
|
|
63
63
|
Requires-Dist: redis>=8.1.0; extra == 'redis'
|
|
64
64
|
Provides-Extra: sqliteai
|
|
65
|
-
Requires-Dist: numpy>=2.5.
|
|
65
|
+
Requires-Dist: numpy>=2.5.4; extra == 'sqliteai'
|
|
66
66
|
Requires-Dist: sqliteai-vector>=1.1.2; extra == 'sqliteai'
|
|
67
67
|
Provides-Extra: zvec
|
|
68
68
|
Requires-Dist: zvec>=0.7.0; extra == 'zvec'
|
|
@@ -362,7 +362,8 @@ from kit import session_scope, transactional, DBError # 等价
|
|
|
362
362
|
| ORM | `upsert(model, data, conflict_fields) -> Model` | 原子插入或更新(`conflict_fields` 须为唯一约束) |
|
|
363
363
|
| ORM | `count(model) -> int` | 记录总数 |
|
|
364
364
|
| 原生 SQL | `query(sql, params=None) -> list[dict]` | 原生查询,返回字典列表 |
|
|
365
|
-
| 原生 SQL | `execute(sql, params=None) -> int` |
|
|
365
|
+
| 原生 SQL | `execute(sql, params=None) -> int` | 原生写入,返回影响行数;`params` 传**字典列表**时批量执行(executemany,行键集须一致、行值须为标量、空列表返回 0) |
|
|
366
|
+
| 原生 SQL | `prepare_statement(sql, params=None) -> (语句, dict \| None)` | **模块级函数**(`from kit.db import prepare_statement`):构造可在**自有事务**内 `session.execute(stmt, bound)` 的语句,支持 `IN :ids`,调用方无须 import `sqlalchemy` |
|
|
366
367
|
| 原生表 | `create_raw_table(table_name, columns, if_not_exists=True) -> None` | 按「列名 → 列定义」建普通表;⚠ 列定义须按目标库方言书写,不做方言翻译 |
|
|
367
368
|
| 原生表 | `raw_insert(table_name, data) -> int` | 插入一行,返回影响行数 |
|
|
368
369
|
| 原生表 | `raw_get_by(table_name, column, value) -> dict \| None` | 按单列等值取首行 |
|
|
@@ -511,6 +512,17 @@ PgKit.search_vectors("my_vec", [0.1] * 384, top_k=5, id_col="vid", column="vec")
|
|
|
511
512
|
> 新增(非破坏性):`VECTOR_CAPABILITIES` 能力声明、`id_col` / `column` 逃生口参数、
|
|
512
513
|
> `insert_vectors` 支持二维 numpy 数组与批量提交。
|
|
513
514
|
|
|
515
|
+
#### 0.2.5 变更(开发中,未发布)
|
|
516
|
+
|
|
517
|
+
| 编号 | 变更 | 影响 |
|
|
518
|
+
|------|------|------|
|
|
519
|
+
| — | 新增**模块级公共函数** `prepare_statement(sql, params=None) -> (语句, 归一化参数)`:供调用方在**自有事务**内执行原生 SQL(含 `IN :ids`),**调用方无须 import `sqlalchemy`**;与 `query` / `execute` 同一实现(单一事实源) | **纯新增**:`query` / `transaction` 的签名与行为**零变更**;共享方法数仍为 **24**(模块级函数不计入) |
|
|
520
|
+
| — | `execute` 的 `params` 支持**字典列表**(executemany,同一语句 + N 行参数一次往返);**空列表**不触库返回 `0` | 向后兼容:单字典 / `None` 形态行为不变;批量要求各行字典**键集一致**、行值为**标量**(与 `IN` 展开互斥),违规在本地抛可操作 `ValueError`(含行号),不触库 |
|
|
521
|
+
| — | 文档:自有事务示例改用 `prepare_statement`(不再需要 `from sqlalchemy import text`);补「批量执行」说明与约束 | 文档 |
|
|
522
|
+
|
|
523
|
+
> ⚠ `:name::type` 这类 PostgreSQL 风格 cast **不被 SQLAlchemy 的 `text()` 识别**(0.2.5 之前亦如此),
|
|
524
|
+
> 请改用 `CAST(:name AS type)`。
|
|
525
|
+
|
|
514
526
|
#### 0.2.4 变更
|
|
515
527
|
|
|
516
528
|
| 编号 | 变更 | 影响 |
|
|
@@ -541,15 +553,16 @@ rows = SQLiteKit.query("SELECT id, title FROM items WHERE id IN :ids",
|
|
|
541
553
|
**自有事务内写向量(推荐:`build_insert_vectors` + `transaction()`)**:
|
|
542
554
|
|
|
543
555
|
```python
|
|
544
|
-
from
|
|
556
|
+
from kit.db import SQLiteKit, prepare_statement # 无须 import sqlalchemy
|
|
545
557
|
|
|
546
558
|
with SQLiteKit.transaction() as session: # 换成 PgKit / MySqlKit 写法相同
|
|
547
|
-
|
|
548
|
-
|
|
559
|
+
stmt, bound = prepare_statement(
|
|
560
|
+
"INSERT INTO docs_item (id, title) VALUES (:id, :t)", {"id": doc_id, "t": title})
|
|
561
|
+
session.execute(stmt, bound) # 第二个值必须一并传入
|
|
549
562
|
for sql, params in SQLiteKit.build_insert_vectors("docs_doc_vectors", vectors,
|
|
550
563
|
ids=row_ids, extra_data=metas,
|
|
551
564
|
id_col="rowid"):
|
|
552
|
-
session.execute(
|
|
565
|
+
session.execute(*prepare_statement(sql, params))
|
|
553
566
|
# 正常退出统一提交;异常自动回滚(多处写入保持同一事务)
|
|
554
567
|
```
|
|
555
568
|
|
|
@@ -292,7 +292,8 @@ from kit import session_scope, transactional, DBError # 等价
|
|
|
292
292
|
| ORM | `upsert(model, data, conflict_fields) -> Model` | 原子插入或更新(`conflict_fields` 须为唯一约束) |
|
|
293
293
|
| ORM | `count(model) -> int` | 记录总数 |
|
|
294
294
|
| 原生 SQL | `query(sql, params=None) -> list[dict]` | 原生查询,返回字典列表 |
|
|
295
|
-
| 原生 SQL | `execute(sql, params=None) -> int` |
|
|
295
|
+
| 原生 SQL | `execute(sql, params=None) -> int` | 原生写入,返回影响行数;`params` 传**字典列表**时批量执行(executemany,行键集须一致、行值须为标量、空列表返回 0) |
|
|
296
|
+
| 原生 SQL | `prepare_statement(sql, params=None) -> (语句, dict \| None)` | **模块级函数**(`from kit.db import prepare_statement`):构造可在**自有事务**内 `session.execute(stmt, bound)` 的语句,支持 `IN :ids`,调用方无须 import `sqlalchemy` |
|
|
296
297
|
| 原生表 | `create_raw_table(table_name, columns, if_not_exists=True) -> None` | 按「列名 → 列定义」建普通表;⚠ 列定义须按目标库方言书写,不做方言翻译 |
|
|
297
298
|
| 原生表 | `raw_insert(table_name, data) -> int` | 插入一行,返回影响行数 |
|
|
298
299
|
| 原生表 | `raw_get_by(table_name, column, value) -> dict \| None` | 按单列等值取首行 |
|
|
@@ -441,6 +442,17 @@ PgKit.search_vectors("my_vec", [0.1] * 384, top_k=5, id_col="vid", column="vec")
|
|
|
441
442
|
> 新增(非破坏性):`VECTOR_CAPABILITIES` 能力声明、`id_col` / `column` 逃生口参数、
|
|
442
443
|
> `insert_vectors` 支持二维 numpy 数组与批量提交。
|
|
443
444
|
|
|
445
|
+
#### 0.2.5 变更(开发中,未发布)
|
|
446
|
+
|
|
447
|
+
| 编号 | 变更 | 影响 |
|
|
448
|
+
|------|------|------|
|
|
449
|
+
| — | 新增**模块级公共函数** `prepare_statement(sql, params=None) -> (语句, 归一化参数)`:供调用方在**自有事务**内执行原生 SQL(含 `IN :ids`),**调用方无须 import `sqlalchemy`**;与 `query` / `execute` 同一实现(单一事实源) | **纯新增**:`query` / `transaction` 的签名与行为**零变更**;共享方法数仍为 **24**(模块级函数不计入) |
|
|
450
|
+
| — | `execute` 的 `params` 支持**字典列表**(executemany,同一语句 + N 行参数一次往返);**空列表**不触库返回 `0` | 向后兼容:单字典 / `None` 形态行为不变;批量要求各行字典**键集一致**、行值为**标量**(与 `IN` 展开互斥),违规在本地抛可操作 `ValueError`(含行号),不触库 |
|
|
451
|
+
| — | 文档:自有事务示例改用 `prepare_statement`(不再需要 `from sqlalchemy import text`);补「批量执行」说明与约束 | 文档 |
|
|
452
|
+
|
|
453
|
+
> ⚠ `:name::type` 这类 PostgreSQL 风格 cast **不被 SQLAlchemy 的 `text()` 识别**(0.2.5 之前亦如此),
|
|
454
|
+
> 请改用 `CAST(:name AS type)`。
|
|
455
|
+
|
|
444
456
|
#### 0.2.4 变更
|
|
445
457
|
|
|
446
458
|
| 编号 | 变更 | 影响 |
|
|
@@ -471,15 +483,16 @@ rows = SQLiteKit.query("SELECT id, title FROM items WHERE id IN :ids",
|
|
|
471
483
|
**自有事务内写向量(推荐:`build_insert_vectors` + `transaction()`)**:
|
|
472
484
|
|
|
473
485
|
```python
|
|
474
|
-
from
|
|
486
|
+
from kit.db import SQLiteKit, prepare_statement # 无须 import sqlalchemy
|
|
475
487
|
|
|
476
488
|
with SQLiteKit.transaction() as session: # 换成 PgKit / MySqlKit 写法相同
|
|
477
|
-
|
|
478
|
-
|
|
489
|
+
stmt, bound = prepare_statement(
|
|
490
|
+
"INSERT INTO docs_item (id, title) VALUES (:id, :t)", {"id": doc_id, "t": title})
|
|
491
|
+
session.execute(stmt, bound) # 第二个值必须一并传入
|
|
479
492
|
for sql, params in SQLiteKit.build_insert_vectors("docs_doc_vectors", vectors,
|
|
480
493
|
ids=row_ids, extra_data=metas,
|
|
481
494
|
id_col="rowid"):
|
|
482
|
-
session.execute(
|
|
495
|
+
session.execute(*prepare_statement(sql, params))
|
|
483
496
|
# 正常退出统一提交;异常自动回滚(多处写入保持同一事务)
|
|
484
497
|
```
|
|
485
498
|
|
|
@@ -23,7 +23,7 @@ Author: Clark Chang
|
|
|
23
23
|
数据库访问通过 kit.db 子包提供(基于 SQLModel)。
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
-
__version__ = "0.2.
|
|
26
|
+
__version__ = "0.2.5"
|
|
27
27
|
|
|
28
28
|
from kit.db import (
|
|
29
29
|
MYSQL,
|
|
@@ -47,6 +47,7 @@ from kit.db import (
|
|
|
47
47
|
get_engine,
|
|
48
48
|
get_session,
|
|
49
49
|
list_tables,
|
|
50
|
+
prepare_statement,
|
|
50
51
|
session_scope,
|
|
51
52
|
table_exists,
|
|
52
53
|
to_vector_literal,
|
|
@@ -85,6 +86,8 @@ __all__ = [
|
|
|
85
86
|
"CRUDBase",
|
|
86
87
|
# 事务
|
|
87
88
|
"transactional",
|
|
89
|
+
# 原生 SQL 语句构造(公共)
|
|
90
|
+
"prepare_statement",
|
|
88
91
|
# 向量工具(公共)
|
|
89
92
|
"to_vector_literal",
|
|
90
93
|
"generate_snowflake_ids",
|
|
@@ -32,6 +32,7 @@ Author: Clark Chang
|
|
|
32
32
|
from kit.db.base_db_kit import (
|
|
33
33
|
DEFAULT_DISTANCE_TYPE,
|
|
34
34
|
generate_snowflake_ids,
|
|
35
|
+
prepare_statement,
|
|
35
36
|
to_vector_literal,
|
|
36
37
|
)
|
|
37
38
|
from kit.db.crud import CRUDBase
|
|
@@ -87,6 +88,9 @@ __all__ = [
|
|
|
87
88
|
"CRUDBase",
|
|
88
89
|
# 事务
|
|
89
90
|
"transactional",
|
|
91
|
+
# 原生 SQL 语句构造(公共)
|
|
92
|
+
# 自有事务内执行原生 SQL(含列表参数 IN :ids)时使用,调用方无须 import sqlalchemy
|
|
93
|
+
"prepare_statement",
|
|
90
94
|
# 向量工具(公共)
|
|
91
95
|
"to_vector_literal",
|
|
92
96
|
"generate_snowflake_ids",
|
|
@@ -201,23 +201,51 @@ def generate_snowflake_ids(count: int) -> builtins.list[int]:
|
|
|
201
201
|
_EXPANDING_TYPES = (list, tuple, set, frozenset)
|
|
202
202
|
|
|
203
203
|
|
|
204
|
-
def
|
|
205
|
-
sql: str, params: FieldData | None
|
|
204
|
+
def prepare_statement(
|
|
205
|
+
sql: str, params: FieldData | None = None
|
|
206
206
|
) -> tuple[TextClause, FieldData | None]:
|
|
207
|
-
"""把 SQL 文本 +
|
|
207
|
+
"""把 SQL 文本 + 参数字典转为(可直接执行的语句, 归一化后的参数)
|
|
208
208
|
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
供调用方在**自有事务**(`*Kit.transaction()`)内执行原生 SQL,
|
|
210
|
+
与 ``query`` / ``execute`` 使用**同一实现**(单一事实源)——后两者等价于
|
|
211
|
+
``session.execute(*prepare_statement(sql, params))``;本函数**不建连接、
|
|
212
|
+
不改状态、不执行**。
|
|
211
213
|
|
|
212
|
-
|
|
214
|
+
典型用法(调用方**无须 import sqlalchemy**)::
|
|
213
215
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
216
|
+
from kit.db import SQLiteKit, prepare_statement
|
|
217
|
+
|
|
218
|
+
stmt, bound = prepare_statement(
|
|
219
|
+
"DELETE FROM t WHERE id IN :ids", {"ids": [1, 2, 3]}
|
|
220
|
+
)
|
|
221
|
+
with SQLiteKit.transaction() as session:
|
|
222
|
+
session.execute(stmt, bound) # 第二个值必须一并传入
|
|
223
|
+
|
|
224
|
+
三条实现约束(与 ``query`` / ``execute`` 现有语义完全一致):
|
|
225
|
+
|
|
226
|
+
1. 只对**确实出现在 SQL 文本中**的参数名做 expanding —— 否则传入"未使用的
|
|
227
|
+
序列参数"会触发 SQLAlchemy 的 ``ArgumentError: This text() construct
|
|
228
|
+
doesn't define a bound parameter …``,该报错对使用方没有意义;
|
|
229
|
+
2. 按类型白名单判断(见 `_EXPANDING_TYPES`),字符串不会被当作列表;
|
|
230
|
+
3. ``set`` / ``frozenset`` 归一化为 ``list`` —— SQLAlchemy 的 expanding 只接受
|
|
231
|
+
**可下标序列**(直接传 set 会抛 ``TypeError: 'set' object is not
|
|
232
|
+
subscriptable``);``IN`` 与顺序无关,故归一化不改变语义。
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
sql: SQL 语句(使用命名参数 ``:param``)
|
|
236
|
+
params: 参数字典,值为 ``list`` / ``tuple`` / ``set`` / ``frozenset`` 的键
|
|
237
|
+
按 ``IN`` 展开(三库通用)
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
``(语句, 归一化后的参数字典)``:执行时**必须**把第二个值一并传给
|
|
241
|
+
``session.execute``;``params`` 为 None 时第二项即 None
|
|
242
|
+
|
|
243
|
+
Note:
|
|
244
|
+
``:name::type`` 这类 PostgreSQL 风格 cast **不被 SQLAlchemy 的 ``text()``
|
|
245
|
+
识别**(该写法在支持本函数之前同样不可用),请改用 ``CAST(:name AS type)``。
|
|
246
|
+
|
|
247
|
+
See Also:
|
|
248
|
+
``query`` / ``execute``:kit 自管事务的原生 SQL 入口(同样支持列表参数)。
|
|
221
249
|
"""
|
|
222
250
|
statement = text(sql)
|
|
223
251
|
if not params:
|
|
@@ -230,6 +258,7 @@ def _prepare_statement(
|
|
|
230
258
|
if isinstance(value, (set, frozenset)):
|
|
231
259
|
value = list(value)
|
|
232
260
|
normalized[name] = value
|
|
261
|
+
# 约束 1:只对确实出现在 SQL 文本中的参数名做 expanding
|
|
233
262
|
if re.search(rf":{re.escape(name)}\b", sql):
|
|
234
263
|
expanding.append(bindparam(name, expanding=True))
|
|
235
264
|
else:
|
|
@@ -240,6 +269,46 @@ def _prepare_statement(
|
|
|
240
269
|
return statement, normalized
|
|
241
270
|
|
|
242
271
|
|
|
272
|
+
def _validate_batch_rows(rows: builtins.list[FieldData]) -> None:
|
|
273
|
+
"""批量执行(executemany)的本地预校验:键集一致 + 不含序列参数
|
|
274
|
+
|
|
275
|
+
约束来自 DBAPI 的 executemany 语义(实测):
|
|
276
|
+
|
|
277
|
+
1. 各行字典的**键集必须完全一致** —— 否则 SQLAlchemy 报
|
|
278
|
+
``StatementError: A value is required for bind parameter 'xxx'``,
|
|
279
|
+
不指明是哪一行,对使用方没有意义;
|
|
280
|
+
2. 行值须为**标量** —— 批量与 ``IN`` 展开(expanding)**互斥**:对批量语句
|
|
281
|
+
做 expanding 标注后按行执行会得到 ``OperationalError: near "?": syntax error``
|
|
282
|
+
这类驱动级错误(三库通用行为)。
|
|
283
|
+
|
|
284
|
+
在本地提前给出**可操作的** ``ValueError``(含行号与差异明细),
|
|
285
|
+
避免把上述两类无意义报错暴露给使用方。
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
rows: 批量参数(非空)
|
|
289
|
+
|
|
290
|
+
Raises:
|
|
291
|
+
ValueError: 键集不一致,或行值中含序列类型
|
|
292
|
+
"""
|
|
293
|
+
base = set(rows[0])
|
|
294
|
+
for index, row in enumerate(rows):
|
|
295
|
+
keys = set(row)
|
|
296
|
+
if keys != base:
|
|
297
|
+
raise ValueError(
|
|
298
|
+
f"批量参数第 {index} 行的键集与第 0 行不一致:"
|
|
299
|
+
f"缺少 {sorted(base - keys)},多出 {sorted(keys - base)};"
|
|
300
|
+
f"executemany 要求各行键集完全一致"
|
|
301
|
+
)
|
|
302
|
+
for name, value in row.items():
|
|
303
|
+
if isinstance(value, _EXPANDING_TYPES):
|
|
304
|
+
raise ValueError(
|
|
305
|
+
f"批量参数第 {index} 行的 {name!r} 是序列类型:"
|
|
306
|
+
f"批量执行(executemany)与 IN 展开(expanding)互斥;"
|
|
307
|
+
f"如需 IN 语义请改用单个参数字典(execute 的单字典形态),"
|
|
308
|
+
f"或逐行调用 execute"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
243
312
|
class BaseDbKit(ABC):
|
|
244
313
|
"""数据库工具类共享基类
|
|
245
314
|
|
|
@@ -606,7 +675,7 @@ class BaseDbKit(ABC):
|
|
|
606
675
|
``IN`` **不保证返回顺序**,需要顺序请自行按 id 重排;参数列表过大时
|
|
607
676
|
(如 PG 单语句参数上限 65535)应分批查询。
|
|
608
677
|
"""
|
|
609
|
-
statement, bound =
|
|
678
|
+
statement, bound = prepare_statement(sql, params)
|
|
610
679
|
with session_scope(cls.DB_TYPE, autocommit=False) as session:
|
|
611
680
|
result = session.execute(statement, bound or {})
|
|
612
681
|
rows = result.fetchall()
|
|
@@ -616,24 +685,66 @@ class BaseDbKit(ABC):
|
|
|
616
685
|
return data
|
|
617
686
|
|
|
618
687
|
@classmethod
|
|
619
|
-
def execute(
|
|
620
|
-
|
|
688
|
+
def execute(
|
|
689
|
+
cls, sql: str, params: FieldData | builtins.list[FieldData] | None = None
|
|
690
|
+
) -> int:
|
|
691
|
+
"""执行原生更新 SQL(INSERT/UPDATE/DELETE),支持批量(executemany)
|
|
621
692
|
|
|
622
693
|
Args:
|
|
623
|
-
sql: SQL 语句(使用命名参数
|
|
624
|
-
params:
|
|
625
|
-
|
|
694
|
+
sql: SQL 语句(使用命名参数 ``:param``)
|
|
695
|
+
params: 两种形态:
|
|
696
|
+
|
|
697
|
+
- **None / 单个参数字典**:执行一次;值为 ``list`` / ``tuple`` /
|
|
698
|
+
``set`` / ``frozenset`` 的参数按 ``IN`` 展开
|
|
699
|
+
(如 ``DELETE FROM t WHERE id IN :ids``,三库通用);
|
|
700
|
+
- **参数字典的列表**:批量执行(executemany,同一语句 + N 行参数,
|
|
701
|
+
一次往返)。⚠ 与 ``IN`` 展开**互斥**(行值须为标量),
|
|
702
|
+
各行字典的**键集必须完全一致**,**空列表**不触库直接返回 0。
|
|
626
703
|
|
|
627
704
|
Returns:
|
|
628
|
-
|
|
705
|
+
受影响的行数(批量形态为各行累加;部分驱动对 executemany 不上报
|
|
706
|
+
精确行数时可能为 ``-1``,请以数据为准)
|
|
707
|
+
|
|
708
|
+
Raises:
|
|
709
|
+
ValueError: 批量形态的行参数键集不一致,或行值中含序列类型
|
|
710
|
+
(批量与 ``IN`` 展开互斥)
|
|
629
711
|
"""
|
|
630
|
-
|
|
712
|
+
if isinstance(params, builtins.list):
|
|
713
|
+
return cls._execute_batch(sql, params)
|
|
714
|
+
statement, bound = prepare_statement(sql, params)
|
|
631
715
|
with session_scope(cls.DB_TYPE) as session:
|
|
632
716
|
result = session.execute(statement, bound or {})
|
|
633
717
|
affected = getattr(result, "rowcount", 0) or 0
|
|
634
718
|
logger.info(f"✅ 执行更新 → 影响 {affected} 行")
|
|
635
719
|
return affected
|
|
636
720
|
|
|
721
|
+
@classmethod
|
|
722
|
+
def _execute_batch(cls, sql: str, rows: builtins.list[FieldData]) -> int:
|
|
723
|
+
"""批量执行(executemany):先本地预校验,再一次往返执行 N 行参数
|
|
724
|
+
|
|
725
|
+
私有方法(不属于公共面):仅 ``execute`` 的列表形态会进入。
|
|
726
|
+
|
|
727
|
+
Args:
|
|
728
|
+
sql: SQL 语句(使用命名参数 ``:param``;**不传** ``params`` 构造语句,
|
|
729
|
+
即不做 ``IN`` 展开——批量与展开参数互斥)
|
|
730
|
+
rows: 参数字典列表(非空;键集一致;值须为标量)
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
受影响的行数(各行累加;驱动不上报时为 ``-1``)
|
|
734
|
+
"""
|
|
735
|
+
if not rows:
|
|
736
|
+
# 空列表语义:不触库、不报错,直接返回 0(DBAPI 对 executemany
|
|
737
|
+
# 传空列表会报 "A value is required for bind parameter")
|
|
738
|
+
logger.info("批量执行收到空参数列表 → 不执行任何语句")
|
|
739
|
+
return 0
|
|
740
|
+
_validate_batch_rows(rows)
|
|
741
|
+
statement, _ = prepare_statement(sql)
|
|
742
|
+
with session_scope(cls.DB_TYPE) as session:
|
|
743
|
+
result = session.execute(statement, rows)
|
|
744
|
+
affected = getattr(result, "rowcount", 0) or 0
|
|
745
|
+
logger.info(f"✅ 批量执行 → {len(rows)} 行参数,影响 {affected} 行")
|
|
746
|
+
return affected
|
|
747
|
+
|
|
637
748
|
# ==================== 原生表操作 ====================
|
|
638
749
|
|
|
639
750
|
@classmethod
|
|
@@ -31,10 +31,10 @@ classifiers = [
|
|
|
31
31
|
# base + extras 须与主工程 pyproject.toml 的 dependencies 保持一致(双向锚定),
|
|
32
32
|
# publish.py --check-only 会自动校验,主工程缺失即报错。
|
|
33
33
|
dependencies = [
|
|
34
|
-
"pydantic>=2.
|
|
34
|
+
"pydantic>=2.14.1",
|
|
35
35
|
"pydantic-settings>=2.15.0",
|
|
36
36
|
"sqlalchemy~=2.0.54",
|
|
37
|
-
"sqlmodel>=0.0.
|
|
37
|
+
"sqlmodel>=0.0.48",
|
|
38
38
|
]
|
|
39
39
|
dynamic = ["version"]
|
|
40
40
|
|
|
@@ -57,15 +57,15 @@ dynamic = ["version"]
|
|
|
57
57
|
pg = ["psycopg2-binary>=2.9.13"]
|
|
58
58
|
mysql = ["pymysql>=1.2.3"]
|
|
59
59
|
sqliteai = [
|
|
60
|
-
"numpy>=2.5.
|
|
60
|
+
"numpy>=2.5.4",
|
|
61
61
|
"sqliteai-vector>=1.1.2",
|
|
62
62
|
]
|
|
63
63
|
zvec = ["zvec>=0.7.0"]
|
|
64
64
|
redis = ["redis>=8.1.0"]
|
|
65
|
-
mcp = ["fastmcp>=4.0
|
|
65
|
+
mcp = ["fastmcp>=4.1.0"]
|
|
66
66
|
ai = [
|
|
67
67
|
"httpx>=0.28.1",
|
|
68
|
-
"python-dotenv>=1.2.
|
|
68
|
+
"python-dotenv>=1.2.4",
|
|
69
69
|
]
|
|
70
70
|
md2doc = [
|
|
71
71
|
"pimd[pdfa]>=2.2.5",
|
|
@@ -73,7 +73,7 @@ md2doc = [
|
|
|
73
73
|
]
|
|
74
74
|
anydoc = [
|
|
75
75
|
"firecrawl-anydoc>=0.2.4",
|
|
76
|
-
"pikepdf>=10.
|
|
76
|
+
"pikepdf>=10.17.0",
|
|
77
77
|
]
|
|
78
78
|
docx = ["docxtpl>=0.20.2"]
|
|
79
79
|
mineru = ["requests>=2.34.2"]
|
|
@@ -81,17 +81,17 @@ marker = ["marker-pdf[full]>=2.0.0"]
|
|
|
81
81
|
all = [
|
|
82
82
|
"psycopg2-binary>=2.9.13",
|
|
83
83
|
"pymysql>=1.2.3",
|
|
84
|
-
"numpy>=2.5.
|
|
84
|
+
"numpy>=2.5.4",
|
|
85
85
|
"sqliteai-vector>=1.1.2",
|
|
86
86
|
"zvec>=0.7.0",
|
|
87
87
|
"redis>=8.1.0",
|
|
88
|
-
"fastmcp>=4.0
|
|
88
|
+
"fastmcp>=4.1.0",
|
|
89
89
|
"httpx>=0.28.1",
|
|
90
|
-
"python-dotenv>=1.2.
|
|
90
|
+
"python-dotenv>=1.2.4",
|
|
91
91
|
"pimd[pdfa]>=2.2.5",
|
|
92
92
|
"markdown-it-py>=4.2.0",
|
|
93
93
|
"firecrawl-anydoc>=0.2.4",
|
|
94
|
-
"pikepdf>=10.
|
|
94
|
+
"pikepdf>=10.17.0",
|
|
95
95
|
"docxtpl>=0.20.2",
|
|
96
96
|
"requests>=2.34.2",
|
|
97
97
|
"marker-pdf[full]>=2.0.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|