sdpy-kit
SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
- 统一数据库访问层:PostgreSQL / MySQL / SQLite,三个 Kit API 完全一致,切换数据库只改一行 import;内置审计字段自动填充(
created_*/updated_*/deleted)与软删除查询过滤; - AI 多模型服务池:chat / embedding / rerank / image 四类能力,多厂商候选链 + 故障自动转移;
- 工具集:Redis / 文件 / 雪花 ID / 离线授权签发与校验 / 文档互转 / Word 模板填充 / MinerU 解析 / Marker 本地解析(PDF + 图片 → md + images/) / 离线 docx 解析(md + images/) / Laya 类型化决策 / 微信公众号文章抓取存储 / 通用网页文章抓取存储 / DeepSeek 搜索与抓取。
要求 Python 3.13+
模块与方法入口
| 模块 | 类 / 入口 | 能力 | 需要 extra |
|---|---|---|---|
kit.config |
get_settings / get_default_logger |
配置单例与统一日志 | 无 |
kit.db |
SQLiteKit / PgKit / MySqlKit / CRUDBase |
统一 CRUD、原生 SQL、原生表操作、事务、表管理 | pg / mysql / sqliteai |
kit.ai |
AI(同步门面)/ 4 个原生异步 Service |
对话 / 向量化 / 重排序 / 文生图 | ai |
kit.redis_kit |
RedisKit |
Redis string / hash / list / set / zset | redis |
kit.file_kit |
FileKit |
文件与目录读写、移动、遍历 | 无 |
kit.common_kit |
CommonKit |
雪花 ID、时间戳 ID、时间格式化 | 无 |
kit.license_kit |
LicenseKit / LicenseVerifier |
Ed25519 离线授权:密钥对、机器码、签发、验签(签名 → 产品 → 机器码 → 有效期) | 无 |
kit.docs.anydoc_kit |
AnyDocKit |
docx / pdf / xlsx 等 → Markdown | anydoc |
kit.docs.docx_kit |
DocxKit |
Word 模板动态填充(章节 / 列表 / 表格) | docx |
kit.docs.rc_docparser_kit |
RcDocParserKit |
离线 docx → Markdown + Images(无模型) | docx-parse |
kit.docs.md2doc_kit |
Md2DocKit |
Markdown → Word / PDF / HTML | md2doc |
kit.docs.mineru_kit |
MinerUKit |
MinerU 文档解析(精准 + Agent 轻量) | mineru |
kit.docs.marker_kit |
MarkerKit |
本地解析(PDF / 图片 → Markdown + Images) | marker |
kit.laya_kit |
LayaKit |
类型化决策:choice / score / noul,返回判断结果 + 置信度 | laya |
kit.crawl.article_models |
ArticleInfo / ArticleResult / SavedOutput 等 |
文章抓取通用模型(三个 *ArticleKit 共用,含 reason 分类与非致命 warnings) |
无 |
kit.crawl.article_io |
resolve_stem / save_article 等 |
落盘公共层(目录命名 / 图片本地化 / YAML 头 / meta.json),三个 *ArticleKit 共用 |
无 |
kit.crawl.wx_article_kit |
WxArticleKit |
微信公众号文章 → Markdown + 图片转存 | 无 |
kit.crawl.web_article_kit |
WebArticleKit |
通用网页文章 → Markdown + 图片转存(不含微信,微信请用 WxArticleKit;明确支持头条) |
web-article |
kit.crawl.deepseek_kit |
DeepSeekKit |
DeepSeek Search / Fetch API:网页搜索 + 单页抓取转 Markdown(save 与另两个 Kit 同契约) |
无(需 DEEPSEEK_API_KEY) |
安装
pip install sdpy-kit # 基础版:config/log + kit.db 核心(SQLite 常规操作零额外依赖)
# 或
uv add sdpy-kit
可选能力按 extras 按需安装(均为延迟导入,不装不影响基础使用,缺依赖时报错会提示安装命令):
| extra | 能力 | 安装 |
|---|---|---|
pg |
PgKit(PostgreSQL 驱动;向量方法另需服务端 pgvector) | pip install "sdpy-kit[pg]" |
mysql |
MySqlKit(MySQL 驱动;向量仅存储,需 MySQL 9.x) | pip install "sdpy-kit[mysql]" |
sqliteai |
SQLiteKit 向量方法(numpy / sqliteai-vector,SQLite 向量扩展) | pip install "sdpy-kit[sqliteai]" |
redis |
RedisKit | pip install "sdpy-kit[redis]" |
ai |
AI 服务池(httpx + python-dotenv) | pip install "sdpy-kit[ai]" |
md2doc |
Md2DocKit:Markdown → Word / PDF / HTML(pimd / fpdf2) | pip install "sdpy-kit[md2doc]" |
anydoc |
AnyDocKit 文档转 Markdown(firecrawl-anydoc + pikepdf) | pip install "sdpy-kit[anydoc]" |
docx |
DocxKit 模板动态填充(docxtpl) | pip install "sdpy-kit[docx]" |
docx-parse |
RcDocParserKit 离线 docx 解析(rc-docparser → Markdown + images/) | pip install "sdpy-kit[docx-parse]" |
mineru |
MinerUKit 解析 API(精准解析 + Agent 轻量解析) | pip install "sdpy-kit[mineru]" |
marker |
MarkerKit 本地解析(marker-pdf → Markdown + Images;PDF + 图片,依赖体积大) | pip install "sdpy-kit[marker]" |
laya |
LayaKit 类型化决策(laya + torch,本地小模型 choice / score / noul) | pip install "sdpy-kit[laya]" |
web-article |
WebArticleKit 通用网页文章抓取转存(trafilatura,纯 HTTP 无浏览器) | pip install "sdpy-kit[web-article]" |
all |
全部能力 | pip install "sdpy-kit[all]" |
配置(.env)
kit 通过 pydantic-settings 读取项目根目录 .env;同名真实环境变量优先于 .env。全部变量都有默认值,按需覆盖即可。
最小样例 .env
# 日志(可选)
LOG_LEVEL=INFO
LOG_TO_FILE=false
# 数据库(三选一,切换只改 DEFAULT_DB_TYPE:postgresql / mysql / sqlite)
DEFAULT_DB_TYPE=sqlite
SQLITE_DB_PATH=./data/sqlite.db
# DB_HOST=localhost
# DB_PORT=5432
# DB_DATABASE=postgres
# DB_USER=admin
# DB_PASSWORD=
# MYSQL_PORT=3306
# MYSQL_DATABASE=mydb
# MYSQL_USER=root
# MYSQL_CHARSET=utf8mb4
# 按需启用以下能力
# REDIS_HOST=localhost
# REDIS_PORT=6379
# REDIS_DB=0
# REDIS_PASSWORD=
# FILE_STORAGE_ROOT=./data
# AI:档案路径 + 你为各模型档案自定的密钥变量名(变量名在 ai_models.json 里声明)
# AI_MODELS_CONFIG_PATH=./config/ai_models.json
# SILICONFLOW_API_KEY=sk-xxxxxx
# MinerU(仅精准解析需要 Key,Agent 轻量解析免登录)
# MINERU_API_KEY=
# Marker(本地解析,可选依赖 sdpy-kit[marker])
# MARKER_MODE=fast # None=按设备自动(GPU→balanced / CPU、MPS→fast)
# MARKER_HF_ENDPOINT=https://hf-mirror.com
# MARKER_HF_DISABLE_XET=true
# MARKER_LLAMA_CPP_BINARY=
# Laya(类型化决策,可选依赖 sdpy-kit[laya])
# LAYA_MODEL=multilingual # multilingual / english / typed-decisions
# LAYA_DEVICE= # 留空由 laya 自动选择
# LAYA_HF_ENDPOINT=https://hf-mirror.com
# LAYA_HF_DISABLE_XET=true
环境变量 ↔ 能力对照表
表中未出现的模块(
kit.crawl.article_models、kit.crawl.article_io、kit.common_kit、kit.docs.docx_kit、kit.docs.md2doc_kit、kit.license_kit)不读取任何环境变量。
基础与日志(影响 kit.config 及所有模块的日志输出)
| 环境变量 | 默认值 | 作用 |
|---|---|---|
LOG_LEVEL |
INFO |
kit 统一日志级别 |
LOG_TO_CONSOLE |
true |
是否输出到控制台 |
LOG_TO_FILE |
false |
是否输出到文件 |
LOG_DIR |
./logs |
日志目录 |
LOG_FILE |
app.log |
日志文件名 |
数据库 → kit.db、SQLiteKit / PgKit / MySqlKit
| 环境变量 | 默认值 | 作用 |
|---|---|---|
DEFAULT_DB_TYPE |
postgresql |
默认数据库类型:postgresql / mysql / sqlite;切换数据库只改这一项 |
DB_HOST |
localhost |
PG / MySQL 共用主机 |
DB_PORT |
5432 |
PostgreSQL 端口 |
DB_DATABASE |
postgres |
PostgreSQL 库名 |
DB_USER |
admin |
PostgreSQL 用户 |
DB_PASSWORD |
1qaz@WSX3edc |
PG / MySQL 共用密码 |
MYSQL_PORT |
3306 |
MySQL 端口(与 PG 不同时覆盖) |
MYSQL_DATABASE |
mydb |
MySQL 库名 |
MYSQL_USER |
root |
MySQL 用户 |
MYSQL_CHARSET |
utf8mb4 |
MySQL 字符集 |
SQLITE_DB_PATH |
./data/sqlite.db |
SQLite 文件路径(:memory: 时自动共享单连接) |
Redis → kit.redis_kit
| 环境变量 | 默认值 | 作用 |
|---|---|---|
REDIS_HOST |
localhost |
Redis 主机 |
REDIS_PORT |
6379 |
Redis 端口 |
REDIS_DB |
0 |
数据库编号 |
REDIS_PASSWORD |
None |
密码,无密码留空 |
文件存储 → kit.file_kit
| 环境变量 | 默认值 | 作用 |
|---|---|---|
FILE_STORAGE_ROOT |
./data |
FileKit 全部相对路径的根目录。⚠ 该目录必须已存在,否则首次调用抛 RuntimeError(根路径不存在) |
AI → kit.ai
| 环境变量 | 默认值 | 作用 |
|---|---|---|
AI_MODELS_CONFIG_PATH |
./config/ai_models.json |
档案文件位置;文件不存在时调用抛 AIConfigError |
KIT_AI_CHAIN_TIMEOUT |
未设置 | 全局整链预算(秒)= 门面同步等待上限。⚠ 不是 Settings 字段,由代码直读 os.environ,无类型/范围校验,非法值仅告警后回退 |
| (密钥,变量名由档案决定) | 无 | 名字写在 ai_models.json 各档案的 api_key 字段(如 SILICONFLOW_API_KEY),值放 .env 或系统环境变量。未配置的档案会被剔除出候选链;该类型全部未配置则调用失败 |
MinerU → kit.docs.mineru_kit
| 环境变量 | 默认值 | 作用 |
|---|---|---|
MINERU_API_KEY |
无 | 精准解析 Token(Agent 轻量解析免登录,不读该变量);缺失时抛 MinerUError |
MINERU_BASE_API_URL |
https://mineru.net |
API 基础地址 |
MINERU_TIMEOUT |
60(1–600) |
单次 HTTP 请求连接超时 |
MINERU_TRANSFER_TIMEOUT |
300(1–3600) |
文件上传 / 结果下载读取超时 |
MINERU_POLL_INTERVAL |
2.0(≥0.5) |
任务轮询间隔 |
MINERU_MAX_WAIT |
600(≥10) |
轮询总超时,超时抛 MinerUError |
Marker → kit.docs.marker_kit
| 环境变量 | 默认值 | 作用 |
|---|---|---|
MARKER_MODE |
无(不写入 config) | 解析模式 balanced / fast;不配置则由 marker 按设备自动选择(GPU→balanced,CPU、MPS→fast) |
MARKER_HF_ENDPOINT |
https://hf-mirror.com |
模型权重下载入口(国内镜像);置空则用 HuggingFace 官方源 |
MARKER_HF_DISABLE_XET |
true |
禁用 HF Xet 存储后端(国内镜像不支持,开启会导致权重下载挂起) |
MARKER_LLAMA_CPP_BINARY |
无 | llama-server 路径(非 NVIDIA 平台完整 VLM 能力);未设置时沿用系统 LLAMA_CPP_BINARY |
这四项由 kit 在导入 marker 之前经
os.environ.setdefault应用:仅当用户未显式设置对应环境变量时生效,不覆盖显式配置。
Laya → kit.laya_kit
| 环境变量 | 默认值 | 作用 |
|---|---|---|
LAYA_MODEL |
multilingual |
检查点:multilingual(多语言含中文)/ english / typed-decisions |
LAYA_DEVICE |
无(不写入 config) | 推理设备(cpu / cuda);不配置则由 laya 自动选择 |
LAYA_HF_ENDPOINT |
https://hf-mirror.com |
检查点权重下载入口(国内镜像);置空则用 HuggingFace 官方源 |
LAYA_HF_DISABLE_XET |
true |
禁用 HF Xet 存储后端(国内镜像不支持,开启会导致权重下载挂起) |
LAYA_HF_*同样由 kit 在导入 laya 之前经os.environ.setdefault应用,不覆盖显式配置;LAYA_MODEL/LAYA_DEVICE由Settings读取(进程内缓存,改后需重启进程)。
DeepSeek Search / Fetch → kit.crawl.deepseek_kit
| 环境变量 | 默认值 | 作用 |
|---|---|---|
DEEPSEEK_API_KEY |
无 | Search / Fetch 鉴权 Key(与模型对话共用同一套);缺失时调用抛 DeepSeekKitError |
DEEPSEEK_BASE_URL |
https://api.deepseek.com |
API 基础地址 |
DEEPSEEK_TIMEOUT |
60(1–600) |
单次 HTTP 请求超时(秒) |
DEEPSEEK_MAX_RETRIES |
2(0–5) |
408 / 429 / 5xx 重试次数(不含首次请求);其它 4xx 不重试 |
文章抓取长度自检 → kit.crawl.wx_article_kit / web_article_kit / deepseek_kit
| 环境变量 | 默认值 | 作用 |
|---|---|---|
CRAWL_MIN_CONTENT_CHARS |
500(≥0) |
正文短于该长度时写入 warnings(非致命,不改成功语义、不截断正文);0 关闭该判据 |
云 OCR(任何 ocr="hosted" 的转换)→ kit.docs.anydoc_kit
| 环境变量 | 默认值 | 作用 |
|---|---|---|
FIRECRAWL_API_KEY |
无 | 扫描件云 OCR 密钥。kit 不读该变量:仅在方法参数 api_key=None 时由 firecrawl-anydoc 自行回落读取 |
FIRECRAWL_API_URL |
无 | 云 OCR 服务地址,回落规则同上 |
环境变量的四类来源
| 类别 | 命名方式 | 当前变量 | 读取方与语义 |
|---|---|---|---|
| ① Settings 变量 | 固定,kit 定义 | 上表中除 KIT_AI_CHAIN_TIMEOUT 与密钥类之外的全部 |
pydantic-settings 读 .env 与真实环境变量(后者优先),带类型与范围校验,缺失用默认值 |
| ② 直读变量 | 固定,kit 代码直接读 os.environ |
KIT_AI_CHAIN_TIMEOUT |
无类型校验,非法值告警后回退 |
| ③ 档案声明变量 | 动态,名字写在 JSON 里 | AI 的 api_key |
名字进 JSON、值进 .env——所以档案文件可以安全入库,密钥留在环境里 |
| ④ 第三方库变量 | 由依赖库定义 | FIRECRAWL_API_KEY、FIRECRAWL_API_URL(anydoc);HF_ENDPOINT / HF_HUB_DISABLE_XET / LLAMA_CPP_BINARY(marker) |
anydoc:kit 只透传 None,由其自行回落;marker:kit 在导入前按 MARKER_* 配置做 os.environ.setdefault(不覆盖显式设置) |
密钥解析优先级
| 能力 | 解析顺序 | 全部缺失时 |
|---|---|---|
| AI | 真实环境变量 > 项目根 .env(首次加载档案时经 load_dotenv(override=False) 注入 os.environ) |
该档案被剔除出候选链;该类型无可用档案则调用报 AIAggregateError |
| MinerU | 仅 Settings(.env / 真实环境变量) |
精准解析接口抛 MinerUError |
AI 的
load_dotenv(override=False)表示已存在的真实环境变量不会被.env覆盖——生产用容器 / 系统环境变量注入时,可以放心保留开发用的.env。
改了配置何时生效
| 改动内容 | 生效时机 |
|---|---|
LOG_*、DB_*、MYSQL_*、REDIS_*、FILE_STORAGE_ROOT、MINERU_*、MARKER_MODE |
get_settings() 带 lru_cache,进程内首次读取后固定 → 重启进程(或调用 AI.reload_config(),它会顺带清空该缓存) |
MARKER_HF_ENDPOINT / MARKER_HF_DISABLE_XET / MARKER_LLAMA_CPP_BINARY |
仅在 import kit.docs.marker_kit 时经 os.environ.setdefault 应用 → 改动需重启进程重新导入模块 |
LAYA_HF_ENDPOINT / LAYA_HF_DISABLE_XET |
仅在 import kit.laya_kit 时经 os.environ.setdefault 应用 → 改动需重启进程重新导入模块 |
AI 密钥写在 .env 里 |
.env 只在加载档案时注入一次 → 重启,或 AI.reload_config(reload_file=True) |
AI 密钥由自己写入 os.environ |
AI.reload_config()(默认 reload_file=False,只重读 os.environ) |
ai_models.json(增删档案、换 model) |
AI.reload_config(reload_file=True) |
KIT_AI_CHAIN_TIMEOUT |
每次调用现读 os.environ → 改 os.environ 立即生效;写在 .env 里的新值要等下一次档案加载 |
kit.config —— 配置与日志
from kit.config import get_settings, get_default_logger
settings = get_settings() # Settings 单例(lru_cache,读取 .env)
logger = get_default_logger(__name__) # 统一日志入口(禁止自建 logging handler)
logger.info("hello %s", settings.FILE_STORAGE_ROOT)
| 入口 | 说明 |
|---|---|
get_settings() -> Settings |
返回全局配置单例,字段名与环境变量同名(如 settings.SQLITE_DB_PATH、settings.DEFAULT_DB_TYPE),完整清单见上文对照表 |
get_default_logger(name=None, log_level=None, log_file=None) -> logging.Logger |
获取统一格式日志器(控制台 + 轮转文件) |
setup_logger(name=None, log_level=None, log_file=None, log_format=..., date_format=..., max_bytes=5*1024*1024, backup_count=5) -> logging.Logger |
完整参数版本,一般无需直接使用 |
kit.db —— 统一数据库访问层
kit/__init__.py 直接重导出 kit.db 的全部公开符号,下列两种写法等价:
from kit.db import session_scope, transactional, DBError # 推荐
from kit import session_scope, transactional, DBError # 等价
1. 三个数据库 Kit(31 个共享方法 API 完全一致)
| 类 | 导入 | 数据库 | extra |
|---|---|---|---|
SQLiteKit |
from kit.db.sqlite_kit import SQLiteKit |
SQLite | 无(常规操作) |
PgKit |
from kit.db.pg_kit import PgKit |
PostgreSQL | pg |
MySqlKit |
from kit.db.mysql_kit import MySqlKit |
MySQL | mysql |
共有方法(31 个,全部为 classmethod;raw_* 内部自动提交,build_raw_* 只拼装不执行)
| 分组 | 方法 | 说明 |
|---|---|---|
| 连接 | connect() -> bool |
建引擎并验证连通 |
| 连接 | close() -> None |
关闭引擎 |
| 连接 | is_connected() -> bool |
探测当前是否可连 |
| 连接 | raw_connection() |
取原生 DBAPI 连接(PG=psycopg2 / MySQL=pymysql / SQLite=sqlite3);调用方负责 commit 与 close,psycopg2 / pymysql 默认不自动提交 |
| ORM | create(model, data) -> Model |
插入单条 |
| ORM | create_multi(model, data_list) -> list[Model] |
批量插入 |
| ORM | get(model, id) -> Model | None |
按主键查询 |
| ORM | get_by(model, field_name, field_value) -> Model | None |
按字段等值查询 |
| ORM | list(model, *, skip=0, limit=100, order_by=None, desc=False) -> list[Model] |
分页 + 排序查询 |
| ORM | update(model, id, data) -> Model |
按主键更新 |
| ORM | delete(model, id) -> bool |
按主键删除 |
| ORM | upsert(model, data, conflict_fields) -> Model |
原子插入或更新(conflict_fields 须为唯一约束) |
| ORM | count(model) -> int |
记录总数 |
| 原生 SQL | query(sql, params=None) -> list[dict] |
原生查询,返回字典列表 |
| 原生 SQL | execute(sql, params=None) -> int |
原生写入,返回影响行数;params 传字典列表时批量执行(executemany,行键集须一致、行值须为标量、空列表返回 0) |
| 原生 SQL | prepare_statement(sql, params=None) -> (语句, dict | None) |
模块级函数(from kit.db import prepare_statement):构造可在自有事务内 session.execute(stmt, bound) 的语句,支持 IN :ids,调用方无须 import sqlalchemy |
| 原生表 | create_raw_table(table_name, columns, if_not_exists=True) -> None |
按「列名 → 列定义」建普通表;⚠ 列定义须按目标库方言书写,不做方言翻译 |
| 原生表 | raw_insert(table_name, data, *, ignore_conflict=False) -> int |
插入一行;ignore_conflict=True 幂等(PG/SQLite ON CONFLICT DO NOTHING、MySQL INSERT IGNORE) |
| 原生表 | raw_update(table_name, data, where, params=None) -> int |
按 where(不含 WHERE)更新;启用审计填充时自动补 updated_* |
| 原生表 | raw_touch(table_name, where, params=None) -> int |
只刷新 updated_*(需先 enable;表无该列抛 DBError) |
| 原生表 | raw_delete(table_name, where, params=None) -> int |
条件删除(where 必填) |
| 原生表 | build_raw_insert / build_raw_update / build_raw_touch / build_raw_delete |
只拼装不执行,返回 (语句, 参数),供自有事务内 session.execute(*builder(...)) |
| 原生表 | raw_get_by(table_name, column, value) -> dict | None |
按单列等值取首行 |
| 事务 | transaction() |
事务上下文管理器,with PgKit.transaction() as session: |
| 表 | list_tables() -> list[str] |
列出全部表名 |
| 表 | table_exists(table_name) -> bool |
判断表是否存在 |
| 表 | create_tables(models) -> None |
按模型列表建表 |
| 表 | get_table_info(table_name) -> list[dict] |
获取列信息(按列序;公共键 4 个:column_name/data_type/is_nullable/column_default) |
列表参数(
IN):query/execute的params中值为list/tuple/set/frozenset时按IN展开,可直接写WHERE id IN :ids(三库通用)。IN不保证顺序;参数列表过大 应分批(PG 单语句上限 65535 参数)。
LIKE的方言差异:SQLite 对 ASCII 不区分大小写,PostgreSQL 区分(用ILIKE); 跨库一致写法:LOWER(col) LIKE LOWER(:pattern)。导出面:
from kit.db import PgKit, SQLiteKit, build_pg_url, to_vector_literal, …(kit顶层与kit.db导出同一套符号)。
from sqlalchemy import BigInteger
from sqlmodel import SQLModel, Field
from kit.common_kit import CommonKit
from kit.db import SQLiteKit # 也可 from kit.db.sqlite_kit import SQLiteKit
class User(SQLModel, table=True):
# 主键:64 位整数 + 雪花 ID(应用侧生成,不用数据库自增)
id: int = Field(default_factory=CommonKit.generate_snowflake_id,
primary_key=True, sa_type=BigInteger)
name: str
SQLiteKit.connect()
SQLiteKit.create_tables([User])
user = SQLiteKit.create(User, {"name": "张三"}) # 自动提交
SQLiteKit.update(User, user.id, {"name": "李四"})
users = SQLiteKit.list(User, skip=0, limit=10, order_by="id", desc=True)
rows = SQLiteKit.query("SELECT * FROM user WHERE name = :name", {"name": "李四"})
affected = SQLiteKit.execute("UPDATE user SET name = '王五' WHERE id = :id", {"id": user.id})
rows = SQLiteKit.query("SELECT * FROM user WHERE id IN :ids", {"ids": [1, 2, 3]}) # 列表参数
with SQLiteKit.transaction() as session: # 多操作原子提交
SQLiteKit.create(User, {"name": "批量一"})
SQLiteKit.create(User, {"name": "批量二"})
切换数据库:把 import 的
SQLiteKit换成PgKit/MySqlKit即可,其余代码不变。 注意三点例外:①create_raw_table的列定义文本须按目标库方言改写;② 向量能力 三库同名同参但能力不同(可由VECTOR_CAPABILITIESfeature-detect),尤其 MySQL 侧检索不可用;③create_vector_table的dims为必填参数。
2. 向量方法(三库同名同参,能力差异化)
签名在
SQLiteKit/PgKit/MySqlKit上完全一致;能力按后端不同(见下表)。 extra 前提:SQLite 需sdpy-kit[sqliteai];PG 需sdpy-kit[pg]且服务端已装 pgvector (create_vector_table会自动尝试CREATE EXTENSION);MySQL 需 9.x(Community 即可存, 但检索需 HeatWave on OCI / MySQL AI)——建表前会校验服务端版本,低于 9.x 抛DBError。
| 方法 | 说明 |
|---|---|
create_vector_table(table_name, dims, id_col="id", column="embedding", extra_cols=None) -> None |
建含向量列的表;dims 必填(SQLite 用 CHECK 落实为数据库层约束;PG 硬约束并回读校验已存在表;MySQL 为上限语义)。extra_cols 支持 {列名: 类型串}(推荐,与 create_raw_table 同形)与 [{"name","type"}] 两种形态 |
insert_vectors(table_name, vectors, ids=None, extra_data=None, id_col="id", column="embedding") -> int |
批量插入向量(自成一事务并提交);ids=None 时由 Kit 生成雪花 ID;接受 list[list[float]] 或二维 numpy 数组 |
build_insert_vectors(table_name, vectors, ids=None, extra_data=None, id_col="id", column="embedding") -> list[tuple[str, list[dict]]] |
只拼装、不执行、不管事务:返回按列集分组的 (SQL, 参数列表),供调用方在自有事务内写向量行(见「自有事务内写向量」) |
search_vectors(table_name, query_vector, top_k=10, distance_type="L2", filter_sql=None, exact=True, id_col="id", column="embedding") -> list[dict] |
KNN 检索,按距离升序返回 {"id", "distance"};exact=True(默认)禁止近似加速;MySQL 侧抛 VectorNotSupportedError |
vector_version() -> str |
向量后端版本;不可用时返回 "unavailable"(MySQL 侧为服务端版本号) |
id_col / column(逃生口):让自建 DDL 的向量表也能读写/检索。需要复合主键、额外约束、
分区或与既有业务表合表时,用 execute() 自建 DDL 再用这两个参数接入;SQLite 侧要求 id_col
为 rowid 别名(INTEGER PRIMARY KEY)。
exact 参数(默认 True = 禁止近似加速):
| 后端 | exact=True(默认) |
exact=False |
是否提速 |
|---|---|---|---|
PgKit |
同事务 SET LOCAL enable_indexscan = off(有索引也精确;代价:该查询不使用任何索引,含过滤列索引) |
不干预查询计划(有索引即近似召回,distance 仍为真值) |
✅ 仅"大表 + 已建索引"时 |
SQLiteKit |
vector_full_scan(真值 L2) |
vector_quantize_scan(量化 L2 = 真值 × 255) |
❌ 不提速(每次调用重建量化) |
MySqlKit |
不适用 | 同左 | — |
能力差异(仅 L2 可跨库比较数值;float32 vs float64,需带容差):
| 项 | SQLiteKit(sqlite-vector) |
PgKit(pgvector) |
MySqlKit(9.x 原生) |
|---|---|---|---|
支持的 distance_type |
仅 L2 | COSINE / L2 / L1 / DOT(负内积) / SQUARED_L2 |
无(检索不可用) |
默认 distance_type |
"L2" |
"L2"(三库统一,不按后端覆写) |
"L2"(形参保留,调用即抛错) |
| 非法取值 | 抛 ValueError |
抛 ValueError |
search_vectors 抛 VectorNotSupportedError |
distance 口径 |
exact=True 真值 L2 / exact=False 量化(×255) |
真实距离 | — |
| 维度约束 | CHECK(仅新表;旧表靠方法内校验) |
强制(不匹配即报错;建表时回读比对) | 上限约束(超限报错) |
filter_sql 与 top_k |
先过滤再取 top_k(与 PG 一致) | 先过滤再取 top_k | 不适用 |
| 索引 | 无(精确路径为全表扫描) | kit 不建索引;自建后须 exact=False 才会被使用 |
无法建(B-Tree 拒绝向量列) |
MySQL
DISTANCE()/VECTOR_DISTANCE()仅 HeatWave on OCI 与 MySQL AI 提供 (Community 实测报1305 FUNCTION ... does not exist);故MySqlKit只覆盖存储侧。
主键与 id(三库统一):向量表主键均为 BIGINT(SQLite 用等价的 64 位 INTEGER PRIMARY KEY),
不使用数据库自增;ids=None 时由 Kit 用 CommonKit.generate_snowflake_id() 生成雪花 ID
(18~19 位,int64)。
⚠ 雪花 ID 超过 2^53,经 JSON 传给 JavaScript 会丢精度,需按字符串传输;
多实例部署需各自 CommonKit.configure(datacenter_id, machine_id)(默认 (1,1) 会重复)。
# SQLite(默认精确:distance 为真值 L2)
from kit.db.sqlite_kit import SQLiteKit
SQLiteKit.create_vector_table("images", 384) # dims 必填
SQLiteKit.insert_vectors("images", [[0.1] * 384, [0.2] * 384])
rows = SQLiteKit.search_vectors("images", [0.1] * 384, top_k=5)
rows = SQLiteKit.search_vectors("images", [0.1] * 384, top_k=5, exact=False) # 旧量化口径(不提速)
# PostgreSQL(签名相同;可选 COSINE/L2/L1/DOT/SQUARED_L2)
from kit.db.pg_kit import PgKit
PgKit.create_vector_table("images", 384)
PgKit.insert_vectors("images", [[0.1] * 384, [0.2] * 384])
rows = PgKit.search_vectors("images", [0.1] * 384, top_k=5, distance_type="COSINE")
# 大表 + 自建索引时才用 exact=False(否则索引不被使用,见上表)
PgKit.execute("CREATE INDEX ON images USING hnsw (embedding vector_cosine_ops)")
rows = PgKit.search_vectors("images", [0.1] * 384, top_k=5,
distance_type="COSINE", exact=False)
# MySQL(仅存储;search_vectors 抛 VectorNotSupportedError)
from kit.db.mysql_kit import MySqlKit
MySqlKit.create_vector_table("images", 384)
MySqlKit.insert_vectors("images", [[0.1] * 384, [0.2] * 384])
# 能力自省(无需 try/except)
PgKit.VECTOR_CAPABILITIES
# {'store': True, 'search': True, 'exact': True,
# 'distances': ('COSINE', 'L2', 'L1', 'DOT', 'SQUARED_L2'),
# 'distance_kind': 'true', 'fast_distance_kind': 'true'}
# 自建 DDL 的表:用 id_col / column 接入
PgKit.execute("CREATE TABLE my_vec (vid BIGINT PRIMARY KEY, vec vector(384))")
PgKit.insert_vectors("my_vec", [[0.1] * 384], ids=[1], id_col="vid", column="vec")
PgKit.search_vectors("my_vec", [0.1] * 384, top_k=5, id_col="vid", column="vec")
⚠
:name::type这类 PostgreSQL 风格 cast 不被 SQLAlchemy 的text()识别, 请改用CAST(:name AS type)。
两段查询范式(推荐用法):search_vectors 只返回 [{"id", "distance"}],载荷二次取回:
hits = SQLiteKit.search_vectors("item_vectors", query_vector, top_k=5)
rows = SQLiteKit.query("SELECT id, title FROM items WHERE id IN :ids",
{"ids": [h["id"] for h in hits]}) # IN 不保证顺序,需按 hits 重排
自有事务内写向量(推荐:build_insert_vectors + transaction()):
from kit.db import SQLiteKit, prepare_statement # 无须 import sqlalchemy
with SQLiteKit.transaction() as session: # 换成 PgKit / MySqlKit 写法相同
stmt, bound = prepare_statement(
"INSERT INTO docs_item (id, title) VALUES (:id, :t)", {"id": doc_id, "t": title})
session.execute(stmt, bound) # 第二个值必须一并传入
for sql, params in SQLiteKit.build_insert_vectors("docs_doc_vectors", vectors,
ids=row_ids, extra_data=metas,
id_col="rowid"):
session.execute(*prepare_statement(sql, params))
# 正常退出统一提交;异常自动回滚(多处写入保持同一事务)
SQLite 的 DBAPI 变体(raw_connection() 支持 :name 命名参数):
conn = SQLiteKit.raw_connection() # 调用方负责 commit / close
try:
for sql, params in SQLiteKit.build_insert_vectors("item_vectors", vectors,
ids=row_ids, extra_data=metas):
conn.executemany(sql, params)
conn.commit()
finally:
conn.close()
⚠ 两点约束:①
build_insert_vectors输出为:name风格,PG / MySQL 的原生 DBAPI 不支持 (psycopg2 / pymysql 需%(name)s),只能经transaction()的 session 执行; ② SQLite 上持有未提交写事务时再调 kit 的自提交方法(如insert_vectors)会因另一个连接 拿不到写锁而失败(OperationalError: database is locked,约 6.4s 后才报错)—— 把写入并入同一个transaction()(如上)即可避免。
相似度换算建议:SQLite 仅支持 L2,若向量入库前已归一化,则
cos = 1 − L2²/2; PG 可直接用distance_type="COSINE"。
3. 审计字段自动填充与软删除过滤(默认关闭)
AuditConfig.enable(...) 后,kit 的写入口自动补齐审计字段;set_soft_delete_filter(True)
后查询自动只看未删除记录。两项默认都关闭,不调用时行为与旧版完全一致。
from kit.db import AuditConfig, PgKit
AuditConfig.enable(user_resolver=get_current_userid) # 启动时调用一次
AuditConfig.set_soft_delete_filter(True) # 可选:查询过滤软删除
填充字段(默认列名 + 内置别名):
| 槽位 | 默认列名 | 内置别名 | 说明 |
|---|---|---|---|
created_at |
created_at |
create_time |
创建时间 |
created_by |
created_by |
— | 创建人 |
updated_at |
updated_at |
update_time |
更新时间 |
updated_by |
updated_by |
— | 更新人 |
deleted |
deleted |
— | 0 正常 / 1 删除 |
内置别名无需配置:表里的时间列若叫 create_time / update_time,同样会被识别;
两套列同时存在时两列都填(同值)。
判定只有两条(同时满足才填):表中存在该列 + 本次写入数据中该字段为空
(None / 空串 / 纯空白;0 与 False 不算空)。显式非空值一律保留,
不存在覆盖语义;「当前值」指本次写入的数据,不是数据库已存值。
| 动作 | created_at / create_time |
updated_at / update_time |
deleted |
|---|---|---|---|
| INSERT | 空才填 | 空才填(与 created 同值) | 空才填 0(字符串列填 "0") |
| UPDATE | 不参与 | 空才填 | 不参与 |
落点:ORM flush(before_flush)、CRUDBase.update_multi / upsert、
raw_insert / raw_update。execute() 手写 SQL 与批量 executemany 不填充。
PgKit.raw_insert("chat_session", {"id": 1, "title": "t"})
# → 自动补 created_at/created_by/updated_at/updated_by 与 deleted=0
PgKit.raw_update("chat_session", {"title": "t2"}, "id = :id", {"id": 1})
# → 只刷 updated_at / updated_by;where 的值全部走绑定参数
PgKit.raw_get_by("chat_session", "id", 1) # 过滤开启时自动排除已删除行
with AuditConfig.include_deleted(): # 需要读已删除记录时临时绕过
PgKit.get(Model, 1)
自定义字段名(列名与约定完全不同时):
AuditConfig.set_fields({"created_at": "create_on", "deleted": "del_flag"})
AuditConfig.fields() # 查看当前「槽位 → 实际列名」
AuditConfig.set_fields(None) # 恢复默认
约定与局限:
set_fields的列名必须是合法标识符(字母 / 数字 / 下划线,不以数字开头), 否则立即ValueError——列名会被拼进 SQL 的标识符位置。deleted支持整数列(填0)与字符串列(填"0",VARCHAR/CHAR/TEXT); boolean 列不支持(会报类型错误)。- 查询过滤严格等值
deleted = 0(字符串列"0");NULL不视为未删除,历史数据需自行回填。 - 写方法与
query()手写 SQL 不追加过滤条件;upsert的冲突回查跳过过滤。 user_resolver返回None/ 空串或抛异常时跳过*_by填充,不影响写入。
原生写工具:raw_insert / raw_update / raw_touch / raw_delete 自带事务;
build_raw_* 只拼装(返回 (语句, 参数)),供 with Kit.transaction() as session: 内使用:
with PgKit.transaction() as session:
session.execute(*PgKit.build_raw_insert("docs_tag", {"id": 1}, ignore_conflict=True))
session.execute(*PgKit.build_raw_touch("docs_item", "id = :id", {"id": 1}))
ignore_conflict=True:PG/SQLiteON CONFLICT DO NOTHING、MySQLINSERT IGNORE(⚠ MySQL 忽略范围更宽,含数据截断告警);冲突被忽略时返回0。raw_touch:只刷新updated_*(含别名update_time),需先enable;表无该列抛DBError。- 写入值:
dict自动编码为 JSON 文本;list仅在 JSON 列编码,其它列抛ValueError(避免被当作IN展开破坏 SQL;数组 / 向量请用字符串或execute())。
4. CRUDBase(配合 session_scope,不自动提交)
from kit.db import CRUDBase, session_scope
user_crud = CRUDBase(User)
with session_scope() as session: # 正常退出提交、异常回滚
user_crud.create(session, {"name": "张三"})
user_crud.update(session, user_crud.get(session, 1), {"name": "李四"})
| 方法 | 说明 |
|---|---|
CRUDBase(model) |
构造,传入 SQLModel 模型类 |
get(session, id) -> Model | None |
按主键取单条 |
get_or_raise(session, id) -> Model |
按主键取,缺失抛 NotFoundError |
get_multi(session, *, skip=0, limit=100, order_by=None, desc=False) -> list[Model] |
分页 + 排序 |
get_by_field(session, field_name, field_value) -> Model | None |
按字段等值取单条 |
get_multi_by_field(session, field_name, field_value, *, skip=0, limit=100) -> list[Model] |
按字段等值分页取多条 |
create(session, obj_in) -> Model |
插入单条(add + flush) |
create_multi(session, objs_in) -> list[Model] |
批量插入(add_all + flush) |
update(session, db_obj, obj_in) -> Model |
更新,入参为模型实例或字典 |
update_multi(session, ids, obj_in) -> int |
单条批量 UPDATE,返回影响行数 |
delete(session, id) -> Model | None |
按主键删除 |
delete_multi(session, ids) -> int |
单条批量 DELETE,返回影响行数 |
count(session) -> int |
记录总数 |
exists(session, id) -> bool |
主键是否存在 |
upsert(session, obj_in, conflict_fields) -> Model |
原生 INSERT ... ON CONFLICT / ON DUPLICATE KEY 原子语句 |
约定:
SQLiteKit / PgKit / MySqlKit方法自动提交;CRUDBase方法不提交,事务边界由外层session_scope或@transactional决定。
5. 引擎 / 会话 / 事务 / 异常
| 符号 | 签名 | 说明 |
|---|---|---|
get_engine |
(db_type=None, **kwargs) -> Engine |
线程安全引擎单例,**kwargs 首次创建时透传 create_engine |
create_tables |
(models, db_type=None) -> None |
按模型列表建表 |
list_tables |
(db_type=None) -> list[str] |
列出全部表 |
table_exists |
(table_name, db_type=None) -> bool |
判断表是否存在 |
close_engine |
(db_type) -> None |
关闭并移除指定类型引擎 |
close_all_engines |
() -> None |
关闭全部引擎 |
session_scope |
(db_type=None, *, autocommit=True) |
唯一会话入口:正常提交、异常回滚、最终关闭 |
get_session |
(db_type=None) -> Session |
裸 Session,调用方自管生命周期 |
transactional |
(db_type=None, *, retry_count=0, retry_delay=0.5) |
事务装饰器:自动注入 session 参数、失败退避重试 |
POSTGRESQL / MYSQL / SQLITE |
"postgresql" / "mysql" / "sqlite" |
数据库类型常量 |
DBError |
继承 SQLAlchemyError |
数据库异常基类 |
NotFoundError |
继承 DBError |
记录不存在 |
from kit.db import transactional
from sqlmodel import Session
@transactional(retry_count=3)
def transfer(session: Session, from_id: int, to_id: int, amount: float) -> None:
# 被装饰函数必须接受 session 参数;退出时自动提交,异常自动回滚并重试
...
6. 模型混入类与查询构建器
from sqlalchemy import BigInteger
from sqlmodel import Field, SQLModel
from kit.common_kit import CommonKit
from kit.db.models import TimestampMixin, SoftDeleteMixin, AuditMixin
class Article(TimestampMixin, SoftDeleteMixin, SQLModel, table=True):
# 主键:64 位整数 + 雪花 ID(应用侧生成,不用数据库自增)
id: int = Field(default_factory=CommonKit.generate_snowflake_id,
primary_key=True, sa_type=BigInteger)
title: str
| 混入类 | 字段 |
|---|---|
TimestampMixin |
created_at(UTC)、updated_at |
SoftDeleteMixin |
is_deleted、deleted_at |
AuditMixin |
时间戳字段 + created_by、updated_by |
查询构建器需显式从子模块导入(未进入 kit.db 顶层导出):
from kit.db.query import QueryBuilder, QueryCache, QueryOptimizer, cached_query
qb = QueryBuilder(User, session)
active = qb.filter(User.name == "张三").all() # 链式不可变,每步返回新实例
page = qb.paginate(page=1, per_page=20) # {"items", "total", ...}
| 类 | 方法 |
|---|---|
QueryBuilder(model, session) |
filter(*conditions)、filter_by(**kwargs)、join(target, onclause=None, isouter=False)、order_by(*columns)、limit(n)、offset(n)、distinct()、only(*field_names)、all()、first()、count()、exists()、paginate(page=1, per_page=20) |
QueryCache(default_ttl=300, maxsize=1000) |
get(key)、set(key, value, ttl=None)、delete(key)、clear()、clean_expired() |
QueryOptimizer(session) |
explain(statement)、suggest_indexes(model, query_fields)、measure(statement, params=None)、stats、clear_stats() |
cached_query(ttl=300, maxsize=1000) |
查询结果缓存装饰器 |
kit.ai —— AI 多模型服务池
安装:pip install "sdpy-kit[ai]"
档案:项目根 config/ai_models.json(见文末「AI 档案文件」)
1. 同步门面 AI(推荐)
from kit.ai.runner import AI, shutdown
text = AI.chat_complete("用一句话介绍 Python") # 单轮对话
hits = AI.rerank("水声通信", ["文档A", "文档B"], top_n=2) # 重排序
url = AI.generate_image("一只在写代码的猫") # 文生图
AI.use("chat", "glm") # 切换当前档案(进程内长期生效)
print(AI.available("chat")) # 查看各档案状态快照
shutdown() # 应用退出前释放资源(幂等)
| 分组 | 方法 | 说明 |
|---|---|---|
| chat | chat_complete(prompt, *, max_tokens=None) -> str |
单轮对话(仅 user 消息) |
| chat | chat_ask(messages, *, max_tokens=None) -> str |
多轮对话,messages 为 [{"role": "user", "content": "..."}] |
| chat | chat_stream(messages, *, max_tokens=None) -> Iterator[str] |
同步流式逐 token 迭代器(建议 with 使用) |
| embedding | embed_text(text) -> list[float] |
单文本向量化 |
| embedding | embed_texts(texts) -> list[list[float]] |
批量向量化 |
| rerank | rerank(query, documents, top_n=None) -> list[tuple[int, float]] |
返回 [(原索引, 相关分)] 降序列表 |
| image | generate_image(prompt, *, size=None, n=1) -> str |
文生图,返回图片 URL |
| 管理 | use(model_type, profile) -> None |
切换该类型当前档案(chat / embedding / rerank / image) |
| 管理 | available(model_type) -> list[ProfileStatus] |
各档案状态快照(含未配置与被剔除者) |
| 管理 | reload_config(*, model_type=None, reload_file=False, probe=True, immediate=False) -> ReloadResult |
配置热重载,改密钥 / 档案后无需重启 |
from kit.ai.runner import AI
with AI.chat_stream([{"role": "user", "content": "写一首短诗"}]) as stream:
for token in stream: # 逐 token 消费
print(token, end="", flush=True)
门面仅面向同步上下文;若调用点本身在 async 函数内,请直接
await原生异步服务,避免阻塞事件循环线程。 模块级另有kit.ai.runner.reload_config(...)、run_sync(coro_factory, *, timeout=120.0)、shutdown()。
2. 原生异步服务
from kit.ai import (
ChatService, EmbeddingService, RerankService, ImageService, # 服务类
ChatRequest, EmbeddingRequest, RerankRequest, ImageRequest, # 请求模型
RerankHit, ModelBuilder, # 结果类型 / 构建器
)
service = ModelBuilder.build_chat() # 老按类型构建:build_chat / build_embedding / build_rerank / build_image
answer = await service.ask(messages) # 或 await service.complete(prompt)
await service.close()
| 类 | 方法 |
|---|---|
ChatService |
ask(messages, *, temperature=None, max_tokens=None, deadline=None) -> str、complete(prompt, *, ...) -> str、stream(messages, *, ...) -> AsyncIterator[str] |
EmbeddingService |
embed_text(text) -> list[float]、embed_texts(texts, *, batch_size=None, concurrency=4) -> list[list[float]]、dimension 属性 |
RerankService |
rerank(query, documents, *, top_n=None) -> list[RerankHit](已按 score 降序) |
ImageService |
generate(prompt, *, size=None, n=1, return_base64=False, ratio=None) -> str |
ModelBuilder |
build(model_type, model=None, *, api_key=None) 及各类型专用 build_chat / build_embedding / build_rerank / build_image |
BaseAIService |
profile 属性、await close()、支持 async with |
3. 异常
from kit.ai.exceptions import (
AIError, AIConfigError, AIRequestError, AITimeoutError,
AIAggregateError, AIResponseError,
)
| 异常 | 触发场景 / 可用属性 |
|---|---|
AIError |
基类 |
AIConfigError |
档案缺失或非法、档案不存在、密钥未配置、必填字段缺失 |
AIRequestError |
单次请求失败;status_code、retry_after(429 时解析 Retry-After) |
AITimeoutError |
请求超时(继承 AIRequestError) |
AIAggregateError |
全部候选失败或整链预算耗尽;tried、statuses、last_error、budget_exhausted |
AIResponseError |
响应结构解析失败、流式数据无法解析 |
kit.redis_kit —— Redis 工具
安装:pip install "sdpy-kit[redis]"
from kit.redis_kit import RedisKit
RedisKit.set("user:1", "clark", expire=60)
RedisKit.get("user:1") # -> "clark"
RedisKit.zadd("rank", {"alice": 1.0, "bob": 2.0})
RedisKit.zrange("rank", 0, -1, withscores=True)
RedisKit.close()
| 分组 | 方法 |
|---|---|
| 通用 | get_client()(上下文管理器)、set(key, value, expire=None) -> bool、get(key) -> RedisValue | None、delete(key) -> bool、exists(key) -> bool、keys(pattern) -> list[str]、close() -> None |
| 批量 | batch_set(key_value_pairs, expire=None) -> bool、batch_get(keys) -> dict、batch_delete(keys) -> bool |
| Hash | hset(key, field, value) -> bool、hget(key, field)、hgetall(key) -> dict、hdel(key, *fields) -> bool |
| List | lpush(key, *values) -> bool、rpush(key, *values) -> bool、lpop(key)、rpop(key)、lrange(key, start, stop) -> list |
| Set | sadd(key, *members) -> bool、srem(key, *members) -> bool、smembers(key) -> list、sismember(key, member) -> bool |
| Sorted Set | zadd(key, mapping) -> bool、zrange(key, start, stop, withscores=False) |
RedisValue = str | int | float | bytes;连接池decode_responses=True,内置指数退避重试。
kit.file_kit —— 文件工具
相对路径均相对于 FILE_STORAGE_ROOT(默认 ./data)。
from kit.file_kit import FileKit
FileKit.write_file("docs/a.txt", "hello") # mode="overwrite"(默认)/ "append"
text = FileKit.read_file("docs/a.txt")
path = FileKit.get_full_path("docs/a.txt") # -> Path 绝对路径
for root, dirs, files in FileKit.walk_dir("docs"):
print(root, files)
FileKit.open_with_default_app(path) # 系统默认程序打开:-> "opened" / "revealed"
| 分组 | 方法 |
|---|---|
| 配置 | configure(root_path) -> None、get_root_path() -> str、get_full_path(relative_path) -> Path |
| 文件 | read_file(relative_path, encoding="utf-8") -> str、write_file(relative_path, content, mode="overwrite", encoding="utf-8") -> bool、move_file(src, dest) -> bool、copy_file(src, dest) -> bool、delete_file(relative_path) -> bool、file_exists(relative_path) -> bool |
| 目录 | move_dir(src, dest) -> bool、copy_dir(src, dest) -> bool、delete_dir(relative_path) -> bool、walk_dir(relative_path="", top_down=True, on_error=None) |
| 命名 | generate_unique_id() -> str、generate_filename(prefix="", extension="") -> str |
| 系统 | open_with_default_app(path) -> OpenResult("opened" / "revealed",失败降级为文件管理器定位) |
kit.common_kit —— 通用工具
from kit.common_kit import CommonKit
CommonKit.configure(datacenter_id=1, machine_id=1) # 各限 1–31
CommonKit.generate_snowflake_id() # 63 位雪花 ID(int)
CommonKit.generate_snow_id() # 雪花 ID(str)
CommonKit.generate_timestamp_id() # 20 位时间戳 ID
CommonKit.format_current_time("yyyy-MM-dd")
| 方法 | 说明 |
|---|---|
configure(datacenter_id, machine_id) -> None |
配置雪花算法数据中心 / 机器 ID(越界抛 ValueError) |
generate_snowflake_id(datacenter_id=None, machine_id=None) -> int |
生成雪花 ID |
generate_snow_id(datacenter_id=None, machine_id=None) -> str |
生成雪花 ID 字符串 |
generate_timestamp_id() -> str |
YYYYMMDDHHMMSSfff + 3 位随机 |
format_current_time(format_pattern="yyyyMMdd") -> str |
支持 yyyyMMdd、yyyyMMddhhmm、yyyyMMddhhmmss、yyyy-MM-dd、yyyy-MM-dd hh:mm、yyyy-MM-dd hh:mm:ss |
reset() -> None |
重置 ID 生成器状态 |
kit.license_kit —— 离线授权签发与校验
零第三方依赖(cryptography 已在 base),按需 import。校验链:签名 → 产品 → 机器码 → 有效期。
from datetime import datetime, timezone
from pathlib import Path
from kit.license_kit import LicenseKit, LicensePayload, LicenseVerifier
# 1. 生成密钥对(一次性;私钥自行保管,不随包分发)
private_pem, public_pem = LicenseKit.generate_keypair()
# 2. 取机器码(16 位十六进制;仅 Windows 支持)
machine = LicenseKit.get_machine_code()
LicenseKit.export_fingerprint_file(Path("fingerprint.txt")) # 或导出文件交给签发方
machine = LicenseKit.parse_fingerprint_file(Path("fingerprint.txt"))
# 3. 签发(离线)
payload = LicensePayload(
product="thinktank",
machine_code=machine,
tier="pro",
customer_id="C-0001",
expires_at=datetime(2027, 12, 31, tzinfo=timezone.utc),
)
token = LicenseKit.sign(payload, private_pem)
# 4. 校验(应用启动 / 请求时)
verifier = LicenseVerifier(public_pem, Path("license.json"))
claims = verifier.verify(product="thinktank", machine_code=machine)
print(claims.tier, claims.expires_at)
| 符号 | 说明 |
|---|---|
LicenseKit.generate_keypair() -> (str, str) |
生成 Ed25519(私钥 PEM, 公钥 PEM) |
LicenseKit.get_machine_code() -> str |
本机机器码(16 位十六进制;⚠ 仅 Windows 平台) |
LicenseKit.export_fingerprint_file(path) -> Path |
导出机器指纹文件 |
LicenseKit.parse_fingerprint_file(path) -> str |
从指纹文件读回机器码 |
LicenseKit.sign(payload, private_key_pem) -> str |
签发授权 token |
LicenseVerifier(public_key_pem, license_path) |
构造验签器(绑定公钥与 license 文件) |
LicenseVerifier.verify(*, product, machine_code, now=None) -> LicensePayload |
全量校验并返回载荷;now=None 取 aware UTC,naive 按 UTC 解释 |
LicenseVerifier.reverify() -> LicensePayload |
用最近一次成功 verify 的上下文重新校验(文件被换会失败) |
LicenseVerifier.file_signature |
当前 license 文件的签名(属性) |
LicensePayload 字段:version / product / machine_code / tier / customer_id /
issued_at / expires_at。
异常:LicenseError(基类)、LicenseFormatError、LicenseSignatureError、
LicenseProductMismatchError、LicenseMachineMismatchError、LicenseExpiredError、
LicenseUnsupportedPlatformError(非 Windows 取机器码时抛)。
kit.docs.anydoc_kit —— 文档转 Markdown
安装:pip install "sdpy-kit[anydoc]"(支持 .doc .docx .ppt .pptx .xls .xlsx .csv .pdf)
from kit.docs.anydoc_kit import AnyDocKit
markdown = AnyDocKit.convert_to_md("report.docx") # 直接转换,失败抛异常
result = AnyDocKit.convert("report.pdf") # 安全转换,异常封装为结果对象
if result.success:
print(result.markdown)
AnyDocKit.convert_to_file("report.docx", "out/report.md")
md = AnyDocKit.convert_bytes_to_md(file_bytes, "csv") # 字节内容直接转,无需落盘
md = AnyDocKit.convert_to_md("scan.pdf", ocr="hosted") # 需 FIRECRAWL_API_KEY
| 方法 | 说明 |
|---|---|
is_supported(file_path) -> bool |
按扩展名判断是否受支持 |
detect_format(file_path) -> str | None |
由扩展名推断规范格式名 |
convert_to_md(file_path, *, ocr="reject", api_key=None, api_url=None) -> str |
文件 → Markdown(非标 PDF 自动修复重试) |
convert_bytes_to_md(data, fmt=None, *, ocr="reject", api_key=None, api_url=None) -> str |
字节内容 → Markdown |
convert(file_path, *, ocr="reject", api_key=None, api_url=None) -> AnyDocResult |
安全转换 |
convert_to_file(src_path, dest_path=None, *, ocr="reject", ...) -> str |
转换并写入 .md,返回目标路径 |
AnyDocResult(success, markdown, file_path, source_format, error)
异常:沿用上游
anydoc.ConvertError体系(MalformedError/NeedsOcrError等子类可直接捕获),AnyDocError是该基类的别名;安全版convert()会把它们封装为AnyDocResult.error。
kit.docs.docx_kit —— Word 模板动态填充
安装:pip install "sdpy-kit[docx]"
from kit.docs.docx_kit import DocxKit, DocxReport, DocxSection, DocxParagraph, DocxTable
DocxKit.create_template("templates/report.docx", title="{{ title }}") # 生成模板骨架
DocxKit.unify_font("templates/report.docx", font_name="宋体")
DocxKit.unify_heading_color("templates/report.docx")
report = DocxReport(
sections=(
DocxSection(
title="第一章 概述",
level=1,
blocks=(
DocxParagraph("本章介绍项目背景。"),
DocxTable(headers=("名称", "数值"), rows=(("甲", "1"),)),
),
),
),
variables={"project": "SDPY"},
)
DocxKit.render("templates/report.docx", "out/report.docx", report)
| 方法 | 说明 |
|---|---|
create_template(dest_path, *, title=None, leading_paragraphs=(), anchor_style=None) -> str |
生成标准模板骨架(含章节循环锚点) |
unify_font(docx_path, *, font_name="宋体") -> str |
统一文档默认与全部样式的字体(原地,幂等) |
unify_heading_color(docx_path, *, color="000000") -> str |
统一 Heading 1..9 字体颜色 |
inspect_template(template_path, *, context=None) -> tuple[str, ...] |
列出模板中未被上下文满足的变量 |
render(template_path, dest_path, report, *, anchor_style=None, strict=True) -> str |
按结构化数据渲染模板并落盘(核心方法) |
数据结构:DocxReport(sections, variables)、DocxSection(title, level=1, blocks=())、DocxParagraph(text)、DocxListItem(text, level=1, ordered=False)、DocxTable(headers, rows=(), style=None)
kit.docs.md2doc_kit —— Markdown 转 Word / PDF / HTML
安装:pip install "sdpy-kit[md2doc]"(PDF 需系统已安装 mainfont 指定字体,默认 SimSun)
from kit.docs.md2doc_kit import Md2DocKit
Md2DocKit.convert_md_to_docx("report.md", "out.docx", generate_toc=True)
Md2DocKit.convert_md_to_html("report.md", title="项目报告")
Md2DocKit.convert_md_to_pdf("report.md", mainfont="KaiTi")
| 方法 | 说明 |
|---|---|
convert_md_to_docx(src_path, dest_path=None, *, generate_toc=False, mainfont="宋体", reference_doc=None) -> str |
Markdown → Word(可插目录、统一字体) |
convert_md_to_html(src_path, dest_path=None, *, title=None) -> str |
Markdown → 独立 HTML5 文件 |
convert_md_to_pdf(src_path, dest_path=None, *, mainfont="SimSun") -> str |
Markdown → PDF(实验性,内部使用) |
dest_path=None时输出到源文件同目录;同一源文件不支持并发转换。
kit.docs.mineru_kit —— MinerU 文档解析
安装:pip install "sdpy-kit[mineru]"(精准解析需 MINERU_API_KEY,Agent 轻量解析免登录)
from kit.docs.mineru_kit import MinerUKit
# 精准解析:公网 URL / 本地文件
result = MinerUKit.parse_url("https://cdn-mineru.openxlab.org.cn/demo/example.pdf", model_version="vlm")
if result.state == "done":
zip_path = MinerUKit.download_zip(result.full_zip_url, "./output")
results = MinerUKit.parse_files(["report1.pdf", "report2.docx"])
# Agent 轻量解析(免登录、单文件 ≤10MB、≤20 页)
r = MinerUKit.agent_parse_url(url, page_range="1-10")
if r.state == "done":
md_path = MinerUKit.download_markdown(r.markdown_url, "./output")
| 分组 | 方法 |
|---|---|
| 精准解析(高层) | parse_url(url, **opts) -> TaskResult、parse_files(file_paths, **opts) -> list[BatchTaskResult]、wait_for_task(task_id, *, poll_interval=None, timeout=None)、wait_for_batch(batch_id, *, expected_count=None, ...)、download_zip(zip_url, dest_dir) -> str |
| 精准解析(低层) | create_task(url, **opts) -> str、get_task_result(task_id) -> TaskResult、create_batch(files, **opts) -> BatchUploadInfo、upload_files(file_paths, upload_urls) -> list[bool]、get_batch_results(batch_id) -> list[BatchTaskResult] |
| Agent 轻量 | agent_parse_url(url, **opts) -> AgentParseResult、agent_parse_file(file_path, **opts) -> AgentParseResult、agent_create_url_task(url, **opts) -> str、agent_create_file_task(file_name, **opts) -> AgentUploadInfo、agent_upload_file(file_path, file_url) -> None、agent_get_result(task_id)、agent_wait_for_result(task_id, *, poll_interval=None, timeout=None)、download_markdown(markdown_url, dest_dir, filename=None) -> str |
| 校验 | is_supported(file_path) -> bool、is_agent_supported(file_path) -> bool |
可选参数(
model_version/is_ocr/enable_formula/enable_table/language/page_ranges/extra_formats/no_cache等)详见各方法 docstring。 结果类型:TaskResult、BatchTaskResult、BatchFile、BatchUploadInfo、AgentParseResult、AgentUploadInfo。
kit.docs.marker_kit —— 本地解析:PDF + 图片 → Markdown + Images
安装:pip install "sdpy-kit[marker]"(依赖 marker-pdf,含 torch 等 80+ 包,体积较大;不含 [full])
支持面:.pdf、.png、.jpg、.jpeg、.webp、.gif、.bmp。docx/pptx/xlsx/html/epub 不在支持面
(marker 侧实现依赖 weasyprint/GTK),传入会抛 ValueError。
from kit.docs.marker_kit import MarkerKit
# 落盘 md + images/(推荐):源文件同目录下建同名子目录
md_path = MarkerKit.convert_to_dir("report.pdf") # -> report/report.md + report/images/
md_path = MarkerKit.convert_to_dir("report.pdf", "out/doc") # 指定产物目录
# 只取 Markdown 文本(图片不落盘)
md = MarkerKit.convert_to_md("report.pdf")
# 安全转换(不抛异常,返回结果对象)
result = MarkerKit.convert("report.pdf", "out/doc")
if result.success:
print(result.md_path, len(result.image_paths), result.page_count)
# 文本层 PDF 离线抽取(跳过 VLM/OCR,无需 llama-server)
md = MarkerKit.convert_to_md("report.pdf", disable_ocr=True)
# 图片输入(无文本层,必须 OCR:仅 disable_ocr=True 时无文字内容)
md = MarkerKit.convert_to_md("figure.png", disable_ocr=True)
MarkerKit.shutdown() # 释放模型与推理服务(幂等)
| 方法 | 说明 |
|---|---|
is_supported(file_path) -> bool |
白名单:.pdf + .png/.jpg/.jpeg/.webp/.gif/.bmp |
convert_to_md(file_path, *, mode=None, force_ocr=None, disable_ocr=None, page_range=None) -> str |
解析并返回 Markdown 文本(图片不落盘) |
convert_to_dir(src_path, output_dir=None, *, mode=None, force_ocr=None, disable_ocr=None, page_range=None, extract_images=True) -> str |
解析并落盘 md + images/,返回 md 绝对路径 |
convert(src_path, output_dir=None, *, ...) -> MarkerResult |
安全版:失败封装为 MarkerResult,不抛异常 |
shutdown() -> None |
释放模型与共享推理服务(幂等) |
产物:
<output_dir>/<源文件名>.md+<output_dir>/images/(md 内以images/<文件名>相对引用;无图片不建空目录),不生成_meta.json。 结果类型:MarkerResult(success/markdown/file_path/output_dir/md_path/image_paths/page_count/error);异常:MarkerError。mode缺省时不写入配置,交由 marker 按设备自动选择;首次使用会下载模型权重(默认走MARKER_HF_ENDPOINT镜像)。 前提条件:文本层 PDF 传disable_ocr=True即可完全离线(无需 llama-server);不传时 marker 默认启用 VLM/OCR, 缺llama-server抛SpawnError。扫描件 / 图片型 PDF / 图片输入要得到文字,必须有 llama.cpp 的llama-server(MARKER_LLAMA_CPP_BINARY),否则只得到空 md 或一条整图引用。PDF 内嵌图片抽取不需要llama-server。
kit.docs.rc_docparser_kit —— 离线 docx → Markdown + Images
安装:pip install "sdpy-kit[docx-parse]"(依赖 rc-docparser,纯 Python,无模型 / OCR / GTK)
支持面:仅 .docx。产物与 MarkerKit 同一契约:<out>/<stem>.md + <out>/images/。
from kit.docs.rc_docparser_kit import RcDocParserKit
md_path = RcDocParserKit.convert_to_dir("report.docx") # -> report/report.md + report/images/
md = RcDocParserKit.convert_to_md("report.docx") # 只取文本(图片不落盘)
result = RcDocParserKit.convert("report.docx", "out/doc")
if result.success:
print(result.md_path, len(result.image_paths))
| 方法 | 说明 |
|---|---|
is_supported(file_path) -> bool |
是否 .docx |
convert_to_md(file_path) -> str |
解析并返回 Markdown 文本 |
convert_to_dir(src_path, output_dir=None, *, extract_images=True) -> str |
落盘 md + images/,返回 md 绝对路径 |
convert(src_path, output_dir=None, *, ...) -> RcDocParserResult |
安全版:失败封装为结果对象 |
产物:
<out>/images/img-NNN-<sha1 前 10>.<ext>(按出现顺序、内容哈希命名);无图不建空目录。 前提:中文标题依赖w:outlineLvl或「标题 N」样式(都没有时退化为正文);合并单元格表格会重建为 等宽 GFM(覆盖位留空,不输出 HTML<table>);页眉页脚/脚注不提取。纯本地,无需模型或 GPU。
kit.crawl.wx_article_kit —— 微信公众号文章抓取存储
零第三方依赖(标准库 urllib + 内置 HTML→Markdown 转换器),无需 extra,按需 import:
from kit.crawl.wx_article_kit import WxArticleKit
# 抓取解析(不落盘)
result = WxArticleKit.fetch("https://mp.weixin.qq.com/s/xxxx")
if result.ok:
print(result.info.title, result.info.author, result.info.publish_time)
# 抓取并存储:<output_dir>/<stem>/ 下 <stem>.md + images/ + meta.json
saved = WxArticleKit.save(url, output_dir="./output", file_name="12345")
print(saved.md_path, saved.images_saved)
# 不传 file_name:目录按本地时间 yymmddhhmmss 自动命名
WxArticleKit.save(url, "./output")
# 不需要 meta.json 时显式关闭
WxArticleKit.save(url, "./output", "12345", write_meta=False)
| 方法 | 说明 |
|---|---|
fetch(url, timeout=20.0) -> ArticleResult |
抓取并解析正文(不落盘);结果对象字段 ok / error / info(ArticleInfo)/ markdown |
save(url, output_dir, file_name=None, write_meta=True, timeout=20.0) -> SavedOutput |
抓取并存储到 <output_dir>/<stem>/;结果对象字段 ok / error / output_dir / md_path / images_dir / title / images_saved / reason / warnings |
落盘结构(三个 *ArticleKit 统一由 kit.crawl.article_io 完成):
<output_dir>/<stem>/
├── <stem>.md # 正文 Markdown(YAML 头:title / author / publish_time / source / source_url)
├── images/ # 图片转存(NNN.ext,正文引用已本地化)
└── meta.json # 结构化元信息(默认产出;write_meta=False 关闭)
stem不传时按本地时间%y%m%d%H%M%S自动命名,同秒碰撞追加-2…-99; 显式传file_name时同名目录覆盖写;- 元信息随 Markdown 的 YAML 头落盘(头部
publish_time为中文日期,如2026年8月4日);ArticleInfo.publish_time与meta.json内仍为 ISO 8601(结构化数据)。 - 无登录态抓取:命中微信风控(环境异常 / 访问过于频繁)时返回
ok=False+ 原因,不破解验证码; 抓取或图片下载失败不抛异常,单张图片失败时正文保留原远程 URL。 - 模型与
WebArticleKit共用kit.crawl.article_models(ArticleInfo含source/site_host); 失败时reason为类型化分类(微信常用risk_control/no_content/fetch_failed)。 - 显式
file_name时同名子目录覆盖写(不追加序号);省略时按时间自动命名,同秒追加-2…;file_name带.md后缀会自动去除,显式传空串则抛ValueError。
kit.crawl.web_article_kit —— 通用网页文章抓取存储
可选依赖(trafilatura),需安装 pip install "sdpy-kit[web-article]",按需 import:
from kit.crawl.web_article_kit import WebArticleKit
# 抓取解析(不落盘)
result = WebArticleKit.fetch("https://blog.csdn.net/xxx/article/details/123")
if result.ok:
print(result.info.source, result.info.title, result.info.publish_time)
# 抓取并存储:<output_dir>/<stem>/ 下 <stem>.md + images/ + meta.json
saved = WebArticleKit.save(url, output_dir="./output", file_name="12345")
print(saved.md_path, saved.images_saved)
# 不传 file_name:目录按本地时间 yymmddhhmmss 自动命名
WebArticleKit.save(url, "./output")
| 方法 | 说明 |
|---|---|
fetch(url, timeout=20.0) -> ArticleResult |
抓取并解析正文(不落盘);结果对象字段 ok / error / info(ArticleInfo)/ markdown |
save(url, output_dir, file_name=None, write_meta=True, timeout=20.0) -> SavedOutput |
抓取并存储到 <output_dir>/<stem>/;字段与 WxArticleKit.save 一致 |
落盘结构与 WxArticleKit 完全一致(<stem>.md + images/ + meta.json):
<stem>.md 的 YAML 头为 title / author / publish_time / source / source_url(publish_time 渲染为中文日期)。
站点覆盖范围(规则表只允许「转交」与「接口适配」两类条目,不做逐站 DOM 补丁):
| 站点类型 | 行为 |
|---|---|
| 通用静态 / 服务端渲染页面(博客、文档站、CSDN 等) | ✅ 直接抽取 |
| 头条(文章页为 JS 空壳) | ✅ 明确支持:走移动端 JSON 接口 |
| 微信公众号 | ❌ 不覆盖:返回提示,请改用 kit.crawl.wx_article_kit.WxArticleKit(该来源有专属解析,二者各自独立) |
| 正文依赖 JavaScript 动态渲染或需登录态的站点 | ❌ 失败并给出类型化说明(不维护域名名单) |
ArticleInfo 为平台中立的通用字段集:url / title / author / publish_time(ISO 8601)/ source
(来源平台名)/ site_host / summary / images / links / code_blocks,便于多平台抓取后写入同一张表;
to_dict() 可直接 JSON 化(即 meta.json 的内容)。模型定义在 kit.crawl.article_models,与
WxArticleKit 共用同一套。
质量信号(均不改变成功语义):
| 字段 | 取值 / 含义 |
|---|---|
reason |
失败时的类型化分类:unsupported_source / fetch_blocked / fetch_failed / no_content / risk_control / auth_failed / rate_limited / config_missing(成功为空串) |
warnings |
非致命抽取自检提示(tuple[str, ...]):正文疑似过短、页面含图片但产物无图;同时写入 warning 日志 |
标题统一去除站点后缀(如 CSDN 的 标题-CSDN博客);刻意不做「与页面 h1 择优」(h1 可能是导航文案)。
kit.crawl.deepseek_kit —— DeepSeek Search / Fetch
零第三方依赖,基础安装即可用(按请求次数计费,0.005 元/次量级)。需配置 API Key:
DEEPSEEK_API_KEY=sk-xxx # .env 或环境变量
DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_TIMEOUT=60
DEEPSEEK_MAX_RETRIES=2 # 仅 408/429/5xx 重试
from kit.crawl.deepseek_kit import DeepSeekKit
# 1. 网页搜索(不落盘)
found = DeepSeekKit.search("DeepSeek V4 发布 site:github.com", count=5, need_content=False)
for item in found.items:
print(item.title, item.url, item.rerank_score, item.authority)
# 2. 单页抓取(不落盘)
page = DeepSeekKit.fetch("https://api-docs.deepseek.com/zh-cn/guides/search")
print(page.info.title, page.markdown)
# 3. 抓取并存储:<output_dir>/<stem>/ 下 <stem>.md + images/ + meta.json
saved = DeepSeekKit.save(url, output_dir="./output", file_name="ds-guide")
| 方法 | 说明 |
|---|---|
search(query, count=10, need_content=False, ori_question=None, localization=None, timeout=None, retries=None) -> SearchResult |
返回 ok / error / reason / items(SearchItem 元组)/ warnings;SearchItem.to_dict() 可直接 JSON 化 |
fetch(url, timeout=None, retries=None) -> ArticleResult |
与 WebArticleKit.fetch 同构(info / markdown / reason / warnings) |
save(url, output_dir, file_name=None, write_meta=True, timeout=None, retries=None) -> SavedOutput |
落盘结构与两个 *ArticleKit 完全一致 |
失败 reason:auth_failed(401/402)/ fetch_blocked(403)/ no_content(404)/
rate_limited(429)/ fetch_failed(408/5xx 重试后仍失败、请求不合法)。
未配置 DEEPSEEK_API_KEY 抛 DeepSeekKitError。实现要求:
- 服务端返回的正文是重度反斜杠转义的伪 Markdown,kit 会统一反转义后再落盘;
- 该接口不返回代码块围栏,代码会被压成普通段落;图片也常缺失。
需要高保真正文(代码块、图片)请改用
WebArticleKit。
AI 档案文件
AI 功能需要在项目根目录提供 config/ai_models.json(chat / embedding / rerank / image 四类型多厂商模型档案)。
文件缺失时,首次使用相应功能会报错,报错信息中附带可直接复制的最小模板;
其中 api_key 字段填写环境变量名,真实密钥配置在 .env。
ai_models.json 可选字段
| 层级 | 字段 | 说明 |
|---|---|---|
| 档案级 | timeout |
请求超时秒数(默认 60) |
| 档案级 | max_tokens |
输出上限(chat 用);AI.chat_ask/chat_complete/chat_stream 也可按调用点传参,逐级回落:调用点 → 档案 → provider 默认 |
| 档案级 | retries |
5xx 重试次数(默认 2;超时不重试,候选链切换才是有效重试) |
| 类型级 | chain_timeout |
整链预算秒数(与 default_profile 同级);门面同步等待上限。未配置时按 sum(候选 timeout) + 10s 自适应,也可用环境变量 KIT_AI_CHAIN_TIMEOUT 全局覆盖 |
候选链切换语义(两类,勿混淆)
- 跨调用切换:
AI.use()与其它请求产生的失败冷却均只影响下一次请求——每次调用的候选计划在开始时定格; - 一次调用内降级:本次调用按开始时定格的计划串行尝试(首帧前失败 / 首帧超时切下一候选),中途的
use/ 冷却变化不影响它。
全部候选失败(或整链预算耗尽)抛 AIAggregateError(AIRequestError 子类):
全候选状态码一致时透传(如整链 429 可用 exc.status_code == 429 判定),
429 的 Retry-After 解析后挂于 exc.retry_after(冷却钳制 5–300s)。
License
MIT
Metadata
Release files for sdpy-kit 0.6.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| sdpy_kit-0.6.2.tar.gz | 241.0 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sdpy_kit-0.6.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 514.8 kB
Release files / sdpy_kit-0.6.2.tar.gz
| Download URL | sdpy_kit-0.6.2.tar.gz |
|---|---|
| Size | 241.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5fce4cfaf2636a9ef4b5e59c330c94561091adb0af80bd5465af98cf081ae049
|
|
BLAKE2b-256 checksum How to use checksums |
0b2fa70f3817b1edc4c961035e96de9b238108eb92a3f1995f36c53dbeca60df
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.24 {"installer":{"name":"uv","version":"0.12.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / sdpy_kit-0.6.2-py3-none-any.whl
| Download URL | sdpy_kit-0.6.2-py3-none-any.whl |
|---|---|
| Size | 273.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
471ebf4da6a6a0b931274b4dd80de06b6d4e0b717affceba56888eb2a0d3d5da
|
|
BLAKE2b-256 checksum How to use checksums |
fb6733bbaac37270fc4a909c325ea7227e461d9e9435661b9eddcbc63704da80
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.24 {"installer":{"name":"uv","version":"0.12.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for sdpy-kit 0.6.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.