feat: 第三优先级完善 — CI/CD、PyPI、pre-commit、CHANGELOG、贡献指南、基准测试、ADR
CI / Test (Python 3.13) (push) Has been cancelled

This commit is contained in:
2026-07-11 20:13:36 +08:00
parent 7e0b9553c8
commit 2ddb6de92f
13 changed files with 483 additions and 12 deletions
+48
View File
@@ -0,0 +1,48 @@
# CI
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
name: Test (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.13"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: uv sync --extra dev --extra all
- name: Run lint (ruff)
run: uv run ruff check src/ tests/
- name: Run type check (mypy)
run: uv run mypy src/ --ignore-missing-imports
- name: Run tests with coverage
run: |
uv run pytest tests/ \
--cov=src \
--cov-report=term-missing \
--cov-report=xml \
-v
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
files: ./coverage.xml
fail_ci_if_error: false
+28
View File
@@ -0,0 +1,28 @@
# Publish to PyPI
name: Publish to PyPI
on:
push:
tags:
- "v*"
jobs:
publish:
name: Build and publish
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Build package
run: uv build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
+41
View File
@@ -0,0 +1,41 @@
# Changelog
所有值得注意的更改都将记录在此文件中。
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
## [Unreleased]
### Added
- 混合检索(BM25 + 向量联合),HybridRetriever + SearchConfig 可配置
- 增量入库(SHA256 文件变更追踪),CLI `--incremental` / `--force` 选项
- Cross-Encoder 结果重排序(BAAI/bge-reranker-base
- Web 管理界面(Vue 3 SPA`/admin`
- .docx 文档支持(markitdown
- 数据导出功能(JSON/CSV),CLI `export` 命令
- Docker 部署方案 + docker-compose(含 GPU profile
- API 审计日志、请求体大小限制、健康检查免限速
- GitHub Actions CI 流水线 + PyPI 发布
- Pre-commit 钩子配置(ruff + mypy
- 性能基准测试(嵌入 + 检索)
### Changed
- Searcher 支持 hybrid/vector 检索模式切换
- Embedder Protocol 修复为标准写法
- PDF/EPUB Splitter 参数名统一为 `source`
### Fixed
- CORS `allow_credentials` 配置修复
- chromadb 不同版本异常类型兼容(NotFoundError
## [0.1.0] - 2026-07-05
### Added
- Markdown 文档解析与语义检索
- 多 Provider 嵌入支持(local/OpenAI/DashScope
- 多格式文档(.md/.txt/.pdf/.html/.epub
- GPU 自动检测加速
- FastAPI HTTP API + Typer CLI
- API Key 认证和速率限制
- 路径遍历安全防护
+54
View File
@@ -0,0 +1,54 @@
# 贡献指南
## 开发环境
```bash
git clone git@github.com:LHY0125/md-vector-db.git
cd md-vector-db
uv sync --extra dev --extra all
uv run pre-commit install
```
## 开发流程
1. **Fork 仓库** → 创建功能分支 `feat/xxx`
2. **写测试**TDD: 先在 `tests/` 下写失败测试
3. **实现功能**: 最少代码让测试通过
4. **运行全部测试**: `uv run pytest tests/ --cov=src -v`
5. **确保覆盖率 ≥ 80%**: `uv run pytest tests/ --cov=src --cov-fail-under=80`
6. **Lint 检查**: `uv run ruff check src/ tests/`
7. **类型检查**: `uv run mypy src/ --ignore-missing-imports`
8. **提交**: 遵循约定式提交格式
9. **创建 PR**: 描述清楚变更内容和测试结果
## 提交消息格式
```
<类型>: <描述>
<可选正文>
```
类型: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`, `ci`
## 代码风格
- Python 3.13+,遵循 PEP 8
- 类型注解覆盖所有 public API
- 中文注释
- 小文件原则(<800 行),函数 <50 行
## 添加新文档格式支持
1.`src/core/splitters/` 下实现 Splitter Protocol
2.`src/core/splitters/registry.py` 注册扩展名
3.`pyproject.toml` 添加可选依赖
4.`tests/` 下添加测试
5. 更新 `README.md`
## 运行基准测试
```bash
uv sync --extra bench
uv run pytest tests/benchmarks/ -v --benchmark-only
```
+14 -1
View File
@@ -1,5 +1,8 @@
# md-vector-db
[![CI](https://lhy-git.liuhangyv.top/Serendipity/md-vector-db/actions/workflows/ci.yml/badge.svg)](https://github.com/LHY0125/md-vector-db/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
Markdown 文档向量数据库 — 将 Markdown 文件自动分块、嵌入、存入 ChromaDB,通过语义检索快速查找相关内容。提供 **CLI 命令行工具**和 **HTTP API** 两种使用方式供其他项目集成。
## 功能特性
@@ -181,7 +184,7 @@ docker exec -it md-vector-db uv run md-vector-db ingest /app/md_docs/doc.md
所有命令均支持 `--config/-c`(配置文件)、`--collection/-C`(集合名,默认 `default`)。
| 命令 | 说明 |
| --------------------------------- | -------------------------------------------------------- |
| ----------------------------------- | -------------------------------------------------------- |
| `ingest <文件路径> --incremental` | 增量入库单文件,自动跳过未变更文件 |
| `ingest <文件路径> --force` | 强制重新入库(忽略增量检查) |
| `ingest-dir <目录路径>` | 递归入库目录下所有支持的文档格式 |
@@ -327,6 +330,16 @@ index-strategy = "unsafe-best-match" # 允许跨源查找
---
---
## 文档
- [API 参考](docs/api_reference.md)
- [架构决策记录](docs/architecture.md)
-
- [贡献指南](CONTRIBUTING.md)
- [变更日志](CHANGELOG.md)
## License
MIT
+68
View File
@@ -0,0 +1,68 @@
# md-vector-db API 参考文档
## REST API
- **Base URL**: `http://localhost:8000/api/v1`
- **认证**: `X-API-Key` Header(取决于 `MD_VECTOR_API_KEY` 环境变量)
- **速率限制**: 60s 窗口内最多 30 请求
### GET /health — 健康检查
无需认证。
```json
{
"status": "ok",
"checks": {
"chromadb": {"status": "ok", "count": 1240},
"embedder": {"status": "ok", "dimension": 512}
}
}
```
### GET /collections — 列出集合
需 API Key。返回 `{"collections": [{"name": "...", "count": N}, ...]}`
### POST /search — 语义检索
| 字段 | 类型 | 必填 | 约束 |
|------|------|------|------|
| query | string | ✅ | 1-2000 字符 |
| top_k | int | ❌ | 1-100,默认 10 |
| collection | string | ❌ | ≤128 字符 |
返回 `{"results": [...], "collection": "..."}`,每条结果含 `id`, `content`, `source_file`, `section_title`, `heading_level`, `score`
### POST /ingest — 入库文档
| 字段 | 类型 | 必填 | 约束 |
|------|------|------|------|
| file_path | string | 二选一 | 安全路径 |
| content | string | 二选一 | ≤500KB |
| file_name | string | 推荐 | 1-255 字符 |
| collection | string | ❌ | ≤128 字符 |
返回 `{"status": "ok", "chunks": N, "file": "...", "collection": "..."}`
### DELETE /documents/{file_name} — 删除文档
查询参数: `collection` (可选)。
| 状态码 | 含义 |
|--------|------|
| 200 | 删除成功 |
| 400 | file_name 不合法 |
| 401 | API Key 无效 |
| 404 | 文档不存在 |
## CLI 命令
```bash
md-vector-db ingest <文件> --incremental --force
md-vector-db ingest-dir <目录> -C <集合>
md-vector-db search "<查询>" -k 10 --mode hybrid --json
md-vector-db stats -C <集合>
md-vector-db export -o output.json -f json
md-vector-db serve -p 8000
```
+43
View File
@@ -0,0 +1,43 @@
# 架构决策记录 (ADR)
## ADR-1: 为什么选择 ChromaDB
**日期**: 2026-07-05 | **状态**: 已采纳
**背景**: 需要一个嵌入式向量数据库来持久化文档嵌入。
**候选**: ChromaDB(嵌入式 SQLite)、Qdrant(独立服务)、FAISS(纯内存)、Milvus(生产级集群)
**决策**: ChromaDB。零运维成本远超吞吐量考量。内置 Collection 概念映射多知识库场景。
**代价**: 高并发下逊于 Qdrant/Milvus。未来可透明迁移(Embedder/Searcher 已隔离 ChromaDB 依赖)。
---
## ADR-2: 为什么默认 bge-small-zh-v1.5
**日期**: 2026-07-05 | **状态**: 已采纳
**候选**: bge-small-zh-v1.5 (512维/23M)、bge-large-zh-v1.5 (1024维/324M)、text2vec-large-chinese、m3e-base
**决策**: bge-small-zh-v1.5。RTX 4060 上 729 chunks 嵌入仅 1.8s,日常精度足够。高精度场景可切换 large 模型或 OpenAI API。
---
## ADR-3: 为什么采用 Protocol 而非 ABC
**日期**: 2026-07-05 | **状态**: 已采纳
**候选**: typing.Protocol(结构化子类型)、abc.ABC(名义子类型)、Callable(丢失类型信息)
**决策**: Protocol。外部模块无需依赖本项目源码即可实现 Splitter/Embedder,对插件化友好。
---
## ADR-4: 为什么使用 rank-bm25 而非集成搜索引擎?
**日期**: 2026-07-11 | **状态**: 已采纳
**候选**: rank-bm25(纯 Python BM25)、Elasticsearch(外部服务)、Whoosh(纯 Python 全文搜索)
**决策**: rank-bm25。零运维、轻量、与现有 ChromaDB 架构匹配。在向量候选上做 BM25 重打分(而非全文索引所有文档),兼顾性能和精度。
+23 -1
View File
@@ -1,7 +1,21 @@
[project]
name = "md-vector-db"
version = "0.1.0"
description = "Markdown 文档向量数据库,支持语义检索"
description = "Markdown 文档向量数据库语义检索、混合检索、REST API"
readme = "README.md"
license = {text = "MIT"}
authors = [
{name = "刘航宇", email = "3364451258@qq.com"},
]
keywords = ["vector-database", "semantic-search", "rag", "chromadb", "markdown"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.13",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Topic :: Text Processing :: Markup",
]
requires-python = ">=3.13"
dependencies = [
"chromadb>=0.5.0",
@@ -15,6 +29,12 @@ dependencies = [
"rank-bm25>=0.2.2",
]
[project.urls]
Homepage = "https://github.com/LHY0125/md-vector-db"
Documentation = "https://github.com/LHY0125/md-vector-db#readme"
Repository = "https://github.com/LHY0125/md-vector-db"
Issues = "https://github.com/LHY0125/md-vector-db/issues"
[project.scripts]
md-vector-db = "src.cli.main:app"
@@ -24,6 +44,7 @@ pdf = ["pymupdf>=1.24.0"]
html = ["beautifulsoup4>=4.12.0"]
epub = ["ebooklib>=0.18"]
docx = ["markitdown>=0.1.0"]
bench = ["pytest-benchmark>=5.0"]
all = ["md-vector-db[pdf,html,epub,docx]", "requests>=2.31.0", "openai>=1.0.0"]
[build-system]
@@ -56,3 +77,4 @@ ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
norecursedirs = ["tests/benchmarks"]
+47
View File
@@ -0,0 +1,47 @@
"""版本号管理 — 更新 pyproject.toml 中的 version."""
import re
import sys
from pathlib import Path
def bump(part: str) -> str:
"""递增版本号.
Args:
part: "major" | "minor" | "patch"
Returns:
新版本号字符串
"""
pyproject = Path(__file__).parent.parent / "pyproject.toml"
content = pyproject.read_text(encoding="utf-8")
match = re.search(r'version\s*=\s*"(\d+)\.(\d+)\.(\d+)"', content)
if not match:
print("错误: 未找到 version 字段", file=sys.stderr)
sys.exit(1)
major, minor, patch = int(match[1]), int(match[2]), int(match[3])
if part == "major":
major += 1
minor = 0
patch = 0
elif part == "minor":
minor += 1
patch = 0
elif part == "patch":
patch += 1
else:
print(f"错误: 未知的版本部分 '{part}',可选: major/minor/patch", file=sys.stderr)
sys.exit(1)
new_version = f"{major}.{minor}.{patch}"
new_content = content.replace(match[0], f'version = "{new_version}"')
pyproject.write_text(new_content, encoding="utf-8")
print(f"版本: {match[1]}.{match[2]}.{match[3]}{new_version}")
return new_version
if __name__ == "__main__":
if len(sys.argv) != 2:
print("用法: python scripts/bump_version.py <major|minor|patch>")
sys.exit(1)
bump(sys.argv[1])
+41
View File
@@ -0,0 +1,41 @@
"""基准测试共享 fixture."""
import pytest
from src.core.config import ChunkConfig, EmbedConfig
from src.core.db import VectorDB
from src.core.embedder import create_embedder
from src.core.ingest import DocumentIngestor
from src.core.search import Searcher
@pytest.fixture(scope="module")
def benchmark_db(tmp_path_factory):
"""模块级共享 ChromaDB 实例."""
persist_dir = tmp_path_factory.mktemp("bench_data")
return VectorDB(persist_dir=str(persist_dir))
@pytest.fixture(scope="module")
def benchmark_embedder():
"""模块级共享 LocalEmbedder."""
config = EmbedConfig(mode="local", local_model="BAAI/bge-small-zh-v1.5")
return create_embedder(config)
@pytest.fixture(scope="module")
def benchmark_searcher(benchmark_db, benchmark_embedder):
"""预填充数据的 Searcher."""
ingestor = DocumentIngestor(
benchmark_db, benchmark_embedder, "bench_collection",
chunk_config=ChunkConfig(max_size=1000, overlap=100),
)
for i in range(100):
content = (
f"# 文档{i}\n\n"
+ "\n\n".join(
f"{j}段用于基准测试。关键词: Python, Rust, GPU, 向量数据库。"
for j in range(5)
)
)
ingestor.ingest_content(content, f"bench_{i}.md")
return Searcher(benchmark_db, benchmark_embedder, "bench_collection")
+23
View File
@@ -0,0 +1,23 @@
"""嵌入性能基准测试."""
import pytest
def test_bench_embed_single(benchmark, benchmark_embedder):
"""单文本嵌入耗时."""
text = "这是一段测试文本,用于测量嵌入速度。"
benchmark(benchmark_embedder.embed, [text])
def test_bench_embed_batch_32(benchmark, benchmark_embedder):
"""批量 32 文本嵌入耗时."""
texts = [f"测试文本第{i}条,模拟真实文档内容。" for i in range(32)]
benchmark(benchmark_embedder.embed, texts)
def test_bench_embed_batch_100(benchmark, benchmark_embedder):
"""批量 100 文本嵌入耗时(GPU 优势显著)."""
texts = [
f"测试文本第{i}条。Python 通用编程语言,用于数据科学和 AI。"
for i in range(100)
]
benchmark(benchmark_embedder.embed, texts)
+17
View File
@@ -0,0 +1,17 @@
"""检索性能基准测试."""
import pytest
def test_bench_search_top5(benchmark, benchmark_searcher):
"""top_k=5 检索耗时."""
benchmark(benchmark_searcher.search, "Python 向量数据库", top_k=5)
def test_bench_search_top20(benchmark, benchmark_searcher):
"""top_k=20 检索耗时."""
benchmark(benchmark_searcher.search, "Rust 编程语言 GPU", top_k=20)
def test_bench_search_cold_start(benchmark, benchmark_searcher):
"""冷启动检索耗时."""
benchmark(benchmark_searcher.search, "GPU 加速 深度学习 嵌入", top_k=10)
Generated
+33 -7
View File
@@ -1206,6 +1206,9 @@ all = [
{ name = "pymupdf" },
{ name = "requests" },
]
bench = [
{ name = "pytest-benchmark" },
]
dev = [
{ name = "httpx" },
{ name = "mypy" },
@@ -1240,6 +1243,7 @@ requires-dist = [
{ 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-benchmark", marker = "extra == 'bench'", specifier = ">=5.0" },
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" },
{ name = "python-dotenv", specifier = ">=1.2.2" },
{ name = "pyyaml", specifier = ">=6.0" },
@@ -1250,7 +1254,7 @@ requires-dist = [
{ name = "typer", specifier = ">=0.12.0" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0" },
]
provides-extras = ["dev", "pdf", "html", "epub", "docx", "all"]
provides-extras = ["dev", "pdf", "html", "epub", "docx", "bench", "all"]
[[package]]
name = "mdurl"
@@ -1559,7 +1563,7 @@ name = "nvidia-cudnn-cu12"
version = "9.1.0.70"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
dependencies = [
{ name = "nvidia-cublas-cu12", marker = "python_full_version >= '3.14' or sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "nvidia-cublas-cu12", marker = "(python_full_version >= '3.14' and sys_platform != 'win32') or sys_platform == 'darwin' or sys_platform == 'linux'" },
]
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741, upload-time = "2024-04-22T15:24:15.253Z" },
@@ -1570,7 +1574,7 @@ name = "nvidia-cufft-cu12"
version = "11.2.1.3"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
dependencies = [
{ name = "nvidia-nvjitlink-cu12", marker = "python_full_version >= '3.14' or sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "nvidia-nvjitlink-cu12", marker = "(python_full_version >= '3.14' and sys_platform != 'win32') or sys_platform == 'darwin' or sys_platform == 'linux'" },
]
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/94/3266821f65b92b3138631e9c8e7fe1fb513804ac934485a8d05776e1dd43/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9", size = 211459117, upload-time = "2024-04-03T20:57:40.402Z" },
@@ -1589,9 +1593,9 @@ name = "nvidia-cusolver-cu12"
version = "11.6.1.9"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
dependencies = [
{ name = "nvidia-cublas-cu12", marker = "python_full_version >= '3.14' or sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "nvidia-cusparse-cu12", marker = "python_full_version >= '3.14' or sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "nvidia-nvjitlink-cu12", marker = "python_full_version >= '3.14' or sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "nvidia-cublas-cu12", marker = "(python_full_version >= '3.14' and sys_platform != 'win32') or sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "nvidia-cusparse-cu12", marker = "(python_full_version >= '3.14' and sys_platform != 'win32') or sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "nvidia-nvjitlink-cu12", marker = "(python_full_version >= '3.14' and sys_platform != 'win32') or sys_platform == 'darwin' or sys_platform == 'linux'" },
]
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/e1/5b9089a4b2a4790dfdea8b3a006052cfecff58139d5a4e34cb1a51df8d6f/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260", size = 127936057, upload-time = "2024-04-03T20:58:28.735Z" },
@@ -1602,7 +1606,7 @@ name = "nvidia-cusparse-cu12"
version = "12.3.1.170"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
dependencies = [
{ name = "nvidia-nvjitlink-cu12", marker = "python_full_version >= '3.14' or sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "nvidia-nvjitlink-cu12", marker = "(python_full_version >= '3.14' and sys_platform != 'win32') or sys_platform == 'darwin' or sys_platform == 'linux'" },
]
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/db/f7/97a9ea26ed4bbbfc2d470994b8b4f338ef663be97b8f677519ac195e113d/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1", size = 207454763, upload-time = "2024-04-03T20:58:59.995Z" },
@@ -1942,6 +1946,15 @@ wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" },
]
[[package]]
name = "py-cpuinfo"
version = "9.0.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" },
]
[[package]]
name = "pybase64"
version = "1.4.3"
@@ -2181,6 +2194,19 @@ 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-benchmark"
version = "5.2.3"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" }
dependencies = [
{ name = "py-cpuinfo" },
{ name = "pytest" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" },
]
[[package]]
name = "pytest-cov"
version = "7.1.0"