From 1372e84be696e1dee16234a6c488975c825920dc Mon Sep 17 00:00:00 2001 From: Michael Ding Date: Mon, 25 May 2026 09:54:43 +0800 Subject: [PATCH 1/2] add superpowers --- .../plans/2026-05-25-convert-url-support.md | 459 ++++++++++++++++++ .../2026-05-25-convert-url-support-design.md | 101 ++++ 2 files changed, 560 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-25-convert-url-support.md create mode 100644 docs/superpowers/specs/2026-05-25-convert-url-support-design.md diff --git a/docs/superpowers/plans/2026-05-25-convert-url-support.md b/docs/superpowers/plans/2026-05-25-convert-url-support.md new file mode 100644 index 0000000..2b1cf78 --- /dev/null +++ b/docs/superpowers/plans/2026-05-25-convert-url-support.md @@ -0,0 +1,459 @@ +# URL 源文件支持 — 实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 让 `kbmate convert` 的 `source_file` 参数支持 `http://`、`https://` 和 `file://` URL。 + +**Architecture:** 新建 `url_downloader.py` 模块封装 URL 检测、Content-Type 探测和文件下载;`main.py` 的 `convert` 函数在开头插入 URL 检测分支。使用 `urllib.request`(stdlib),零新增依赖。 + +**Tech Stack:** Python 3.12, urllib (stdlib), typer + +--- + +## 文件结构 + +| 文件 | 操作 | 职责 | +|------|------|------| +| `src/kbmate_cli/url_downloader.py` | 创建 | URL 检测、Content-Type 探测、下载到临时目录 | +| `src/kbmate_cli/main.py` | 修改 | 在 convert 函数开头添加 URL 检测分支 | +| `tests/test_url_downloader.py` | 创建 | url_downloader 模块的单元测试 | +| `tests/test_cli.py` | 修改 | 添加 URL 路径的 CLI 集成测试 | + +--- + +### Task 1: `url_downloader.py` 核心逻辑 + 测试 + +**Files:** +- Create: `src/kbmate_cli/url_downloader.py` +- Create: `tests/test_url_downloader.py` + +- [ ] **Step 1: 在 `tests/test_url_downloader.py` 中编写 `is_url` 和 `guess_ext_from_url` 的测试** + +```python +import pytest +from pathlib import Path +from kbmate_cli.url_downloader import is_url, guess_ext_from_url + + +class TestIsUrl: + def test_http(self): + assert is_url("http://example.com/doc.pdf") is True + + def test_https(self): + assert is_url("https://example.com/doc.pdf") is True + + def test_file_protocol(self): + assert is_url("file:///home/user/doc.pdf") is True + + def test_local_path(self): + assert is_url("/home/user/doc.pdf") is False + + def test_relative_path(self): + assert is_url("doc.pdf") is False + + +class TestGuessExtFromUrl: + def test_pdf(self): + assert guess_ext_from_url("https://example.com/doc.pdf") == ".pdf" + + def test_docx(self): + assert guess_ext_from_url("https://example.com/report.docx") == ".docx" + + def test_no_ext(self): + assert guess_ext_from_url("https://example.com/download") is None + + def test_query_string(self): + assert guess_ext_from_url("https://example.com/file.pdf?token=abc") == ".pdf" +``` + +- [ ] **Step 2: 运行测试验证失败** + +Run: `uv run pytest tests/test_url_downloader.py -v` +Expected: ImportError (模块不存在) + +- [ ] **Step 3: 实现 `is_url` 和 `guess_ext_from_url`** + +```python +from pathlib import Path +from urllib.parse import urlparse +from urllib.request import Request, urlopen +from urllib.error import URLError +import tempfile +import shutil + + +def is_url(s: str) -> bool: + return s.startswith("http://") or s.startswith("https://") or s.startswith("file://") + + +def guess_ext_from_url(url: str) -> str | None: + path = urlparse(url).path + ext = Path(path).suffix.lower() + return ext if ext in (".pdf", ".docx") else None +``` + +- [ ] **Step 4: 运行测试验证通过** + +Run: `uv run pytest tests/test_url_downloader.py -v` +Expected: All 6 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add tests/test_url_downloader.py src/kbmate_cli/url_downloader.py +git commit -m "feat: add is_url and guess_ext_from_url to url_downloader" +``` + +- [ ] **Step 6: 在 `tests/test_url_downloader.py` 中编写 `probe_content_type`、`resolve_file_type` 和 `download_to_temp` 的测试** + +```python +from unittest.mock import patch, MagicMock +from kbmate_cli.url_downloader import ( + is_url, guess_ext_from_url, + probe_content_type, resolve_file_type, + download_to_temp, print_cleanup_hint, +) + + +class TestProbeContentType: + @patch("kbmate_cli.url_downloader.urlopen") + def test_pdf_content_type(self, mock_urlopen): + mock_resp = MagicMock() + mock_resp.headers = {"Content-Type": "application/pdf"} + mock_urlopen.return_value.__enter__.return_value = mock_resp + assert probe_content_type("https://example.com/doc") == ".pdf" + + @patch("kbmate_cli.url_downloader.urlopen") + def test_docx_content_type(self, mock_urlopen): + mock_resp = MagicMock() + mock_resp.headers = {"Content-Type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"} + mock_urlopen.return_value.__enter__.return_value = mock_resp + assert probe_content_type("https://example.com/doc") == ".docx" + + @patch("kbmate_cli.url_downloader.urlopen") + def test_unknown_content_type(self, mock_urlopen): + mock_resp = MagicMock() + mock_resp.headers = {"Content-Type": "application/octet-stream"} + mock_urlopen.return_value.__enter__.return_value = mock_resp + assert probe_content_type("https://example.com/doc") is None + + @patch("kbmate_cli.url_downloader.urlopen") + def test_network_error_returns_none(self, mock_urlopen): + mock_urlopen.side_effect = URLError("connection failed") + assert probe_content_type("https://example.com/doc") is None + + +class TestResolveFileType: + @patch("kbmate_cli.url_downloader.probe_content_type") + def test_probe_success(self, mock_probe): + mock_probe.return_value = ".pdf" + assert resolve_file_type("https://example.com/doc") == ".pdf" + mock_probe.assert_called_once() + + @patch("kbmate_cli.url_downloader.probe_content_type") + def test_probe_fallback_to_url(self, mock_probe): + mock_probe.return_value = None + assert resolve_file_type("https://example.com/doc.pdf") == ".pdf" + + @patch("kbmate_cli.url_downloader.probe_content_type") + def test_no_match_raises(self, mock_probe): + mock_probe.return_value = None + with pytest.raises(ValueError, match="cannot determine file type"): + resolve_file_type("https://example.com/doc") + + +class TestDownloadToTemp: + @patch("kbmate_cli.url_downloader.urlopen") + def test_download_success(self, mock_urlopen): + mock_resp = MagicMock() + mock_resp.read.return_value = b"%PDF-1.4 fake content" + mock_resp.__enter__.return_value = mock_resp + mock_urlopen.return_value = mock_resp + + result = download_to_temp("https://example.com/doc.pdf", ".pdf") + assert isinstance(result, Path) + assert result.suffix == ".pdf" + assert result.exists() + assert result.read_bytes() == b"%PDF-1.4 fake content" + result.unlink() # cleanup + + @patch("kbmate_cli.url_downloader.urlopen") + def test_network_error_raises(self, mock_urlopen): + mock_urlopen.side_effect = URLError("connection refused") + with pytest.raises(URLError): + download_to_temp("https://example.com/doc.pdf", ".pdf") + + +class TestPrintCleanupHint: + def test_prints_message(self, capsys): + p = Path("/tmp/test_file.pdf") + print_cleanup_hint(p) + captured = capsys.readouterr() + assert "/tmp/test_file.pdf" in captured.out + assert "手动删除" in captured.out +``` + +- [ ] **Step 7: 运行测试验证失败** + +Run: `uv run pytest tests/test_url_downloader.py -v` +Expected: New tests FAIL with ImportError or function not defined + +- [ ] **Step 8: 实现 `probe_content_type`、`resolve_file_type`、`download_to_temp`、`print_cleanup_hint`** + +在 `url_downloader.py` 追加: + +```python +_CONTENT_TYPE_MAP = { + "application/pdf": ".pdf", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx", +} + + +def probe_content_type(url: str) -> str | None: + req = Request(url, method="HEAD") + try: + with urlopen(req, timeout=10) as resp: + ct = resp.headers.get("Content-Type", "").split(";")[0].strip() + return _CONTENT_TYPE_MAP.get(ct) + except URLError: + return None + + +def resolve_file_type(url: str) -> str: + ext = probe_content_type(url) + if ext: + return ext + ext = guess_ext_from_url(url) + if ext: + return ext + raise ValueError(f"cannot determine file type for URL: {url}") + + +def download_to_temp(url: str, suffix: str) -> Path: + tmp_dir = Path(tempfile.gettempdir()) + tmp_file = tmp_dir / f"kbmate-{next(tempfile._get_candidate_names())}{suffix}" + req = Request(url) + with urlopen(req, timeout=30) as resp: + tmp_file.write_bytes(resp.read()) + return tmp_file + + +def print_cleanup_hint(path: Path) -> None: + typer.echo(f"临时文件已保存至: {path},如不需要请手动删除") +``` + +需要在文件顶部添加 `import typer`。 + +- [ ] **Step 9: 运行测试验证通过** + +Run: `uv run pytest tests/test_url_downloader.py -v` +Expected: All tests PASS + +- [ ] **Step 10: Commit** + +```bash +git add tests/test_url_downloader.py src/kbmate_cli/url_downloader.py +git commit -m "feat: add probe, download, and cleanup functions to url_downloader" +``` + +--- + +### Task 2: 集成到 CLI + +**Files:** +- Modify: `src/kbmate_cli/main.py` +- Modify: `tests/test_cli.py` + +- [ ] **Step 1: 在 `tests/test_cli.py` 中添加 URL 和 `file://` 的 CLI 集成测试** + +```python +from unittest.mock import patch +from kbmate_cli.main import app + + +def test_convert_file_url(): + """file:// URL 应解析为本地路径并正常工作""" + pdf = FIXTURE_DIR / "eigent README CN.pdf" + file_url = f"file://{pdf.resolve()}" + result = runner.invoke(app, ["convert", file_url, "--output-dir", "/tmp/test_cli_file_url"]) + assert result.exit_code == 0, f"Failed with output: {result.output}" + + +@patch("kbmate_cli.url_downloader.urlopen") +def test_convert_http_url(mock_urlopen): + """http URL 应下载后转换""" + # Mock HEAD probe + head_resp = MagicMock() + head_resp.headers = {"Content-Type": "application/pdf"} + head_resp.__enter__.return_value = head_resp + + # Mock GET download + pdf_path = FIXTURE_DIR / "eigent README CN.pdf" + download_resp = MagicMock() + download_resp.read.return_value = pdf_path.read_bytes() + download_resp.__enter__.return_value = download_resp + + mock_urlopen.side_effect = [head_resp, download_resp] + + result = runner.invoke( + app, ["convert", "https://example.com/doc.pdf", "--output-dir", "/tmp/test_cli_http_url"] + ) + assert result.exit_code == 0, f"Failed with output: {result.output}" + assert "临时文件已保存至" in result.stdout +``` + +需要补充 import: + +```python +from unittest.mock import patch, MagicMock +``` + +- [ ] **Step 2: 运行测试验证失败** + +Run: `uv run pytest tests/test_cli.py::test_convert_file_url tests/test_cli.py::test_convert_http_url -v` +Expected: `test_convert_file_url` FAIL 是因为 `file://` 路径 `src.exists()` 不通过;`test_convert_http_url` 取决于 mock 行为 + +- [ ] **Step 3: 修改 `main.py` 添加 URL 检测分支** + +在 `main.py` 开头增加 import: + +```python +from kbmate_cli.url_downloader import is_url, resolve_file_type, download_to_temp, print_cleanup_hint +from urllib.parse import urlparse +``` + +修改 `convert` 函数,在现有 `src = Path(source_file)` 之前插入 URL 检测逻辑: + +```python +@app.command() +def convert( + source_file: str = typer.Argument(..., help="Path or URL to the .docx or .pdf file"), + output_dir: str = typer.Option("raw", help="Output directory"), +): + if is_url(source_file): + if source_file.startswith("file://"): + source_file = urlparse(source_file).path + + else: + suffix = resolve_file_type(source_file) + temp_path = download_to_temp(source_file, suffix) + source_file = str(temp_path) + + src = Path(source_file) + if not src.exists(): + typer.echo(f"Error: file not found: {source_file}", err=True) + raise typer.Exit(code=1) + + # ... 后续代码不变 ... +``` + +并在 `md_path.write_text` 和 `typer.echo` 之间插入清理提示: + +```python + md_path.write_text(markdown_content, encoding="utf-8") + typer.echo(f"Converted: {src} -> {md_path}") + if temp_path: + print_cleanup_hint(temp_path) +``` + +需要声明 `temp_path` 并在函数末尾访问,所以需要重构为在 try/finally 中处理,或使用一个变量跟踪。推荐在函数顶部声明 `temp_path = None`: + +在 `src = Path(source_file)` 前添加 `temp_path = None`,然后在 `else` 分支中赋值。最后在函数末尾判断: + +```python + md_path.write_text(markdown_content, encoding="utf-8") + typer.echo(f"Converted: {src} -> {md_path}") + if temp_path: + print_cleanup_hint(temp_path) +``` + +最终的 `convert` 函数: + +```python +@app.command() +def convert( + source_file: str = typer.Argument(..., help="Path or URL to the .docx or .pdf file"), + output_dir: str = typer.Option("raw", help="Output directory"), +): + temp_path: Path | None = None + + if is_url(source_file): + if source_file.startswith("file://"): + source_file = urlparse(source_file).path + else: + suffix = resolve_file_type(source_file) + temp_path = download_to_temp(source_file, suffix) + source_file = str(temp_path) + + src = Path(source_file) + if not src.exists(): + typer.echo(f"Error: file not found: {source_file}", err=True) + raise typer.Exit(code=1) + + ext = src.suffix.lower() + if ext not in (".pdf", ".docx"): + typer.echo(f"Error: unsupported format: {ext} (supported: .pdf, .docx)", err=True) + raise typer.Exit(code=1) + + out_dir = Path(output_dir) + assets_parent = out_dir / "assets" + + sanitized_ref, _ = _md_path(str(assets_parent), f"{src.stem}.x") + safe_stem = Path(sanitized_ref).stem + + assets_dir = assets_parent / safe_stem + converts_dir = out_dir / "converts" + converts_dir.mkdir(parents=True, exist_ok=True) + assets_dir.mkdir(parents=True, exist_ok=True) + + markdown_content: str = "" + + if ext == ".pdf": + from kbmate_cli.pdf_converter import convert_pdf + + markdown_content = convert_pdf(str(src), str(assets_dir)) + + from kbmate_cli.image_helper import extract_and_relink_images + + markdown_content = extract_and_relink_images( + markdown_content, str(assets_dir), str(assets_dir) + ) + + elif ext == ".docx": + from kbmate_cli.docx_converter import convert_docx + + pandoc_output = assets_dir / "pandoc_output" + markdown_content = convert_docx(str(src), str(pandoc_output)) + + from kbmate_cli.image_helper import normalize_image_refs, extract_and_relink_images + + markdown_content = normalize_image_refs(markdown_content) + markdown_content = extract_and_relink_images( + markdown_content, str(pandoc_output), str(assets_dir) + ) + if pandoc_output.exists(): + import shutil + + shutil.rmtree(pandoc_output) + + md_path = converts_dir / f"{safe_stem}.md" + md_path.write_text(markdown_content, encoding="utf-8") + typer.echo(f"Converted: {src} -> {md_path}") + if temp_path: + print_cleanup_hint(temp_path) +``` + +- [ ] **Step 4: 运行测试验证通过** + +Run: `uv run pytest tests/test_cli.py::test_convert_file_url tests/test_cli.py::test_convert_http_url -v` +Expected: Both PASS + +Run: `uv run pytest` — 全部已有测试也不应被破坏 +Expected: All tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add src/kbmate_cli/main.py tests/test_cli.py +git commit -m "feat: integrate URL support into kbmate convert command" +``` diff --git a/docs/superpowers/specs/2026-05-25-convert-url-support-design.md b/docs/superpowers/specs/2026-05-25-convert-url-support-design.md new file mode 100644 index 0000000..03deffe --- /dev/null +++ b/docs/superpowers/specs/2026-05-25-convert-url-support-design.md @@ -0,0 +1,101 @@ +# URL 源文件支持 — 设计文档 + +## 概述 + +`kbmate convert` 命令目前只接受本地文件路径作为 `source_file` 参数。本特性使其能够自动检测 URL(`http://` / `https://` 开头),从远端下载文件到临时目录后再进行转换。 + +## 方案选择 + +采用 **方案 1:新增 `url_downloader.py` 模块**,职责清晰、测试方便、零新增依赖。 + +## 详细设计 + +### 1. URL 自动检测 + +`convert` 函数的 `source_file` 参数检测规则: + +- 以 `http://` 或 `https://` 开头 → 视为远程 URL,走下载流程 +- 以 `file://` 开头 → 解析为本地文件路径,走现有本地路径逻辑 +- 否则 → 保持原有本地文件路径逻辑 + +`file://` 路径通过 `urllib.parse.urlparse` 解析后取 `path` 部分,支持绝对路径(`file:///home/user/doc.pdf`)和相对路径(`file://../doc.pdf`)。 + +### 2. 新模块 `src/kbmate_cli/url_downloader.py` + +提供以下函数: + +| 函数 | 签名 | 说明 | +|------|------|------| +| `is_url(s)` | `(s: str) -> bool` | 检测是否 http/https 开头 | +| `probe_content_type(url)` | `(url: str) -> str | None` | HEAD 请求获取 Content-Type | +| `guess_ext_from_url(url)` | `(url: str) -> str | None` | 从 URL 路径提取扩展名 | +| `resolve_file_type(url)` | `(url: str) -> str` | probe 优先 + guess fallback,返回后缀(`.pdf` / `.docx`) | +| `download_to_temp(url, suffix)` | `(url: str, suffix: str) -> Path` | 下载到 `tempfile.gettempdir()`,返回 Path | +| `print_cleanup_hint(path)` | `(path: Path) -> None` | 打印提示信息,告知用户临时文件位置并可手动删除 | + +### 3. `main.py` 改动 + +在 `convert` 函数开头插入 URL 检测分支: + +``` +if is_url(source_file): + suffix = resolve_file_type(source_file) + temp_path = download_to_temp(source_file, suffix) + try: + # 将 temp_path 作为 source_file 传给现有逻辑 + ... 复用现有路径校验 + 转换流程... + finally: + print_cleanup_hint(temp_path) +else: + ... 现有逻辑不变 ... +``` + +### 4. 文件类型检测策略 + +1. 先发 HEAD 请求,读取 `Content-Type` header + - `application/pdf` → `.pdf` + - `application/vnd.openxmlformats-officedocument.wordprocessingml.document` → `.docx` +2. 如果 HEAD 请求失败或 Content-Type 不匹配,fallback 到 URL 路径扩展名 +3. 两者都不可识别 → 抛错误提示不支持的格式 + +### 5. 错误处理 + +- 网络错误(超时、连接拒绝、DNS 解析失败)→ `typer.Exit` 输出清晰中文提示 +- 下载文件大小为 0 → 报错并退出 +- Content-Type 和 URL 扩展名都无法识别 → 提示无法识别文件类型 +- 临时文件写入失败 → 报错并退出 + +### 6. 临时文件管理 + +- 下载到 `tempfile.gettempdir()` / `kbmate-*` 命名的临时文件 +- 转换完成后不自动删除,而是打印消息: + `临时文件已保存至: /tmp/kbmate-xxxxx/xxx.pdf,如不需要请手动删除` +- 用户自行决定何时清理 + +### 7. 测试计划 + +新增测试文件 `tests/test_url_downloader.py`: + +| 测试 | 说明 | +|------|------| +| `test_is_url_http` | http 开头返回 True | +| `test_is_url_https` | https 开头返回 True | +| `test_is_url_local_path` | 本地路径返回 False | +| `test_guess_ext_from_url_pdf` | URL 含 .pdf 返回 `.pdf` | +| `test_guess_ext_from_url_docx` | URL 含 .docx 返回 `.docx` | +| `test_guess_ext_from_url_no_ext` | URL 无扩展名返回 None | +| `test_resolve_file_type_probe_pdf` | Content-Type 为 PDF 时识别 | +| `test_resolve_file_type_probe_docx` | Content-Type 为 DOCX 时识别 | +| `test_resolve_file_type_fallback_to_url` | probe 失败时 fallback 到 URL | +| `test_resolve_file_type_no_match` | 均无匹配时抛出异常 | +| `test_download_to_temp` | 下载成功返回有效 Path(mock 网络) | +| `test_download_to_temp_network_error` | 网络错误时抛出异常 | + +更新 `tests/test_cli.py` 添加一个 CLI 集成测试(mock 下载),验证 URL 路径能走通 `convert` 命令。 + +### 8. 不做的事 + +- 不支持 `ftp://` 等其他协议 +- 不添加认证(Basic Auth、Token 等) +- 不支持重定向跟随之外的 HTTP 特性 +- 不缓存下载文件(每次都是新下载) From b2b963abf565e2cd3e38279396aebebc299ba001 Mon Sep 17 00:00:00 2001 From: Michael Ding Date: Mon, 25 May 2026 10:17:18 +0800 Subject: [PATCH 2/2] bump version to 0.2.0 and add CI workflow with pytest-cov --- .github/workflows/ci.yml | 32 ++++++++++++ .gitignore | 1 + pyproject.toml | 6 ++- uv.lock | 106 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d3b1b7e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y pandoc + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Set up Python ${{ matrix.python-version }} + run: uv python pin ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --dev + + - name: Run tests with coverage + run: uv run pytest diff --git a/.gitignore b/.gitignore index 505a3b1..69f8916 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ wheels/ # Virtual environments .venv +.coverage diff --git a/pyproject.toml b/pyproject.toml index 4a30cb0..f2bfad7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kbmate-cli" -version = "0.1.0" +version = "0.2.0" description = "CLI tools for knowledge base management" readme = "README.md" requires-python = ">=3.12" @@ -20,7 +20,11 @@ build-backend = "hatchling.build" [tool.uv] package = true +[tool.pytest.ini_options] +addopts = "--cov=src --cov-report=term-missing --cov-fail-under=85" + [dependency-groups] dev = [ "pytest>=9.0.3", + "pytest-cov>=6.0.0", ] diff --git a/uv.lock b/uv.lock index eda5801..8610471 100644 --- a/uv.lock +++ b/uv.lock @@ -32,6 +32,90 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" }, ] +[[package]] +name = "coverage" +version = "7.14.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/09/1e/2f996b2c8415cbb6f54b0f5ec1ee850c96d7911961afb4fc05f4a89d8c58/coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662" }, + { url = "https://mirrors.aliyun.com/pypi/packages/75/cf/a8f4b43a16e194b0261257ad28ded5853ec052570afef4a84e1d81189f3b/coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67" }, + { url = "https://mirrors.aliyun.com/pypi/packages/22/ec/c936d495fcd67f48f03a9c4ad3297ff80d1f222a5df3980f15b34c186c21/coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5c/42/5af63f636cc62a4a2b1b3ba9146f6ee6f53a35a50d5cefc54d5670f60999/coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/26/d3/a225317bd2012132a27e1176d51660b826f99bb975876463c44ea0d7ee5a/coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f1/7f/9e65495298c3ea414742998539c37d048b5e81cc818fb1828cc6b51d10bf/coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/94/46/1522b524a35bdad22b2b8c4f9d32d0a104b524726ec380b2db68db1746f5/coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f3/e9/cdf00d38817742c541ade405e115a3f7bf36e6f2a8b99d4f209861b85a2d/coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/38/fc/5e7877cf5f902d08a17ff1c532511476d87e1bea355bd5028cb97f902e79/coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/18/9d/50f05a72dff8487464fdd4178dda5daed642a060e60afb644e3d45123559/coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595" }, + { url = "https://mirrors.aliyun.com/pypi/packages/00/3f/6f61ffe6439df266c3cf60f5c99cfaa21103d0210d706a42fc6c30683ff8/coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27" }, + { url = "https://mirrors.aliyun.com/pypi/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/74/18/9f7fe62f659f24b7a82a0be56bf94c1bd0a89e0ae7ab4c668f6e82404294/coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6b/76/b7c66ee3c66e1b0f9d894c8125983aa0c03fb2336f2fd16559f9c966157f/coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66" }, + { url = "https://mirrors.aliyun.com/pypi/packages/44/6f/9ad575d505b4d805b254febc8a5b338a2efe278f8786e56ff1cb8413f9c3/coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca" }, + { url = "https://mirrors.aliyun.com/pypi/packages/29/1e/51adf17738976e8f2b85ddef7b7aa12a0838b056c92f175941d8862767c1/coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9e/7b/5bfd7ac1df3b881c2ac7a5cbc99c7609e6296c402f5ef587cd81c6f355b3/coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7d/38/1d37d316b174fad3843a1d76dbdfe4398771c9ecd0515935dd9ece9cd627/coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367" }, + { url = "https://mirrors.aliyun.com/pypi/packages/34/46/746704f95980ba220214e1a41e18cec5aea80a898eaa53c51bf2d645ff36/coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e1/b9/bbe87206d9687b192352f893797825b5f5b15ecd3aa9c68fbff0c074d77b/coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087" }, + { url = "https://mirrors.aliyun.com/pypi/packages/46/57/b8cdb12ac0d73ef0243218bd5e22c9df8f92edab8018213a86aec67c5324/coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1f/d4/5002019538b2036ce3c84340f54d2fd5100d55b0a6b0894eee56128d03c7/coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52" }, + { url = "https://mirrors.aliyun.com/pypi/packages/37/53/20c5009477660f084e6ed60bc02a91894b8e234e617e86ecfd9aaf78e27b/coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ae/ab/3cf6427ac9c1f1db747dbb1ce71dde47984876d4c2cfd018a3fef0a78d4d/coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a3/99/118daa192f95e3a6cb2740100fbf8797cda1734b4134ef0b5d501a7fa8f3/coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e6/f1/a46cc0c013be170216253184a32366d7cbdb9252feaec866b05c2d12a894/coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90" }, + { url = "https://mirrors.aliyun.com/pypi/packages/64/8c/9c30a3d311a34177fa432995be7fbfc64477d8bac5630bd38055b1c9b424/coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9a/cd/3fb5e06c3badefd0c1b47e2044fdca67f8220a4ec2e7fcfb476aa0a67c6c/coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a8/e6/fbc322325c7294d3e22c1ad6b79e45d0806b25228c8e5842aed6d8169aa7/coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/08/92/c497b264bec1673c47cc77e26f760fcda4654cabf1f39546d1a23a3b8c35/coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426" }, + { url = "https://mirrors.aliyun.com/pypi/packages/78/fc/045da320987f401af5d2815d351e8aa799aec859f60e29f445e3089eeedb/coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1b/ae/227b1e379497fb7a4fc3286e620f80c8a1e7cec66d45695a01639eb1af65/coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a0/f5/3570342900f2acea31d33ff1590c5d8bac1a8e1a2e1c6d34a5d5e61de681/coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90" }, + { url = "https://mirrors.aliyun.com/pypi/packages/16/29/de1bbc01c935b28f89b1dc3db85b011c055e843a8e5e3b83141c3f80af7f/coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/35/95/f53890b0bf2fc10ab168e05d38869215e73ca24c4cb521c3bb0eb62fe16b/coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ed/ea/c919e259081dd2bdf0e43b87209709ba7ec2e4117c2a7f5185379c43463c/coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1a/2c/c2831889705a81dc5d1c6ca12e4d8e9b95dfc146d153488a6c0ea685d28e/coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5a/a9/2fcae5003cac3d63fe344d2166243c2756935f48420863c5272b240d550b/coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3f/bb/18e94d7b14b9b398164197114a587a04ab7c9fdbe1d237eef57311c5e883/coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917" }, + { url = "https://mirrors.aliyun.com/pypi/packages/db/56/4f14fad782b035c81c4ffd09159e7103d42bb1d93ac8496d04b90a11b7da/coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63" }, + { url = "https://mirrors.aliyun.com/pypi/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa" }, + { url = "https://mirrors.aliyun.com/pypi/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca" }, + { url = "https://mirrors.aliyun.com/pypi/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84" }, + { url = "https://mirrors.aliyun.com/pypi/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db" }, + { url = "https://mirrors.aliyun.com/pypi/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644" }, + { url = "https://mirrors.aliyun.com/pypi/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1" }, +] + [[package]] name = "flatbuffers" version = "25.12.19" @@ -51,7 +135,7 @@ wheels = [ [[package]] name = "kbmate-cli" -version = "0.1.0" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "pymupdf4llm" }, @@ -62,6 +146,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "pytest" }, + { name = "pytest-cov" }, ] [package.metadata] @@ -72,7 +157,10 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "pytest", specifier = ">=9.0.3" }] +dev = [ + { name = "pytest", specifier = ">=9.0.3" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, +] [[package]] name = "markdown-it-py" @@ -313,6 +401,20 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9" }, ] +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678" }, +] + [[package]] name = "pyyaml" version = "6.0.3"