Skip to main content

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_CAPABILITIES feature-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_column_dimension(table_name, column="embedding") -> int | None 查列声明的维度:PG 为精确维度(vector(1024)→1024);MySQL 为最大上限(未指定维度落 2048);SQLite 恒 None(无元数据)。列不存在 / 非向量列 / 无维度声明 → None,不抛异常。口径差异由 VECTOR_CAPABILITIES["dimension_kind"] 声明("exact" / "max" / None)
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(仅新表;旧表靠方法内校验) 强制(不匹配即报错;建表时回读比对) 上限(VECTOR(N) 的 N 是最大维度,写入须 ≤ N,超限报 1406 Data too long)
维度查询 恒 None(无元数据) 精确维度(dimension_kind="exact") 最大上限(dimension_kind="max")
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 只覆盖存储侧。

维度上限(插件侧实测 pgvector 0.8.7):vector ≤ 16000 维;hnsw / ivfflat 索引 ≤ 2000 维;halfvec 表达式索引 ≤ 4000 维。因此 1024 维表可建 HNSW, 2560 / 4096 维表建不了。

主键与 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',
#  'dimension_kind': 'exact'}      # MySQL='max' / SQLite=None

# 查询库列声明的维度(守卫「配置 vs 库列」用;查不到返回 None)
PgKit.vector_column_dimension("images", "embedding")   # 384

# 自建 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/SQLite ON CONFLICT DO NOTHING、MySQL INSERT 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

embedding 维度契约:ai_models.json 里 embedding 档案的 dimension 是目标向量维度 (不是模型原生维度),EmbeddingService 保证输出恒等于它:

模型返回维 vs dimension 行为
相等 原样返回(不归一化,逐字节不变)
更大 截断到该维度并做 L2 归一化(依赖模型的 MRL 支持;非 MRL 模型截断后质量会退化)
更小 抛 AIResponseError(降维只能变小,不能放大)

用途:让「建表维度 = 配置维度 = 输出维度」共用一个数值—— create_vector_table(t, dims) 与 vector_column_dimension(t, col) 都取该配置值, 换用原生维度更大的模型时无需重构向量列(换模型后需全量重新入库)。

⚠ 启用候选链时,链内各档案的 dimension 必须一致:故障切换后输出维度跟随被选中档案的配置。

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 文件(图片输出 <figure><img>)
convert_md_to_pdf(src_path, dest_path=None, *, mainfont="SimSun") -> str Markdown → PDF(实验性,内部使用;图片等比限宽居中)

dest_path=None 时输出到源文件同目录;同一源文件不支持并发转换。

图片:![alt](相对路径) 以 Markdown 源文件所在目录为基准解析,宽度等比限幅 (96dpi 还原自然尺寸、不放大),超高先分页/按版心收紧;缺图、越界路径、非图片 格式一律回落 alt 文本,不抛异常。远程地址在 PDF / Word 中不下载(回落 alt), 在 HTML 中直出 <img src="https://…"> 交给浏览器。HTML 输出保留相对路径,需与 images/ 目录保持同层级才能在浏览器中显示。


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.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sdpy-kit 0.6.4
File Size Uploaded Provenance
sdpy_kit-0.6.4.tar.gz 253.0 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for sdpy-kit 0.6.4
File Interpreter ABI Platform
sdpy_kit-0.6.4-py3-none-any.whl Python 3 none any Details

Total release size: 538.0 kB

Release files / sdpy_kit-0.6.4.tar.gz

Download URL sdpy_kit-0.6.4.tar.gz
Size 253.0 kB
Tags Source
SHA-256 checksum
How to use checksums
21d5ff49c28beff9d25e515a38ef0dca6f678cac55aaa5ed2e20315aaa25b899
BLAKE2b-256 checksum
How to use checksums
734cb23fc6e6f98e1422cf0c2e129a09da43a60edfb102367b26ffa789088847
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.4-py3-none-any.whl

Download URL sdpy_kit-0.6.4-py3-none-any.whl
Size 285.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ea82f611a7d56ee29e58437dbf97aa2243b5770923025afd9b9064709d319689
BLAKE2b-256 checksum
How to use checksums
13a8190660735b9cb2a2717b7f4f6bb40f547be7d6aaf9730defa149a9b546f7
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.4

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.

Release history Release notifications | RSS feed

This release

0.6.4 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page