Compare commits
34 Commits
11b6171e21
...
ec3898ecb7
| Author | SHA1 | Date | |
|---|---|---|---|
| ec3898ecb7 | |||
| 20eec96e92 | |||
| f8ec682474 | |||
| 9dd1efeec9 | |||
| e99c090152 | |||
| 76ae81c772 | |||
| 788f610d3f | |||
| 2e95f68864 | |||
| 3308636086 | |||
| ebfae1f60c | |||
| 29bdfcfb18 | |||
| 276a1401f4 | |||
| b0319f366a | |||
| 925499b05b | |||
| e0190e8d2d | |||
| a368e640c1 | |||
| fc010cda06 | |||
| b97cff8857 | |||
| 6a5be5f3e7 | |||
| 8445af7be0 | |||
| 298f13721c | |||
| 1eb6f44ef4 | |||
| 0a4a4c766c | |||
| 284a4b9e09 | |||
| 3b8b585f31 | |||
| eda080bc05 | |||
| b8afc9043d | |||
| 9db0c15dee | |||
| 30716f10dc | |||
| 69fb8cac9f | |||
| cde4ae7ddc | |||
| 13d63ba6ff | |||
| 4dc51dccd2 | |||
| 30c4d65cf0 |
@@ -11,7 +11,7 @@ Markdown 文档向量数据库 — 将 .md 文件分块 → 嵌入 → 存入 Ch
|
||||
```bash
|
||||
# --- 安装与测试 ---
|
||||
uv sync # 安装依赖(lockfile 已锁定 CUDA torch)
|
||||
uv run pytest tests/ -v # 全部测试 (46 个)
|
||||
uv run pytest tests/ -v # 全部测试 (115+ 个)
|
||||
uv run pytest tests/test_api.py -v # 单个测试模块
|
||||
uv run pytest tests/ -v -k "test_search" # 按名称过滤
|
||||
|
||||
@@ -31,12 +31,19 @@ uv run python scripts/ingest_obsidian.py
|
||||
## 架构
|
||||
|
||||
```
|
||||
core/ # 核心逻辑(不依赖 server/cli)
|
||||
├── config.py # YAML → dataclass, load_dotenv() 加载 .env
|
||||
├── db.py # VectorDB: 线程安全的 ChromaDB 封装
|
||||
├── embedder.py # 策略模式: LocalEmbedder / OpenAIEmbedder / DashscopeEmbedder
|
||||
├── ingest.py # MarkdownSplitter(混合分块) + DocumentIngestor(分批嵌入)
|
||||
└── search.py # Searcher: 语义检索 + 源文件管理
|
||||
├── core/ # 核心逻辑(不依赖 server/cli)
|
||||
│ ├── config.py # YAML → dataclass, load_dotenv() 加载 .env
|
||||
│ ├── db.py # VectorDB: 线程安全的 ChromaDB 封装
|
||||
│ ├── embedder.py # 策略模式: LocalEmbedder / OpenAIEmbedder / DashscopeEmbedder
|
||||
│ ├── ingest.py # DocumentIngestor: 按扩展名自动选择 Splitter
|
||||
│ ├── search.py # Searcher: 语义检索 + 源文件管理
|
||||
│ └── splitters/ # 文档分块器包
|
||||
│ ├── base.py # Splitter(Protocol) + BaseTextSplitter(ABC)
|
||||
│ ├── markdown.py # MarkdownSplitter: 标题+段落混合分块
|
||||
│ ├── text.py # TextSplitter: 纯文本段落切分
|
||||
│ ├── pdf.py # PDFSplitter: pymupdf 提取文字
|
||||
│ ├── html.py # HTMLSplitter: bs4 去标签
|
||||
│ └── registry.py # 扩展名 → Splitter 自动选择
|
||||
|
||||
server/ # FastAPI HTTP 层
|
||||
├── app.py # Depends(get_state) 依赖注入, 速率限制中间件
|
||||
@@ -46,7 +53,9 @@ server/ # FastAPI HTTP 层
|
||||
cli/main.py # Typer CLI,5 个命令 + --config 选项
|
||||
```
|
||||
|
||||
**数据流**: MD 文件 → `MarkdownSplitter.split()` → `batch_embed()` → `ChromaDB collection.add()` → `Searcher.search()`
|
||||
**数据流**: 文件 → `get_splitter(path)` 自动选择 → `Splitter.split()` → `batch_embed()` → `ChromaDB collection.add()` → `Searcher.search()`
|
||||
|
||||
**支持的格式**: `.md` / `.txt` / `.pdf` / `.html` — 安装可选依赖: `uv sync --extra all`
|
||||
|
||||
**依赖方向**: `config` ← `db` ← `embedder` ← `ingest`/`search` ← `server`/`cli`
|
||||
|
||||
@@ -111,10 +120,18 @@ cli/main.py # Typer CLI,5 个命令 + --config 选项
|
||||
|
||||
| 集合名 | 来源 | 文件数 | chunks | 说明 |
|
||||
|--------|------|--------|--------|------|
|
||||
| `novel_taohou` | `D:\Code\doing_exercises\exercise\Novel\我有太后罩着,你们有什么\原有章节剧情` | 220 | 670 | 小说章节(GPU bge-small-v1.5) |
|
||||
| `obsidian_blog` | `D:\Code\Obsidian` | 51 | 3,611 | 博客笔记(GPU bge-small-v1.5) |
|
||||
| `default` | 测试文件 | 2 | ~30 | test-guide.md + stdin-doc.md |
|
||||
|
||||
搜索时务必用 `-C obsidian_blog` 指定博客集合,否则只会搜到 default 中的测试数据。
|
||||
搜索时务必用 `-C` 指定集合,否则只会搜到 default 中的测试数据。
|
||||
|
||||
**小说搜索示例**:
|
||||
```bash
|
||||
uv run md-vector-db search "张莽和孙太后的关系" -k 3 -C novel_taohou
|
||||
uv run md-vector-db search "抄家事件" -k 5 -C novel_taohou
|
||||
uv run md-vector-db search "文谦变法" -k 3 -C novel_taohou
|
||||
```
|
||||
|
||||
## 已知问题 / 注意事项
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ Markdown 文档向量数据库 — 将 Markdown 文件自动分块、嵌入、
|
||||
- **多集合**: 支持多项目数据隔离,不同知识库存入不同 ChromaDB collection
|
||||
- **HTTP API**: FastAPI 提供 RESTful 接口,附带 Swagger 文档
|
||||
- **安全**: 可选 API Key 认证、速率限制、路径遍历防护
|
||||
- **多格式文档**: 支持 `.md` / `.txt` / `.pdf` / `.html`,按扩展名自动选择分块器,可通过 `Splitter` Protocol 扩展
|
||||
- **去重**: 同一文件重复入库自动覆盖旧版本(基于路径 SHA256 哈希)
|
||||
|
||||
## 快速开始
|
||||
@@ -21,6 +22,9 @@ Markdown 文档向量数据库 — 将 Markdown 文件自动分块、嵌入、
|
||||
git clone git@lhy-git.liuhangyv.top:Serendipity/md-vector-db.git
|
||||
cd md-vector-db
|
||||
uv sync
|
||||
|
||||
# 安装 PDF + HTML 支持(可选)
|
||||
uv sync --extra all
|
||||
```
|
||||
|
||||
### 2. 配置
|
||||
@@ -220,7 +224,15 @@ md-vector-db/
|
||||
│ │ ├── db.py # ChromaDB 封装(线程安全)
|
||||
│ │ ├── embedder.py # 嵌入器(Local/OpenAI/Dashscope)
|
||||
│ │ ├── ingest.py # 混合分块 + 入库
|
||||
│ │ └── search.py # 语义检索
|
||||
│ │ ├── search.py # 语义检索
|
||||
│ │ └── splitters/ # 文档分块器包
|
||||
│ │ ├── base.py # Splitter(Protocol) + BaseTextSplitter(ABC)
|
||||
│ │ ├── markdown.py # MarkdownSplitter
|
||||
│ │ ├── text.py # TextSplitter
|
||||
│ │ ├── pdf.py # PDFSplitter
|
||||
│ │ ├── html.py # HTMLSplitter
|
||||
│ │ ├── epub.py # EPUBSplitter
|
||||
│ │ └── registry.py # 扩展名 → Splitter 自动选择
|
||||
│ ├── server/ # FastAPI HTTP 层
|
||||
│ │ ├── app.py # 路由 + 中间件
|
||||
│ │ ├── deps.py # 依赖注入(AppState)
|
||||
@@ -232,13 +244,13 @@ md-vector-db/
|
||||
├── scripts/
|
||||
│ ├── serve.py # 快速启动脚本
|
||||
│ └── ingest_obsidian.py # 批量入库 Obsidian 知识库
|
||||
└── tests/ # 测试(46 个)
|
||||
└── tests/ # 测试(115+ 个)
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
uv run pytest tests/ -v # 全部测试 (46 个)
|
||||
uv run pytest tests/ -v # 全部测试 (115+ 个)
|
||||
uv run pytest tests/test_embedder.py -v # 嵌入器测试
|
||||
uv run pytest tests/ -v -k "search" # 按名称过滤
|
||||
uv run pytest tests/ -v --cov=src --cov-report=term-missing # 覆盖率
|
||||
@@ -246,7 +258,7 @@ uv run pytest tests/ -v --cov=src --cov-report=term-missing # 覆盖率
|
||||
|
||||
## 架构
|
||||
|
||||
**数据流**: MD 文件 → `MarkdownSplitter.split()` (标题→段落分块) → `batch_embed()` (分批嵌入) → `ChromaDB collection.add()` → `Searcher.search()` 查询
|
||||
**数据流**: 文件 → `get_splitter(path)` 自动选择 → `Splitter.split()` → `batch_embed()` (分批嵌入) → `ChromaDB collection.add()` → `Searcher.search()` 查询
|
||||
|
||||
**依赖方向**: `config` ← `db` ← `embedder` ← `ingest` / `search` ← `server` / `cli`
|
||||
|
||||
|
||||
@@ -19,3 +19,5 @@ chunk:
|
||||
server:
|
||||
host: 0.0.0.0
|
||||
port: 8000
|
||||
# ssl_keyfile: "" # HTTPS 私钥路径(设置后启用 HTTPS)
|
||||
# ssl_certfile: "" # HTTPS 证书路径(设置后启用 HTTPS)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,142 @@
|
||||
# 多格式文档支持 — 设计文档
|
||||
|
||||
**日期**: 2026-07-10
|
||||
**版本**: 1.0
|
||||
|
||||
## 目标
|
||||
|
||||
将 md-vector-db 从"仅 Markdown"扩展为支持 `.txt`、`.pdf`、`.html` 的多格式文档向量数据库。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不支持 `.docx`、`.pptx`、`.epub` 等 Office/电子书格式(留待后续扩展)
|
||||
- 不改变现有的嵌入和检索流程
|
||||
- 不改变 HTTP API 的请求/响应模型
|
||||
|
||||
## 架构
|
||||
|
||||
### 模块结构
|
||||
|
||||
```
|
||||
src/core/
|
||||
├── splitters/ # 新建目录
|
||||
│ ├── __init__.py # 导出 registry + 所有 Splitter
|
||||
│ ├── base.py # Splitter Protocol + BaseTextSplitter (ABC)
|
||||
│ ├── markdown.py # MarkdownSplitter (从 ingest.py 移入)
|
||||
│ ├── text.py # TextSplitter (纯文本按段落+标点硬切)
|
||||
│ ├── pdf.py # PDFSplitter (pymupdf → TextSplitter)
|
||||
│ ├── html.py # HTMLSplitter (bs4 → TextSplitter)
|
||||
│ └── registry.py # 工厂 + 扩展名→Splitter 映射表
|
||||
├── ingest.py # 精简,用 registry.get_splitter() 自动选择
|
||||
```
|
||||
|
||||
### 类继承
|
||||
|
||||
```
|
||||
Splitter (Protocol)
|
||||
└── BaseTextSplitter (ABC) # max_size, overlap, _split_single_paragraph
|
||||
├── MarkdownSplitter # 已有,从 ingest.py 移入
|
||||
├── TextSplitter # 纯文本:按 \n\n 切段落,超长按标点硬切
|
||||
├── PDFSplitter # 读取 PDF → extract_text() → 委托 TextSplitter
|
||||
└── HTMLSplitter # 读取 HTML → get_text() → 委托 TextSplitter
|
||||
```
|
||||
|
||||
`PDFSplitter` 和 `HTMLSplitter` **不继承** `BaseTextSplitter`,而是组合一个 `TextSplitter` 实例。它们实现 `Splitter` Protocol,在 `split()` 中:提取纯文本 → 委托 `TextSplitter.split()`。
|
||||
|
||||
### 注册表
|
||||
|
||||
`registry.py` 维护默认扩展名映射:
|
||||
|
||||
```python
|
||||
_DEFAULT_MAP = {
|
||||
".md": "markdown",
|
||||
".markdown": "markdown",
|
||||
".txt": "text",
|
||||
".pdf": "pdf",
|
||||
".html": "html",
|
||||
".htm": "html",
|
||||
}
|
||||
|
||||
def get_splitter(file_path: str, **config) -> Splitter:
|
||||
"""根据扩展名自动选择 Splitter,未匹配回退到 TextSplitter."""
|
||||
```
|
||||
|
||||
## 依赖策略
|
||||
|
||||
- `pymupdf` 和 `beautifulsoup4` 作为**可选依赖**
|
||||
- 首次使用 PDF/HTML 格式时才 `import`,库缺失时抛 `ImportError` 带安装提示
|
||||
- 安装方式:
|
||||
|
||||
```bash
|
||||
uv sync --extra pdf # PDF 支持
|
||||
uv sync --extra html # HTML 支持
|
||||
uv sync --extra all # 全部可选依赖
|
||||
```
|
||||
|
||||
## ingest.py 改动
|
||||
|
||||
### DocumentIngestor.__init__
|
||||
|
||||
已有 `splitter` 可选参数,**保持不变**。显式传入的 splitter 覆盖自动选择。
|
||||
|
||||
### ingest_file()
|
||||
|
||||
改为使用 `get_splitter(file_path, ...)` 自动选择 splitter:
|
||||
|
||||
```python
|
||||
def ingest_file(self, file_path: str) -> int:
|
||||
splitter = self.splitter or get_splitter(file_path,
|
||||
max_size=..., overlap=...)
|
||||
...
|
||||
```
|
||||
|
||||
### ingest_directory()
|
||||
|
||||
将 `rglob("*.md")` 改为遍历所有支持格式:
|
||||
|
||||
```python
|
||||
_SUPPORTED_SUFFIXES = {".md", ".markdown", ".txt", ".pdf", ".html", ".htm"}
|
||||
|
||||
def ingest_directory(self, dir_path: str) -> dict[str, int]:
|
||||
for f in Path(dir_path).rglob("*"):
|
||||
if f.suffix.lower() in _SUPPORTED_SUFFIXES:
|
||||
...
|
||||
```
|
||||
|
||||
## 数据流
|
||||
|
||||
```
|
||||
文件路径 → get_splitter(path)
|
||||
├── .md → MarkdownSplitter.split(text)
|
||||
├── .txt → TextSplitter.split(text)
|
||||
├── .pdf → PDFSplitter.split(binary)
|
||||
│ └── pymupdf 提取文字 → TextSplitter.split(text)
|
||||
└── .html → HTMLSplitter.split(html)
|
||||
└── bs4 去标签 → TextSplitter.split(text)
|
||||
↓
|
||||
batch_embed(chunks)
|
||||
↓
|
||||
ChromaDB.add()
|
||||
```
|
||||
|
||||
## 测试策略
|
||||
|
||||
- `tests/test_splitters_text.py` — TextSplitter 段落切分、硬切、overlap
|
||||
- `tests/test_splitters_registry.py` — 扩展名映射、回退逻辑、自定义注册
|
||||
- `tests/test_splitters_pdf.py` — PDF 提取文字 + 分块(需 pymupdf)
|
||||
- `tests/test_splitters_html.py` — HTML 去标签 + 分块(需 bs4)
|
||||
- `tests/test_ingest.py` — 补 `ingest_directory` 多格式遍历测试
|
||||
|
||||
## CLI/API 影响
|
||||
|
||||
- **CLI**:零改动。`ingest` 和 `ingest-dir` 自动获得多格式能力
|
||||
- **API**:零改动。`POST /api/v1/ingest` 的 `file_path` 自动支持
|
||||
- **`ingest_content()`**:仍默认使用 `MarkdownSplitter`(处理 Markdown 字符串),显式传入 content 时不变
|
||||
|
||||
## 风险
|
||||
|
||||
| 风险 | 缓解 |
|
||||
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| PDF 提取失败(扫描件/图片 PDF) | 抛明确异常,跳过该文件 |
|
||||
| HTML 标签复杂导致去标签不干净 | 用`soup.get_text(separator="\n")` 保留段落结构 |
|
||||
| `ingest.py` 迁移 MarkdownSplitter 后旧 import 路径失效 | 在`ingest.py` 保留兼容 import:`from src.core.splitters.markdown import MarkdownSplitter` |
|
||||
@@ -0,0 +1,369 @@
|
||||
# md-vector-db 全面安全与质量审计报告
|
||||
|
||||
> **审计日期**: 2026-07-10
|
||||
> **审计范围**: 全部源代码(22 个源文件)、13 个测试文件、配置、文档
|
||||
> **测试基线**: 90 个测试全部通过
|
||||
> **项目版本**: commit `3b8b585`
|
||||
|
||||
---
|
||||
|
||||
## 一、审计概览
|
||||
|
||||
本次审计由三个并行代理分别审查 **核心模块**(config/db/embedder/ingest/search/security/splitters)、**服务层与 CLI**(app/auth/deps/main.py/scripts)、**测试与配置**(tests/pyproject.toml/config.yaml/文档),最终整合为本报告。
|
||||
|
||||
### 问题统计
|
||||
|
||||
| 严重级别 | 数量 | 含义 |
|
||||
|----------|------|------|
|
||||
| **CRITICAL** | **1** | 阻断 — 数据重复处理浪费 GPU 资源 |
|
||||
| **HIGH** | **13** | 警告 — 合并前应修复 |
|
||||
| **MEDIUM** | **11** | 信息 — 应考虑修复 |
|
||||
| **LOW** | **10** | 注意 — 可选修复 |
|
||||
| **总计** | **35** | |
|
||||
|
||||
### 覆盖评估
|
||||
|
||||
| 模块 | 覆盖程度 |
|
||||
|------|----------|
|
||||
| `core/security.py` | ✅ 优秀 — 所有路径穿越场景已覆盖 |
|
||||
| `core/config.py` | ✅ 良好 — YAML 加载/默认值/环境变量 |
|
||||
| `core/embedder.py` | ✅ 良好 — 所有 provider 初始化和 batch_embed |
|
||||
| `core/splitters/*` (除 markdown) | ✅ 良好 |
|
||||
| `core/db.py` | ⚠️ 基本 — 缺少 list_collections、write_guard、并发测试 |
|
||||
| `core/ingest.py` | ❌ 不足 — 缺少 ingest_file、ingest_directory |
|
||||
| `core/search.py` | ❌ 不足 — 缺少 3/4 方法测试 |
|
||||
| `server/deps.py` / `app.py` | ⚠️ 基本 — 缺少认证/速率限制/CORS 配置测试 |
|
||||
| `server/auth.py` | ❌ 零覆盖 |
|
||||
| `cli/main.py` | ❌ 零覆盖 |
|
||||
| **整体估算** | **~55-60%**(低于 80% 目标) |
|
||||
|
||||
---
|
||||
|
||||
## 二、CRITICAL 级别(1 个)
|
||||
|
||||
### [CRIT-1] `ingest_obsidian.py` 顶层 .md 文件被重复入库
|
||||
|
||||
- **文件**: `scripts/ingest_obsidian.py`,第 54-77 行
|
||||
- **根因**: 第一轮用 `rglob("*.md")` 递归扫描所有文件,第二轮用 `glob("*.md")` 再次收集顶层文件。由于 `rglob` 已经包含顶层文件,每个顶层 `.md` 被处理两次。
|
||||
- **影响**: 嵌入计算浪费(GPU 资源),入库时间翻倍于顶层文件数量。第二次入库虽然会覆盖第一次(file_name 相同,先删后加),但嵌入已完成,白白消耗了 GPU。
|
||||
- **修复**: 删除第二轮循环(第 67-77 行),`rglob` 已覆盖所有文件。
|
||||
|
||||
---
|
||||
|
||||
## 三、HIGH 级别(13 个)
|
||||
|
||||
### 安全
|
||||
|
||||
#### [HIGH-1] CORS `allow_credentials=True` 与 `allow_origins="*"` 冲突
|
||||
|
||||
- **文件**: `src/server/app.py`,第 66-72 行
|
||||
- **根因**: CORS 规范禁止 `Access-Control-Allow-Origin: *` 与 `Access-Control-Allow-Credentials: true` 同时使用。浏览器会拒绝此配置。当前默认 `CORS_ORIGINS="*"` 且 `allow_credentials=True` 直接违反规范。
|
||||
- **此外**: `*` 允许任意来源跨域访问,安全上过于宽松。
|
||||
- **修复**:
|
||||
```python
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=os.environ.get("CORS_ORIGINS", "http://localhost:3000").split(","),
|
||||
allow_credentials=False, # 默认关闭
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
#### [HIGH-2] `search_documents` 端点缺少异常处理
|
||||
|
||||
- **文件**: `src/server/app.py`,第 147-155 行
|
||||
- **根因**: `ingest_document` 有完整的 `try/except` 包裹,但 `search_documents` 完全没有。如果嵌入模型加载失败或 ChromaDB 查询异常,会返回 FastAPI 默认的原始 traceback,泄露内部路径和调用栈。
|
||||
- **修复**: 添加异常捕获:
|
||||
```python
|
||||
try:
|
||||
searcher = state.get_searcher(req.collection)
|
||||
results = searcher.search(req.query, top_k=req.top_k)
|
||||
return {"results": results, "collection": searcher.collection_name}
|
||||
except Exception:
|
||||
logger.exception("检索失败")
|
||||
raise HTTPException(status_code=500, detail="服务器内部错误")
|
||||
```
|
||||
|
||||
#### [HIGH-3] `delete_document` 的 `collection` 查询参数缺少输入校验
|
||||
|
||||
- **文件**: `src/server/app.py`,第 163 行
|
||||
- **根因**: `IngestRequest.collection` 和 `SearchRequest.collection` 都使用了 `pattern=r"^[a-zA-Z0-9_-]+$"` 校验,但 `delete_document` 的 `collection` 查询参数没有任何正则校验。ChromaDB collection 名称允许的字符集有限,传入非法字符可能导致未预期的 ChromaDB 内部错误。
|
||||
- **修复**: 添加 `Query(default=None, max_length=128, pattern=r"^[a-zA-Z0-9_-]+$")`。
|
||||
|
||||
#### [HIGH-4] CLI 与 API 的路径安全检查逻辑不一致
|
||||
|
||||
- **文件**: `src/cli/main.py` 第 85 行 vs `src/server/app.py` 第 125-130 行
|
||||
- **根因**: CLI 仅用 `is_safe_path()` 做基础检查(拒绝 `..` + 绝对路径),而 API 还额外做了 `os.path.commonpath` 绑定当前工作目录的检查。两处策略不同增加了安全漏洞风险。
|
||||
- **修复**: 提取统一的 `is_path_within_workspace()` 函数到 `security.py`,两处共用:
|
||||
```python
|
||||
def is_path_within_workspace(path_str: str) -> bool:
|
||||
"""检查路径是否在当前工作目录内(防路径穿越)。"""
|
||||
if not is_safe_path(path_str):
|
||||
return False
|
||||
path = Path(path_str).resolve()
|
||||
cwd = Path.cwd().resolve()
|
||||
try:
|
||||
return Path(os.path.commonpath([str(path), str(cwd)])) == cwd
|
||||
except ValueError:
|
||||
return False
|
||||
```
|
||||
|
||||
### 功能
|
||||
|
||||
#### [HIGH-5] chunk 配置项被硬编码覆盖 — YAML 中的 chunk 设置完全无效
|
||||
|
||||
- **文件**: `src/core/ingest.py`,第 46 行
|
||||
- **根因**: `get_splitter(file_path, max_size=1000, overlap=100)` 写死了值。用户无论怎么修改 `config.yaml` 里的 `chunk.max_size` 和 `chunk.overlap`,都完全不会生效。
|
||||
- **影响**: 所有通过 API/CLI 正常流程的入库都使用硬编码值。`AppConfig.chunk` 数据类从未在正常入库链路中被读取。
|
||||
- **修复**: `DocumentIngestor` 构造函数接受 `ChunkConfig` 参数,`deps.py` 的 `AppState.get_ingestor` 传入 `self.config.chunk`。
|
||||
|
||||
#### [HIGH-6] CLI `serve` 命令双重加载嵌入模型
|
||||
|
||||
- **文件**: `src/cli/main.py` 第 174 行 + `src/server/deps.py`
|
||||
- **根因**: `serve` 命令调用 `get_state().config`,触发 `AppState.__init__` → `create_embedder()` 加载模型(第一次)。uvicorn 子进程启动后,`src.server.app:app` 生命周期内再次调用 `get_state()` → 加载模型(第二次)。对于 `serve` 命令,只需要服务配置(host, port, SSL),不需要加载 ML 模型。
|
||||
- **影响**: 模型被加载两次,浪费几 GB 内存和数秒启动时间。
|
||||
- **修复**: 将配置加载与模型加载解耦——提供一个不初始化模型的轻量配置读取函数给 CLI `serve` 命令使用。
|
||||
|
||||
### 代码质量
|
||||
|
||||
#### [HIGH-7] PDF 文件句柄泄漏 — fitz.Document 在异常路径下未关闭
|
||||
|
||||
- **文件**: `src/core/splitters/pdf.py`,第 34-43 行
|
||||
- **根因**: `doc.close()` 在 `try` 块内,如果在 `for page in doc` 或 `page.get_text()` 期间抛出异常,`close()` 永远不会被调用。
|
||||
- **修复**: 使用 `with fitz.open(pdf_path) as doc:`(pymupdf 支持上下文管理器协议)。
|
||||
|
||||
#### [HIGH-8] embedder.py 中环境变量操作不是线程安全的
|
||||
|
||||
- **文件**: `src/core/embedder.py`,第 80-90 行
|
||||
- **根因**: `os.environ["HF_ENDPOINT"]` 的读写没有锁保护。如果多个线程同时创建 `LocalEmbedder` 实例,可能产生竞态——线程 A 设置的值被 B 覆盖,A 的 `finally` 恢复时读到 B 的值。
|
||||
- **修复**: 使用线程锁保护该区域,或通过 `huggingface_hub` 的 API 在进程启动时统一设置而非按实例设置。
|
||||
|
||||
#### [HIGH-9] `ingest_obsidian.py` 初始化阶段无异常处理
|
||||
|
||||
- **文件**: `scripts/ingest_obsidian.py`,第 32-35 行
|
||||
- **根因**: `load_config()` / `VectorDB()` / `create_embedder()` / `DocumentIngestor()` 任一失败都会直接崩溃,没有友好的错误信息或恢复路径。
|
||||
- **修复**: 添加顶层 `try/except` 包裹,捕获异常后打印 traceback 并以 `sys.exit(1)` 退出。
|
||||
|
||||
### 测试缺口
|
||||
|
||||
#### [HIGH-10] CLI 模块零测试覆盖
|
||||
|
||||
- **文件**: `src/cli/main.py`(216 行,5 个命令)
|
||||
- **影响**: 通配符展开、stdin 输入、JSON 格式化输出、路径安全检查等逻辑无自动化验证。
|
||||
- **修复**: 创建 `tests/test_cli.py`,使用 `typer.testing.CliRunner` 覆盖每个命令。
|
||||
|
||||
#### [HIGH-11] 认证与速率限制零单元测试
|
||||
|
||||
- **文件**: `src/server/auth.py`(58 行)
|
||||
- **影响**: `verify_api_key()` 和 `RateLimiter` 两个安全关键组件完全没有独立测试。所有 API 测试因未设置 `MD_VECTOR_API_KEY` 而跳过认证验证。
|
||||
- **修复**: 创建 `tests/test_auth.py`,覆盖密钥匹配/不匹配/未提供、速率限制正常/超限/过期恢复、并发线程安全。
|
||||
|
||||
#### [HIGH-12] `ingest_file` / `ingest_directory` 零测试
|
||||
|
||||
- **文件**: `src/core/ingest.py`(121 行)
|
||||
- **影响**: 包含扩展名检测、路径哈希、PDF/EPUB 二进制特殊处理、目录递归扫描和过滤逻辑,全部未测试。
|
||||
- **修复**: 在 `tests/test_ingest.py` 中补充文件入库和多格式目录入库的集成测试。
|
||||
|
||||
#### [HIGH-13] `Searcher` 三个方法零测试
|
||||
|
||||
- **文件**: `src/core/search.py`(97 行)
|
||||
- **影响**: `list_sources()`、`delete_by_source()`、`get_collection_info()` 以及 `search(source_file=...)` 过滤均无测试。
|
||||
- **修复**: 在 `tests/test_search.py` 中补充。
|
||||
|
||||
---
|
||||
|
||||
## 四、MEDIUM 级别(11 个)
|
||||
|
||||
### [MED-1] `content` 模式默认 file_name 为 `"untitled.md"` 导致数据竞态覆盖
|
||||
|
||||
- **文件**: `src/server/app.py`,第 137 行
|
||||
- **根因**: 多个并发请求不提供 `file_name` 时都写入 `"untitled.md"`。`ingest_content` 先 `_remove_by_source(file_name)` 再 `_add_chunks()`,且两次操作之间锁释放,存在竞态——后完成的请求覆盖先完成的请求数据。
|
||||
- **修复**: 自动生成 UUID 唯一名称:`file_name = req.file_name or f"untitled_{uuid.uuid4().hex[:8]}.md"`
|
||||
|
||||
### [MED-2] `X-XSS-Protection` 头已过时且可能引入安全问题
|
||||
|
||||
- **文件**: `src/server/app.py`,第 81 行
|
||||
- **根因**: 此响应头已被所有现代浏览器废弃。旧版 IE 的 XSS Auditor 自身存在安全漏洞。OWASP 等安全组织明确建议不要设置此头。
|
||||
- **修复**: 替换为 `Content-Security-Policy: default-src 'self'`
|
||||
|
||||
### [MED-3] `EXPECTED_API_KEY` 在模块导入时求值,存在加载顺序隐患
|
||||
|
||||
- **文件**: `src/server/auth.py`,第 14 行
|
||||
- **根因**: 该常量在模块被 import 时求值。虽然目前 `config.py` 先调用 `load_dotenv()` 再触发导入链,但这是一个隐式依赖——如果有人调整导入顺序或在测试中直接 import `auth.py`,`EXPECTED_API_KEY` 可能为空。
|
||||
- **修复**: 改为惰性求值函数:`def _get_api_key() -> str: return os.environ.get("MD_VECTOR_API_KEY", "")`
|
||||
|
||||
### [MED-4] ingest.py 与 search.py 存在去重删除逻辑重复
|
||||
|
||||
- **文件**: `ingest.py:108-120` 和 `search.py:83-97`
|
||||
- **根因**: 两处 `_remove_by_source` / `delete_by_source` 逻辑几乎一模一样:获取匹配文档、删除 IDs、捕获 `ValueError`。
|
||||
- **修复**: 统一放到 `VectorDB.delete_by_metadata(where)` 方法中。
|
||||
|
||||
### [MED-5] `list_collections_with_stats` 静默吞掉所有异常
|
||||
|
||||
- **文件**: `src/server/deps.py`,第 54-59 行
|
||||
- **根因**: 当 ChromaDB 不可用时,返回空列表而不是错误。调用方向用户报告"没有任何集合",而非"数据库连接失败"。错误语义被扭曲。
|
||||
- **修复**: 让异常向上传播,由 API 层统一处理并返回适当的 HTTP 错误。
|
||||
|
||||
### [MED-6] EPUB 缺失依赖测试设计有误
|
||||
|
||||
- **文件**: `tests/test_splitters_epub.py`,第 51-56 行
|
||||
- **根因**: `test_epub_missing_dependency_message` 声称测试"未安装 ebooklib 时应给出明确提示",但模块顶部有 `pytest.importorskip("ebooklib", ...)`,意味着当 ebooklib 未安装时整个文件被 skip,该测试永远不会在目标场景下执行。
|
||||
- **修复**: 将此测试移到独立文件中(不使用 `importorskip`),或使用 `monkeypatch` 模拟 `ImportError`。
|
||||
|
||||
### [MED-7] 环境变量测试使用 `importlib.reload` 过于脆弱
|
||||
|
||||
- **文件**: `tests/test_config.py`,第 89-106 行
|
||||
- **根因**: `importlib.reload(config)` 有全局副作用,可能影响后续测试。`finally` 块中的清理逻辑也不完整。
|
||||
- **修复**: 使用 `monkeypatch.setenv` + 重新实例化代替 `reload`。
|
||||
|
||||
### [MED-8] 速率限制器的配置值硬编码
|
||||
|
||||
- **文件**: `src/server/auth.py`,第 58 行
|
||||
- **根因**: `RateLimiter(max_requests=30, window_seconds=60)` 无法通过环境变量或配置文件调整。
|
||||
- **修复**: 从环境变量读取:`max_requests=int(os.environ.get("RATE_LIMIT_MAX", "30"))`
|
||||
|
||||
### [MED-9] `ingest_obsidian.py` 存在硬编码的绝对回退路径
|
||||
|
||||
- **文件**: `scripts/ingest_obsidian.py`,第 47-51 行
|
||||
- **根因**: `targets` 中的三个路径仅在本机有效,在其他开发者的机器上毫无意义。如果恰好存在同名目录,会意外入库其他数据。
|
||||
- **修复**: 移除硬编码回退路径,直接报错提示用户提供参数。
|
||||
|
||||
### [MED-10] MarkdownSplitter 测试组织不清晰
|
||||
|
||||
- **文件**: 分散在 `tests/test_ingest.py` 中
|
||||
- **根因**: Markdown 分块测试与入仓器测试混在一起,缺少 Markdown 特有的边界测试:无标题文档、深层标题、代码块内含 `#` 号被误识别为标题等。
|
||||
- **修复**: 提取到 `tests/test_splitters_markdown.py`,补充边界测试。
|
||||
|
||||
### [MED-11] `splitters/__init__.py` 未导出 HTMLSplitter
|
||||
|
||||
- **文件**: `src/core/splitters/__init__.py`,第 10-20 行
|
||||
- **根因**: `HTMLSplitter` 在第 7 行被 import 但未加入 `__all__` 列表,而 `PDFSplitter` 和 `EPUBSplitter` 都在列表中。
|
||||
- **修复**: 在 `__all__` 中加入 `"HTMLSplitter"`。
|
||||
|
||||
---
|
||||
|
||||
## 五、LOW 级别(10 个)
|
||||
|
||||
| # | 文件 | 问题 | 建议 |
|
||||
|---|------|------|------|
|
||||
| L-1 | `src/core/splitters/epub.py:44` | EPUB 硬编码 UTF-8 解码,非 UTF-8 文件会丢失章节 | 添加编码检测回退(先 UTF-8,失败后 chardet) |
|
||||
| L-2 | `src/core/embedder.py:24` | 模块级 `import requests` 依赖传递性依赖项 | 改为惰性导入(在 DashscopeEmbedder.embed 内部) |
|
||||
| L-3 | `src/core/embedder.py:176` | `list.sort()` 原地修改 API 响应 | 改为 `sorted(embeddings_raw, key=...)` |
|
||||
| L-4 | `src/core/config.py:16` | `DEFAULT_CONFIG_PATH` 使用相对路径 | 统一使用 `load_config` 的路径解析逻辑 |
|
||||
| L-5 | `src/core/splitters/registry.py:20` | `_custom_registry` 模块级字典无线程锁保护 | 添加 `threading.Lock` 或文档说明仅限启动时调用 |
|
||||
| L-6 | `pyproject.toml:21` | `dev` 依赖缺少 `pytest-cov` 和 `ruff`/`mypy` | 添加 `pytest-cov>=5.0`、`ruff>=0.8.0`、`mypy>=1.13` 及相关 tool 配置节 |
|
||||
| L-7 | `config.yaml` | `host: 0.0.0.0` 与代码默认值 `127.0.0.1` 不一致 | 统一默认值或在配置文件中添加注释说明差异 |
|
||||
| L-8 | `config.yaml` | SSL 字段(ssl_keyfile/ssl_certfile)未在配置文件中暴露 | 添加注释说明可用配置 |
|
||||
| L-9 | `scripts/serve.py` | `DeprecationWarning` 被 Python 默认过滤,多数用户看不到 | 改为 `print(..., file=sys.stderr)` 或 `FutureWarning` |
|
||||
| L-10 | `CLAUDE.md` / `README.md` | 文档中测试数量写"46 个",实际 90 个;数据流未反映多格式支持 | 更新为实际数量,数据流加入 `get_splitter()` 描述 |
|
||||
|
||||
---
|
||||
|
||||
## 六、架构与设计评估
|
||||
|
||||
### 优点
|
||||
|
||||
1. **策略模式设计良好** — `Embedder(Protocol)` 和 `Splitter(Protocol)` 接口清晰,新增 provider/格式只需实现接口并注册
|
||||
2. **扩展名自动路由** — `registry.py` 的 `get_splitter()` 工厂 + 回退到 TextSplitter 的设计优雅
|
||||
3. **线程安全基础扎实** — `VectorDB._write_lock` + `deps.py` 的 `_state_lock`/`_cache_lock` 构成多层保护
|
||||
4. **安全防御分层** — API Key 认证 → 速率限制 → 路径穿越防护,防御纵深合理
|
||||
5. **配置热加载友好** — `load_config()` 支持文件变更检测,适合长期运行的服务
|
||||
|
||||
### 需要改进的架构问题
|
||||
|
||||
1. **配置与模型加载未解耦** — `AppState.__init__` 做了太多事(配置+模型+数据库),导致 CLI `serve` 双重加载。建议拆分为 `load_config_only()` 和 `init_full_state()`
|
||||
2. **chunk 配置未贯传整个链路** — `config.yaml → AppConfig.chunk` 存在但被 `ingest.py` 硬编码覆盖。应该在 `DocumentIngestor` 构造函数中接受 `ChunkConfig`
|
||||
3. **路径安全检查逻辑分散** — CLI 和 API 各有一套检查,应该提取为统一的 `is_path_within_workspace()`
|
||||
4. **去重删除逻辑重复** — `ingest.py` 和 `search.py` 中几乎相同的代码应合并到 `VectorDB`
|
||||
|
||||
---
|
||||
|
||||
## 七、测试策略建议
|
||||
|
||||
### 当前测试分布
|
||||
|
||||
```
|
||||
tests/
|
||||
├── test_api.py # 14 个 — API 端点基本功能
|
||||
├── test_config.py # 7 个 — 配置加载
|
||||
├── test_db.py # 4 个 — 数据库 CRUD
|
||||
├── test_deps.py # 11 个 — AppState 生命周期
|
||||
├── test_embedder.py # 8 个 — 嵌入 provider
|
||||
├── test_ingest.py # 11 个 — 入仓(偏 MarkdownSplitter)
|
||||
├── test_search.py # 8 个 — 基本搜索
|
||||
├── test_security.py # 11 个 — 路径安全
|
||||
├── test_splitters.py # 10 个 — TextSplitter + 注册表
|
||||
├── test_splitters_html.py # 4 个
|
||||
├── test_splitters_pdf.py # 3 个
|
||||
├── test_splitters_epub.py # 3 个
|
||||
└── (缺失)
|
||||
├── test_auth.py ❌ 认证与速率限制
|
||||
├── test_cli.py ❌ CLI 命令
|
||||
└── test_splitters_markdown.py ❌ Markdown 边界
|
||||
```
|
||||
|
||||
### 建议新增测试
|
||||
|
||||
1. **`tests/test_cli.py`** — CLI 5 个命令(使用 `CliRunner`)
|
||||
2. **`tests/test_auth.py`** — `verify_api_key` + `RateLimiter`
|
||||
3. **`tests/test_splitters_markdown.py`** — 标题边界、代码块内 `#` 号、空文档
|
||||
4. **补充 `tests/test_ingest.py`** — `ingest_file` / `ingest_directory`
|
||||
5. **补充 `tests/test_search.py`** — `list_sources` / `delete_by_source` / `get_collection_info`
|
||||
|
||||
---
|
||||
|
||||
## 八、修复优先级路线图
|
||||
|
||||
### 第一阶段(立即修复 — 1 个 CRITICAL)
|
||||
|
||||
| 问题 | 文件 | 工作量 |
|
||||
|------|------|--------|
|
||||
| CRIT-1: 顶层 .md 文件重复入库 | `scripts/ingest_obsidian.py` | 5 分钟 |
|
||||
|
||||
### 第二阶段(尽快修复 — 13 个 HIGH)
|
||||
|
||||
| 问题 | 领域 | 工作量 |
|
||||
|------|------|--------|
|
||||
| HIGH-5: chunk 配置被子覆盖 | 功能 | 30 分钟 |
|
||||
| HIGH-7: PDF 文件句柄泄漏 | 资源 | 5 分钟 |
|
||||
| HIGH-1: CORS 配置冲突 | 安全 | 5 分钟 |
|
||||
| HIGH-2: search 端点无异常处理 | 安全 | 5 分钟 |
|
||||
| HIGH-3: delete collection 缺少校验 | 安全 | 5 分钟 |
|
||||
| HIGH-4: 路径安全检查不一致 | 安全 | 20 分钟 |
|
||||
| HIGH-6: serve 双重加载模型 | 性能 | 30 分钟 |
|
||||
| HIGH-8: HF_ENDPOINT 非线程安全 | 稳定性 | 15 分钟 |
|
||||
| HIGH-9: ingest_obsidian 无异常处理 | 稳定性 | 10 分钟 |
|
||||
| HIGH-10: CLI 零测试 | 测试 | 1-2 小时 |
|
||||
| HIGH-11: auth 零测试 | 测试 | 1 小时 |
|
||||
| HIGH-12: ingest_file/dir 零测试 | 测试 | 1 小时 |
|
||||
| HIGH-13: Searcher 方法零测试 | 测试 | 30 分钟 |
|
||||
|
||||
### 第三阶段(计划修复 — 11 个 MEDIUM)
|
||||
|
||||
涉及竞态覆盖、过时安全头、重复代码、错误语义、测试设计等。
|
||||
|
||||
### 第四阶段(择机修复 — 10 个 LOW)
|
||||
|
||||
涉及文档过时、编码回退、线程锁、工具链配置等。
|
||||
|
||||
---
|
||||
|
||||
## 九、合规性检查
|
||||
|
||||
对照用户全局安全规则(`~/.claude/rules/ecc/zh/security.md`)进行逐项检查:
|
||||
|
||||
| 检查项 | 状态 | 说明 |
|
||||
|--------|------|------|
|
||||
| 无硬编码密钥 | ✅ 通过 | API Key 均从环境变量读取 |
|
||||
| 所有用户输入已验证 | ⚠️ 部分 | `delete_document` 的 collection 参数缺少正则校验 |
|
||||
| SQL 注入防护 | N/A | 无 SQL 数据库 |
|
||||
| XSS 防护 | ⚠️ 部分 | `X-XSS-Protection` 已过时,应换为 CSP |
|
||||
| CSRF 保护 | ⚠️ 部分 | CORS 配置存在规范冲突 |
|
||||
| 认证/授权已验证 | ✅ 通过 | API Key 认证 + hmac.compare_digest |
|
||||
| 所有端点启用速率限制 | ✅ 通过 | RateLimiter 中间件 |
|
||||
| 错误消息不泄露敏感数据 | ⚠️ 部分 | `search_documents` 缺少异常处理会泄露 traceback |
|
||||
|
||||
---
|
||||
|
||||
*审计人: Claude Code(多代理协作审查)*
|
||||
*下次审计建议: 在修复 CRITICAL 和 HIGH 问题后进行复验*
|
||||
+16
-4
@@ -18,10 +18,11 @@ dependencies = [
|
||||
md-vector-db = "src.cli.main:app"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=8.0",
|
||||
"httpx>=0.27.0",
|
||||
]
|
||||
dev = ["pytest>=8.0", "httpx>=0.27.0", "pytest-cov>=5.0", "ruff>=0.8.0", "mypy>=1.13"]
|
||||
pdf = ["pymupdf>=1.24.0"]
|
||||
html = ["beautifulsoup4>=4.12.0"]
|
||||
epub = ["ebooklib>=0.18"]
|
||||
all = ["md-vector-db[pdf,html,epub]", "requests>=2.31.0", "openai>=1.0.0"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
@@ -34,6 +35,17 @@ packages = ["src/"]
|
||||
find-links = ["D:/settings/Language/Python/库"]
|
||||
index-strategy = "unsafe-best-match"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py313"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "N", "W"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.13"
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["src"]
|
||||
|
||||
@@ -29,10 +29,16 @@ def log(msg):
|
||||
|
||||
t0 = time.time()
|
||||
log("初始化...")
|
||||
try:
|
||||
cfg = load_config()
|
||||
db = VectorDB(persist_dir=cfg.chroma.persist_dir)
|
||||
embedder = create_embedder(cfg.embed)
|
||||
ingestor = DocumentIngestor(db, embedder, "obsidian_blog")
|
||||
except Exception as e:
|
||||
log(f"初始化失败: {e}")
|
||||
import traceback
|
||||
log(traceback.format_exc())
|
||||
sys.exit(1)
|
||||
|
||||
# 从 CLI 参数或环境变量获取目录列表
|
||||
if len(sys.argv) > 1:
|
||||
@@ -43,12 +49,9 @@ else:
|
||||
dirs = [d.strip() for d in env_dirs.split(",") if d.strip()]
|
||||
targets = [(Path(d).name, d) for d in dirs]
|
||||
else:
|
||||
# 回退默认路径 (仅在本机可用)
|
||||
targets = [
|
||||
("博客", "D:/Code/Obsidian/博客"),
|
||||
("Club", "D:/Code/Obsidian/Club-Service-Guide"),
|
||||
("halo", "D:/Code/Obsidian/obsidian-halo"),
|
||||
]
|
||||
log("错误: 未指定目标目录。请通过命令行参数或 OBSIDIAN_DIRS 环境变量提供。")
|
||||
log("用法: uv run python scripts/ingest_obsidian.py <目录1> [目录2] ...")
|
||||
sys.exit(1)
|
||||
files = []
|
||||
skipped = []
|
||||
for label, d in targets:
|
||||
@@ -63,18 +66,6 @@ for label, d in targets:
|
||||
skipped.append((f.name, size))
|
||||
continue
|
||||
files.append((label, str(f)))
|
||||
# 顶层 .md 文件
|
||||
for target_info in targets:
|
||||
d = target_info[1]
|
||||
p = Path(d)
|
||||
if not p.exists():
|
||||
continue
|
||||
for f in p.glob("*.md"):
|
||||
sz = f.stat().st_size
|
||||
if sz > MAX_SIZE:
|
||||
skipped.append((f.name, sz))
|
||||
else:
|
||||
files.append(("顶层/" + p.name, str(f)))
|
||||
|
||||
log(f"待处理: {len(files)} 个文件")
|
||||
if skipped:
|
||||
|
||||
+4
-5
@@ -1,11 +1,10 @@
|
||||
"""便捷启动脚本 — 已废弃, 请使用 `uv run md-vector-db serve`."""
|
||||
import sys
|
||||
import uvicorn
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"scripts/serve.py 已废弃, 请使用 `uv run md-vector-db serve`",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
print(
|
||||
"[废弃] scripts/serve.py 已废弃, 请使用 `uv run md-vector-db serve`",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+8
-6
@@ -17,7 +17,7 @@ if sys.stdout.encoding != "utf-8":
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from src.core.config import DEFAULT_CONFIG_PATH
|
||||
from src.core.security import is_safe_path
|
||||
from src.core.security import is_path_within_workspace
|
||||
from src.server.deps import get_state, get_default_collection
|
||||
|
||||
app = typer.Typer(
|
||||
@@ -82,7 +82,7 @@ def ingest(
|
||||
if file_paths:
|
||||
total = 0
|
||||
for fp in file_paths:
|
||||
if not is_safe_path(fp):
|
||||
if not is_path_within_workspace(fp):
|
||||
typer.echo(f"[SKIP] 不安全的路径: {fp}", err=True)
|
||||
continue
|
||||
# 支持通配符 (shell 展开或 Python glob)
|
||||
@@ -90,7 +90,7 @@ def ingest(
|
||||
if "*" in fp or "?" in fp:
|
||||
matches = _glob.glob(fp, recursive=True)
|
||||
for m in matches:
|
||||
if not is_safe_path(m):
|
||||
if not is_path_within_workspace(m):
|
||||
typer.echo(f"[SKIP] 不安全的路径: {m}", err=True)
|
||||
continue
|
||||
c = ingestor.ingest_file(m)
|
||||
@@ -117,7 +117,7 @@ def ingest_dir(
|
||||
collection: CollectionOpt = None,
|
||||
):
|
||||
_init_config(config)
|
||||
if not is_safe_path(dir_path):
|
||||
if not is_path_within_workspace(dir_path):
|
||||
typer.echo(f"错误: 不安全的路径 — {dir_path}", err=True)
|
||||
raise typer.Exit(code=1)
|
||||
state = get_state()
|
||||
@@ -168,10 +168,12 @@ def serve(
|
||||
):
|
||||
# 传递 config 给 uvicorn 子进程 (通过环境变量)
|
||||
os.environ["MD_VECTOR_CONFIG"] = config
|
||||
# 使用 load_config 而非 get_state(),避免在 CLI 主进程加载嵌入模型
|
||||
from src.core.config import load_config
|
||||
|
||||
cfg = load_config(config)
|
||||
typer.echo(f"启动服务: http://localhost:{port}")
|
||||
typer.echo(f"API 文档: http://localhost:{port}/docs")
|
||||
# 加载配置以获取 SSL 设置
|
||||
cfg = get_state().config
|
||||
ssl_kwargs = {}
|
||||
if cfg.server.ssl_keyfile and cfg.server.ssl_certfile:
|
||||
ssl_kwargs["ssl_keyfile"] = cfg.server.ssl_keyfile
|
||||
|
||||
@@ -46,6 +46,25 @@ class VectorDB:
|
||||
except ValueError:
|
||||
pass # collection 不存在则忽略
|
||||
|
||||
def delete_by_source(self, collection_name: str, file_name: str) -> bool:
|
||||
"""按 source_file 删除文档 (线程安全)."""
|
||||
import logging
|
||||
_logger = logging.getLogger("md-vector-db")
|
||||
collection = self.get_or_create_collection(collection_name)
|
||||
try:
|
||||
with self._write_lock:
|
||||
existing = collection.get(
|
||||
where={"source_file": file_name}
|
||||
)
|
||||
if existing and existing["ids"]:
|
||||
collection.delete(ids=existing["ids"])
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
except Exception:
|
||||
_logger.exception("删除文档失败: %s (collection=%s)", file_name, collection_name)
|
||||
return False
|
||||
|
||||
def close(self) -> None:
|
||||
"""释放数据库连接."""
|
||||
self.client.close()
|
||||
|
||||
@@ -18,17 +18,18 @@
|
||||
vectors = batch_embed(embedder, long_text_list)
|
||||
"""
|
||||
import os
|
||||
import threading
|
||||
import logging
|
||||
from typing import Protocol
|
||||
|
||||
import requests # noqa: F401 — DashscopeEmbedder 使用
|
||||
|
||||
from src.core.config import EmbedConfig
|
||||
|
||||
logger = logging.getLogger("md-vector-db")
|
||||
|
||||
_HF_MIRROR = os.environ.get("HF_MIRROR", "https://hf-mirror.com")
|
||||
|
||||
_HF_ENV_LOCK = threading.Lock()
|
||||
|
||||
# -- Provider 默认配置 --
|
||||
_PROVIDER_DEFAULTS: dict[str, dict[str, str | int]] = {
|
||||
"openai": {
|
||||
@@ -78,12 +79,14 @@ class LocalEmbedder:
|
||||
# 通过 HF_ENDPOINT 环境变量设置镜像(sentence-transformers 依赖 huggingface_hub)
|
||||
# 临时设置仅用于模型下载,下载完成后还原
|
||||
old_endpoint = os.environ.get("HF_ENDPOINT")
|
||||
with _HF_ENV_LOCK:
|
||||
os.environ["HF_ENDPOINT"] = _HF_MIRROR
|
||||
try:
|
||||
self._model = SentenceTransformer(
|
||||
config.local_model, device=device
|
||||
)
|
||||
finally:
|
||||
with _HF_ENV_LOCK:
|
||||
if old_endpoint is not None:
|
||||
os.environ["HF_ENDPOINT"] = old_endpoint
|
||||
else:
|
||||
@@ -151,6 +154,7 @@ class DashscopeEmbedder(_BaseAPIEmbedder):
|
||||
def embed(self, texts: list[str]) -> list[list[float]]:
|
||||
if not texts:
|
||||
raise ValueError("文本列表不能为空")
|
||||
import requests # 惰性导入(仅 DashScope 使用)
|
||||
resp = requests.post(
|
||||
self._api_base,
|
||||
headers={
|
||||
@@ -173,8 +177,8 @@ class DashscopeEmbedder(_BaseAPIEmbedder):
|
||||
if not isinstance(embeddings_raw, list):
|
||||
raise ValueError(f"DashScope embeddings 不是列表: {type(embeddings_raw)}")
|
||||
# 按 text_index 排序确保顺序
|
||||
embeddings_raw.sort(key=lambda x: x.get("text_index", 0))
|
||||
return [e["embedding"] for e in embeddings_raw]
|
||||
embeddings_raw_sorted = sorted(embeddings_raw, key=lambda x: x.get("text_index", 0))
|
||||
return [e["embedding"] for e in embeddings_raw_sorted]
|
||||
|
||||
|
||||
# -- 工厂函数 --
|
||||
|
||||
+42
-194
@@ -1,179 +1,18 @@
|
||||
"""Markdown 文档解析与入库模块."""
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Protocol
|
||||
|
||||
from src.core.config import ChunkConfig
|
||||
from src.core.db import VectorDB
|
||||
from src.core.embedder import Embedder, batch_embed
|
||||
from src.core.splitters.markdown import MarkdownSplitter # 兼容旧 import 路径
|
||||
from src.core.splitters.base import Splitter # 兼容旧 import 路径
|
||||
from src.core.splitters.registry import SUPPORTED_SUFFIXES, get_splitter
|
||||
|
||||
logger = logging.getLogger("md-vector-db")
|
||||
|
||||
|
||||
class Splitter(Protocol):
|
||||
"""文档分块器接口 — 将文本拆分为带元数据的 chunk 列表.
|
||||
|
||||
每个 chunk 为 dict: {"content": str, "section_title": str, "heading_level": int, ...}
|
||||
"""
|
||||
|
||||
def split(self, text: str, source_file: str = "") -> list[dict]: ...
|
||||
|
||||
|
||||
class MarkdownSplitter:
|
||||
"""Markdown 混合分块器:先按标题拆,超长再按段落拆."""
|
||||
|
||||
def __init__(self, max_size: int = 1000, overlap: int = 100):
|
||||
self.max_size = max_size
|
||||
self.overlap = overlap
|
||||
|
||||
def split(self, text: str, source_file: str = "") -> list[dict]:
|
||||
"""将 Markdown 文本拆分为带元数据的 chunk 列表."""
|
||||
if not text.strip():
|
||||
return []
|
||||
|
||||
sections = self._split_by_headings(text)
|
||||
chunks = []
|
||||
|
||||
for section in sections:
|
||||
if len(section["content"]) <= self.max_size:
|
||||
chunks.append(section)
|
||||
else:
|
||||
sub_chunks = self._split_by_paragraphs(
|
||||
section["content"],
|
||||
section["section_title"],
|
||||
section["heading_level"],
|
||||
)
|
||||
chunks.extend(sub_chunks)
|
||||
|
||||
# 为所有 chunk 补充 source_file 和 chunk_index
|
||||
for i, chunk in enumerate(chunks):
|
||||
chunk["source_file"] = source_file or chunk.get("source_file", "")
|
||||
chunk["chunk_index"] = i
|
||||
|
||||
return chunks
|
||||
|
||||
def _split_by_headings(self, text: str) -> list[dict]:
|
||||
"""按 Markdown 标题拆分."""
|
||||
heading_pattern = re.compile(r"^(#{1,6})\s+(.+)$", re.MULTILINE)
|
||||
matches = list(heading_pattern.finditer(text))
|
||||
|
||||
if not matches:
|
||||
return [{
|
||||
"content": text.strip(),
|
||||
"section_title": "",
|
||||
"heading_level": 0,
|
||||
}]
|
||||
|
||||
sections = []
|
||||
for i, match in enumerate(matches):
|
||||
level = len(match.group(1))
|
||||
title = match.group(2).strip()
|
||||
start = match.end()
|
||||
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
|
||||
content = text[start:end].strip()
|
||||
|
||||
if content:
|
||||
sections.append({
|
||||
"content": f"{match.group(0)}\n{content}",
|
||||
"section_title": title,
|
||||
"heading_level": level,
|
||||
})
|
||||
|
||||
# 处理第一个标题之前的内容
|
||||
if matches and matches[0].start() > 0:
|
||||
preamble = text[:matches[0].start()].strip()
|
||||
if preamble:
|
||||
sections.insert(0, {
|
||||
"content": preamble,
|
||||
"section_title": "",
|
||||
"heading_level": 0,
|
||||
})
|
||||
|
||||
return sections
|
||||
|
||||
def _split_by_paragraphs(
|
||||
self, text: str, section_title: str, heading_level: int
|
||||
) -> list[dict]:
|
||||
"""按段落边界拆分超长章节.
|
||||
|
||||
优先在段落边界拆分,若单个段落仍超长则按字符硬切。
|
||||
"""
|
||||
paragraphs = re.split(r"\n\n+", text)
|
||||
chunks = []
|
||||
current = ""
|
||||
|
||||
for para in paragraphs:
|
||||
# 单一段落超出 max_size 时直接硬切
|
||||
if len(para) > self.max_size:
|
||||
# 先 flush 当前累积
|
||||
if current.strip():
|
||||
chunks.append({
|
||||
"content": current.strip(),
|
||||
"section_title": section_title,
|
||||
"heading_level": heading_level,
|
||||
})
|
||||
current = ""
|
||||
# 硬切该段落
|
||||
for sub in self._split_single_paragraph(para):
|
||||
chunks.append({
|
||||
"content": sub,
|
||||
"section_title": section_title,
|
||||
"heading_level": heading_level,
|
||||
})
|
||||
continue
|
||||
|
||||
if len(current) + len(para) > self.max_size and current:
|
||||
chunks.append({
|
||||
"content": current.strip(),
|
||||
"section_title": section_title,
|
||||
"heading_level": heading_level,
|
||||
})
|
||||
# overlap: 保留上一块的末尾部分
|
||||
if self.overlap > 0 and len(current) > self.overlap:
|
||||
current = current[-self.overlap:] + "\n\n" + para
|
||||
else:
|
||||
current = para
|
||||
else:
|
||||
if current:
|
||||
current += "\n\n" + para
|
||||
else:
|
||||
current = para
|
||||
|
||||
if current.strip():
|
||||
chunks.append({
|
||||
"content": current.strip(),
|
||||
"section_title": section_title,
|
||||
"heading_level": heading_level,
|
||||
})
|
||||
|
||||
return chunks
|
||||
|
||||
def _split_single_paragraph(self, text: str) -> list[str]:
|
||||
"""按字符边界拆分单个超长段落(带 overlap)。"""
|
||||
parts = []
|
||||
start = 0
|
||||
while start < len(text):
|
||||
end = start + self.max_size
|
||||
if end >= len(text):
|
||||
parts.append(text[start:].strip())
|
||||
break
|
||||
# 尝试在句号或空格处断开
|
||||
break_point = end
|
||||
for sep in ("。", "!", "?", "\n", ". ", " "):
|
||||
pos = text.rfind(sep, start, end)
|
||||
if pos > start:
|
||||
break_point = pos + len(sep)
|
||||
break
|
||||
part = text[start:break_point].strip()
|
||||
if part:
|
||||
parts.append(part)
|
||||
# 确保 start 始终前进(避免分隔符距 start 小于 overlap 时 start 回退导致死循环)
|
||||
next_start = break_point - self.overlap if self.overlap > 0 else break_point
|
||||
start = max(start + 1, next_start)
|
||||
return parts
|
||||
|
||||
|
||||
class DocumentIngestor:
|
||||
"""文档入库器: 读取 MD 文件 → 分块 → 嵌入 → 入库."""
|
||||
|
||||
@@ -183,46 +22,59 @@ class DocumentIngestor:
|
||||
embedder: Embedder,
|
||||
collection_name: str,
|
||||
splitter: Splitter | None = None,
|
||||
chunk_config: ChunkConfig | None = None,
|
||||
):
|
||||
self.db = db
|
||||
self.embedder = embedder
|
||||
self.collection_name = collection_name
|
||||
self.splitter = splitter or MarkdownSplitter()
|
||||
self.chunk_config = chunk_config or ChunkConfig()
|
||||
|
||||
@property
|
||||
def collection(self):
|
||||
return self.db.get_or_create_collection(self.collection_name)
|
||||
|
||||
def ingest_file(self, file_path: str) -> int:
|
||||
"""入库单个 Markdown 文件, 返回 chunk 数量.
|
||||
"""入库单个文件, 返回 chunk 数量.
|
||||
|
||||
使用文件路径的 SHA256 前 12 位 + 文件名作为唯一标识,
|
||||
避免不同目录下同名文件冲突.
|
||||
根据文件扩展名自动选择 Splitter(.md→MarkdownSplitter, .txt→TextSplitter, .pdf→PDFSplitter 等)。
|
||||
使用文件路径的 SHA256 前 12 位 + 文件名作为唯一标识。
|
||||
"""
|
||||
import hashlib
|
||||
path = Path(file_path).resolve()
|
||||
content = path.read_text(encoding="utf-8")
|
||||
# 用路径 hash 保证同名文件在不同目录下不冲突
|
||||
path_hash = hashlib.sha256(str(path).encode()).hexdigest()[:12]
|
||||
file_name = f"{path_hash}_{path.name}"
|
||||
|
||||
return self.ingest_content(content, file_name)
|
||||
splitter = self.splitter or get_splitter(
|
||||
file_path,
|
||||
max_size=self.chunk_config.max_size,
|
||||
overlap=self.chunk_config.overlap,
|
||||
)
|
||||
|
||||
def ingest_content(self, content: str, file_name: str) -> int:
|
||||
"""入库 Markdown 内容(无需实际文件)."""
|
||||
# 去重:先删旧 chunks
|
||||
# PDF/EPUB 二进制文件特殊处理:splitter 内部读取文件
|
||||
suffix = path.suffix.lower()
|
||||
if suffix in (".pdf", ".epub"):
|
||||
chunks = splitter.split(str(path), source_file=file_name)
|
||||
return self._add_chunks(chunks, file_name)
|
||||
|
||||
content = path.read_text(encoding="utf-8")
|
||||
return self._ingest_with_splitter(content, file_name, splitter)
|
||||
|
||||
def _ingest_with_splitter(self, content: str, file_name: str, splitter) -> int:
|
||||
"""分块 + 嵌入 + 入库(文本文件通用路径)."""
|
||||
self._remove_by_source(file_name)
|
||||
|
||||
# 分块
|
||||
chunks = self.splitter.split(content, source_file=file_name)
|
||||
chunks = splitter.split(content, source_file=file_name)
|
||||
return self._add_chunks(chunks, file_name)
|
||||
|
||||
def _add_chunks(self, chunks: list[dict], file_name: str) -> int:
|
||||
"""嵌入 + 写入 ChromaDB(分块已完成)."""
|
||||
if not chunks:
|
||||
return 0
|
||||
|
||||
# 分批嵌入 (避免大文档 OOM)
|
||||
texts = [c["content"] for c in chunks]
|
||||
embeddings = batch_embed(self.embedder, texts)
|
||||
|
||||
# 入库
|
||||
ids = [f"{file_name}_{i}" for i in range(len(chunks))]
|
||||
metadatas = [
|
||||
{
|
||||
@@ -244,24 +96,20 @@ class DocumentIngestor:
|
||||
|
||||
return len(chunks)
|
||||
|
||||
def ingest_content(self, content: str, file_name: str) -> int:
|
||||
"""入库 Markdown 内容(无需实际文件)。若未指定 splitter,默认用 MarkdownSplitter."""
|
||||
splitter = self.splitter or MarkdownSplitter()
|
||||
return self._ingest_with_splitter(content, file_name, splitter)
|
||||
|
||||
def ingest_directory(self, dir_path: str) -> dict[str, int]:
|
||||
"""入库目录下所有 Markdown 文件."""
|
||||
"""入库目录下所有支持的文档格式."""
|
||||
results = {}
|
||||
for md_file in Path(dir_path).rglob("*.md"):
|
||||
count = self.ingest_file(str(md_file))
|
||||
results[md_file.name] = count
|
||||
for f in Path(dir_path).rglob("*"):
|
||||
if f.suffix.lower() in SUPPORTED_SUFFIXES:
|
||||
count = self.ingest_file(str(f))
|
||||
results[f.name] = count
|
||||
return results
|
||||
|
||||
def _remove_by_source(self, file_name: str) -> None:
|
||||
"""按 source_file 删除已有 chunks."""
|
||||
try:
|
||||
with self.db.write_lock:
|
||||
existing = self.collection.get(
|
||||
where={"source_file": file_name}
|
||||
)
|
||||
if existing and existing["ids"]:
|
||||
self.collection.delete(ids=existing["ids"])
|
||||
except ValueError:
|
||||
pass # collection 为空时 ChromaDB 抛 ValueError
|
||||
except Exception:
|
||||
logger.exception("去重检查失败: %s", file_name)
|
||||
"""按 source_file 删除已有 chunks(委托 VectorDB)."""
|
||||
self.db.delete_by_source(self.collection_name, file_name)
|
||||
|
||||
+2
-14
@@ -81,17 +81,5 @@ class Searcher:
|
||||
return sorted(sources)
|
||||
|
||||
def delete_by_source(self, file_name: str) -> bool:
|
||||
"""按文件名删除文档 (线程安全)."""
|
||||
try:
|
||||
with self.db.write_lock:
|
||||
existing = self.collection.get(
|
||||
where={"source_file": file_name}
|
||||
)
|
||||
if existing and existing["ids"]:
|
||||
self.collection.delete(ids=existing["ids"])
|
||||
return True
|
||||
except ValueError:
|
||||
pass # collection 为空时 ChromaDB 抛 ValueError
|
||||
except Exception:
|
||||
logger.exception("删除文档失败: %s", file_name)
|
||||
return False
|
||||
"""按文件名删除文档 (委托 VectorDB)."""
|
||||
return self.db.delete_by_source(self.collection_name, file_name)
|
||||
|
||||
@@ -26,3 +26,23 @@ def is_safe_path(path_str: str) -> bool:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def is_path_within_workspace(path_str: str) -> bool:
|
||||
"""检查路径是否在当前工作目录内(防路径穿越 + 目录绑定).
|
||||
|
||||
同时检查:
|
||||
1. 路径不含 .. 穿越组件且非绝对路径
|
||||
2. resolve 后的路径位于当前工作目录内
|
||||
"""
|
||||
if not is_safe_path(path_str):
|
||||
return False
|
||||
|
||||
from pathlib import Path
|
||||
path = Path(path_str).resolve()
|
||||
cwd = Path.cwd().resolve()
|
||||
try:
|
||||
common = Path(os.path.commonpath([str(path), str(cwd)]))
|
||||
except ValueError:
|
||||
return False
|
||||
return common == cwd
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
"""文档分块器包 — 支持 Markdown / 纯文本 / PDF / HTML / EPUB."""
|
||||
|
||||
from src.core.splitters.base import Splitter, BaseTextSplitter
|
||||
from src.core.splitters.markdown import MarkdownSplitter
|
||||
from src.core.splitters.text import TextSplitter
|
||||
from src.core.splitters.pdf import PDFSplitter
|
||||
from src.core.splitters.html import HTMLSplitter
|
||||
from src.core.splitters.epub import EPUBSplitter
|
||||
from src.core.splitters.registry import get_splitter, register_splitter, SUPPORTED_SUFFIXES
|
||||
|
||||
__all__ = [
|
||||
"Splitter",
|
||||
"BaseTextSplitter",
|
||||
"MarkdownSplitter",
|
||||
"TextSplitter",
|
||||
"PDFSplitter",
|
||||
"HTMLSplitter",
|
||||
"EPUBSplitter",
|
||||
"get_splitter",
|
||||
"register_splitter",
|
||||
"SUPPORTED_SUFFIXES",
|
||||
]
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Splitter Protocol 和文本切分基类."""
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class Splitter(Protocol):
|
||||
"""文档分块器接口 — 将文本拆分为带元数据的 chunk 列表.
|
||||
|
||||
每个 chunk 为 dict: {"content": str, "section_title": str, "heading_level": int, ...}
|
||||
"""
|
||||
|
||||
def split(self, text: str, source_file: str = "") -> list[dict]: ...
|
||||
|
||||
|
||||
class BaseTextSplitter(ABC):
|
||||
"""文本切分基类 — 提供段落切分和硬切逻辑,子类实现 split()."""
|
||||
|
||||
def __init__(self, max_size: int = 1000, overlap: int = 100):
|
||||
self.max_size = max_size
|
||||
self.overlap = overlap
|
||||
|
||||
@abstractmethod
|
||||
def split(self, text: str, source_file: str = "") -> list[dict]: ...
|
||||
|
||||
def _split_by_paragraphs(
|
||||
self, text: str, section_title: str = "", heading_level: int = 0
|
||||
) -> list[dict]:
|
||||
"""按段落边界拆分超长文本,若单段仍超长则硬切."""
|
||||
paragraphs = re.split(r"\n\n+", text)
|
||||
chunks = []
|
||||
current = ""
|
||||
|
||||
for para in paragraphs:
|
||||
if len(para) > self.max_size:
|
||||
if current.strip():
|
||||
chunks.append(self._make_chunk(current, section_title, heading_level))
|
||||
current = ""
|
||||
for sub in self._split_single_paragraph(para):
|
||||
chunks.append(self._make_chunk(sub, section_title, heading_level))
|
||||
continue
|
||||
|
||||
if len(current) + len(para) > self.max_size and current:
|
||||
chunks.append(self._make_chunk(current, section_title, heading_level))
|
||||
if self.overlap > 0 and len(current) > self.overlap:
|
||||
current = current[-self.overlap:] + "\n\n" + para
|
||||
else:
|
||||
current = para
|
||||
else:
|
||||
current = f"{current}\n\n{para}" if current else para
|
||||
|
||||
if current.strip():
|
||||
chunks.append(self._make_chunk(current, section_title, heading_level))
|
||||
|
||||
return chunks
|
||||
|
||||
def _split_single_paragraph(self, text: str) -> list[str]:
|
||||
"""按字符边界硬切单个超长段落(带 overlap)."""
|
||||
parts = []
|
||||
start = 0
|
||||
while start < len(text):
|
||||
end = start + self.max_size
|
||||
if end >= len(text):
|
||||
parts.append(text[start:].strip())
|
||||
break
|
||||
break_point = end
|
||||
for sep in ("。", "!", "?", "\n", ". ", " "):
|
||||
pos = text.rfind(sep, start, end)
|
||||
if pos > start:
|
||||
break_point = pos + len(sep)
|
||||
break
|
||||
part = text[start:break_point].strip()
|
||||
if part:
|
||||
parts.append(part)
|
||||
next_start = break_point - self.overlap if self.overlap > 0 else break_point
|
||||
start = max(start + 1, next_start)
|
||||
return parts
|
||||
|
||||
@staticmethod
|
||||
def _make_chunk(content: str, section_title: str, heading_level: int) -> dict:
|
||||
return {
|
||||
"content": content.strip(),
|
||||
"section_title": section_title,
|
||||
"heading_level": heading_level,
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
"""EPUB 电子书分块器 — 使用 ebooklib 提取文字后委托 TextSplitter."""
|
||||
import logging
|
||||
from src.core.splitters.text import TextSplitter
|
||||
|
||||
logger = logging.getLogger("md-vector-db")
|
||||
|
||||
|
||||
class EPUBSplitter:
|
||||
"""EPUB 分块器:ebooklib 提取各章节文字 → TextSplitter 分块.
|
||||
|
||||
实现 Splitter Protocol,内部组合 TextSplitter 实例。
|
||||
split() 的 text 参数实际接收 EPUB 文件路径(非文本内容)。
|
||||
"""
|
||||
|
||||
def __init__(self, max_size: int = 1000, overlap: int = 100):
|
||||
self._text_splitter = TextSplitter(max_size=max_size, overlap=overlap)
|
||||
|
||||
def split(self, text: str, source_file: str = "") -> list[dict]:
|
||||
"""从 EPUB 文件提取各章节文字并分块.
|
||||
|
||||
Args:
|
||||
text: EPUB 文件路径(非文本内容,由 ingest_file 传入)
|
||||
source_file: 来源文件名
|
||||
"""
|
||||
try:
|
||||
import ebooklib
|
||||
from ebooklib import epub
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"EPUB 支持需要 ebooklib 库. 请执行: uv sync --extra epub"
|
||||
)
|
||||
|
||||
epub_path = text
|
||||
try:
|
||||
book = epub.read_epub(epub_path)
|
||||
except Exception as e:
|
||||
logger.error("EPUB 解析失败: %s — %s", epub_path, e)
|
||||
raise ValueError(f"EPUB 解析失败: {e}") from e
|
||||
|
||||
extracted_chapters = []
|
||||
for item in book.get_items_of_type(ebooklib.ITEM_DOCUMENT):
|
||||
try:
|
||||
content = item.get_content().decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
try:
|
||||
content = item.get_content().decode("utf-8-sig")
|
||||
except UnicodeDecodeError:
|
||||
try:
|
||||
content = item.get_content().decode("latin-1")
|
||||
except UnicodeDecodeError:
|
||||
logger.warning("EPUB 跳过一个无法解码的章节: %s", item.get_name())
|
||||
continue
|
||||
|
||||
# 用 BeautifulSoup 去标签(如果可用),否则保留原样
|
||||
try:
|
||||
from bs4 import BeautifulSoup
|
||||
soup = BeautifulSoup(content, "html.parser")
|
||||
for tag in soup(["script", "style"]):
|
||||
tag.decompose()
|
||||
text_content = soup.get_text(separator="\n")
|
||||
except ImportError:
|
||||
# 无 bs4 时手动去除简单标签
|
||||
import re
|
||||
text_content = re.sub(r"<[^>]+>", "", content)
|
||||
|
||||
if text_content.strip():
|
||||
extracted_chapters.append(text_content)
|
||||
|
||||
if not extracted_chapters:
|
||||
return []
|
||||
|
||||
full_text = "\n\n".join(extracted_chapters)
|
||||
return self._text_splitter.split(full_text, source_file=source_file)
|
||||
@@ -0,0 +1,44 @@
|
||||
"""HTML 文档分块器 — 使用 BeautifulSoup 去标签后委托 TextSplitter."""
|
||||
import logging
|
||||
from src.core.splitters.text import TextSplitter
|
||||
|
||||
logger = logging.getLogger("md-vector-db")
|
||||
|
||||
|
||||
class HTMLSplitter:
|
||||
"""HTML 分块器:bs4 去标签提取文字 → TextSplitter 分块.
|
||||
|
||||
实现 Splitter Protocol,内部组合 TextSplitter 实例。
|
||||
"""
|
||||
|
||||
def __init__(self, max_size: int = 1000, overlap: int = 100):
|
||||
self._text_splitter = TextSplitter(max_size=max_size, overlap=overlap)
|
||||
|
||||
def split(self, text: str, source_file: str = "") -> list[dict]:
|
||||
"""从 HTML 文本去标签并分块.
|
||||
|
||||
Args:
|
||||
text: HTML 文本内容
|
||||
source_file: 来源文件名
|
||||
"""
|
||||
try:
|
||||
from bs4 import BeautifulSoup
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"HTML 支持需要 beautifulsoup4 库. 请执行: uv sync --extra html"
|
||||
)
|
||||
|
||||
try:
|
||||
soup = BeautifulSoup(text, "html.parser")
|
||||
# 移除 script/style 标签,避免 JS/CSS 内容混入
|
||||
for tag in soup(["script", "style"]):
|
||||
tag.decompose()
|
||||
plain_text = soup.get_text(separator="\n")
|
||||
except Exception as e:
|
||||
logger.error("HTML 解析失败: %s — %s", source_file, e)
|
||||
raise ValueError(f"HTML 解析失败: {e}") from e
|
||||
|
||||
if not plain_text.strip():
|
||||
return []
|
||||
|
||||
return self._text_splitter.split(plain_text, source_file=source_file)
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Markdown 文档分块器."""
|
||||
import re
|
||||
from src.core.splitters.base import BaseTextSplitter
|
||||
|
||||
|
||||
class MarkdownSplitter(BaseTextSplitter):
|
||||
"""Markdown 混合分块器:先按标题拆,超长再按段落拆."""
|
||||
|
||||
def split(self, text: str, source_file: str = "") -> list[dict]:
|
||||
if not text.strip():
|
||||
return []
|
||||
|
||||
sections = self._split_by_headings(text)
|
||||
chunks = []
|
||||
|
||||
for section in sections:
|
||||
if len(section["content"]) <= self.max_size:
|
||||
chunks.append(section)
|
||||
else:
|
||||
sub_chunks = self._split_by_paragraphs(
|
||||
section["content"],
|
||||
section["section_title"],
|
||||
section["heading_level"],
|
||||
)
|
||||
chunks.extend(sub_chunks)
|
||||
|
||||
for i, chunk in enumerate(chunks):
|
||||
chunk["source_file"] = source_file or chunk.get("source_file", "")
|
||||
chunk["chunk_index"] = i
|
||||
|
||||
return chunks
|
||||
|
||||
def _split_by_headings(self, text: str) -> list[dict]:
|
||||
# 识别代码块范围,跳过其中的假标题
|
||||
code_block_ranges = []
|
||||
fence_re = re.compile(r"^```", re.MULTILINE)
|
||||
fences = [m.start() for m in fence_re.finditer(text)]
|
||||
for i in range(0, len(fences), 2):
|
||||
if i + 1 < len(fences):
|
||||
code_block_ranges.append((fences[i], fences[i + 1]))
|
||||
|
||||
def in_code_block(pos: int) -> bool:
|
||||
return any(start <= pos <= end for start, end in code_block_ranges)
|
||||
|
||||
heading_pattern = re.compile(r"^(#{1,6})\s+(.+)$", re.MULTILINE)
|
||||
all_matches = list(heading_pattern.finditer(text))
|
||||
matches = [m for m in all_matches if not in_code_block(m.start())]
|
||||
|
||||
if not matches:
|
||||
return [{
|
||||
"content": text.strip(),
|
||||
"section_title": "",
|
||||
"heading_level": 0,
|
||||
}]
|
||||
|
||||
sections = []
|
||||
for i, match in enumerate(matches):
|
||||
level = len(match.group(1))
|
||||
title = match.group(2).strip()
|
||||
start = match.end()
|
||||
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
|
||||
content = text[start:end].strip()
|
||||
|
||||
if content:
|
||||
sections.append({
|
||||
"content": f"{match.group(0)}\n{content}",
|
||||
"section_title": title,
|
||||
"heading_level": level,
|
||||
})
|
||||
|
||||
if matches and matches[0].start() > 0:
|
||||
preamble = text[:matches[0].start()].strip()
|
||||
if preamble:
|
||||
sections.insert(0, {
|
||||
"content": preamble,
|
||||
"section_title": "",
|
||||
"heading_level": 0,
|
||||
})
|
||||
|
||||
return sections
|
||||
@@ -0,0 +1,48 @@
|
||||
"""PDF 文档分块器 — 使用 pymupdf 提取文字后委托 TextSplitter."""
|
||||
import logging
|
||||
from src.core.splitters.text import TextSplitter
|
||||
|
||||
logger = logging.getLogger("md-vector-db")
|
||||
|
||||
|
||||
class PDFSplitter:
|
||||
"""PDF 分块器:pymupdf 提取文字 → TextSplitter 分块.
|
||||
|
||||
实现 Splitter Protocol,内部组合 TextSplitter 实例。
|
||||
注意: split() 的 text 参数实际接收 PDF 文件路径(非文本内容)。
|
||||
"""
|
||||
|
||||
def __init__(self, max_size: int = 1000, overlap: int = 100):
|
||||
self._text_splitter = TextSplitter(max_size=max_size, overlap=overlap)
|
||||
|
||||
def split(self, text: str, source_file: str = "") -> list[dict]:
|
||||
"""从 PDF 文件提取文字并分块.
|
||||
|
||||
Args:
|
||||
text: PDF 文件路径(非文本内容!由 ingest_file 传入)
|
||||
source_file: 来源文件名
|
||||
"""
|
||||
try:
|
||||
import fitz # pymupdf
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"PDF 支持需要 pymupdf 库. 请执行: uv sync --extra pdf"
|
||||
)
|
||||
|
||||
pdf_path = text # text 参数实际是文件路径
|
||||
extracted_pages = []
|
||||
try:
|
||||
with fitz.open(pdf_path) as doc:
|
||||
for page in doc:
|
||||
page_text = page.get_text()
|
||||
if page_text.strip():
|
||||
extracted_pages.append(page_text)
|
||||
except Exception as e:
|
||||
logger.error("PDF 解析失败: %s — %s", pdf_path, e)
|
||||
raise ValueError(f"PDF 解析失败: {e}") from e
|
||||
|
||||
if not extracted_pages:
|
||||
return []
|
||||
|
||||
full_text = "\n\n".join(extracted_pages)
|
||||
return self._text_splitter.split(full_text, source_file=source_file)
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Splitter 注册表 — 按文件扩展名自动选择分块器."""
|
||||
from pathlib import Path
|
||||
from src.core.splitters.base import Splitter
|
||||
|
||||
# 扩展名 → Splitter 类名映射
|
||||
_DEFAULT_MAP: dict[str, str] = {
|
||||
".md": "markdown",
|
||||
".markdown": "markdown",
|
||||
".txt": "text",
|
||||
".pdf": "pdf",
|
||||
".html": "html",
|
||||
".htm": "html",
|
||||
".epub": "epub",
|
||||
}
|
||||
|
||||
# 所有支持的扩展名集合(供外部遍历文件使用)
|
||||
SUPPORTED_SUFFIXES = frozenset(_DEFAULT_MAP.keys())
|
||||
|
||||
# 用户可注册自定义 Splitter
|
||||
_custom_registry: dict[str, type[Splitter]] = {}
|
||||
|
||||
|
||||
def register_splitter(ext: str, splitter_cls: type[Splitter]) -> None:
|
||||
"""注册自定义 Splitter 类.
|
||||
|
||||
注意: 此函数非线程安全,请在程序启动时调用(单线程阶段)。
|
||||
运行时动态注册需自行加锁。
|
||||
"""
|
||||
ext = ext.lower() if ext.startswith(".") else f".{ext}"
|
||||
_custom_registry[ext] = splitter_cls
|
||||
|
||||
|
||||
def get_splitter(
|
||||
file_path: str,
|
||||
max_size: int = 1000,
|
||||
overlap: int = 100,
|
||||
) -> Splitter:
|
||||
"""根据文件扩展名自动选择 Splitter,未匹配回退到 TextSplitter.
|
||||
|
||||
Args:
|
||||
file_path: 文件路径(用于提取扩展名)
|
||||
max_size: 分块最大字符数
|
||||
overlap: 相邻块重叠字符数
|
||||
|
||||
Returns:
|
||||
对应格式的 Splitter 实例
|
||||
"""
|
||||
ext = Path(file_path).suffix.lower()
|
||||
|
||||
# 优先查用户自定义注册
|
||||
if ext in _custom_registry:
|
||||
return _custom_registry[ext](max_size=max_size, overlap=overlap)
|
||||
|
||||
kind = _DEFAULT_MAP.get(ext, "text")
|
||||
|
||||
if kind == "markdown":
|
||||
from src.core.splitters.markdown import MarkdownSplitter
|
||||
return MarkdownSplitter(max_size=max_size, overlap=overlap)
|
||||
|
||||
if kind == "text":
|
||||
from src.core.splitters.text import TextSplitter
|
||||
return TextSplitter(max_size=max_size, overlap=overlap)
|
||||
|
||||
if kind == "pdf":
|
||||
from src.core.splitters.pdf import PDFSplitter
|
||||
return PDFSplitter(max_size=max_size, overlap=overlap)
|
||||
|
||||
if kind == "html":
|
||||
from src.core.splitters.html import HTMLSplitter
|
||||
return HTMLSplitter(max_size=max_size, overlap=overlap)
|
||||
|
||||
if kind == "epub":
|
||||
from src.core.splitters.epub import EPUBSplitter
|
||||
return EPUBSplitter(max_size=max_size, overlap=overlap)
|
||||
|
||||
# 回退
|
||||
from src.core.splitters.text import TextSplitter
|
||||
return TextSplitter(max_size=max_size, overlap=overlap)
|
||||
@@ -0,0 +1,17 @@
|
||||
"""纯文本分块器 — 按段落双换行切分."""
|
||||
from src.core.splitters.base import BaseTextSplitter
|
||||
|
||||
|
||||
class TextSplitter(BaseTextSplitter):
|
||||
"""纯文本分块器:按 \n\n 切段落,超长按标点硬切."""
|
||||
|
||||
def split(self, text: str, source_file: str = "") -> list[dict]:
|
||||
if not text.strip():
|
||||
return []
|
||||
|
||||
chunks = self._split_by_paragraphs(text)
|
||||
for i, chunk in enumerate(chunks):
|
||||
chunk["source_file"] = source_file
|
||||
chunk["chunk_index"] = i
|
||||
|
||||
return chunks
|
||||
+23
-18
@@ -1,16 +1,17 @@
|
||||
"""FastAPI 服务层."""
|
||||
import os
|
||||
import uuid
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Depends, Request
|
||||
from fastapi import FastAPI, HTTPException, Depends, Request, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import RedirectResponse
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
from src.core.security import is_safe_path
|
||||
from src.core.security import is_path_within_workspace
|
||||
from src.server.auth import verify_api_key, rate_limiter
|
||||
from src.server.deps import get_state, AppState
|
||||
|
||||
@@ -65,10 +66,10 @@ app = FastAPI(title="md-vector-db", version="0.1.0", lifespan=lifespan)
|
||||
# CORS 中间件
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=os.environ.get("CORS_ORIGINS", "*").split(","),
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
allow_origins=os.environ.get("CORS_ORIGINS", "http://localhost:3000").split(","),
|
||||
allow_credentials=False,
|
||||
allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
|
||||
allow_headers=["Content-Type", "Authorization", "X-API-Key"],
|
||||
)
|
||||
|
||||
|
||||
@@ -78,7 +79,7 @@ async def security_headers_middleware(request: Request, call_next):
|
||||
response = await call_next(request)
|
||||
response.headers["X-Content-Type-Options"] = "nosniff"
|
||||
response.headers["X-Frame-Options"] = "DENY"
|
||||
response.headers["X-XSS-Protection"] = "1; mode=block"
|
||||
response.headers["Content-Security-Policy"] = "default-src 'self'"
|
||||
response.headers["Referrer-Policy"] = "no-referrer"
|
||||
return response
|
||||
|
||||
@@ -105,7 +106,11 @@ def list_collections(
|
||||
state: AppState = Depends(get_state),
|
||||
_: bool = Depends(verify_api_key),
|
||||
):
|
||||
try:
|
||||
return {"collections": state.list_collections_with_stats()}
|
||||
except Exception:
|
||||
logger.exception("列出集合失败")
|
||||
raise HTTPException(status_code=500, detail="服务器内部错误")
|
||||
|
||||
|
||||
@app.post("/api/v1/ingest")
|
||||
@@ -117,24 +122,16 @@ def ingest_document(
|
||||
ingestor = state.get_ingestor(req.collection)
|
||||
try:
|
||||
if req.file_path:
|
||||
if not is_safe_path(req.file_path):
|
||||
if not is_path_within_workspace(req.file_path):
|
||||
raise HTTPException(status_code=400, detail="不允许的路径")
|
||||
path = Path(req.file_path).resolve()
|
||||
cwd = Path.cwd().resolve()
|
||||
# 用 commonpath 替代字符串 startswith 比较 (Windows 大小写安全)
|
||||
try:
|
||||
common = Path(os.path.commonpath([str(path), str(cwd)]))
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="不允许访问当前目录外的路径")
|
||||
if common != cwd:
|
||||
raise HTTPException(status_code=400, detail="不允许访问当前目录外的路径")
|
||||
if not path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"文件不存在: {path.name}")
|
||||
count = ingestor.ingest_file(str(path))
|
||||
file_name = path.name
|
||||
else:
|
||||
# content 模式 (file_path/content 互斥由 Pydantic 校验保证)
|
||||
file_name = req.file_name or "untitled.md"
|
||||
file_name = req.file_name or f"untitled_{uuid.uuid4().hex[:8]}.md"
|
||||
count = ingestor.ingest_content(req.content, file_name)
|
||||
return {"status": "ok", "chunks": count, "file": file_name, "collection": ingestor.collection_name}
|
||||
except HTTPException:
|
||||
@@ -150,9 +147,15 @@ def search_documents(
|
||||
state: AppState = Depends(get_state),
|
||||
_: bool = Depends(verify_api_key),
|
||||
):
|
||||
try:
|
||||
searcher = state.get_searcher(req.collection)
|
||||
results = searcher.search(req.query, top_k=req.top_k)
|
||||
return {"results": results, "collection": searcher.collection_name}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception("检索失败")
|
||||
raise HTTPException(status_code=500, detail="服务器内部错误")
|
||||
|
||||
|
||||
@app.delete("/api/v1/documents/{file_name}")
|
||||
@@ -160,7 +163,9 @@ def delete_document(
|
||||
file_name: str,
|
||||
state: AppState = Depends(get_state),
|
||||
_: bool = Depends(verify_api_key),
|
||||
collection: str | None = None,
|
||||
collection: str | None = Query(
|
||||
default=None, max_length=128, pattern=r"^[a-zA-Z0-9_-]+$",
|
||||
),
|
||||
):
|
||||
if not file_name or len(file_name) > 512:
|
||||
raise HTTPException(status_code=400, detail="file_name 长度应在 1-512 之间")
|
||||
|
||||
+10
-6
@@ -11,7 +11,9 @@ from fastapi import Header, HTTPException, Request
|
||||
logger = logging.getLogger("md-vector-db")
|
||||
|
||||
# -- API Key 认证 --
|
||||
EXPECTED_API_KEY = os.environ.get("MD_VECTOR_API_KEY", "")
|
||||
def _get_expected_api_key() -> str:
|
||||
"""惰性获取 API Key(每次调用重新从环境变量读取)."""
|
||||
return os.environ.get("MD_VECTOR_API_KEY", "")
|
||||
|
||||
|
||||
def verify_api_key(x_api_key: str | None = Header(None)):
|
||||
@@ -19,8 +21,9 @@ def verify_api_key(x_api_key: str | None = Header(None)):
|
||||
|
||||
使用恒定时间比较防止时序攻击.
|
||||
"""
|
||||
if EXPECTED_API_KEY:
|
||||
if x_api_key is None or not hmac.compare_digest(x_api_key, EXPECTED_API_KEY):
|
||||
expected = _get_expected_api_key()
|
||||
if expected:
|
||||
if x_api_key is None or not hmac.compare_digest(x_api_key, expected):
|
||||
logger.warning("API Key 认证失败")
|
||||
raise HTTPException(status_code=401, detail="无效的 API Key")
|
||||
return True
|
||||
@@ -41,8 +44,6 @@ class RateLimiter:
|
||||
with self._lock:
|
||||
records = self._store[client_id]
|
||||
records[:] = [t for t in records if now - t < self.window]
|
||||
if not records:
|
||||
del self._store[client_id] # 清理空 key,防止内存泄漏
|
||||
if len(records) >= self.max_requests:
|
||||
return False
|
||||
records.append(now)
|
||||
@@ -55,4 +56,7 @@ class RateLimiter:
|
||||
return True
|
||||
|
||||
|
||||
rate_limiter = RateLimiter(max_requests=30, window_seconds=60)
|
||||
rate_limiter = RateLimiter(
|
||||
max_requests=int(os.environ.get("RATE_LIMIT_MAX", "30")),
|
||||
window_seconds=int(os.environ.get("RATE_LIMIT_WINDOW", "60")),
|
||||
)
|
||||
|
||||
+4
-4
@@ -45,17 +45,17 @@ class AppState:
|
||||
name = collection or self.default_collection
|
||||
with self._cache_lock:
|
||||
if name not in self._ingestors:
|
||||
self._ingestors[name] = DocumentIngestor(self.db, self.embedder, name)
|
||||
self._ingestors[name] = DocumentIngestor(
|
||||
self.db, self.embedder, name,
|
||||
chunk_config=self.config.chunk,
|
||||
)
|
||||
return self._ingestors[name]
|
||||
|
||||
def list_collections_with_stats(self) -> list[dict]:
|
||||
"""列出所有 collection 及其统计(直接从 ChromaDB 查询)."""
|
||||
result = []
|
||||
try:
|
||||
for coll in self.db.list_collections():
|
||||
result.append({"name": coll.name, "count": coll.count()})
|
||||
except Exception:
|
||||
logger.exception("列出集合失败")
|
||||
return result
|
||||
|
||||
def is_healthy(self) -> dict:
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""认证与速率限制测试."""
|
||||
import time
|
||||
import threading
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from src.server.auth import verify_api_key, RateLimiter
|
||||
|
||||
|
||||
class TestVerifyApiKey:
|
||||
"""API Key 认证测试."""
|
||||
|
||||
def test_passes_when_no_key_configured(self, monkeypatch):
|
||||
"""未设置环境变量时跳过认证."""
|
||||
monkeypatch.setenv("MD_VECTOR_API_KEY", "")
|
||||
import src.server.auth as auth
|
||||
monkeypatch.setattr(auth, "_get_expected_api_key", lambda: "")
|
||||
result = verify_api_key(x_api_key=None)
|
||||
assert result is True
|
||||
|
||||
def test_rejects_when_key_required_but_not_provided(self, monkeypatch):
|
||||
"""已设置密钥但请求未提供."""
|
||||
import src.server.auth as auth
|
||||
monkeypatch.setattr(auth, "_get_expected_api_key", lambda: "secret123")
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
verify_api_key(x_api_key=None)
|
||||
assert exc.value.status_code == 401
|
||||
|
||||
def test_rejects_wrong_key(self, monkeypatch):
|
||||
"""错误的密钥被拒绝."""
|
||||
import src.server.auth as auth
|
||||
monkeypatch.setattr(auth, "_get_expected_api_key", lambda: "secret123")
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
verify_api_key(x_api_key="wrong-key")
|
||||
assert exc.value.status_code == 401
|
||||
|
||||
def test_accepts_correct_key(self, monkeypatch):
|
||||
"""正确的密钥通过认证."""
|
||||
import src.server.auth as auth
|
||||
monkeypatch.setattr(auth, "_get_expected_api_key", lambda: "secret123")
|
||||
result = verify_api_key(x_api_key="secret123")
|
||||
assert result is True
|
||||
|
||||
|
||||
class TestRateLimiter:
|
||||
"""速率限制器测试."""
|
||||
|
||||
def test_allows_within_limit(self):
|
||||
"""未超限时允许请求."""
|
||||
limiter = RateLimiter(max_requests=5, window_seconds=60)
|
||||
for _ in range(5):
|
||||
assert limiter.is_allowed("client-1") is True
|
||||
|
||||
def test_blocks_when_exceeded(self):
|
||||
"""超限后拒绝."""
|
||||
limiter = RateLimiter(max_requests=2, window_seconds=60)
|
||||
assert limiter.is_allowed("client-2") is True
|
||||
assert limiter.is_allowed("client-2") is True
|
||||
assert limiter.is_allowed("client-2") is False
|
||||
|
||||
def test_different_clients_independent(self):
|
||||
"""不同客户端独立计数."""
|
||||
limiter = RateLimiter(max_requests=1, window_seconds=60)
|
||||
assert limiter.is_allowed("client-a") is True
|
||||
assert limiter.is_allowed("client-b") is True
|
||||
|
||||
def test_window_expires(self, monkeypatch):
|
||||
"""时间窗口过期后恢复."""
|
||||
limiter = RateLimiter(max_requests=1, window_seconds=1)
|
||||
assert limiter.is_allowed("client-3") is True
|
||||
assert limiter.is_allowed("client-3") is False
|
||||
fake_now = time.time() + 2.0
|
||||
monkeypatch.setattr(time, "time", lambda: fake_now)
|
||||
assert limiter.is_allowed("client-3") is True
|
||||
|
||||
def test_concurrent_access(self):
|
||||
"""并发访问不产生竞态."""
|
||||
limiter = RateLimiter(max_requests=100, window_seconds=60)
|
||||
errors = []
|
||||
def make_requests():
|
||||
try:
|
||||
for _ in range(50):
|
||||
limiter.is_allowed("concurrent")
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
threads = [threading.Thread(target=make_requests) for _ in range(10)]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
assert len(errors) == 0
|
||||
@@ -0,0 +1,62 @@
|
||||
"""CLI 命令测试."""
|
||||
from typer.testing import CliRunner
|
||||
from src.cli.main import app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
class TestCLIIngest:
|
||||
"""ingest 命令测试."""
|
||||
|
||||
def test_ingest_no_args_shows_usage(self):
|
||||
"""无参数时显示用法提示."""
|
||||
result = runner.invoke(app, ["ingest"])
|
||||
assert result.exit_code == 1
|
||||
assert "用法" in result.stderr
|
||||
|
||||
def test_ingest_nonexistent_file_skips(self, tmp_path):
|
||||
"""不存在的文件优雅跳过."""
|
||||
result = runner.invoke(app, ["ingest", str(tmp_path / "nonexistent.md")])
|
||||
assert "SKIP" in result.stderr or result.exit_code != 0
|
||||
|
||||
|
||||
class TestCLISearch:
|
||||
"""search 命令测试."""
|
||||
|
||||
def test_search_basic(self, monkeypatch):
|
||||
"""search 命令可执行."""
|
||||
monkeypatch.setenv("MD_VECTOR_CONFIG", "config.yaml")
|
||||
result = runner.invoke(app, ["search", "测试查询", "-k", "1"])
|
||||
assert isinstance(result.exit_code, int)
|
||||
|
||||
|
||||
class TestCLIStats:
|
||||
"""stats 命令测试."""
|
||||
|
||||
def test_stats_basic(self, monkeypatch):
|
||||
"""stats 命令可执行."""
|
||||
monkeypatch.setenv("MD_VECTOR_CONFIG", "config.yaml")
|
||||
result = runner.invoke(app, ["stats"])
|
||||
assert isinstance(result.exit_code, int)
|
||||
|
||||
|
||||
class TestCLIJSONOutput:
|
||||
"""--json 输出测试."""
|
||||
|
||||
def test_search_json_valid(self, monkeypatch):
|
||||
"""search --json 输出合法 JSON."""
|
||||
import json
|
||||
monkeypatch.setenv("MD_VECTOR_CONFIG", "config.yaml")
|
||||
result = runner.invoke(app, ["search", "测试", "--json", "-k", "1"])
|
||||
if result.stdout.strip():
|
||||
data = json.loads(result.stdout)
|
||||
assert isinstance(data, list)
|
||||
|
||||
def test_stats_json_valid(self, monkeypatch):
|
||||
"""stats --json 输出合法 JSON."""
|
||||
import json
|
||||
monkeypatch.setenv("MD_VECTOR_CONFIG", "config.yaml")
|
||||
result = runner.invoke(app, ["stats", "--json"])
|
||||
if result.stdout.strip():
|
||||
data = json.loads(result.stdout)
|
||||
assert isinstance(data, dict)
|
||||
+4
-11
@@ -87,20 +87,13 @@ class TestEmbedConfigEnvVar:
|
||||
"""api_key 从环境变量读取."""
|
||||
|
||||
def test_api_key_from_env(self, monkeypatch):
|
||||
"""从环境变量读取 API Key."""
|
||||
monkeypatch.setenv("EMBED_API_KEY", "sk-env-test")
|
||||
import importlib
|
||||
from src.core import config
|
||||
importlib.reload(config)
|
||||
cfg = config.EmbedConfig(mode="api")
|
||||
cfg = EmbedConfig(mode="api")
|
||||
assert cfg.api_key == "sk-env-test"
|
||||
# 清理
|
||||
importlib.reload(config)
|
||||
|
||||
def test_api_key_empty_when_not_set(self, monkeypatch):
|
||||
"""未设置时返回空字符串."""
|
||||
monkeypatch.delenv("EMBED_API_KEY", raising=False)
|
||||
import importlib
|
||||
from src.core import config
|
||||
importlib.reload(config)
|
||||
cfg = config.EmbedConfig(mode="api")
|
||||
cfg = EmbedConfig(mode="api")
|
||||
assert cfg.api_key == ""
|
||||
importlib.reload(config)
|
||||
|
||||
+77
-1
@@ -4,7 +4,8 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.ingest import MarkdownSplitter, DocumentIngestor
|
||||
from src.core.splitters import MarkdownSplitter
|
||||
from src.core.ingest import DocumentIngestor
|
||||
|
||||
|
||||
class TestMarkdownSplitter:
|
||||
@@ -145,3 +146,78 @@ class TestIngestorIntegration:
|
||||
c1 = ingestor.ingest_content("# A", "dup.md")
|
||||
c2 = ingestor.ingest_content("# B", "dup.md")
|
||||
assert ingestor.collection.count() == c2
|
||||
|
||||
|
||||
class TestIngestFile:
|
||||
"""ingest_file 方法测试."""
|
||||
|
||||
def test_ingest_file_markdown(self, tmp_path):
|
||||
"""通过文件路径入库 .md 文件."""
|
||||
from src.core.config import EmbedConfig
|
||||
from src.core.db import VectorDB
|
||||
from src.core.embedder import create_embedder
|
||||
from src.core.ingest import DocumentIngestor
|
||||
|
||||
md_file = tmp_path / "hello.md"
|
||||
md_file.write_text("# 测试\n这是测试内容。", encoding="utf-8")
|
||||
|
||||
db = VectorDB(persist_dir=str(tmp_path / "db"))
|
||||
embedder = create_embedder(EmbedConfig(mode="local"))
|
||||
ingestor = DocumentIngestor(db, embedder, "test_file")
|
||||
|
||||
count = ingestor.ingest_file(str(md_file))
|
||||
assert count > 0
|
||||
assert ingestor.collection.count() == count
|
||||
|
||||
def test_ingest_file_text(self, tmp_path):
|
||||
"""通过文件路径入库 .txt 文件."""
|
||||
from src.core.config import EmbedConfig
|
||||
from src.core.db import VectorDB
|
||||
from src.core.embedder import create_embedder
|
||||
from src.core.ingest import DocumentIngestor
|
||||
|
||||
txt_file = tmp_path / "notes.txt"
|
||||
txt_file.write_text("这是一段纯文本内容。\n\n第二段内容在这里。", encoding="utf-8")
|
||||
|
||||
db = VectorDB(persist_dir=str(tmp_path / "db"))
|
||||
embedder = create_embedder(EmbedConfig(mode="local"))
|
||||
ingestor = DocumentIngestor(db, embedder, "test_txt")
|
||||
|
||||
count = ingestor.ingest_file(str(txt_file))
|
||||
assert count > 0
|
||||
|
||||
|
||||
class TestIngestDirectory:
|
||||
"""ingest_directory 方法测试."""
|
||||
|
||||
def test_ingest_directory_mixed_formats(self, tmp_path):
|
||||
"""入库包含多种格式的目录."""
|
||||
from src.core.config import EmbedConfig
|
||||
from src.core.db import VectorDB
|
||||
from src.core.embedder import create_embedder
|
||||
from src.core.ingest import DocumentIngestor
|
||||
|
||||
(tmp_path / "a.md").write_text("# A\n内容 A", encoding="utf-8")
|
||||
(tmp_path / "b.txt").write_text("内容 B", encoding="utf-8")
|
||||
(tmp_path / "not_supported.xyz").write_text("不应被处理", encoding="utf-8")
|
||||
|
||||
db = VectorDB(persist_dir=str(tmp_path / "db"))
|
||||
embedder = create_embedder(EmbedConfig(mode="local"))
|
||||
ingestor = DocumentIngestor(db, embedder, "test_dir")
|
||||
|
||||
results = ingestor.ingest_directory(str(tmp_path))
|
||||
assert len(results) >= 2 # a.md + b.txt, .xyz 被忽略
|
||||
|
||||
def test_ingest_directory_empty(self, tmp_path):
|
||||
"""空目录返回空结果."""
|
||||
from src.core.config import EmbedConfig
|
||||
from src.core.db import VectorDB
|
||||
from src.core.embedder import create_embedder
|
||||
from src.core.ingest import DocumentIngestor
|
||||
|
||||
db = VectorDB(persist_dir=str(tmp_path / "db"))
|
||||
embedder = create_embedder(EmbedConfig(mode="local"))
|
||||
ingestor = DocumentIngestor(db, embedder, "test_empty_dir")
|
||||
|
||||
results = ingestor.ingest_directory(str(tmp_path))
|
||||
assert results == {}
|
||||
|
||||
@@ -74,3 +74,51 @@ class TestSearcher:
|
||||
"""无语义匹配时不崩溃."""
|
||||
results = searcher.search("xyzxyz不存在的内容abcabc", top_k=3)
|
||||
assert isinstance(results, list)
|
||||
|
||||
def test_list_sources(self, searcher):
|
||||
"""list_sources 返回已入库的源文件列表."""
|
||||
sources = searcher.list_sources()
|
||||
assert isinstance(sources, list)
|
||||
|
||||
def test_get_collection_info(self, searcher):
|
||||
"""get_collection_info 返回 collection 信息."""
|
||||
info = searcher.get_collection_info()
|
||||
assert info["name"] == "test_search"
|
||||
assert info["count"] > 0
|
||||
|
||||
def test_delete_by_source(self, searcher):
|
||||
"""delete_by_source 删除源文件的所有 chunks."""
|
||||
sources_before = searcher.list_sources()
|
||||
if sources_before:
|
||||
target = sources_before[0]
|
||||
result = searcher.delete_by_source(target)
|
||||
assert result is True
|
||||
sources_after = searcher.list_sources()
|
||||
assert target not in sources_after
|
||||
|
||||
def test_search_with_source_filter(self, searcher):
|
||||
"""带 source_file 过滤的搜索."""
|
||||
sources = searcher.list_sources()
|
||||
if sources:
|
||||
results = searcher.search("测试", top_k=3, source_file=sources[0])
|
||||
assert isinstance(results, list)
|
||||
for r in results:
|
||||
assert r["source_file"] == sources[0]
|
||||
|
||||
def test_delete_by_source_nonexistent(self, searcher):
|
||||
"""删除不存在的源文件返回 False."""
|
||||
result = searcher.delete_by_source("nonexistent_file_xyz.md")
|
||||
assert result is False
|
||||
|
||||
def test_list_sources_empty_collection(self, tmp_path):
|
||||
"""空 collection 的 list_sources 返回空列表."""
|
||||
from src.core.config import EmbedConfig
|
||||
from src.core.db import VectorDB
|
||||
from src.core.embedder import create_embedder
|
||||
from src.core.search import Searcher
|
||||
|
||||
db = VectorDB(persist_dir=str(tmp_path))
|
||||
embedder = create_embedder(EmbedConfig(mode="local"))
|
||||
searcher = Searcher(db, embedder, "empty_coll")
|
||||
sources = searcher.list_sources()
|
||||
assert sources == []
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Splitter 注册表和 TextSplitter 测试."""
|
||||
import pytest
|
||||
from src.core.splitters import TextSplitter, MarkdownSplitter, get_splitter, register_splitter, SUPPORTED_SUFFIXES
|
||||
|
||||
|
||||
class TestTextSplitter:
|
||||
"""TextSplitter 纯文本分块测试."""
|
||||
|
||||
def test_empty_text(self):
|
||||
s = TextSplitter()
|
||||
assert s.split("") == []
|
||||
assert s.split(" \n\n ") == []
|
||||
|
||||
def test_short_text_single_chunk(self):
|
||||
s = TextSplitter(max_size=1000)
|
||||
chunks = s.split("这是一段短文本。", source_file="test.txt")
|
||||
assert len(chunks) == 1
|
||||
assert chunks[0]["source_file"] == "test.txt"
|
||||
assert chunks[0]["content"] == "这是一段短文本。"
|
||||
|
||||
def test_long_paragraph_split(self):
|
||||
s = TextSplitter(max_size=50, overlap=10)
|
||||
long_text = "这是第一句。" * 20
|
||||
chunks = s.split(long_text, source_file="long.txt")
|
||||
assert len(chunks) > 1
|
||||
for c in chunks:
|
||||
assert len(c["content"]) <= 60 # max_size + 少许容差
|
||||
|
||||
def test_paragraph_boundary_split(self):
|
||||
s = TextSplitter(max_size=100)
|
||||
text = "短段落A。\n\n短段落B。\n\n短段落C。"
|
||||
chunks = s.split(text)
|
||||
assert len(chunks) >= 1
|
||||
assert all("content" in c for c in chunks)
|
||||
|
||||
def test_chunk_metadata(self):
|
||||
s = TextSplitter()
|
||||
chunks = s.split("测试内容。", source_file="doc.txt")
|
||||
assert chunks[0]["source_file"] == "doc.txt"
|
||||
assert chunks[0]["section_title"] == ""
|
||||
assert chunks[0]["heading_level"] == 0
|
||||
assert chunks[0]["chunk_index"] == 0
|
||||
|
||||
|
||||
class TestRegistry:
|
||||
"""注册表测试."""
|
||||
|
||||
def test_get_splitter_for_md(self):
|
||||
s = get_splitter("doc.md")
|
||||
assert isinstance(s, MarkdownSplitter)
|
||||
|
||||
def test_get_splitter_for_txt(self):
|
||||
s = get_splitter("notes.txt")
|
||||
assert isinstance(s, TextSplitter)
|
||||
|
||||
def test_get_splitter_fallback(self):
|
||||
s = get_splitter("data.xyz")
|
||||
assert isinstance(s, TextSplitter)
|
||||
|
||||
def test_supported_suffixes(self):
|
||||
assert ".md" in SUPPORTED_SUFFIXES
|
||||
assert ".txt" in SUPPORTED_SUFFIXES
|
||||
assert ".pdf" in SUPPORTED_SUFFIXES
|
||||
assert ".html" in SUPPORTED_SUFFIXES
|
||||
|
||||
def test_custom_register(self):
|
||||
class FakeSplitter:
|
||||
def __init__(self, max_size=1000, overlap=100): pass
|
||||
def split(self, text, source_file=""): return []
|
||||
|
||||
register_splitter(".fake", FakeSplitter)
|
||||
s = get_splitter("test.fake")
|
||||
assert isinstance(s, FakeSplitter)
|
||||
@@ -0,0 +1,68 @@
|
||||
"""EPUBSplitter 测试."""
|
||||
import pytest
|
||||
|
||||
|
||||
class TestEPUBSplitterMissingDep:
|
||||
"""缺失依赖时的行为测试(不 skip 整个文件)."""
|
||||
|
||||
def test_split_raises_clear_import_error(self, monkeypatch):
|
||||
"""未安装 ebooklib 时给出明确提示."""
|
||||
from src.core.splitters.epub import EPUBSplitter
|
||||
s = EPUBSplitter()
|
||||
import builtins
|
||||
original_import = builtins.__import__
|
||||
|
||||
def mock_import(name, *args, **kwargs):
|
||||
if name == "ebooklib" or name.startswith("ebooklib."):
|
||||
raise ImportError("No module named 'ebooklib'")
|
||||
return original_import(name, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(builtins, "__import__", mock_import)
|
||||
with pytest.raises(ImportError, match="ebooklib"):
|
||||
s.split("dummy.epub", source_file="test.epub")
|
||||
|
||||
|
||||
class TestEPUBSplitter:
|
||||
"""EPUBSplitter 测试(需 ebooklib)."""
|
||||
ebooklib = pytest.importorskip("ebooklib", reason="ebooklib 未安装")
|
||||
|
||||
def test_split_simple_epub(self, tmp_path):
|
||||
"""用 ebooklib 创建一个简单 EPUB 并测试分块."""
|
||||
from src.core.splitters.epub import EPUBSplitter
|
||||
from ebooklib import epub
|
||||
|
||||
epub_path = tmp_path / "test.epub"
|
||||
|
||||
book = epub.EpubBook()
|
||||
book.set_identifier("test123")
|
||||
book.set_title("测试书名")
|
||||
book.set_language("zh")
|
||||
|
||||
chapter = epub.EpubHtml(
|
||||
title="第一章",
|
||||
file_name="chap01.xhtml",
|
||||
lang="zh",
|
||||
)
|
||||
chapter.content = "<h1>第一章</h1><p>这是EPUB电子书的内容。</p><p>第二段文字在这里。</p>"
|
||||
book.add_item(chapter)
|
||||
|
||||
book.toc = [epub.Link("chap01.xhtml", "第一章", "chap01")]
|
||||
book.add_item(epub.EpubNcx())
|
||||
book.add_item(epub.EpubNav())
|
||||
book.spine = ["nav", chapter]
|
||||
|
||||
epub.write_epub(str(epub_path), book)
|
||||
|
||||
s = EPUBSplitter(max_size=500)
|
||||
chunks = s.split(str(epub_path), source_file="test.epub")
|
||||
assert len(chunks) >= 1
|
||||
all_text = "".join(c["content"] for c in chunks)
|
||||
assert "第一章" in all_text
|
||||
assert "EPUB电子书" in all_text
|
||||
assert "第二段文字" in all_text
|
||||
|
||||
def test_epub_has_split_method(self):
|
||||
"""EPUBSplitter 遵循 Splitter Protocol."""
|
||||
from src.core.splitters.epub import EPUBSplitter
|
||||
s = EPUBSplitter()
|
||||
assert hasattr(s, "split")
|
||||
@@ -0,0 +1,42 @@
|
||||
"""HTMLSplitter 测试."""
|
||||
import pytest
|
||||
|
||||
bs4 = pytest.importorskip("bs4", reason="beautifulsoup4 未安装")
|
||||
|
||||
|
||||
class TestHTMLSplitter:
|
||||
"""HTMLSplitter 测试(需 beautifulsoup4)."""
|
||||
|
||||
def test_split_simple_html(self):
|
||||
from src.core.splitters.html import HTMLSplitter
|
||||
html = "<html><body><h1>标题</h1><p>这是段落内容。</p><p>第二段。</p></body></html>"
|
||||
s = HTMLSplitter(max_size=500)
|
||||
chunks = s.split(html, source_file="test.html")
|
||||
assert len(chunks) >= 1
|
||||
all_text = "".join(c["content"] for c in chunks)
|
||||
assert "标题" in all_text
|
||||
assert "段落内容" in all_text
|
||||
assert "第二段" in all_text
|
||||
|
||||
def test_strips_script_and_style(self):
|
||||
from src.core.splitters.html import HTMLSplitter
|
||||
html = """<html><head><style>.a{color:red}</style><script>alert('xss')</script></head>
|
||||
<body><p>可见内容。</p></body></html>"""
|
||||
s = HTMLSplitter()
|
||||
chunks = s.split(html, source_file="test.html")
|
||||
all_text = "".join(c["content"] for c in chunks)
|
||||
assert "可见内容" in all_text
|
||||
assert "alert" not in all_text
|
||||
assert ".a{color:red}" not in all_text
|
||||
|
||||
def test_empty_html(self):
|
||||
from src.core.splitters.html import HTMLSplitter
|
||||
s = HTMLSplitter()
|
||||
assert s.split("<html></html>") == []
|
||||
assert s.split("") == []
|
||||
|
||||
def test_html_has_split_method(self):
|
||||
"""HTMLSplitter 遵循 Splitter Protocol."""
|
||||
from src.core.splitters.html import HTMLSplitter
|
||||
s = HTMLSplitter()
|
||||
assert hasattr(s, "split")
|
||||
@@ -0,0 +1,43 @@
|
||||
"""MarkdownSplitter 边界测试."""
|
||||
import pytest
|
||||
from src.core.splitters import MarkdownSplitter
|
||||
|
||||
|
||||
class TestMarkdownSplitterEdgeCases:
|
||||
"""Markdown 分块边界情况."""
|
||||
|
||||
@pytest.fixture
|
||||
def splitter(self):
|
||||
return MarkdownSplitter(max_size=1000, overlap=100)
|
||||
|
||||
def test_no_headings_document(self, splitter):
|
||||
"""无标题文档正常分块."""
|
||||
md = "这是一段没有标题的纯文本。\n\n第二段内容。"
|
||||
chunks = splitter.split(md, source_file="nohead.md")
|
||||
assert len(chunks) >= 1
|
||||
|
||||
def test_deep_headings(self, splitter):
|
||||
"""h4-h6 深层标题."""
|
||||
md = "# 一级\n## 二级\n### 三级\n#### 四级\n内容在这里。\n##### 五级\n更多内容。\n###### 六级\n最深的内容。"
|
||||
chunks = splitter.split(md, source_file="deep.md")
|
||||
assert len(chunks) >= 1
|
||||
|
||||
def test_hash_in_code_block_not_heading(self, splitter):
|
||||
"""代码块中的 # 号不被误识别为标题."""
|
||||
md = "# 真实标题\n这是内容。\n```python\n# 这不是标题,是注释\nx = 1\n## 这也不是标题\n```\n更多内容。"
|
||||
chunks = splitter.split(md, source_file="codehash.md")
|
||||
section_titles = [c.get("section_title", "") for c in chunks]
|
||||
for title in section_titles:
|
||||
assert "不是标题" not in title
|
||||
|
||||
def test_adjacent_headings_empty_content(self, splitter):
|
||||
"""标题后紧接标题(空内容)."""
|
||||
md = "# 标题 A\n# 标题 B\n内容 B。"
|
||||
chunks = splitter.split(md, source_file="adjacent.md")
|
||||
assert len(chunks) >= 1
|
||||
|
||||
def test_only_headings_no_content(self, splitter):
|
||||
"""仅有标题无正文."""
|
||||
md = "# 只有标题\n## 没有内容"
|
||||
chunks = splitter.split(md, source_file="headingsonly.md")
|
||||
assert isinstance(chunks, list)
|
||||
@@ -0,0 +1,49 @@
|
||||
"""PDFSplitter 测试."""
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
pymupdf = pytest.importorskip("fitz", reason="pymupdf 未安装")
|
||||
|
||||
|
||||
class TestPDFSplitter:
|
||||
"""PDFSplitter 测试(需 pymupdf)."""
|
||||
|
||||
def test_split_simple_pdf(self, tmp_path):
|
||||
"""用 pymupdf 创建一个简单 PDF 并测试分块."""
|
||||
from src.core.splitters.pdf import PDFSplitter
|
||||
import fitz
|
||||
|
||||
pdf_path = tmp_path / "test.pdf"
|
||||
doc = fitz.open()
|
||||
# 插入纯 ASCII 文本避免 CJK 字体编码问题
|
||||
doc.new_page().insert_text((72, 72), "This is PDF document content.\n\nSecond paragraph text.")
|
||||
doc.save(str(pdf_path))
|
||||
doc.close()
|
||||
|
||||
s = PDFSplitter(max_size=500)
|
||||
chunks = s.split(str(pdf_path), source_file="test.pdf")
|
||||
assert len(chunks) >= 1
|
||||
all_text = "".join(c["content"] for c in chunks)
|
||||
assert "PDF document" in all_text
|
||||
assert "Second paragraph" in all_text
|
||||
|
||||
def test_empty_pdf(self, tmp_path):
|
||||
"""空 PDF(有页但无文字)返回空列表."""
|
||||
from src.core.splitters.pdf import PDFSplitter
|
||||
import fitz
|
||||
|
||||
pdf_path = tmp_path / "empty.pdf"
|
||||
doc = fitz.open()
|
||||
doc.new_page() # pymupdf 必须有至少一页才能保存
|
||||
doc.save(str(pdf_path))
|
||||
doc.close()
|
||||
|
||||
s = PDFSplitter()
|
||||
chunks = s.split(str(pdf_path), source_file="empty.pdf")
|
||||
assert chunks == []
|
||||
|
||||
def test_pdf_has_split_method(self):
|
||||
"""PDFSplitter 遵循 Splitter Protocol."""
|
||||
from src.core.splitters.pdf import PDFSplitter
|
||||
s = PDFSplitter()
|
||||
assert hasattr(s, "split")
|
||||
@@ -2,7 +2,8 @@ version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.14'",
|
||||
"python_full_version >= '3.15'",
|
||||
"python_full_version == '3.14.*'",
|
||||
"python_full_version < '3.14' and sys_platform != 'darwin' and sys_platform != 'linux'",
|
||||
"(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux')",
|
||||
]
|
||||
@@ -139,6 +140,47 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ast-serialize"
|
||||
version = "0.6.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/ad/0d70a3a2d6e01968d985415259e8ec7ad3f777903f9b1c1f3c8c44642c60/ast_serialize-0.6.0.tar.gz", hash = "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe", size = 61489, upload-time = "2026-06-30T20:02:55.555Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/12/3e5f575f156555547c250a8b0d1347517a3a20fc7f4492e9703a69d4f45e/ast_serialize-0.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264", size = 1177640, upload-time = "2026-06-30T20:02:06.708Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/a4/921a9e27951627983b0f368859ea00f8330a551dc0bf4c2fdcb11855a98b/ast_serialize-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc", size = 1168111, upload-time = "2026-06-30T20:02:08.366Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/00/69/950cf404de7b8782cf95e5c1237e25e2aa46177b287f39f9eeddf481fd6f/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516", size = 1227656, upload-time = "2026-06-30T20:02:09.843Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4c/a8/46f8f6a6479d9d2273980957bb091a506c55f5b95d3c029ee58518a78407/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089", size = 1227706, upload-time = "2026-06-30T20:02:11.367Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/b9/9ac415bda0a40e49eab8fea3b2741c19c98bb84d57d62c4cfc6230eb67be/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c", size = 1431705, upload-time = "2026-06-30T20:02:12.737Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/06/8807115d441444879f7561b5eede5ac18fc80392f11826d61ccf31f503b1/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8", size = 1249533, upload-time = "2026-06-30T20:02:14.571Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/c0/c2ba82ef9618650357d9421a1fdb27ffec862a7f57e8e2de82a3ccd11e12/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f", size = 1252619, upload-time = "2026-06-30T20:02:16.219Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/a7/fa31d52dd4102cede29fb9634e98d214129b2783b4f95528c6dc6a8f6587/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66", size = 1242983, upload-time = "2026-06-30T20:02:17.813Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/20/ddf742b5ad3c4bafd3466f2265037cfd99bc1b9a5ee46a5d58c90d523242/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b", size = 1296148, upload-time = "2026-06-30T20:02:19.146Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/cb/9f6f217cce8b3b632c5568b478d195a35e79dce4dbe309438cb89ba6ea4f/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a", size = 1403826, upload-time = "2026-06-30T20:02:20.696Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/f8/9d16d4f0107a183924425cc0e7618d8bf76f96b45afa9ff19f924ed1ad57/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554", size = 1502943, upload-time = "2026-06-30T20:02:22.034Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/dd/bbc1c38756350dddf7e24acae1c9482ef42051c267417e019aecc1ed4075/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f", size = 1497632, upload-time = "2026-06-30T20:02:23.517Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/7e/9daffefcf5b97e6bb4c3e0b3c024c1aee9722f23d3cf7cd2ff80d6fb4a40/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec", size = 1448858, upload-time = "2026-06-30T20:02:24.889Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/1f/f9baaab81a677ea0af7d2458cac2f94ebcc85958f8a3c15ba9d9e5dab653/ast_serialize-0.6.0-cp314-cp314t-win32.whl", hash = "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a", size = 1052600, upload-time = "2026-06-30T20:02:26.263Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/1f/41b535866519512d8cf6669cb2cff7823b7672bb6279c0333b4ff89d7d9f/ast_serialize-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24", size = 1095570, upload-time = "2026-06-30T20:02:27.639Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/64/e472fe3e3a2d33d874b987e8518aedf24562919e3b6161a4fa1797e89c0f/ast_serialize-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14", size = 1067267, upload-time = "2026-06-30T20:02:28.949Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/19/ac8348ae8711c9b5ae834634f635780cab62a0f5e6f988882e048b89c2ae/ast_serialize-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596", size = 1185367, upload-time = "2026-06-30T20:02:30.427Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/f6/ec7ec652c51db77c2f61d8573338e13e4704303265ccc658cb4031d9f354/ast_serialize-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad", size = 1178657, upload-time = "2026-06-30T20:02:31.964Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a", size = 1238620, upload-time = "2026-06-30T20:02:33.664Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/21/087957bba486242afc52f49b2d9e21c9dad00289356cf9efe67084015a9d/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081", size = 1236075, upload-time = "2026-06-30T20:02:34.936Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/04/78128bbb170071c2c72a210a181f1c00e11cc1cec60a8beef747b07f9201/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77", size = 1441348, upload-time = "2026-06-30T20:02:36.245Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/64/62fb99d6faf199b4c3e5b08a07136e9a0d7664bb249c6de3670e5b63e9b6/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790", size = 1258580, upload-time = "2026-06-30T20:02:37.53Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/87/b4d6c38e0ccd5e85dc54cecdf933a152c60b28fe5d993a6d8a72fa6d5896/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7", size = 1261693, upload-time = "2026-06-30T20:02:39.123Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/4b/3676ca2191f39bafb75f93f99b2f429ec464586158fece2165f3572805dc/ast_serialize-0.6.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b", size = 1252517, upload-time = "2026-06-30T20:02:40.511Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/58/494ef8c4b4acb2f4a265ac934caf45f792a08fe27d6b853de35ad991941a/ast_serialize-0.6.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b", size = 1304843, upload-time = "2026-06-30T20:02:41.961Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/f2/13736d920ab3d49bbee80ef1a277dd7b7aaf3b3545efd9d2a8114fe05525/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32", size = 1413698, upload-time = "2026-06-30T20:02:44.179Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/5a/e046f3899e2acba4677d7427b76431443a1aa1a0e583dfb05b55b69d55cf/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b", size = 1512209, upload-time = "2026-06-30T20:02:45.584Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/c7/e42aaca7bb2d22a7c06d5a8c7930086c5a334e93d716e6fa5e6647a4515f/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a", size = 1508464, upload-time = "2026-06-30T20:02:46.942Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/93/5524a3dc6c3f593de3228ed9cbef73afa047625b7000ec21b7f58e6eb4d4/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e", size = 1457164, upload-time = "2026-06-30T20:02:48.294Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/c0/36a6ffb4d653cf621427b4c4928671f53ad800c453474de2b82564a44ad9/ast_serialize-0.6.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b", size = 863014, upload-time = "2026-06-30T20:02:49.742Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/c7/7d5ad8b49e1278e1c2a1e0274bd7850560b3f09313aa00c13bc8d5544792/ast_serialize-0.6.0-cp39-abi3-win32.whl", hash = "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1", size = 1063165, upload-time = "2026-06-30T20:02:50.98Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/ae/6710c14ecb276031cf10249f6adf5a59e2d3fdb3b5183bd59f70524067ee/ast_serialize-0.6.0-cp39-abi3-win_amd64.whl", hash = "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e", size = 1101444, upload-time = "2026-06-30T20:02:52.554Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "26.1.0"
|
||||
@@ -214,6 +256,19 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
dependencies = [
|
||||
{ name = "soupsieve" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "build"
|
||||
version = "1.5.0"
|
||||
@@ -357,6 +412,69 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.15.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/8b/adeb62ea8951f13c4c7fef2e7a85e1a06b499c8d8237ea589d496029e53f/coverage-7.15.0.tar.gz", hash = "sha256:9ac3fe7a1435986463eaa8ee253ae2f2a268709ba4ae5c7dd1f52a05391ad78f", size = 925362, upload-time = "2026-07-02T13:10:50.535Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/04/145a3748098bcc86b631a85408d2c3dc5c104e0bd86d605468239b25b6c4/coverage-7.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5be4caf3b28836f078abe700f8944dac4a65d78f16d6c600c89cb624e5535782", size = 220863, upload-time = "2026-07-02T13:09:25.371Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/5c/4ed55708fed2c64b63c9bc5715daef670872202101938869b7fe5d5fbb8f/coverage-7.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dd58ad1404704303ca8d4f4b8a1095e7cbc7040ef17a66df1e6619aa10176430", size = 221230, upload-time = "2026-07-02T13:09:26.897Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/19/3a80b97d3b2a5c77a01ae359c6bed20c13738fe3d9380f08616d4fec0281/coverage-7.15.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bbcbb317c2e5ded5b21104af81c29f391be2af98d065693ffbe8d23949b948e5", size = 252227, upload-time = "2026-07-02T13:09:28.543Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/fa/b70062750686bd7da454da27927622f48bbac6990ac7a4c4a4653e7b0036/coverage-7.15.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:27f31ecb458da3f859aab3f15ada871eb7a7768807d88df4a9f186bb17737970", size = 254823, upload-time = "2026-07-02T13:09:30.177Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/09/dad6a75a2e561b9dc5086a8c5257a7591d584246f67e23e70d2995b89ab6/coverage-7.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fb759be317fdc62e0f56bffdf61cfcb45c7761ad6b71e3e583e71a67ae753c", size = 256059, upload-time = "2026-07-02T13:09:31.979Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/e7/b5d2941fa9564573d44b693a871ff3156f0c42cbefe977a09fa7fdc59971/coverage-7.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5cf007add5ab4bb8fa9f4c77e3732127c9e6cad501d7db43355fbfafca0be84", size = 258190, upload-time = "2026-07-02T13:09:34.035Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/1d/8e895bcde3c57ccd46d896dda5f2b3d5df761a1b0c6c9d450d175dedc632/coverage-7.15.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc78d9843bd576fbe2118248258d485e968dc535f95ed504a7b0867ba9b51389", size = 252456, upload-time = "2026-07-02T13:09:35.765Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/4c/f6997da343ddeb959be82c3b05322793f92c071ad45f7cb8a96336e2dd5f/coverage-7.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a263060f1de0b4b74b4e089c2a70b8003b3781c733329a9c8fd54995328f9950", size = 254192, upload-time = "2026-07-02T13:09:37.445Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/17/27/a0bc09d032267b9da89d95a2d874cfbef2a5aebbf0e87cf7aba221d79a99/coverage-7.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c48decf16e0dfd5b049c7d5e82200c23c08126719142998d4f172444e3d0529e", size = 252153, upload-time = "2026-07-02T13:09:39.422Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/c0/77fc233d9fba07b244c40948c53fe27308b8f21732fb3417f87fbd6fd992/coverage-7.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:08fb028000ed0aaa0a4cbdfbb98be7cb42f370db973fbbb469733505ab20e13e", size = 256310, upload-time = "2026-07-02T13:09:41.006Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/24/601cecfb5825becacb8d45219a018a3b55b9dbaec624efdb0ea249d08be2/coverage-7.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb7dc0c3b7d8a1077abea0b8546ebc5e26d6ef6ecefc2f0f5ad2b8a53bdad837", size = 251974, upload-time = "2026-07-02T13:09:42.733Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/1e/6f45e5a5b3d5484318d368702af6716b5ab8913b0428bec981a562fcf296/coverage-7.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cb3602054ccbe9f0d8c2dc04bbeba90d5719236e2cd06e042ddd6d3fc7b6e37", size = 253745, upload-time = "2026-07-02T13:09:44.376Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/db/4df027a77bd11d0e527f44c53557c76e54ad027413d0304252ea3a78d67e/coverage-7.15.0-cp313-cp313-win32.whl", hash = "sha256:0bf781da64326b677be344df505171435b6f58716108606621d5d27d964fff8b", size = 222902, upload-time = "2026-07-02T13:09:46.122Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/10/0355894d34e231f2c5449e71287e81a50793a325df2e2b027b7bcd9dfd19/coverage-7.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:2c57a275078ee3fa185f83e400f765bc764a549de66d99b47881645cbd4ea629", size = 223444, upload-time = "2026-07-02T13:09:47.687Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/ef/bb725f263befaaff851203ab338e68af15e195d7f7b5f323162532d9b6a8/coverage-7.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:3812c61afc6685c7999b39320779ab8f43b7a3081fdb0def39976e56fbdb9a21", size = 222839, upload-time = "2026-07-02T13:09:49.717Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/9c/1e3ca54f72a3185ece06c58d871099898c48f0ed6430d17b6ab75f0d180a/coverage-7.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:41cb79af843222e11da87127ad0ecbfa878abadd0f770a4a99391a27d3887324", size = 220906, upload-time = "2026-07-02T13:09:51.339Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/37/f718613d83b274880382f6b67e78f3802549ae39b0b3e65ae5b5974df56e/coverage-7.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d2008989ef8fe54188d3f3bfa2e3099b025af11e90a6a1b9e7dc433d04263d8", size = 221239, upload-time = "2026-07-02T13:09:53.138Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/ce/22bae91e0b75445f68d365c7643ed0aa4880bbf77450ee74ca65bdae53a7/coverage-7.15.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:769e8ece11a596315ebf5aa7ec383aeeed016c091d2bf6363ffb996d41529092", size = 252286, upload-time = "2026-07-02T13:09:54.996Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/1e/bec5e32aa508615d9d7a2790effb25fb4dc28606e995816afe400b25ece3/coverage-7.15.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:65a6b6164ee5c39e2f3803f314292d6c61a607ba7fee253d1e03c42dc3903502", size = 254789, upload-time = "2026-07-02T13:09:56.678Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/17/29/0e865435b4354e4a7c03b1b7920046d31d0a273d55decefea27e011cb9bf/coverage-7.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75128817f95a5c45bb01d65fd2d8b9cb54bbe03d81608fb70e3e14b437ad56c2", size = 256135, upload-time = "2026-07-02T13:09:58.343Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/ff/33a870b58a13325d62fc0a6c8f01fa0ff667cef60c7498e2382a147dfa18/coverage-7.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9887bb428fe2d4cd4bee89bac1a6c9932f484afd5b36fbd4ff6ea5f825bb1f5e", size = 258449, upload-time = "2026-07-02T13:10:00.057Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/7b/6fffe596bf3ddba8462758d02c5dad730fd91055a6634aa2e4226229181a/coverage-7.15.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0bfc0be1f702042207a93a00523b1065ee1fe951e96edf311581c0bbc2e34888", size = 252313, upload-time = "2026-07-02T13:10:01.946Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/1b/11468dd6c1676ab831a70cb9a8d4e198e8607fa0b7220ab918b73fe9bfbd/coverage-7.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f64627d55def5a43282d70e08396672692f77e4da610a5bb8bb4060b432b6859", size = 254142, upload-time = "2026-07-02T13:10:04.065Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/41/29328e21d16b1b95092c30dd700e08cf915bd3734f836df8f3bdb0e8fa9f/coverage-7.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2c6f0fa473003905c6d5bac328ee4eba9fbea654f15bc24b8a3274b23363fa99", size = 252108, upload-time = "2026-07-02T13:10:06.11Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/de/05ccfb990439655b35afbfd8e0d13fe66677565a7d4eb38c3f5ef2635e1c/coverage-7.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2bcf9afaf064172c6ec3c58a325a9957ad1178c05dd934e25f253321776e0676", size = 256385, upload-time = "2026-07-02T13:10:08.141Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/0e/486828a3d2695ea7a2609f17ff572f6b01905e608379440a11da4b8dffbe/coverage-7.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:baf06bc987115d6fb938d403f7eab684a057766c490367999a2b71a6883110c6", size = 251923, upload-time = "2026-07-02T13:10:10.179Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/c7/03582b6715f078e5e558354c87616d945b9894cda2dace8e4009b17035e4/coverage-7.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f0405f2ff97b1c4c0e782cb32e02f32369bcf2e6b618b591d67e1ea754575dfe", size = 253580, upload-time = "2026-07-02T13:10:12.052Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/db/dc/9e578bbaf2ecb4959a81b7e7601ad8cca772cba2892e8d144cb749b4a71a/coverage-7.15.0-cp314-cp314-win32.whl", hash = "sha256:ab282853ed5fbd64bbb162f19cb8fcb7087187508a6374b4f9c34ec1577c4e8f", size = 223107, upload-time = "2026-07-02T13:10:13.994Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/3e/c8c3b75d8dbe0e35f7b0cc3ff5e949fc59500f70b21d0398813f66740664/coverage-7.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bb3040e9f4bbe26fcb0cd7cc85ac63e630d3f3a9c74f027abf4caa27e706663", size = 223597, upload-time = "2026-07-02T13:10:15.906Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/bc/3cbc9fb036eb388519bccd521f783499c39b64256013fbc362782f196fe1/coverage-7.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:346771144d34f7fa84ec28386f78e0f31653f33cf35e19d253d5b35f9e8201da", size = 223020, upload-time = "2026-07-02T13:10:17.844Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/00/199c4a8d656dff63102577a056c0fce2ff6a79e40adac092fc986c49cbf1/coverage-7.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d34a010905fb6401324ba016b5da03d574967f7b21ce48ea41e66f0f1f95f641", size = 221638, upload-time = "2026-07-02T13:10:19.703Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/8e/9d0092c96a3d3a26951ecc7020826aa57bcb1b119ca81acbba996884ab13/coverage-7.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bb25d825d885ca8036795dacfc3924d33091fc76d71ebc99420c6b79e77d96fa", size = 221903, upload-time = "2026-07-02T13:10:21.514Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/b4/c0ca3028f42c9a08e51feb4561ef1192e5de99797cd1db5b04590c215bda/coverage-7.15.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:94c9686bfe8a9a6810297aecbd99beaa3445f9e8dc2f80b1382cca0d86b64461", size = 263267, upload-time = "2026-07-02T13:10:23.261Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/aa/a375e3846e5d3c013dc600b2a3231089055c73d77f5393dd2192a8d64da6/coverage-7.15.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9bd671c25f9d85f09d7ec481d0e43d5139f486c06a37139847a7ce569788af72", size = 265390, upload-time = "2026-07-02T13:10:25.152Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/e1/5783cdabb797305e1c9e4809fea496d31834c51fa772514f73dc148bcfc9/coverage-7.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:110cbdf8d2e216577312cf06ccf85539c0e5a5420ef747e4a4719b5e483c88cd", size = 267811, upload-time = "2026-07-02T13:10:27.249Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/31/96d8bbf58b8e9193bc8389574a91a0db48355ee98feb66aa6bf8d1b32eea/coverage-7.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c5d4619214f1d9993e7b00a8600d14614b7e9d84e89507460b126aa5e6559e5", size = 268928, upload-time = "2026-07-02T13:10:29.242Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/7a/5294567e811a1cb7eda93140c628fa050d66189da28da320f93d1d815c73/coverage-7.15.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:781a704516e2d8346fbbd5be6c6f3412dd824785146528b3a01816f26c081007", size = 262378, upload-time = "2026-07-02T13:10:31.107Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/3f/3f48538421f899f28946f90a3d272136a4686e1abf461cc9249a783ee0f3/coverage-7.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd4a1b44bcb65ee29e947ac92bbee04956df3a6bfc6143641bb6cae7ede00fc9", size = 265263, upload-time = "2026-07-02T13:10:32.942Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/d3/092df15efcab8a9c1467ee960eb8019bbad3f9300d115d89ea6195f369ff/coverage-7.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0e4950c9d6d3e39c64c991814ff315e2d0b9cb8152363594212c9e55208c0a8f", size = 262866, upload-time = "2026-07-02T13:10:35.104Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/ab/0254d2b88665efb2c57ad368cc77ab5de3435bd8d5add4729c1b0e79431e/coverage-7.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:fe9c87ff42e5472d80d21704972e1f96e104a0a599d77c5e35db5a3c562e2571", size = 266599, upload-time = "2026-07-02T13:10:37.05Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/79/1cfa4023e489ce6fbc7be4a5d442dbc375edb4f4fda39a352cedb53263c2/coverage-7.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f00d5ae1dd2fe13fb8186e3e7d37bcbd8b25c0d764ff7d1b32cef9be058510a8", size = 261714, upload-time = "2026-07-02T13:10:38.966Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/eb/fee5c8665656be63f497418d410484637c438172568688e8ac92e06574e7/coverage-7.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:363ab38cc78b615f11c9cac3cf1d7eef950c18b9fdedfb9066f59461dcf84d68", size = 264025, upload-time = "2026-07-02T13:10:40.789Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/99/63005db722f91edc81abc16302f9cc2f6228c1679e46e15be9ae144b14d0/coverage-7.15.0-cp314-cp314t-win32.whl", hash = "sha256:54fd9c53a5fafff509195f1b6a3f9be615d8e8362a3629ff1de23d270c03c86b", size = 223413, upload-time = "2026-07-02T13:10:42.597Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/e8/2bc6181c4fb06f1a6b981eb85330cc57bfad7e3f710fc9c9d350013ba228/coverage-7.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:87b47553097ba185ed964866078e7e63adea9f5f51b5f39691c34f30afd21080", size = 224245, upload-time = "2026-07-02T13:10:44.47Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/b8/4d959bf9cc45d0cfed2f4d35cafcab978cdb6ea02eb5100009cd740632a3/coverage-7.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aeefb2dd178fe7eee79f0ad25d75855cb35ee9ed472db2c5ea06f5b4fd00cec5", size = 223558, upload-time = "2026-07-02T13:10:46.368Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/30/21b2ad45959cd50e909e02ebac1e30b4ceb7162e91c11d4c570223a458b7/coverage-7.15.0-py3-none-any.whl", hash = "sha256:56da6a4cbe8f7e9e80bd072ca9cefe67d7106a440a7ec06519ec6507ac94ad19", size = 212632, upload-time = "2026-07-02T13:10:48.641Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "durationpy"
|
||||
version = "0.10"
|
||||
@@ -366,6 +484,19 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ebooklib"
|
||||
version = "0.20"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
dependencies = [
|
||||
{ name = "lxml" },
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/85/322e8882a582d4b707220d1929cfb74c125f2ba513991edbce40dbc462de/ebooklib-0.20.tar.gz", hash = "sha256:35e2f9d7d39907be8d39ae2deb261b19848945903ae3dbb6577b187ead69e985", size = 127066, upload-time = "2025-10-26T20:56:20.968Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/ee/aa015c5de8b0dc42a8e507eae8c2de5d1c0e068c896858fec6d502402ed6/ebooklib-0.20-py3-none-any.whl", hash = "sha256:fff5322517a37e31c972d27be7d982cc3928c16b3dcc5fd7e8f7c0f5d7bcf42b", size = 40995, upload-time = "2025-10-26T20:56:19.104Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.139.0"
|
||||
@@ -681,6 +812,56 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiter"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "joblib"
|
||||
version = "1.5.3"
|
||||
@@ -738,6 +919,117 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/2c/5c160dbdef7123f8cc97fd8ece7e0198627a426a2a49614845e9086feb8d/kubernetes-36.0.2-py2.py3-none-any.whl", hash = "sha256:faf9b5241b58de0c4a5069f2a0ffc8ac06fece7215156cd3d3ba081a78a858b6", size = 4617568, upload-time = "2026-06-01T18:20:28.737Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "librt"
|
||||
version = "0.13.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/3b/18e7b63255297a2bdc9c25c8d6d4ca8eca9f63aceb1252c0f7427ac7099e/librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3", size = 151027, upload-time = "2026-07-08T12:25:19.638Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/68/e2248452c00d1a03b45fee1752cdc8f790a476efd2402b75181da88a9e61/librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c", size = 155152, upload-time = "2026-07-08T12:25:20.851Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/16/52b1c99bf19057a062aac39c900cbb81499f6f75d6c537c14463d247ba78/librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c", size = 502499, upload-time = "2026-07-08T12:25:22.055Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/54/b811151805c795f55e0dedee6ec687b75f9982a8105d240ea3910737a77b/librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b", size = 496108, upload-time = "2026-07-08T12:25:23.296Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/f8/094d6b2bd93f3fdaa54db54cc788c4a365333bddad65ab02e04da0b1d004/librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9", size = 531576, upload-time = "2026-07-08T12:25:24.648Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/40/541733d5755824f968f7ec39d78ffbd75d145964157ae5e69a09ec6d7326/librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db", size = 524390, upload-time = "2026-07-08T12:25:25.898Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/b5/255673cfdbf5ba663339d36cd863c897289ab4337577e19f9405ce059f36/librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6", size = 543053, upload-time = "2026-07-08T12:25:27.436Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/11/ab5005e9c9850710f21e354201bf090646349d3fabf5f951eaf70235729e/librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7", size = 546387, upload-time = "2026-07-08T12:25:28.65Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/04/a5d7ce1d1df1afd15ca283dcdf7530ac073e12d69ae8c40879dda96f7868/librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1", size = 535970, upload-time = "2026-07-08T12:25:30.171Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/76/927e267a6daa290174ac281b23c9804c8829b042ade9c6f24a065f540958/librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a", size = 573582, upload-time = "2026-07-08T12:25:31.507Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/24/b6c5213efe39c19f9e13605644d0cf063b4ddaa33ac2e45b088e23a70e2e/librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628", size = 82189, upload-time = "2026-07-08T12:25:32.675Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4c/00/d29736be177a906ac0b84a5b04b4fbfa22c776dc2f366de4172b0f968c08/librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927", size = 106193, upload-time = "2026-07-08T12:25:33.692Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/ac/aff6fb45393cb8912f39dfb156ef6b2d1cadb207ff465fc8f66141054be8/librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650", size = 126962, upload-time = "2026-07-08T12:25:34.769Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/3a/d68cb2b334d53fd30fac81d3a489ce4ba0d9506f4df43fcf676b68352b19/librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566", size = 112127, upload-time = "2026-07-08T12:25:35.981Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818, upload-time = "2026-07-08T12:25:37.203Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071, upload-time = "2026-07-08T12:25:39.399Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168, upload-time = "2026-07-08T12:25:40.641Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/bc/b8550c75775127fd31a5f20e8775997f7b527ad661fc8ddccd7497c064f7/librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180", size = 491054, upload-time = "2026-07-08T12:25:41.905Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/14/4d0204867623df3f33f86efd3d3692ba5e01321443f4d6eab35a22697618/librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6", size = 523006, upload-time = "2026-07-08T12:25:43.327Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/0a/c45fc9a260934696bace1ac5df1e148ac92bd71767aee3bf7cd7a4534f4c/librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9", size = 515058, upload-time = "2026-07-08T12:25:44.541Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/0a/50c5ce45b326854ef8fa6ae4c36cf5142e5c55315eaf9e51d0ae73ac4da3/librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007", size = 534025, upload-time = "2026-07-08T12:25:45.825Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/2d/08c413c8f93fc13b8103624fce38e5caa86cd08cbbc8465870ab287af54b/librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0", size = 540557, upload-time = "2026-07-08T12:25:47.059Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/c1/93af71fb4a364952210051811dd4e40174e79656b050c89cacac18af3330/librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1", size = 523201, upload-time = "2026-07-08T12:25:48.392Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/6e/9766f07b676a4889d9f8bc2864e9ba5fff165653143ef4dda7df6aa34d16/librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d", size = 565740, upload-time = "2026-07-08T12:25:49.678Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/1e/664e3472ce2b6e10e9b83f29d4a36eb982ff6b5a169ae7567bba3a4c4ff5/librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16", size = 81611, upload-time = "2026-07-08T12:25:50.857Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/d4/8582a4d65e2234673685e07309d02c230b28a85724eb0acbf13f019b7f6e/librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37", size = 100106, upload-time = "2026-07-08T12:25:52.03Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/ce/0cb99efe6086b46cd985dc26672166fae312a239690e75871f7fafbd3fc5/librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39", size = 121209, upload-time = "2026-07-08T12:25:53.166Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/85/4f3ccb083a3c9b0d42e223acdb3c3f507953324a59cdcab4826e8e2e3b89/librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6", size = 106404, upload-time = "2026-07-08T12:25:54.253Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/77/333191499538c8e8189de7a4cba8e6f49ee949fd6d6e6324b21fd1522466/librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5", size = 159231, upload-time = "2026-07-08T12:25:55.432Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/9e/2aa83758f22c278b837a1d8025898434ce2b8bff36678d5330ecaef56dff/librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46", size = 161300, upload-time = "2026-07-08T12:25:56.585Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/c0/86791e936553ca763d6b3c2fb4d31d596cd00e14fa631c283a40ba01559a/librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e", size = 582056, upload-time = "2026-07-08T12:25:58.144Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/d3/a9ec15984a185e000c4d2a16ba28bd623124ad4c38a10974c7ff78e3a893/librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a", size = 562758, upload-time = "2026-07-08T12:25:59.544Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/af/dbe36b78b19c06a55097f99305e4ea9458e2273e6ae16a3cbecaad7ee978/librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22", size = 602095, upload-time = "2026-07-08T12:26:00.991Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/a8/2966891b4dd2830f5203fbee92ac2c4947653a2390ba73dfa44244fad025/librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c", size = 593452, upload-time = "2026-07-08T12:26:02.352Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/f5/4df8bfc8405ecf8c0d525b4d69636f694bdd8620b313ec8b76e54a5926cc/librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0", size = 623729, upload-time = "2026-07-08T12:26:04.294Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/13/9ac202dffc8db06f75d06c08c2f9f6ff054be67d21272dcc078fa1cc0c57/librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04", size = 617077, upload-time = "2026-07-08T12:26:05.845Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/f0/ebe38610716aee5cb28efd95089bb90192096179802779381e1c5dcf239c/librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61", size = 599561, upload-time = "2026-07-08T12:26:07.21Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/5c/c2e72e236fff7abc716d5b1753b8b8cd3ea85ac46fe17d2e7c51d4e1c723/librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9", size = 645511, upload-time = "2026-07-08T12:26:08.562Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/99/6203ce619dee940d6bfbe099ec3fe4be00a68e9d60f70abf906cf124fe66/librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18", size = 104357, upload-time = "2026-07-08T12:26:09.828Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/dd/843b6314087c41657c7036d7914d8f294bdf9b580aa8513ea0588c8e9a3d/librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259", size = 126998, upload-time = "2026-07-08T12:26:10.975Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "6.1.1"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.2.0"
|
||||
@@ -818,25 +1110,53 @@ dependencies = [
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
all = [
|
||||
{ name = "beautifulsoup4" },
|
||||
{ name = "ebooklib" },
|
||||
{ name = "openai" },
|
||||
{ name = "pymupdf" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
dev = [
|
||||
{ name = "httpx" },
|
||||
{ name = "mypy" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
epub = [
|
||||
{ name = "ebooklib" },
|
||||
]
|
||||
html = [
|
||||
{ name = "beautifulsoup4" },
|
||||
]
|
||||
pdf = [
|
||||
{ name = "pymupdf" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "beautifulsoup4", marker = "extra == 'html'", specifier = ">=4.12.0" },
|
||||
{ name = "chromadb", specifier = ">=0.5.0" },
|
||||
{ name = "ebooklib", marker = "extra == 'epub'", specifier = ">=0.18" },
|
||||
{ name = "fastapi", specifier = ">=0.115.0" },
|
||||
{ name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27.0" },
|
||||
{ name = "markdown-it-py", specifier = ">=3.0.0" },
|
||||
{ name = "md-vector-db", extras = ["pdf", "html", "epub"], marker = "extra == 'all'" },
|
||||
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.13" },
|
||||
{ name = "openai", marker = "extra == 'all'", specifier = ">=1.0.0" },
|
||||
{ name = "pymupdf", marker = "extra == 'pdf'", specifier = ">=1.24.0" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
|
||||
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.2.2" },
|
||||
{ name = "pyyaml", specifier = ">=6.0" },
|
||||
{ name = "requests", marker = "extra == 'all'", specifier = ">=2.31.0" },
|
||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.0" },
|
||||
{ name = "sentence-transformers", specifier = ">=3.0.0" },
|
||||
{ name = "typer", specifier = ">=0.12.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
provides-extras = ["dev", "pdf", "html", "epub", "all"]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
@@ -1003,6 +1323,53 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "2.2.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
dependencies = [
|
||||
{ name = "ast-serialize" },
|
||||
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "mypy-extensions" },
|
||||
{ name = "pathspec" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/7e/be536678c6ae49ef058aba4b483d8c7bc104f471479016066f345bc1f5f8/mypy-2.2.0.tar.gz", hash = "sha256:2cdd99d48590dce6f6b7f1961eda75386364398fcdaad86923bc0f0231bf9baf", size = 3950939, upload-time = "2026-07-08T01:37:27.335Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/5f/2d7a9ac5646274cd6e77ce3abcc2a9ece760c2b21f4c4b9f301711e07855/mypy-2.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6968f27347ef539c443ddfd6897e79db525ddb8c856aa8fbf14c34f310ca5193", size = 14931618, upload-time = "2026-07-08T01:33:51.702Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/8a/1adaa7caaa104f87021b1ac71252d62e646e9b623d77900ac7a0ae252bf3/mypy-2.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3df226d2a0ae2c3b03845af217800a68e2965d4b14914c99b78d3a2c8ae23299", size = 13857718, upload-time = "2026-07-08T01:35:27.555Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/15/b11586b5aebbb82213e297fc30a6fcf3bed6a9deea3739cd8dd87621f3fd/mypy-2.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc53553996aca2094216ad9306a6f06c5265d206c1bcb54dd367560bd3557825", size = 14059704, upload-time = "2026-07-08T01:33:57.363Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/db/071e05ab442596bdf7a845e830d5ef7128a0175281038245b171a6b16873/mypy-2.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:996abf2f0bebf572556c60720e8dc0cf5292b64060fa68d7f2bc9caa48e01b6f", size = 15128719, upload-time = "2026-07-08T01:33:33.855Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/1c/c4b84eafb85ee315da72471523cc1bf7d7c42164085c42333601da7a8817/mypy-2.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ec287c2381898c652bf8ff79448627fe1a9ee76d22005181fac7315a485c7108", size = 15378692, upload-time = "2026-07-08T01:34:10.468Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/a4/59a0ee94877fdfe2958cec9b6add72a75393063c79cb60ab4026dd5e10c2/mypy-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2c967a7685fa93fcf1a778b3ebe76e756b28ba14655f539d7b61ff3da69352a", size = 11150911, upload-time = "2026-07-08T01:35:21.603Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/e4/6a9144be50180ed43d8c92de9b03dff504daa92b5bcc0353e8960799a23b/mypy-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:d59a4b80351ec92e5f7415fcdd008bd77fcbefc7adf9cbc7ffe4eb9f71617734", size = 10125389, upload-time = "2026-07-08T01:36:36.164Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/73/32/0aa8d8d197023ca6040f7b25a486cb47037b6350b0d3bae657c8f85fb43f/mypy-2.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1f6c3d76853071409ac58fc0aadfb276a22af5f190fdaa02152a858088a39ebd", size = 14926083, upload-time = "2026-07-08T01:36:02.365Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/7c/35bbe0cb10e6699f90e988e537aaf4282a6c16e37f58848a242eb0a98bde/mypy-2.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bdaa177e80cc3292824d4ef3670b5b58771ee8d57c290e0c9c89e7968212332c", size = 13879985, upload-time = "2026-07-08T01:36:31.093Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/0c/1597fbebd873e9b63452317740ae3dd32692cec5da180cc65acd96cd28cf/mypy-2.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80923e6d6e7878291f537ee11052f974954c20cb569798429a5dc265eb780b47", size = 14076883, upload-time = "2026-07-08T01:34:21.789Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/35/2ec021a83ec01b5d522639f78d8b36adade7fa4821db0f48fd6d82e861f3/mypy-2.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f24bd465a09077c8d64be8f19a6646db467a55490fd315fe7871afe6bb9645", size = 15103567, upload-time = "2026-07-08T01:36:47.717Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/3a/8cb3529f6d6800c7d069935e5c83a05d80263847b8a947cf6b0b16a9e958/mypy-2.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:db34595464869f474708e769413d1d739fc33a69850f253757b9a4cc20bc1fec", size = 15354641, upload-time = "2026-07-08T01:36:41.592Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/4d/320bc9a9553f8a9db5e847ec5ded762ef7ed7403c76c4ba2e8181c80e2f0/mypy-2.2.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b48092132c7b0ef4322773fecae62fc5b0bc339be348badeec8af502122a4a51", size = 7694355, upload-time = "2026-07-08T01:37:03.291Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/05/bf3b349e2f885cd3aab488111bb9049439c28bc028dac5073350d3df8fbe/mypy-2.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:6fc0e98b95e31755ca06d89f75fafa7820fbb3ea2caace6d83cba17625cd0acb", size = 11329146, upload-time = "2026-07-08T01:35:38.318Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/a5/558b06e6cfe17ab88bb38f7b370b6bc68a74ba177c9e138db9748e422d2d/mypy-2.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:bc73a5b4d40e8a3e6b12ef82eb0c90430964e34016a36c2aff4e3bfe37ba41f0", size = 10316586, upload-time = "2026-07-08T01:36:25.458Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/21/f0b96f19a9b8ba111a45ffbe9508e818b7f6990469b38f6888943f7bfd3a/mypy-2.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6257bd4b4c0ae2148548b869a1ff3758e38645b92c8fe65eca401866c3c551c1", size = 15922565, upload-time = "2026-07-08T01:35:56.282Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/f2/1dbcb20b0865d5e992541450a8c73f2fcc90f8bd7d8a4b81313e16934870/mypy-2.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dfa22b3ae862ac1ce76f5976ddd402651b5f090bcfd49c6d0484b8983a29eaf8", size = 14816515, upload-time = "2026-07-08T01:34:58.167Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/a2/18cce9c7d5b4d14010d1f13836da11b234dda917b17ca8671fc32c136997/mypy-2.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30a430bf26fe8cf372f3933fbd83e633d6561868803645a20e4e6d4523f52a3b", size = 15272246, upload-time = "2026-07-08T01:37:08.72Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/71/24d720c7924829bd675cbde2d0fa779f50abf676ca617f53d6a8bfef5fa7/mypy-2.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d58655a60e823b1a4c9ebcda072897fb0193c2f3e6f8e7c433e152aa4cb00233", size = 16226295, upload-time = "2026-07-08T01:34:51.861Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/5e/785730990fc863ad8340b4ab44ac4ca23270aecff92c180ccdf27f9f5869/mypy-2.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d4452c955caf14e28bb046cbd0c3671272e6381630a8b81b0da9713558148890", size = 16493275, upload-time = "2026-07-08T01:35:09.337Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/33/55b1edf16f639f153972380d6977b81f65509c5b8f9c86b58b94b7990b03/mypy-2.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:68f5b7f7f755200f68c7181e3dfb28be9858162257690e539759c9f57721e388", size = 11749038, upload-time = "2026-07-08T01:35:50.071Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/36/67424748a4e65e97f0e05bf00df379dfb6c2d817f82cc3a4ce5c96d99beb/mypy-2.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:78d201accfafce3801d978f2b8dbbd473a9ce364cc0a0dfd9192fe47d977e129", size = 10704254, upload-time = "2026-07-08T01:37:17.971Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/cb/142c2097ca02c0d295b00625ff946808bdda65acda17d163c680d8a6a474/mypy-2.2.0-py3-none-any.whl", hash = "sha256:ecc138da861e932d1344214da4bae866b21900a9c2778824b51fe2fb47f5180e", size = 2726094, upload-time = "2026-07-08T01:34:00.075Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "narwhals"
|
||||
version = "2.23.0"
|
||||
@@ -1215,6 +1582,25 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/f6/2bac21f722aa45d876d4a51f26bd0ef30e704068a3cd5021a5a7cd784271/onnxruntime-1.27.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:370d211e1ceeac4cd5f45301655463ac59e27cdc74d9f7aeb2d19ff4b7a76715", size = 18670781, upload-time = "2026-06-15T22:43:17.151Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "2.45.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "distro" },
|
||||
{ name = "httpx" },
|
||||
{ name = "jiter" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/60/d4219875289b11d2c2f7da93c36283da224a2e55865ed865ab64e0ce9217/openai-2.45.0.tar.gz", hash = "sha256:10d34ca9c5643bce775852fddbfc172505cb1d4de1ccd101696c3ecff358765d", size = 1109653, upload-time = "2026-07-09T18:02:44.091Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/b0/2291689e3ec4723fbf5bbf3b54afcd7b160f9ddc98ca7aedfd0132af5677/openai-2.45.0-py3-none-any.whl", hash = "sha256:5df105f5f8c9b711fcb9d06d2d3888cebc82506db216484c14a4e53cdf651777", size = 1629470, upload-time = "2026-07-09T18:02:42.21Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-api"
|
||||
version = "1.43.0"
|
||||
@@ -1352,6 +1738,15 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "1.1.1"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
@@ -1641,6 +2036,23 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pymupdf"
|
||||
version = "1.28.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/e9/6d6c5d6c0a3551bffd47681a6240caf941727f195b45593cf20ab36f018f/pymupdf-1.28.0.tar.gz", hash = "sha256:e53f3567403a92da15caa9e7ae0164327fff48817e9f40175367fb9de524258d", size = 87637751, upload-time = "2026-06-29T09:08:47.547Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/b7/88043e38cc7529de070f0c9bd267fa258035cca0b4ad5260536b994594a7/pymupdf-1.28.0-cp310-abi3-macosx_10_15_x86_64.whl", hash = "sha256:892b89ba88e8f98b53133b62877a9dc9b5e7dc6a4aeb837b612db56a8d2e03ac", size = 24597385, upload-time = "2026-06-29T09:03:30.608Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/33/f4/23775bbda0781b61fc398cc75079a2b0e64696d8fcf93271748883e9627e/pymupdf-1.28.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:4d692dcf44d3566ae96bc6f6346c6ad432274a29ba617bf7a9fe18009e24adb4", size = 23828292, upload-time = "2026-06-29T09:03:46.129Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/f5/bf75fc7a415722f8b33662054f82d88520c0cbfd4c36d0e08aeaec605e49/pymupdf-1.28.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:47a5c29ed4eb0744de9c4e37bb49b1259b18d4d75fcc8a7c130f7c9fa15956f6", size = 25045507, upload-time = "2026-06-29T09:04:03.86Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/69/5d12c9f1f2d76f28383d6110a069c79fbfced5a4f97bb1ee6e8354f52bb7/pymupdf-1.28.0-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:44f0973f5e5edbaec95bc34b64e71d1959d4ee90b1328de1b4f4f5b4fa78673f", size = 25716599, upload-time = "2026-06-29T09:04:19.367Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/b4/ec0e017bc42857cc86bd651441dbc41cc18be48d4698ecd27aac491e0c9a/pymupdf-1.28.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4d61ec323a706e153a12e262e51febfb43eeaa20977785ace135d18d48bcdc83", size = 25940489, upload-time = "2026-06-29T09:04:36.624Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/86/f831fef09013f33b3c9c09fb3923f2ff53e1e437f6ace14b8ae46392f558/pymupdf-1.28.0-cp310-abi3-win32.whl", hash = "sha256:caea2b3b67347fd79e5d15ed7929b0e886aac594ea228073b6d39de0078189da", size = 18489703, upload-time = "2026-06-29T20:50:30.599Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/5d/1a03f53eb0449900469335fcfc742ca28e3ba159b7d650e0921d50b8b308/pymupdf-1.28.0-cp310-abi3-win_amd64.whl", hash = "sha256:e01e90fd86abfeb37ceb921eddb951f988a11d45ff6ce6b7664f2039849068ec", size = 19773102, upload-time = "2026-06-29T09:04:49.773Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/f6/1e52ce243ca792254f6223b4017c5667194c146ce9b88baf37bc5eb3d1c9/pymupdf-1.28.0-cp313-abi3-pyemscripten_2025_0_wasm32.whl", hash = "sha256:74c6d00ba2a9aad3a635db73b07c15db462b480741d831a34a75a56535ebc22b", size = 18357011, upload-time = "2026-06-29T20:50:50.353Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/b1/46b5b3d8ef3cc71114667cf10c4d8b33f39af97253af32e9a0986775b638/pymupdf-1.28.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:b3e1399c7a64c6914239116a369efcdaac4cfb9e838bde2656d7accc4a85c72d", size = 25753599, upload-time = "2026-06-29T09:05:09.398Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pypika"
|
||||
version = "0.51.1"
|
||||
@@ -1675,6 +2087,20 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "7.1.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
dependencies = [
|
||||
{ name = "coverage" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
@@ -1939,6 +2365,31 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.21"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/36/6f65aa9989acdec45d417192d8f4e7921931d8a6cf87ac74bce3eed98a8e/ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500", size = 4769401, upload-time = "2026-07-09T20:01:34.005Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/c6/ede15cac6839f3dbce52565c8f5164a8210e669c7bc4decb03e5bdf47d0d/ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d", size = 10854342, upload-time = "2026-07-09T20:00:53.998Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/9d/d825b07ee7ea9e2d61df92a860033c94e06e7300d50a1c2653aac27d24fe/ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd", size = 11139539, upload-time = "2026-07-09T20:00:57.809Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/de/3b107712e642f063c7a9e0887c427b22cb44097de5aab36c05f2e280670c/ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646", size = 10595437, upload-time = "2026-07-09T20:01:00.006Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/6f/b4523cc90ba239ede441447a19d0c968846a3012e5a0b0c5b62831a3d5e3/ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be", size = 10990053, upload-time = "2026-07-09T20:01:02.187Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/cc/c6a9872a5375f0628875481cf2f66b13d7d865bf3ca2e57f91c7e762d976/ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd", size = 10666096, upload-time = "2026-07-09T20:01:04.299Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/97/c621f7a17e097f1790fa3af6374138823b330b2d03fc38337945daca212c/ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41", size = 11537011, upload-time = "2026-07-09T20:01:06.771Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/51/d928727e476e25ccc57c6f449ffd80241a651a973ad949d39cfb2a771d28/ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86", size = 12347101, upload-time = "2026-07-09T20:01:08.859Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/88/8cd62026802b16018ad06931d87997cf795ba2a6239ab659606c87d96bf0/ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67", size = 11572001, upload-time = "2026-07-09T20:01:11.092Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/97/f63084cf55444fc110e8cb985ebfcc592af47f597d44453d778cb81bc156/ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8", size = 11549239, upload-time = "2026-07-09T20:01:13.27Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/77/f107da4a2874b7715914b03f09ba9c54424de3ff8a1cc5d015d3ee2ce0ac/ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075", size = 11535340, upload-time = "2026-07-09T20:01:15.206Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/e9/601deb322d3303a7bf212b0100ead6f2ee3f6a044d89c30f2f92bf83c731/ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341", size = 10964048, upload-time = "2026-07-09T20:01:17.723Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/2e/0f2176d1e99c15192caea19c8c3a0a955246b4cb4de795042eeb616345cd/ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d", size = 10667055, upload-time = "2026-07-09T20:01:19.73Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/60/abd74a02e0c4214f12a68becfd30af7165cfdcb0e661ecdc60bbb949c09a/ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233", size = 11242043, upload-time = "2026-07-09T20:01:21.947Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/c6/583075d8ccabb4b229345edcaf1545eb3d8d6be90f686a479d7e94088bbf/ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f", size = 11648064, upload-time = "2026-07-09T20:01:24.023Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/3c/37d0ecb729a7cc2d393ea7dce316fc585680f35d93b8d62139d7d0a3700c/ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd", size = 10896555, upload-time = "2026-07-09T20:01:26.941Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/b8/e43466b2a6067ce91e669068f6e28d6c719a920f014b070d5c8731725de3/ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3", size = 12038772, upload-time = "2026-07-09T20:01:29.497Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/75/e90ab9aeece218a9fc5a5bc3ec97d0ee6bb3c4ff95869463c1de58e29a1c/ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8", size = 11375265, upload-time = "2026-07-09T20:01:31.772Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "safetensors"
|
||||
version = "0.8.0"
|
||||
@@ -2084,6 +2535,24 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.8.4"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "1.3.1"
|
||||
@@ -2193,7 +2662,8 @@ name = "torch"
|
||||
version = "2.6.0+cu124"
|
||||
source = { registry = "D:/settings/Language/Python/库" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.14'",
|
||||
"python_full_version >= '3.15'",
|
||||
"python_full_version == '3.14.*'",
|
||||
"python_full_version < '3.14' and sys_platform != 'darwin' and sys_platform != 'linux'",
|
||||
]
|
||||
dependencies = [
|
||||
|
||||
Reference in New Issue
Block a user