feat: migrate src/ second-brain + tests + python tooling (#17)
* feat(src): migrate second-brain RAG CLI + tests * fix(embedder): strip hardcoded ai.slaid098.dev endpoint * refactor: rename slaid098/opencode to opencode-config * docs(handoff): add pr-5 handoff + ADR-001 * fix(docs): rebase handoff/ADR naming to PR number + drop dangling ADR-009 refs * fix(tests): update script paths + assertions for opencode-config migration * fix(pyproject): update cov + ruff paths config/scripts -> .opencode/scripts * docs: update project map + handoff + ADR --------- Co-authored-by: opencode-agent <agent@slaid098.dev>
This commit is contained in:
parent
a8f9aa0bc6
commit
6005c88203
29 changed files with 5241 additions and 4 deletions
16
.pre-commit-config.yaml
Normal file
16
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.18.2
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies:
|
||||
- httpx
|
||||
- numpy
|
||||
- tenacity
|
||||
|
|
@ -1 +1 @@
|
|||
3.12
|
||||
3.13
|
||||
|
|
|
|||
|
|
@ -17,5 +17,5 @@ Accepted
|
|||
|
||||
## Альтернативы
|
||||
|
||||
- Сохранить config/ + bind-mount — отклонено (LSP-конфликт ADR-009, не zero-config)
|
||||
- Сохранить config/ + bind-mount — отклонено (LSP-конфликт pyproject.toml, не zero-config)
|
||||
- Дропнуть skills (использовать только глобальные) — отклонено (skills нужны в репо для публичного shareable config)
|
||||
|
|
|
|||
20
docs/decisions/003-pr-17-migrate-src-tests.md
Normal file
20
docs/decisions/003-pr-17-migrate-src-tests.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# ADR-003: Migrate src/ second-brain + tests from fix-branch
|
||||
|
||||
## Статус
|
||||
|
||||
Accepted
|
||||
|
||||
## Контекст
|
||||
|
||||
Миграция Python RAG CLI (`second-brain`) из приватного репо в публичный. Source: ветка `fix/pipeline-status/review-next-by-verdict` (содержит фикс pipeline-status REVIEW verdict branching). Hardcoded приватный API endpoint `ai.slaid098.dev` в embedder.py.
|
||||
|
||||
## Решение
|
||||
|
||||
- Копировать файлы из fix-ветки (не master) — фикс включён автоматически
|
||||
- Strip `ai.slaid098.dev` → env-only `AI_PROVIDER_API_URL`
|
||||
- Rename `slaid098/opencode` → `slaid098/opencode-config` в URLs и тестах
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- Дропнуть `src/memory/` полностью (memory plugin `@mathew-cf/opencode-memory` покрывает RAG) — отклонено, second-brain используется как standalone CLI
|
||||
- Копировать из master + отдельный PR для фикса — отклонено, fix-ветка fast-forward, проще взять целиком
|
||||
29
docs/handoff/pr-17-migrate-src-tests-python.md
Normal file
29
docs/handoff/pr-17-migrate-src-tests-python.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# PR #17: Migrate src/ + tests/ + Python tooling
|
||||
|
||||
## Что сделано
|
||||
|
||||
- Перенесён Python-пакет `second-brain` (RAG CLI): `src/memory/` (6 файлов)
|
||||
- Перенесены тесты: `tests/` (15 файлов: 13 .py + 1 .mjs + 1 .ts)
|
||||
- Перенесён Python tooling: `pyproject.toml`, `uv.lock`, `.pre-commit-config.yaml`, `.python-version`
|
||||
- `src/memory/embedder.py`: вынесен hardcoded endpoint `ai.slaid098.dev` → env-only (`AI_PROVIDER_API_URL`)
|
||||
- `pyproject.toml`: rename `slaid098/opencode` → `slaid098/opencode-config` в [project.urls]
|
||||
- Тесты: mock URLs обновлены `slaid098/opencode` → `slaid098/opencode-config`
|
||||
- Source: ветка `fix/pipeline-status/review-next-by-verdict` (включает фикс `610452f` + тесты `01212b5`)
|
||||
|
||||
## Почему
|
||||
|
||||
Миграция из приватного репо `slaid098/opencode` в публичный `slaid098/opencode-config`. Hardcoded приватный API endpoint удалён — env-only для публичного релиза.
|
||||
|
||||
## Pending
|
||||
|
||||
- `pipeline-status.py` (fix `610452f`) мигрирует через #7 (`.opencode/scripts/`), не через #5
|
||||
- `pyproject.toml` в корне может триггерить LSP-конфликт на bare-metal Windows — отслеживать
|
||||
|
||||
## Watch out
|
||||
|
||||
- Issue #5 заявляла "14 tests файлов", по факту 15 (13 .py + .mjs + .ts) — расхождение +1
|
||||
- `config/scripts/pipeline-status.py` НЕ в scope #5 — он в `config/scripts/` → мигрирует через #7
|
||||
- `tests/test_pipeline_status.py` содержит тесты для REQUEST_CHANGES/NEEDS_DISCUSSION NEXT (из fix-ветки)
|
||||
- 10 из 15 тестов падают (FileNotFoundError) — они зависят от `config/scripts/` и `config/tools/` которых нет в репо (мигрируют через #7)
|
||||
- `tests/test_embedder.py` обновлён: `os.environ.setdefault("AI_PROVIDER_API_URL", "http://test/v1")` перед import — необходим из-за strip hardcoded URL в embedder.py
|
||||
- `ruff format` реформатнул 2 файла (test_pipeline_status.py, test_spec_status.py) — строки превысили 100 chars после добавления `-config` к URL
|
||||
|
|
@ -54,6 +54,33 @@ opencode-config/
|
|||
│ ├── handoff/ # PR handoffs (pr-<N>-<slug>.md)
|
||||
│ ├── decisions/ # ADRs (NNN-pr-<N>-<slug>.md)
|
||||
│ └── project-map/ # This file — structure snapshot
|
||||
├── src/ # Python RAG CLI (second-brain) — PR#17
|
||||
│ └── memory/
|
||||
│ ├── __init__.py
|
||||
│ ├── __main__.py # Entry point for `python -m memory`
|
||||
│ ├── cli.py # CLI commands
|
||||
│ ├── embedder.py # Embedding via AI_PROVIDER_API_URL (env-only)
|
||||
│ ├── index.py # Indexing
|
||||
│ └── search.py # Search
|
||||
├── tests/ # pytest + TS/MJS test suite — PR#17
|
||||
│ ├── _ts_loader.mjs # TS test loader (imports pipeline-status.ts / spec-status.ts)
|
||||
│ ├── test_check_adr_refs.py # adr-check.yml validator
|
||||
│ ├── test_check_permissions.py # permissions-check.yml validator
|
||||
│ ├── test_cli.py # src/memory/cli.py
|
||||
│ ├── test_embedder.py # src/memory/embedder.py (mocks AI_PROVIDER_API_URL)
|
||||
│ ├── test_index.py # src/memory/index.py
|
||||
│ ├── test_observability.py # .opencode/scripts/observability.py
|
||||
│ ├── test_pipeline_status.py # .opencode/scripts/pipeline-status.py (REVIEW verdict branching)
|
||||
│ ├── test_pipeline_status_adr.py
|
||||
│ ├── test_pipeline_status_ci.py
|
||||
│ ├── test_pipeline_status_tool.py
|
||||
│ ├── test_pipeline_status_tool.ts # TS wrapper test (mjs loader)
|
||||
│ ├── test_search.py # src/memory/search.py
|
||||
│ ├── test_spec_status.py # .opencode/scripts/spec-status.py
|
||||
│ └── test_spec_status_tool.py
|
||||
├── pyproject.toml # Python project (uv, ruff, pytest config)
|
||||
├── uv.lock # Locked deps for Python project
|
||||
├── .pre-commit-config.yaml # ruff + UV hooks
|
||||
├── app_data/
|
||||
│ ├── workspaces/ # Agent working directory (.gitkeep)
|
||||
│ └── ssh/ # SSH keys, not in git (.gitkeep)
|
||||
|
|
@ -66,8 +93,8 @@ opencode-config/
|
|||
|
||||
## Pending (future PRs)
|
||||
|
||||
- `src/` — Python RAG CLI (second-brain) — after #5 (PR#17)
|
||||
- `tests/` — pytest test suite — after #5
|
||||
- `.opencode/scripts/pipeline-status.py` fix `610452f` — PR#7 (config/scripts/ migration)
|
||||
- `.opencode/tools/` TS wrappers — PR#7 (config/tools/ migration)
|
||||
|
||||
## Update Protocol
|
||||
|
||||
|
|
|
|||
128
pyproject.toml
Normal file
128
pyproject.toml
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "second-brain"
|
||||
version = "0.1.0"
|
||||
description = "Docker-based AI coding assistant with persistent memory"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
requires-python = ">=3.12"
|
||||
authors = [{ name = "slaid098" }]
|
||||
keywords = []
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"httpx",
|
||||
"numpy",
|
||||
"tenacity",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=8.0",
|
||||
"pytest-cov>=5.0",
|
||||
"pytest-timeout>=2.2",
|
||||
"mypy>=1.10",
|
||||
"ruff>=0.5",
|
||||
"xenon>=0.9",
|
||||
"pre-commit>=3.7",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/slaid098/opencode-config"
|
||||
Repository = "https://github.com/slaid098/opencode-config"
|
||||
Issues = "https://github.com/slaid098/opencode-config/issues"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src"]
|
||||
|
||||
# ── Ruff ──────────────────────────────────────────────────────────────────
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py312"
|
||||
line-length = 100
|
||||
src = ["src", "tests"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", "W",
|
||||
"F",
|
||||
"I",
|
||||
"B",
|
||||
"UP",
|
||||
"SIM",
|
||||
"C90",
|
||||
"PL",
|
||||
"RUF",
|
||||
"S",
|
||||
"TRY",
|
||||
"LOG",
|
||||
]
|
||||
ignore = [
|
||||
"S101",
|
||||
"S311",
|
||||
"RUF001",
|
||||
"RUF002",
|
||||
"RUF003",
|
||||
"TRY003",
|
||||
"PLR2004",
|
||||
"S106",
|
||||
]
|
||||
|
||||
[tool.ruff.lint.mccabe]
|
||||
max-complexity = 10
|
||||
|
||||
[tool.ruff.lint.pylint]
|
||||
max-args = 5
|
||||
max-branches = 12
|
||||
max-returns = 5
|
||||
max-statements = 50
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/*" = ["S101", "PLR2004", "S106", "S603", "S607"]
|
||||
".opencode/scripts/*" = ["S603", "S607"]
|
||||
|
||||
# ── mypy ──────────────────────────────────────────────────────────────────
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.12"
|
||||
strict = true
|
||||
explicit_package_bases = true
|
||||
warn_return_any = true
|
||||
disallow_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
check_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
warn_redundant_casts = true
|
||||
warn_unused_ignores = true
|
||||
|
||||
# ── pytest ────────────────────────────────────────────────────────────────
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "--cov=src --cov=.opencode/scripts --cov-report=term-missing --cov-fail-under=80 --timeout=120"
|
||||
testpaths = ["tests"]
|
||||
asyncio_mode = "auto"
|
||||
|
||||
# ── coverage ──────────────────────────────────────────────────────────────
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["src", ".opencode/scripts"]
|
||||
branch = true
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"if __name__ == .__main__.:",
|
||||
"if TYPE_CHECKING:",
|
||||
]
|
||||
0
src/memory/__init__.py
Normal file
0
src/memory/__init__.py
Normal file
4
src/memory/__main__.py
Normal file
4
src/memory/__main__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from src.memory.cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
34
src/memory/cli.py
Normal file
34
src/memory/cli.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import argparse
|
||||
|
||||
from src.memory.index import run_index
|
||||
from src.memory.search import run_search
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> None:
|
||||
parser = argparse.ArgumentParser(prog="rag")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
search_p = sub.add_parser("search")
|
||||
search_p.add_argument("query")
|
||||
search_p.add_argument("-i", "--index-dir", required=True)
|
||||
search_p.add_argument("-k", type=int, default=15)
|
||||
search_p.add_argument("--json", action="store_true")
|
||||
|
||||
index_p = sub.add_parser("index")
|
||||
index_p.add_argument("memory_dir")
|
||||
index_p.add_argument("-o", "--output", required=True)
|
||||
|
||||
sub.add_parser("download")
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.command == "search":
|
||||
run_search(args)
|
||||
elif args.command == "index":
|
||||
run_index(args)
|
||||
elif args.command == "download":
|
||||
print("Gemini API: model download not needed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
48
src/memory/embedder.py
Normal file
48
src/memory/embedder.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import os
|
||||
|
||||
import httpx
|
||||
from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential
|
||||
|
||||
API_URL = os.environ.get("AI_PROVIDER_API_URL")
|
||||
if not API_URL:
|
||||
raise RuntimeError("AI_PROVIDER_API_URL env var not set")
|
||||
API_KEY = os.environ.get("AI_PROVIDER_API_KEY", "")
|
||||
EMBEDDING_MODEL = "gemini-embedding-2-preview"
|
||||
|
||||
|
||||
def _is_retryable(exc: BaseException) -> bool:
|
||||
if isinstance(exc, (httpx.ConnectError, httpx.TimeoutException)):
|
||||
return True
|
||||
if isinstance(exc, httpx.HTTPStatusError):
|
||||
status = exc.response.status_code
|
||||
return status == 429 or status >= 500
|
||||
return False
|
||||
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=1, min=2, max=10),
|
||||
retry=retry_if_exception(_is_retryable),
|
||||
)
|
||||
def _call_embedding_api(
|
||||
client: httpx.Client,
|
||||
url: str,
|
||||
headers: dict[str, str],
|
||||
payload: dict[str, str | list[str]],
|
||||
) -> list[list[float]]:
|
||||
resp = client.post(url, json=payload, headers=headers)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
return [d["embedding"] for d in data["data"]]
|
||||
|
||||
|
||||
def embed_texts(texts: list[str]) -> list[list[float]]:
|
||||
if not texts:
|
||||
return []
|
||||
|
||||
url = f"{API_URL}/embeddings"
|
||||
headers = {"Authorization": f"Bearer {API_KEY}"}
|
||||
payload: dict[str, str | list[str]] = {"model": EMBEDDING_MODEL, "input": texts}
|
||||
|
||||
with httpx.Client(timeout=120.0) as client:
|
||||
return _call_embedding_api(client, url, headers, payload)
|
||||
49
src/memory/index.py
Normal file
49
src/memory/index.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from src.memory.embedder import embed_texts
|
||||
|
||||
INDEX_FILENAME = "index.json"
|
||||
|
||||
|
||||
def _extract_text(content: str) -> str:
|
||||
if content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
return parts[2].strip()
|
||||
return content.strip()
|
||||
|
||||
|
||||
def run_index(args: argparse.Namespace) -> None:
|
||||
memory_dir = Path(args.memory_dir)
|
||||
output_dir = Path(args.output)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
md_files = sorted(memory_dir.rglob("*.md"))
|
||||
md_files = [f for f in md_files if ".rag" not in f.parts]
|
||||
|
||||
if not md_files:
|
||||
print("No .md files found")
|
||||
return
|
||||
|
||||
texts: list[str] = []
|
||||
file_map: list[dict[str, str]] = []
|
||||
|
||||
for fpath in md_files:
|
||||
rel = fpath.relative_to(memory_dir)
|
||||
content = fpath.read_text(encoding="utf-8")
|
||||
text = _extract_text(content)
|
||||
texts.append(text)
|
||||
file_map.append({"source": str(rel), "text": text[:500]})
|
||||
|
||||
embeddings = embed_texts(texts)
|
||||
|
||||
index = {
|
||||
"files": [{**fm, "embedding": emb} for fm, emb in zip(file_map, embeddings, strict=False)],
|
||||
}
|
||||
|
||||
(output_dir / INDEX_FILENAME).write_text(
|
||||
json.dumps(index, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
print(f"Indexed {len(md_files)} files to {output_dir / INDEX_FILENAME}")
|
||||
42
src/memory/search.py
Normal file
42
src/memory/search.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from src.memory.embedder import embed_texts
|
||||
|
||||
|
||||
def _cosine_sim(a: np.ndarray, b: np.ndarray) -> float:
|
||||
norm_a = np.linalg.norm(a)
|
||||
norm_b = np.linalg.norm(b)
|
||||
if norm_a == 0 or norm_b == 0:
|
||||
return 0.0
|
||||
return float(np.dot(a, b) / (norm_a * norm_b))
|
||||
|
||||
|
||||
def run_search(args: argparse.Namespace) -> None:
|
||||
index_path = Path(args.index_dir) / "index.json"
|
||||
if not index_path.exists():
|
||||
json.dump([], sys.stdout)
|
||||
return
|
||||
|
||||
index = json.loads(index_path.read_text(encoding="utf-8"))
|
||||
files = index.get("files", [])
|
||||
if not files:
|
||||
json.dump([], sys.stdout)
|
||||
return
|
||||
|
||||
query_emb = embed_texts([args.query])[0]
|
||||
query_vec = np.array(query_emb)
|
||||
|
||||
results: list[dict[str, str | float]] = []
|
||||
for f in files:
|
||||
file_vec = np.array(f["embedding"])
|
||||
sim = _cosine_sim(query_vec, file_vec)
|
||||
results.append({"source": f["source"], "score": sim, "text": f["text"]})
|
||||
|
||||
results.sort(key=lambda r: r["score"], reverse=True)
|
||||
top = results[: args.k]
|
||||
|
||||
print(json.dumps(top, ensure_ascii=False))
|
||||
187
tests/_ts_loader.mjs
Normal file
187
tests/_ts_loader.mjs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
// Test harness for .opencode/tools/pipeline-status.ts.
|
||||
//
|
||||
// Why this file: there is no bun/tsx/esbuild on the CI runner (only node +
|
||||
// pytest). The pipeline-status.ts tool is a TypeScript module (Bun runtime
|
||||
// for opencode, not runnable with plain `node`). To exercise the tool's
|
||||
// execute() function in pytest, we strip TS-only syntax (import type
|
||||
// annotations + ESM import -> CJS require) and load the resulting JS as a
|
||||
// CommonJS module. The tool() factory from @opencode-ai/plugin is identity
|
||||
// (returns its argument), so we can replace it with a passthrough shim
|
||||
// without changing the tool semantics.
|
||||
//
|
||||
// This file is consumed by tests/test_pipeline_status_tool.py via:
|
||||
// node tests/_ts_loader.mjs <mode> [args...]
|
||||
// where <mode> is one of: load, exec, exec_real.
|
||||
//
|
||||
// - load: print the {description, args keys} of the tool — sanity check.
|
||||
// - exec: call execute({pr_number: <int>}) with a stubbed spawnSync that
|
||||
// returns whatever argv it received, plus a fixture stdout. Used by unit
|
||||
// tests to verify args passed and output trimming.
|
||||
// - exec_real: call execute({pr_number: <int>}) with the REAL spawnSync,
|
||||
// used by the integration test against the real pipeline-status.py.
|
||||
|
||||
import { readFileSync } from "node:fs"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import path from "node:path"
|
||||
import { spawnSync } from "node:child_process"
|
||||
|
||||
const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), "..", "..")
|
||||
const DEFAULT_TS_FILE = path.join(REPO_ROOT, ".opencode", "tools", "pipeline-status.ts")
|
||||
// Parameterize via env var TS_FILE (relative to REPO_ROOT) so other TS tool
|
||||
// wrappers (e.g. spec-status.ts) can be loaded by the same harness without
|
||||
// breaking existing callers that don't set TS_FILE (default: pipeline-status.ts).
|
||||
const TS_FILE = process.env.TS_FILE
|
||||
? path.resolve(REPO_ROOT, process.env.TS_FILE)
|
||||
: DEFAULT_TS_FILE
|
||||
|
||||
// Minimal zod shim covering the methods the tool actually uses:
|
||||
// tool.schema.number().describe("...")
|
||||
// At runtime (in opencode/Bun) `tool.schema` is the real zod. Here we
|
||||
// only need a chainable builder that returns an object with .describe()
|
||||
// — no validation occurs in execute().
|
||||
function makeZodShim() {
|
||||
const chain = () => {
|
||||
const obj = {
|
||||
describe() { return obj },
|
||||
optional() { return obj },
|
||||
// Add more methods as future tool definitions need them.
|
||||
}
|
||||
return obj
|
||||
}
|
||||
return {
|
||||
number: chain,
|
||||
string: chain,
|
||||
boolean: chain,
|
||||
array: chain,
|
||||
object: chain,
|
||||
}
|
||||
}
|
||||
const zodShim = makeZodShim()
|
||||
|
||||
function stripTs(src) {
|
||||
// Minimal TS -> JS for this specific file:
|
||||
// 1) `import { spawnSync } from "child_process"` -> `const { spawnSync } = require("child_process")`
|
||||
// 2) `import { tool } from "@opencode-ai/plugin"` -> `const tool = (x) => x`
|
||||
// 3) `import path from "path"` -> `const path = require("path")`
|
||||
// 4) `import.meta.dir` -> a stub pointing at .opencode/tools (so the script
|
||||
// path resolves to .opencode/scripts/pipeline-status.py)
|
||||
// 5) `args: z.ZodObject` -> the args are referenced inside execute as
|
||||
// `args.pr_number`; the schema itself is unused at runtime here.
|
||||
// 6) Strip `: type` annotations and `async execute(args)` stays.
|
||||
let out = src
|
||||
out = out.replace(/^import\s+\{\s*spawnSync\s*\}\s+from\s+["']child_process["'];?\s*$/m, 'const { spawnSync } = require("child_process");')
|
||||
out = out.replace(/^import\s+path\s+from\s+["']path["'];?\s*$/m, 'const path = require("path");')
|
||||
out = out.replace(/^import\s+\{\s*tool\s*\}\s+from\s+["']@opencode-ai\/plugin["'];?\s*$/m, 'const tool = (x) => x;')
|
||||
// Replace `import.meta.dir` with the directory of the TS file.
|
||||
out = out.replace(/import\.meta\.dir/g, JSON.stringify(path.dirname(TS_FILE)))
|
||||
return out
|
||||
}
|
||||
|
||||
function loadTool(spawnSyncImpl) {
|
||||
// Provide a CommonJS module sandbox so the tool file's `export default`
|
||||
// becomes accessible via `module.exports.default`.
|
||||
let src = stripTs(readFileSync(TS_FILE, "utf-8"))
|
||||
// Strip the require/const declarations we replace with sandbox args, so
|
||||
// we don't get "Identifier already declared" between Function args and
|
||||
// the in-source `const { spawnSync } = require(...)` lines.
|
||||
src = src.replace(/^const\s+\{\s*spawnSync\s*\}\s*=\s*require\(["']child_process["']\);?\s*$/m, "")
|
||||
src = src.replace(/^const\s+path\s*=\s*require\(["']path["']\);?\s*$/m, "")
|
||||
src = src.replace(/^const\s+tool\s*=\s*\(x\)\s*=>\s*x;?\s*$/m, "")
|
||||
// Convert `export default tool({...})` into `module.exports.default = tool({...})`
|
||||
const cjs = src.replace(/^export default /m, "module.exports.default = ")
|
||||
// tool shim with .schema = zodShim (since pipeline-status.ts uses tool.schema.number())
|
||||
const toolShim = (x) => x
|
||||
toolShim.schema = zodShim
|
||||
const fn = new Function(
|
||||
"module",
|
||||
"require",
|
||||
"spawnSync",
|
||||
"path",
|
||||
"tool",
|
||||
cjs + "\nreturn module.exports.default;",
|
||||
)
|
||||
return fn({ exports: {} }, (name) => {
|
||||
if (name === "child_process") return { spawnSync: spawnSyncImpl }
|
||||
if (name === "path") return path
|
||||
throw new Error("unexpected require: " + name)
|
||||
}, spawnSyncImpl, path, toolShim)
|
||||
}
|
||||
|
||||
function buildExecArgs(tool, rawValue) {
|
||||
// Interpret ``rawValue`` (argv string) as the tool's first declared arg.
|
||||
// - pipeline-status.ts: ``pr_number`` (int) → parseInt
|
||||
// - spec-status.ts: ``validate`` (bool) → /true/i match
|
||||
// Detection is dynamic so the harness works for any single-arg tool
|
||||
// without hardcoding tool names. If the tool declares no args, return {}.
|
||||
const keys = Object.keys(tool.args || {})
|
||||
if (keys.length === 0) return {}
|
||||
const first = keys[0]
|
||||
if (first === "pr_number") return { pr_number: parseInt(rawValue, 10) }
|
||||
if (first === "validate") return { validate: /^true$/i.test(rawValue || "") }
|
||||
// Fallback heuristic: numeric → int, else bool-ish.
|
||||
return { [first]: rawValue }
|
||||
}
|
||||
|
||||
function main() {
|
||||
const mode = process.argv[2]
|
||||
if (!mode) {
|
||||
console.error("usage: node _ts_loader.mjs <mode> [args...]")
|
||||
process.exit(2)
|
||||
}
|
||||
if (mode === "load") {
|
||||
const t = loadTool(spawnSync)
|
||||
console.log(JSON.stringify({ description: t.description, args: Object.keys(t.args) }))
|
||||
return
|
||||
}
|
||||
if (mode === "exec_stub") {
|
||||
// Args: <first_arg_value> <stub_status> <stub_stdout> <stub_stderr>
|
||||
// The first arg is interpreted based on which tool is loaded:
|
||||
// - pipeline-status.ts declares ``pr_number`` (int)
|
||||
// - spec-status.ts declares ``validate`` (bool)
|
||||
// Detected dynamically from ``t.args`` keys so the harness stays generic.
|
||||
const stubStatus = parseInt(process.argv[4], 10)
|
||||
const stubStdout = process.argv[5]
|
||||
const stubStderr = process.argv[6] || ""
|
||||
const callLog = []
|
||||
const stub = (cmd, args, opts) => {
|
||||
callLog.push({ cmd, args, opts })
|
||||
return { status: stubStatus, stdout: stubStdout, stderr: stubStderr }
|
||||
}
|
||||
const t = loadTool(stub)
|
||||
const execArgs = buildExecArgs(t, process.argv[3])
|
||||
t.execute(execArgs, {
|
||||
// ToolContext — only fields the tool actually touches. Our tool
|
||||
// touches none of the ctx fields, so this can be empty-ish.
|
||||
sessionID: "test", messageID: "test", agent: "test",
|
||||
directory: REPO_ROOT, worktree: REPO_ROOT,
|
||||
abort: new AbortController().signal,
|
||||
metadata() {}, async ask() {},
|
||||
}).then(
|
||||
(result) => {
|
||||
console.log(JSON.stringify({ result, calls: callLog }))
|
||||
},
|
||||
(err) => {
|
||||
console.log(JSON.stringify({ error: String(err), calls: callLog }))
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
if (mode === "exec_real") {
|
||||
const t = loadTool(spawnSync) // use real spawnSync
|
||||
const execArgs = buildExecArgs(t, process.argv[3])
|
||||
t.execute(execArgs, {
|
||||
sessionID: "test", messageID: "test", agent: "test",
|
||||
directory: REPO_ROOT, worktree: REPO_ROOT,
|
||||
abort: new AbortController().signal,
|
||||
metadata() {}, async ask() {},
|
||||
}).then(
|
||||
(result) => console.log(JSON.stringify({ result })),
|
||||
(err) => console.log(JSON.stringify({ error: String(err) })),
|
||||
)
|
||||
return
|
||||
}
|
||||
console.error("unknown mode: " + mode)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
main()
|
||||
157
tests/test_check_adr_refs.py
Normal file
157
tests/test_check_adr_refs.py
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
"""Tests for .opencode/scripts/check-adr-refs.py — dangling ADR reference guard.
|
||||
|
||||
Covers the deterministic ADR-ref guard: clean repo passes, a dangling
|
||||
``ADR-999`` in a temp handoff is detected, self-reference inside an
|
||||
``NNN-*.md`` file is excluded, a valid cross-ref (``ADR-002``) passes,
|
||||
a file with no ADR refs passes, and ``node_modules/`` is excluded.
|
||||
|
||||
Strategy mirrors ``tests/test_check_permissions.py``:
|
||||
- ``test_clean_repo_passes`` runs the script as a subprocess on the
|
||||
real (clean) repo — black-box, returncode 0, OK message in stdout.
|
||||
- The remaining tests load the script in-process via importlib and
|
||||
monkeypatch ``REPO_ROOT`` / ``ADR_DIR`` to point at a tmp_path tree,
|
||||
then call ``main()`` and assert on captured stdout / SystemExit code.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
SCRIPT_PATH = REPO_ROOT / ".opencode" / "scripts" / "check-adr-refs.py"
|
||||
|
||||
|
||||
def _load_script():
|
||||
spec = importlib.util.spec_from_file_location("check_adr_refs", SCRIPT_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules["check_adr_refs"] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
car = _load_script()
|
||||
|
||||
|
||||
# ── clean repo (subprocess black-box) ───────────────────────────────────────
|
||||
|
||||
|
||||
def test_clean_repo_passes():
|
||||
"""On the current (clean) repo the script exits 0 with the OK message."""
|
||||
result = subprocess.run(
|
||||
["python3", str(SCRIPT_PATH)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "OK: No dangling ADR references." in result.stdout
|
||||
|
||||
|
||||
# ── helpers for in-process tests ────────────────────────────────────────────
|
||||
|
||||
|
||||
def _setup_tmp_repo(tmp_path: Path, monkeypatch) -> tuple[Path, Path]:
|
||||
"""Point the script's REPO_ROOT and ADR_DIR at a tmp_path tree.
|
||||
|
||||
Creates the ``docs/decisions/`` dir and returns (repo_root, adr_dir).
|
||||
"""
|
||||
repo = tmp_path / "repo"
|
||||
adr_dir = repo / "docs" / "decisions"
|
||||
adr_dir.mkdir(parents=True)
|
||||
handoff_dir = repo / "docs" / "handoff"
|
||||
handoff_dir.mkdir(parents=True)
|
||||
monkeypatch.setattr(car, "REPO_ROOT", repo)
|
||||
monkeypatch.setattr(car, "ADR_DIR", adr_dir)
|
||||
return repo, adr_dir
|
||||
|
||||
|
||||
# ── dangling reference detected ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_dangling_ref_detected(tmp_path, monkeypatch, capsys):
|
||||
"""A handoff referencing ADR-999 (no 999-*.md) → exit 1 + ADR-999 in output."""
|
||||
repo, _adr_dir = _setup_tmp_repo(tmp_path, monkeypatch)
|
||||
(repo / "docs" / "handoff" / "pr-1-test.md").write_text("See ADR-999 for context.\n")
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
car.main()
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
captured = capsys.readouterr()
|
||||
assert "ADR-999" in captured.out
|
||||
assert "FAIL" in captured.out
|
||||
|
||||
|
||||
# ── self-reference excluded ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_self_reference_excluded(tmp_path, monkeypatch, capsys):
|
||||
"""An ADR file ``019-pr-94-test.md`` mentioning ADR-019 → passes (self-ref)."""
|
||||
_repo, adr_dir = _setup_tmp_repo(tmp_path, monkeypatch)
|
||||
(adr_dir / "019-pr-94-test.md").write_text("# ADR-019: self-reference is OK.\n")
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
car.main()
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
captured = capsys.readouterr()
|
||||
assert "OK: No dangling ADR references." in captured.out
|
||||
|
||||
|
||||
# ── valid cross-reference passes ────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_valid_cross_ref_passes(tmp_path, monkeypatch, capsys):
|
||||
"""A handoff referencing ADR-002 when 002-*.md exists → passes."""
|
||||
repo, adr_dir = _setup_tmp_repo(tmp_path, monkeypatch)
|
||||
(adr_dir / "002-pr-56-pipeline-mandatory-adr.md").write_text("# ADR-002\n")
|
||||
(repo / "docs" / "handoff" / "pr-3-test.md").write_text("Per ADR-002, ADR is mandatory.\n")
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
car.main()
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
captured = capsys.readouterr()
|
||||
assert "OK: No dangling ADR references." in captured.out
|
||||
|
||||
|
||||
# ── no ADR refs in a file passes ────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_no_adr_refs_passes(tmp_path, monkeypatch, capsys):
|
||||
"""A handoff with no ADR-NNN references → passes."""
|
||||
repo, _adr_dir = _setup_tmp_repo(tmp_path, monkeypatch)
|
||||
(repo / "docs" / "handoff" / "pr-4-test.md").write_text("No ADR references here.\n")
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
car.main()
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
captured = capsys.readouterr()
|
||||
assert "OK: No dangling ADR references." in captured.out
|
||||
|
||||
|
||||
# ── node_modules excluded ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_exclude_node_modules(tmp_path, monkeypatch, capsys):
|
||||
"""A ``.md`` file under ``node_modules/`` with ADR-999 is NOT scanned."""
|
||||
repo, _adr_dir = _setup_tmp_repo(tmp_path, monkeypatch)
|
||||
nm = repo / "node_modules" / "some-pkg"
|
||||
nm.mkdir(parents=True)
|
||||
(nm / "README.md").write_text("Dangling ADR-999 should be ignored.\n")
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
car.main()
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
captured = capsys.readouterr()
|
||||
assert "OK: No dangling ADR references." in captured.out
|
||||
assert "ADR-999" not in captured.out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
152
tests/test_check_permissions.py
Normal file
152
tests/test_check_permissions.py
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
"""Tests for .opencode/scripts/check-permissions.py — dangerous permission rules guard.
|
||||
|
||||
Covers the ``DANGEROUS_PATTERNS`` guard: clean configs pass, ``gh pr checks*``
|
||||
in agent frontmatter or in ``opencode.json`` triggers a violation with the
|
||||
ADR-005 reference.
|
||||
|
||||
Strategy:
|
||||
- ``test_clean_configs_pass`` runs the script as a subprocess on the real
|
||||
(clean) repo configs — black-box, returncode 0, OK message in stdout.
|
||||
- ``test_gh_pr_checks_in_agent_violation`` writes a temporary agent .md file
|
||||
into a temp ``AGENTS_DIR`` and points the script at it via monkeypatch, then
|
||||
calls ``main`` in-process and captures stdout/stderr.
|
||||
- ``test_gh_pr_checks_in_opencode_json_violation`` mocks
|
||||
``parse_global_bash_rules`` to return a rule containing ``gh pr checks*``
|
||||
and asserts ``check_rules`` flags it with the ADR-005 reference.
|
||||
- ``test_gh_pr_view_statuscheckrollup_violation`` does the same for the
|
||||
narrower ``gh pr view *--json statusCheckRollup*`` pattern.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
SCRIPT_PATH = REPO_ROOT / ".opencode" / "scripts" / "check-permissions.py"
|
||||
|
||||
|
||||
def _load_script():
|
||||
spec = importlib.util.spec_from_file_location("check_permissions", SCRIPT_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules["check_permissions"] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
cp = _load_script()
|
||||
|
||||
|
||||
# ── clean configs (subprocess black-box) ────────────────────────────────────
|
||||
|
||||
|
||||
def test_clean_configs_pass():
|
||||
"""On current (clean) repo configs the script exits 0 with OK message."""
|
||||
result = subprocess.run(
|
||||
["python3", str(SCRIPT_PATH)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "OK: No dangerous permission rules found." in result.stdout
|
||||
|
||||
|
||||
# ── gh pr checks in agent frontmatter (in-process, tmp AGENTS_DIR) ──────────
|
||||
|
||||
|
||||
def test_gh_pr_checks_in_agent_violation(tmp_path, monkeypatch, capsys):
|
||||
"""A temp agent .md with ``"gh pr checks*": allow`` → exit 1 + ADR-005."""
|
||||
agents_dir = tmp_path / "agents"
|
||||
agents_dir.mkdir()
|
||||
agent_file = agents_dir / "test-agent.md"
|
||||
agent_file.write_text(
|
||||
"---\n"
|
||||
"description: test agent with dangerous rule\n"
|
||||
"mode: subagent\n"
|
||||
"permission:\n"
|
||||
" bash:\n"
|
||||
' "*": deny\n'
|
||||
' "gh pr checks*": allow\n'
|
||||
"---\n\n"
|
||||
"# test agent\n"
|
||||
)
|
||||
|
||||
monkeypatch.setattr(cp, "AGENTS_DIR", agents_dir)
|
||||
monkeypatch.setattr(cp, "OPENCODE_JSON", tmp_path / "missing-opencode.json")
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
cp.main()
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
captured = capsys.readouterr()
|
||||
assert "gh pr checks" in captured.out
|
||||
assert "ADR-005" in captured.out
|
||||
assert "Checks API" in captured.out
|
||||
|
||||
|
||||
# ── gh pr checks in opencode.json (mock parse_global_bash_rules) ────────────
|
||||
|
||||
|
||||
def test_gh_pr_checks_in_opencode_json_violation(monkeypatch):
|
||||
"""Mock parse_global_bash_rules returns gh pr checks* → check_rules flags it."""
|
||||
fake_rules = {"gh pr checks*": "allow"}
|
||||
monkeypatch.setattr(cp, "parse_global_bash_rules", lambda _path: fake_rules)
|
||||
|
||||
violations = cp.check_rules(fake_rules, "opencode.json")
|
||||
assert len(violations) == 1
|
||||
v = violations[0]
|
||||
assert "gh pr checks" in v
|
||||
assert "ADR-005" in v
|
||||
assert "Checks API" in v
|
||||
|
||||
|
||||
# ── gh pr view --json statusCheckRollup (mock parse_global_bash_rules) ──────
|
||||
|
||||
|
||||
def test_gh_pr_view_statuscheckrollup_violation():
|
||||
"""The narrower statusCheckRollup pattern is also flagged with ADR-005."""
|
||||
fake_rules = {"gh pr view *--json statusCheckRollup*": "allow"}
|
||||
violations = cp.check_rules(fake_rules, "opencode.json")
|
||||
assert len(violations) == 1
|
||||
v = violations[0]
|
||||
assert "statusCheckRollup" in v
|
||||
assert "ADR-005" in v
|
||||
assert "Checks API" in v
|
||||
|
||||
|
||||
# ── deny action is not a violation ──────────────────────────────────────────
|
||||
|
||||
|
||||
def test_deny_action_not_violation():
|
||||
"""A dangerous pattern with action=deny is not flagged (only allow is)."""
|
||||
fake_rules = {"gh pr checks*": "deny"}
|
||||
violations = cp.check_rules(fake_rules, "opencode.json")
|
||||
assert violations == []
|
||||
|
||||
|
||||
# ── gh pr merge in agent allow-list (CI guard, ADR-017) ──────────────────────
|
||||
|
||||
|
||||
def test_gh_pr_merge_in_agent_detected(tmp_path, monkeypatch):
|
||||
"""gh pr merge* in agent allow-list triggers violation (CI guard)."""
|
||||
agent_file = tmp_path / "agents" / "reviewer.md"
|
||||
agent_file.parent.mkdir(parents=True)
|
||||
agent_file.write_text(
|
||||
'---\nmode: subagent\npermission:\n bash:\n "gh pr merge*": allow\n---\ntest agent\n'
|
||||
)
|
||||
monkeypatch.setattr(cp, "AGENTS_DIR", tmp_path / "agents")
|
||||
monkeypatch.setattr(cp, "OPENCODE_JSON", tmp_path / "nonexistent.json")
|
||||
violations = cp.check_rules(
|
||||
cp.parse_agent_bash_rules(agent_file),
|
||||
f"agents/{agent_file.name}",
|
||||
)
|
||||
assert len(violations) == 1
|
||||
assert "gh pr merge*" in violations[0]
|
||||
assert "merge is done by main agent" in violations[0]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
35
tests/test_cli.py
Normal file
35
tests/test_cli.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def test_cli_download() -> None:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "src.memory", "download"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "not needed" in result.stdout
|
||||
|
||||
|
||||
def test_cli_search_no_index(tmp_path) -> None:
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"src.memory",
|
||||
"search",
|
||||
"test query",
|
||||
"-i",
|
||||
str(tmp_path),
|
||||
"-k",
|
||||
"5",
|
||||
"--json",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert result.stdout.strip() == "[]"
|
||||
62
tests/test_embedder.py
Normal file
62
tests/test_embedder.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import os
|
||||
|
||||
os.environ.setdefault("AI_PROVIDER_API_URL", "http://test/v1")
|
||||
|
||||
from typing import NoReturn
|
||||
from unittest.mock import patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from src.memory.embedder import embed_texts
|
||||
|
||||
|
||||
def test_embed_texts_success() -> None:
|
||||
fake_embedding = [0.1, 0.2, 0.3]
|
||||
|
||||
class FakeResponse:
|
||||
status_code = 200
|
||||
|
||||
def json(self):
|
||||
return {
|
||||
"data": [{"embedding": fake_embedding, "index": 0}],
|
||||
"model": "gemini-embedding-2-preview",
|
||||
}
|
||||
|
||||
def raise_for_status(self) -> None:
|
||||
pass
|
||||
|
||||
def mock_post(self, url, **kwargs):
|
||||
assert "embeddings" in url
|
||||
return FakeResponse()
|
||||
|
||||
with patch.object(httpx.Client, "post", mock_post):
|
||||
result = embed_texts(["test text"])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] == fake_embedding
|
||||
|
||||
|
||||
def test_embed_texts_empty() -> None:
|
||||
assert embed_texts([]) == []
|
||||
|
||||
|
||||
def test_embed_texts_api_error() -> None:
|
||||
class FakeErrorResponse:
|
||||
status_code = 401
|
||||
|
||||
def raise_for_status(self) -> NoReturn:
|
||||
msg = "Unauthorized"
|
||||
raise httpx.HTTPStatusError(
|
||||
msg,
|
||||
request=None,
|
||||
response=self,
|
||||
)
|
||||
|
||||
def json(self):
|
||||
return {}
|
||||
|
||||
def mock_post(self, url, **kwargs):
|
||||
return FakeErrorResponse()
|
||||
|
||||
with patch.object(httpx.Client, "post", mock_post), pytest.raises(httpx.HTTPStatusError):
|
||||
embed_texts(["test"])
|
||||
52
tests/test_index.py
Normal file
52
tests/test_index.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import json
|
||||
from argparse import Namespace
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from src.memory.index import _extract_text, run_index
|
||||
|
||||
|
||||
class TestExtractText:
|
||||
def test_with_frontmatter(self) -> None:
|
||||
md = "---\ntitle: test\n---\n\nbody content"
|
||||
assert _extract_text(md) == "body content"
|
||||
|
||||
def test_no_frontmatter(self) -> None:
|
||||
md = "just content"
|
||||
assert _extract_text(md) == "just content"
|
||||
|
||||
def test_empty(self) -> None:
|
||||
assert _extract_text("") == ""
|
||||
|
||||
|
||||
class TestRunIndex:
|
||||
def test_index_creates_json(self, tmp_path: Path) -> None:
|
||||
memory_dir = tmp_path / "memory"
|
||||
index_dir = tmp_path / ".rag"
|
||||
memory_dir.mkdir(parents=True)
|
||||
|
||||
(memory_dir / "test.md").write_text("---\ntitle: test\n---\n\nhello world")
|
||||
|
||||
def fake_embed_texts(texts):
|
||||
return [[0.1, 0.2, 0.3]] * len(texts)
|
||||
|
||||
with patch("src.memory.index.embed_texts", fake_embed_texts):
|
||||
args = Namespace(memory_dir=str(memory_dir), output=str(index_dir))
|
||||
run_index(args)
|
||||
|
||||
assert (index_dir / "index.json").exists()
|
||||
index = json.loads((index_dir / "index.json").read_text())
|
||||
assert len(index["files"]) == 1
|
||||
assert index["files"][0]["source"] == "test.md"
|
||||
assert index["files"][0]["embedding"] == [0.1, 0.2, 0.3]
|
||||
|
||||
def test_no_md_files(self, tmp_path: Path, capsys) -> None:
|
||||
memory_dir = tmp_path / "empty"
|
||||
index_dir = tmp_path / ".rag"
|
||||
memory_dir.mkdir(parents=True)
|
||||
|
||||
args = Namespace(memory_dir=str(memory_dir), output=str(index_dir))
|
||||
run_index(args)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "No .md files found" in captured.out
|
||||
341
tests/test_observability.py
Normal file
341
tests/test_observability.py
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
"""Tests for .opencode/scripts/observability.py — log parser for denials/errors.
|
||||
|
||||
Pattern: direct calls for parse_line/_process_line, tmp_path + monkeypatch on
|
||||
obs.LOG_PATH for main() tests. Loading via importlib.util.spec_from_file_location
|
||||
(same pattern as test_pipeline_status.py / test_spec_status.py).
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
SCRIPT_PATH = Path(__file__).resolve().parent.parent / ".opencode" / "scripts" / "observability.py"
|
||||
spec = importlib.util.spec_from_file_location("observability", SCRIPT_PATH)
|
||||
obs = importlib.util.module_from_spec(spec)
|
||||
sys.modules["observability"] = obs
|
||||
spec.loader.exec_module(obs)
|
||||
|
||||
|
||||
# ── parse_line ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_parse_line_with_timestamp_and_run():
|
||||
assert obs.parse_line("timestamp=2026-07-20T10:00:00 run=abc123") == (
|
||||
"2026-07-20T10:00:00",
|
||||
"abc123",
|
||||
)
|
||||
|
||||
|
||||
def test_parse_line_no_timestamp():
|
||||
assert obs.parse_line("run=abc123") == ("?", "abc123")
|
||||
|
||||
|
||||
def test_parse_line_no_run():
|
||||
assert obs.parse_line("timestamp=2026-07-20T10:00:00") == (
|
||||
"2026-07-20T10:00:00",
|
||||
"?",
|
||||
)
|
||||
|
||||
|
||||
def test_parse_line_no_matches():
|
||||
assert obs.parse_line("random line") == ("?", "?")
|
||||
|
||||
|
||||
def test_parse_line_empty():
|
||||
assert obs.parse_line("") == ("?", "?")
|
||||
|
||||
|
||||
# ── _process_line: sessions ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_process_line_session_created():
|
||||
sessions: dict = {}
|
||||
denials: list = []
|
||||
errors: list = []
|
||||
obs._process_line(
|
||||
'message=created agent=build run=abc id=sess1 title="Test"',
|
||||
sessions,
|
||||
denials,
|
||||
errors,
|
||||
)
|
||||
assert sessions == {"abc": {"agent": "build", "session": "sess1", "title": "Test"}}
|
||||
assert denials == []
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_process_line_session_no_agent():
|
||||
# `agent=` present but value empty — `agent=(\S+)` regex misses → fallback "unknown".
|
||||
# NOTE: spec issue #111 used `message=created run=abc id=sess1` (no `agent=` at all),
|
||||
# but `observability.py:23` requires `"agent=" in line` to enter the session branch.
|
||||
# Fix: `agent=` with empty value exercises the `unknown` fallback (the spec's intent).
|
||||
sessions: dict = {}
|
||||
denials: list = []
|
||||
errors: list = []
|
||||
obs._process_line(
|
||||
"message=created agent= run=abc id=sess1",
|
||||
sessions,
|
||||
denials,
|
||||
errors,
|
||||
)
|
||||
assert sessions == {"abc": {"agent": "unknown", "session": "sess1", "title": ""}}
|
||||
|
||||
|
||||
def test_process_line_session_no_title():
|
||||
sessions: dict = {}
|
||||
denials: list = []
|
||||
errors: list = []
|
||||
obs._process_line(
|
||||
"message=created agent=build run=abc id=sess1",
|
||||
sessions,
|
||||
denials,
|
||||
errors,
|
||||
)
|
||||
assert sessions == {"abc": {"agent": "build", "session": "sess1", "title": ""}}
|
||||
|
||||
|
||||
# ── _process_line: denials ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_process_line_denial():
|
||||
sessions: dict = {}
|
||||
denials: list = []
|
||||
errors: list = []
|
||||
obs._process_line(
|
||||
'action.action=deny run=abc pattern="rm *" permission=bash',
|
||||
sessions,
|
||||
denials,
|
||||
errors,
|
||||
)
|
||||
assert len(denials) == 1
|
||||
d = denials[0]
|
||||
assert d["run"] == "abc"
|
||||
assert d["pattern"] == "rm *"
|
||||
assert d["perm"] == "bash"
|
||||
assert d["ts"] == "?"
|
||||
assert sessions == {}
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_process_line_denial_no_pattern():
|
||||
sessions: dict = {}
|
||||
denials: list = []
|
||||
errors: list = []
|
||||
obs._process_line(
|
||||
"action.action=deny run=abc permission=bash",
|
||||
sessions,
|
||||
denials,
|
||||
errors,
|
||||
)
|
||||
assert len(denials) == 1
|
||||
assert denials[0]["pattern"] == "?"
|
||||
|
||||
|
||||
# ── _process_line: errors ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_process_line_error():
|
||||
sessions: dict = {}
|
||||
denials: list = []
|
||||
errors: list = []
|
||||
obs._process_line(
|
||||
"message=process level=ERROR run=abc error=ToolExecFailed session.id=sess1",
|
||||
sessions,
|
||||
denials,
|
||||
errors,
|
||||
)
|
||||
assert len(errors) == 1
|
||||
e = errors[0]
|
||||
assert e["run"] == "abc"
|
||||
assert e["error"] == "ToolExecFailed"
|
||||
assert e["session"] == "sess1"
|
||||
assert e["ts"] == "?"
|
||||
assert sessions == {}
|
||||
assert denials == []
|
||||
|
||||
|
||||
def test_process_line_error_no_session():
|
||||
sessions: dict = {}
|
||||
denials: list = []
|
||||
errors: list = []
|
||||
obs._process_line(
|
||||
"message=process level=ERROR run=abc error=ToolExecFailed",
|
||||
sessions,
|
||||
denials,
|
||||
errors,
|
||||
)
|
||||
assert len(errors) == 1
|
||||
assert errors[0]["session"] == "?"
|
||||
|
||||
|
||||
def test_process_line_error_no_error_field():
|
||||
"""No error= field → error default 'unknown'."""
|
||||
sessions: dict = {}
|
||||
denials: list = []
|
||||
errors: list = []
|
||||
obs._process_line(
|
||||
"message=process level=ERROR run=abc session.id=sess1",
|
||||
sessions,
|
||||
denials,
|
||||
errors,
|
||||
)
|
||||
assert len(errors) == 1
|
||||
assert errors[0]["error"] == "unknown"
|
||||
|
||||
|
||||
# ── _process_line: unmatched ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_process_line_unmatched():
|
||||
sessions: dict = {}
|
||||
denials: list = []
|
||||
errors: list = []
|
||||
obs._process_line("random log line", sessions, denials, errors)
|
||||
assert sessions == {}
|
||||
assert denials == []
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_process_line_message_process_no_error():
|
||||
"""message=process without level=ERROR → not classified as error."""
|
||||
sessions: dict = {}
|
||||
denials: list = []
|
||||
errors: list = []
|
||||
obs._process_line(
|
||||
"message=process level=INFO run=abc",
|
||||
sessions,
|
||||
denials,
|
||||
errors,
|
||||
)
|
||||
assert sessions == {}
|
||||
assert denials == []
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_process_line_created_without_agent_keyword():
|
||||
"""message=created present but no 'agent=' substring → not a session line."""
|
||||
sessions: dict = {}
|
||||
denials: list = []
|
||||
errors: list = []
|
||||
obs._process_line(
|
||||
"message=created run=abc id=sess1",
|
||||
sessions,
|
||||
denials,
|
||||
errors,
|
||||
)
|
||||
# 'message=created' in line AND 'agent=' in line — second condition fails,
|
||||
# so falls through. No 'action.action=deny' or 'message=process level=ERROR'.
|
||||
assert sessions == {}
|
||||
assert denials == []
|
||||
assert errors == []
|
||||
|
||||
|
||||
# ── main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_main_no_log_file(tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.setattr(obs, "LOG_PATH", tmp_path / "nonexistent.log")
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
obs.main()
|
||||
assert exc_info.value.code == 1
|
||||
captured = capsys.readouterr()
|
||||
assert "Log file not found" in captured.out
|
||||
|
||||
|
||||
def test_main_empty_log(tmp_path, monkeypatch, capsys):
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text("")
|
||||
monkeypatch.setattr(obs, "LOG_PATH", log_file)
|
||||
obs.main()
|
||||
captured = capsys.readouterr()
|
||||
assert "No denials or errors found in log." in captured.out
|
||||
|
||||
|
||||
def test_main_only_sessions(tmp_path, monkeypatch, capsys):
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text('message=created agent=build run=abc id=sess1 title="Test"\n')
|
||||
monkeypatch.setattr(obs, "LOG_PATH", log_file)
|
||||
obs.main()
|
||||
captured = capsys.readouterr()
|
||||
assert "No denials or errors found in log." in captured.out
|
||||
|
||||
|
||||
def test_main_with_denials(tmp_path, monkeypatch, capsys):
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text('action.action=deny run=abc pattern="rm *" permission=bash\n')
|
||||
monkeypatch.setattr(obs, "LOG_PATH", log_file)
|
||||
obs.main()
|
||||
captured = capsys.readouterr()
|
||||
assert "## Permission Denials" in captured.out
|
||||
assert "`rm *`" in captured.out
|
||||
assert "bash" in captured.out
|
||||
assert "unknown" in captured.out # agent lookup falls back to unknown
|
||||
|
||||
|
||||
def test_main_with_errors(tmp_path, monkeypatch, capsys):
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text(
|
||||
"message=process level=ERROR run=abc error=ToolExecFailed session.id=sess1\n"
|
||||
)
|
||||
monkeypatch.setattr(obs, "LOG_PATH", log_file)
|
||||
obs.main()
|
||||
captured = capsys.readouterr()
|
||||
assert "## Process Errors" in captured.out
|
||||
assert "ToolExecFailed" in captured.out
|
||||
assert "sess1" in captured.out
|
||||
|
||||
|
||||
def test_main_mixed(tmp_path, monkeypatch, capsys):
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text(
|
||||
'message=created agent=build run=abc id=sess1 title="Test"\n'
|
||||
'action.action=deny run=abc pattern="rm *" permission=bash\n'
|
||||
"message=process level=ERROR run=abc error=ToolExecFailed session.id=sess1\n"
|
||||
)
|
||||
monkeypatch.setattr(obs, "LOG_PATH", log_file)
|
||||
obs.main()
|
||||
captured = capsys.readouterr()
|
||||
assert "# Observability Report" in captured.out
|
||||
assert "## Permission Denials" in captured.out
|
||||
assert "### build (1 denials)" in captured.out
|
||||
assert "## Process Errors" in captured.out
|
||||
# Denial row references session from sessions dict.
|
||||
assert "sess1" in captured.out
|
||||
|
||||
|
||||
def test_main_denials_grouped_by_agent(tmp_path, monkeypatch, capsys):
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text(
|
||||
'message=created agent=zebra run=r1 id=s1 title="A"\n'
|
||||
'message=created agent=alpha run=r2 id=s2 title="B"\n'
|
||||
'action.action=deny run=r1 pattern="p1" permission=bash\n'
|
||||
'action.action=deny run=r2 pattern="p2" permission=edit\n'
|
||||
)
|
||||
monkeypatch.setattr(obs, "LOG_PATH", log_file)
|
||||
obs.main()
|
||||
captured = capsys.readouterr()
|
||||
out = captured.out
|
||||
# sorted alphabetically by agent
|
||||
alpha_idx = out.find("### alpha")
|
||||
zebra_idx = out.find("### zebra")
|
||||
assert alpha_idx != -1
|
||||
assert zebra_idx != -1
|
||||
assert alpha_idx < zebra_idx
|
||||
|
||||
|
||||
def test_main_errors_last_20(tmp_path, monkeypatch, capsys):
|
||||
log_file = tmp_path / "test.log"
|
||||
lines = []
|
||||
for i in range(25):
|
||||
lines.append(f"message=process level=ERROR run=run{i} error=Err{i} session.id=s{i}\n")
|
||||
log_file.write_text("".join(lines))
|
||||
monkeypatch.setattr(obs, "LOG_PATH", log_file)
|
||||
obs.main()
|
||||
captured = capsys.readouterr()
|
||||
out = captured.out
|
||||
# Last 20 of 25 errors: indices 5..24 → Err5..Err24
|
||||
assert "Err24" in out
|
||||
assert "Err5" in out
|
||||
assert "Err4" not in out # not in last 20
|
||||
assert "Err0" not in out
|
||||
1178
tests/test_pipeline_status.py
Normal file
1178
tests/test_pipeline_status.py
Normal file
File diff suppressed because it is too large
Load diff
53
tests/test_pipeline_status_adr.py
Normal file
53
tests/test_pipeline_status_adr.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""Tests for .opencode/scripts/pipeline-status.py — mandatory ADR by PR#.
|
||||
|
||||
Verifies the deterministic check_adr: ADR is required for every PR and is
|
||||
found by ``*-pr-<N>-*.md`` filename pattern (no regex guessing in handoff).
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_PATH = (
|
||||
Path(__file__).resolve().parent.parent / ".opencode" / "scripts" / "pipeline-status.py"
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location("pipeline_status", SCRIPT_PATH)
|
||||
ps = importlib.util.module_from_spec(spec)
|
||||
sys.modules["pipeline_status"] = ps
|
||||
spec.loader.exec_module(ps)
|
||||
|
||||
|
||||
def test_adr_exists_for_pr(tmp_path, monkeypatch):
|
||||
adr_dir = tmp_path / "decisions"
|
||||
adr_dir.mkdir()
|
||||
(adr_dir / "002-pr-55-test.md").write_text("# ADR-002")
|
||||
monkeypatch.setattr(ps, "ADR_DIR", adr_dir)
|
||||
|
||||
result = ps.check_adr(55)
|
||||
|
||||
assert result.status == ps.PhaseStatus.DONE
|
||||
assert "002-pr-55-test.md" in result.detail
|
||||
|
||||
|
||||
def test_adr_missing_for_pr(tmp_path, monkeypatch):
|
||||
adr_dir = tmp_path / "decisions"
|
||||
adr_dir.mkdir()
|
||||
monkeypatch.setattr(ps, "ADR_DIR", adr_dir)
|
||||
|
||||
result = ps.check_adr(55)
|
||||
|
||||
assert result.status == ps.PhaseStatus.NOT_DONE
|
||||
assert "не найден" in result.detail
|
||||
assert "scaffold-handoff.sh 55" in result.detail
|
||||
|
||||
|
||||
def test_old_pr_without_pr_in_filename(tmp_path, monkeypatch):
|
||||
adr_dir = tmp_path / "decisions"
|
||||
adr_dir.mkdir()
|
||||
(adr_dir / "001-data-bus-pipeline.md").write_text("# ADR-001")
|
||||
monkeypatch.setattr(ps, "ADR_DIR", adr_dir)
|
||||
|
||||
result = ps.check_adr(46)
|
||||
|
||||
assert result.status == ps.PhaseStatus.NOT_DONE
|
||||
assert "не найден" in result.detail
|
||||
501
tests/test_pipeline_status_ci.py
Normal file
501
tests/test_pipeline_status_ci.py
Normal file
|
|
@ -0,0 +1,501 @@
|
|||
"""Tests for ``check_ci`` in .opencode/scripts/pipeline-status.py — CI phase.
|
||||
|
||||
Covers Actions API gate via ``gh api repos/<owner>/<repo>/actions/runs``.
|
||||
All gh/git calls are mocked via monkeypatch on the module's ``run_cmd``
|
||||
helper. ``time.sleep`` is mocked to no-op via autouse fixture (polling loop
|
||||
would otherwise hang tests for up to 5 minutes).
|
||||
|
||||
Mocking strategy: ``check_ci`` issues calls in sequence:
|
||||
1. ``gh pr view N --json headRefName`` → returns branch name JSON (once)
|
||||
2. ``git remote get-url origin`` → returns remote URL (for ``get_repo_full_name``)
|
||||
3. ``gh api repos/<org>/<repo>/actions/runs --jq <filter>`` → returns CI run
|
||||
JSON; may be called multiple times (polling loop / no-runs retries).
|
||||
|
||||
``mock_run_cmd`` dispatches by command prefix (single response per prefix).
|
||||
``mock_run_cmd_seq`` supports a sequence of responses for the ``gh api``
|
||||
prefix (consumed in order; last repeats if exhausted) — for polling tests.
|
||||
|
||||
``get_repo_full_name`` is cached via ``functools.cache`` — cleared before
|
||||
each test via the ``_clear_repo_cache`` autouse fixture.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
SCRIPT_PATH = (
|
||||
Path(__file__).resolve().parent.parent / ".opencode" / "scripts" / "pipeline-status.py"
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location("pipeline_status_ci", SCRIPT_PATH)
|
||||
ps = importlib.util.module_from_spec(spec)
|
||||
sys.modules["pipeline_status_ci"] = ps
|
||||
spec.loader.exec_module(ps)
|
||||
|
||||
GIT_REMOTE_MOCK: tuple[tuple[str, ...], tuple[int, str, str]] = (
|
||||
("git", "remote"),
|
||||
(0, "https://github.com/slaid098/opencode-config.git\n", ""),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_repo_cache():
|
||||
"""Clear ``functools.cache`` on ``get_repo_full_name`` + mock ``time.sleep``.
|
||||
|
||||
``time.sleep`` must be mocked — polling loop sleeps ``CI_POLL_INTERVAL``
|
||||
between queries, up to ``CI_WAIT_TIMEOUT`` (default 300s). Without mock,
|
||||
tests polling until timeout would hang for minutes.
|
||||
"""
|
||||
ps.get_repo_full_name.cache_clear()
|
||||
sleep_calls: list[float] = []
|
||||
monkey = pytest.MonkeyPatch()
|
||||
|
||||
def _record_sleep(seconds: float) -> None:
|
||||
sleep_calls.append(seconds)
|
||||
|
||||
monkey.setattr(ps.time, "sleep", _record_sleep)
|
||||
ps._test_sleep_calls = sleep_calls # type: ignore[attr-defined]
|
||||
yield
|
||||
monkey.undo()
|
||||
ps.get_repo_full_name.cache_clear()
|
||||
del ps._test_sleep_calls # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def mock_run_cmd(responses: dict[tuple, tuple[int, str, str]]):
|
||||
"""Factory: mock run_cmd matching by command prefix (single response).
|
||||
|
||||
Keys are tuples of leading args (e.g. ("gh", "pr", "view")),
|
||||
values are (returncode, stdout, stderr).
|
||||
|
||||
Automatically includes a ``git remote get-url origin`` mock
|
||||
(``GIT_REMOTE_MOCK``) so ``get_repo_full_name()`` works without extra
|
||||
boilerplate in every test.
|
||||
"""
|
||||
merged = {GIT_REMOTE_MOCK[0]: GIT_REMOTE_MOCK[1], **responses}
|
||||
|
||||
def _mock(args: list[str]) -> tuple[int, str, str]:
|
||||
for prefix, result in merged.items():
|
||||
if tuple(args[: len(prefix)]) == tuple(prefix):
|
||||
return result
|
||||
return (1, "", f"unmocked call: {args}")
|
||||
|
||||
return _mock
|
||||
|
||||
|
||||
def mock_run_cmd_seq(
|
||||
pr_view: tuple[int, str, str] = (0, '{"headRefName": "feat/test-branch"}', ""),
|
||||
api_responses: list[tuple[int, str, str]] | None = None,
|
||||
extra: dict[tuple, tuple[int, str, str]] | None = None,
|
||||
):
|
||||
"""Factory: mock run_cmd with sequence of responses for ``gh api``.
|
||||
|
||||
``gh pr view`` returns ``pr_view`` for every call (single response).
|
||||
``gh api`` returns ``api_responses[i]`` on the i-th call; if exhausted,
|
||||
repeats the last response (so polling loops don't run out of responses
|
||||
and hit the "unmocked" fallback). ``extra`` adds fixed overrides for
|
||||
other prefixes (e.g. ``git remote``).
|
||||
"""
|
||||
api_responses = api_responses or []
|
||||
api_idx = [0]
|
||||
merged = {GIT_REMOTE_MOCK[0]: GIT_REMOTE_MOCK[1], **(extra or {})}
|
||||
|
||||
def _mock(args: list[str]) -> tuple[int, str, str]:
|
||||
for prefix, result in merged.items():
|
||||
if tuple(args[: len(prefix)]) == tuple(prefix):
|
||||
return result
|
||||
if tuple(args[:3]) == ("gh", "pr", "view"):
|
||||
return pr_view
|
||||
if tuple(args[:2]) == ("gh", "api"):
|
||||
if not api_responses:
|
||||
return (1, "", "no api_responses configured")
|
||||
idx = min(api_idx[0], len(api_responses) - 1)
|
||||
api_idx[0] += 1
|
||||
return api_responses[idx]
|
||||
return (1, "", f"unmocked call: {args}")
|
||||
|
||||
return _mock
|
||||
|
||||
|
||||
# ── check_ci — completed (no polling) ──────────────────────────────────────────
|
||||
|
||||
|
||||
def test_ci_success(monkeypatch):
|
||||
"""CI run completed & success → DONE, 0 sleeps."""
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
"run_cmd",
|
||||
mock_run_cmd(
|
||||
{
|
||||
("gh", "pr", "view"): (
|
||||
0,
|
||||
'{"headRefName": "feat/test-branch"}',
|
||||
"",
|
||||
),
|
||||
("gh", "api"): (
|
||||
0,
|
||||
'{"status": "completed", "conclusion": "success"}',
|
||||
"",
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
result = ps.check_ci(46)
|
||||
assert result.status == ps.PhaseStatus.DONE
|
||||
assert "CI green" in result.detail
|
||||
assert ps._test_sleep_calls == [] # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def test_ci_failure(monkeypatch):
|
||||
"""CI run completed but conclusion=failure → NOT_DONE, 0 sleeps (don't wait for fail)."""
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
"run_cmd",
|
||||
mock_run_cmd(
|
||||
{
|
||||
("gh", "pr", "view"): (
|
||||
0,
|
||||
'{"headRefName": "feat/test-branch"}',
|
||||
"",
|
||||
),
|
||||
("gh", "api"): (
|
||||
0,
|
||||
'{"status": "completed", "conclusion": "failure"}',
|
||||
"",
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
result = ps.check_ci(46)
|
||||
assert result.status == ps.PhaseStatus.NOT_DONE
|
||||
assert "failure" in result.detail
|
||||
assert ps._test_sleep_calls == [] # type: ignore[attr-defined]
|
||||
|
||||
|
||||
# ── check_ci — polling loop ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_ci_wait_then_success(monkeypatch):
|
||||
"""in_progress → in_progress → completed+success → DONE, sleep called 2 times."""
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
"run_cmd",
|
||||
mock_run_cmd_seq(
|
||||
api_responses=[
|
||||
(0, '{"status": "in_progress", "conclusion": null}', ""),
|
||||
(0, '{"status": "in_progress", "conclusion": null}', ""),
|
||||
(0, '{"status": "completed", "conclusion": "success"}', ""),
|
||||
],
|
||||
),
|
||||
)
|
||||
result = ps.check_ci(46)
|
||||
assert result.status == ps.PhaseStatus.DONE
|
||||
assert "CI green" in result.detail
|
||||
assert len(ps._test_sleep_calls) == 2 # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def test_ci_wait_timeout(monkeypatch):
|
||||
"""All calls in_progress, timeout 1s via env → AMBIGUOUS with "после 1s"."""
|
||||
monkeypatch.setenv("OPENCODE_CI_WAIT_TIMEOUT", "1")
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
"run_cmd",
|
||||
mock_run_cmd_seq(
|
||||
api_responses=[
|
||||
(0, '{"status": "in_progress", "conclusion": null}', ""),
|
||||
],
|
||||
),
|
||||
)
|
||||
result = ps.check_ci(46)
|
||||
assert result.status == ps.PhaseStatus.AMBIGUOUS
|
||||
assert "после 1s" in result.detail
|
||||
assert ps._test_sleep_calls == [] # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def test_ci_wait_poll_interval(monkeypatch):
|
||||
"""Poll interval is used as sleep argument — capture via mock."""
|
||||
monkeypatch.setenv("OPENCODE_CI_POLL_INTERVAL", "7")
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
"run_cmd",
|
||||
mock_run_cmd_seq(
|
||||
api_responses=[
|
||||
(0, '{"status": "in_progress", "conclusion": null}', ""),
|
||||
(0, '{"status": "completed", "conclusion": "success"}', ""),
|
||||
],
|
||||
),
|
||||
)
|
||||
result = ps.check_ci(46)
|
||||
assert result.status == ps.PhaseStatus.DONE
|
||||
assert ps._test_sleep_calls == [7] # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def test_ci_in_progress(monkeypatch):
|
||||
"""status=in_progress, all polls in_progress, timeout 1s → AMBIGUOUS."""
|
||||
monkeypatch.setenv("OPENCODE_CI_WAIT_TIMEOUT", "1")
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
"run_cmd",
|
||||
mock_run_cmd_seq(
|
||||
api_responses=[
|
||||
(0, '{"status": "in_progress", "conclusion": null}', ""),
|
||||
],
|
||||
),
|
||||
)
|
||||
result = ps.check_ci(46)
|
||||
assert result.status == ps.PhaseStatus.AMBIGUOUS
|
||||
assert "после 1s" in result.detail
|
||||
|
||||
|
||||
def test_ci_queued(monkeypatch):
|
||||
"""status=queued, all polls in_progress, timeout 1s → AMBIGUOUS."""
|
||||
monkeypatch.setenv("OPENCODE_CI_WAIT_TIMEOUT", "1")
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
"run_cmd",
|
||||
mock_run_cmd_seq(
|
||||
api_responses=[
|
||||
(0, '{"status": "queued", "conclusion": null}', ""),
|
||||
],
|
||||
),
|
||||
)
|
||||
result = ps.check_ci(46)
|
||||
assert result.status == ps.PhaseStatus.AMBIGUOUS
|
||||
assert "после 1s" in result.detail
|
||||
|
||||
|
||||
# ── check_ci — no runs (short retry) ───────────────────────────────────────────
|
||||
|
||||
|
||||
def test_ci_no_runs_retry_then_appears(monkeypatch):
|
||||
"""1st call null, 2nd null, 3rd success → DONE (after CI_NO_RUNS_RETRY retries)."""
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
"run_cmd",
|
||||
mock_run_cmd_seq(
|
||||
api_responses=[
|
||||
(0, "null", ""),
|
||||
(0, "null", ""),
|
||||
(0, '{"status": "completed", "conclusion": "success"}', ""),
|
||||
],
|
||||
),
|
||||
)
|
||||
result = ps.check_ci(46)
|
||||
assert result.status == ps.PhaseStatus.DONE
|
||||
assert "CI green" in result.detail
|
||||
|
||||
|
||||
def test_ci_no_runs_retry_exhausted(monkeypatch):
|
||||
"""All calls null, CI_NO_RUNS_RETRY=3 → AMBIGUOUS."""
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
"run_cmd",
|
||||
mock_run_cmd_seq(
|
||||
api_responses=[
|
||||
(0, "null", ""),
|
||||
],
|
||||
),
|
||||
)
|
||||
result = ps.check_ci(46)
|
||||
assert result.status == ps.PhaseStatus.AMBIGUOUS
|
||||
assert "нет CI run" in result.detail
|
||||
|
||||
|
||||
def test_ci_no_runs(monkeypatch):
|
||||
"""No CI run (jq null) with single response → AMBIGUOUS (retry exhausted)."""
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
"run_cmd",
|
||||
mock_run_cmd(
|
||||
{
|
||||
("gh", "pr", "view"): (
|
||||
0,
|
||||
'{"headRefName": "feat/test-branch"}',
|
||||
"",
|
||||
),
|
||||
("gh", "api"): (0, "null", ""),
|
||||
}
|
||||
),
|
||||
)
|
||||
result = ps.check_ci(46)
|
||||
assert result.status == ps.PhaseStatus.AMBIGUOUS
|
||||
assert "нет CI run" in result.detail
|
||||
|
||||
|
||||
# ── check_ci — API error (no retry) ────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_ci_api_error(monkeypatch):
|
||||
"""Actions API returns rc=1 (403) → AMBIGUOUS, no retries/sleeps."""
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
"run_cmd",
|
||||
mock_run_cmd(
|
||||
{
|
||||
("gh", "pr", "view"): (
|
||||
0,
|
||||
'{"headRefName": "feat/test-branch"}',
|
||||
"",
|
||||
),
|
||||
("gh", "api"): (
|
||||
1,
|
||||
"",
|
||||
"HTTP 403: Forbidden",
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
result = ps.check_ci(46)
|
||||
assert result.status == ps.PhaseStatus.AMBIGUOUS
|
||||
assert "Actions API" in result.detail
|
||||
assert ps._test_sleep_calls == [] # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def test_ci_api_error_no_retry(monkeypatch):
|
||||
"""1st call rc=1 (403) → сразу AMBIGUOUS, 0 sleeps/retries."""
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
"run_cmd",
|
||||
mock_run_cmd_seq(
|
||||
api_responses=[
|
||||
(1, "", "HTTP 403: Forbidden"),
|
||||
(0, '{"status": "completed", "conclusion": "success"}', ""),
|
||||
],
|
||||
),
|
||||
)
|
||||
result = ps.check_ci(46)
|
||||
assert result.status == ps.PhaseStatus.AMBIGUOUS
|
||||
assert "Actions API" in result.detail
|
||||
assert ps._test_sleep_calls == [] # type: ignore[attr-defined]
|
||||
|
||||
|
||||
# ── check_ci — config priority (CLI > env > constant) ──────────────────────────
|
||||
|
||||
|
||||
def test_ci_wait_env_var_override(monkeypatch):
|
||||
"""OPENCODE_CI_WAIT_TIMEOUT=1 → timeout 1s instead of default 300s."""
|
||||
monkeypatch.setenv("OPENCODE_CI_WAIT_TIMEOUT", "1")
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
"run_cmd",
|
||||
mock_run_cmd_seq(
|
||||
api_responses=[
|
||||
(0, '{"status": "in_progress", "conclusion": null}', ""),
|
||||
],
|
||||
),
|
||||
)
|
||||
result = ps.check_ci(46)
|
||||
assert result.status == ps.PhaseStatus.AMBIGUOUS
|
||||
assert "после 1s" in result.detail
|
||||
|
||||
|
||||
def test_ci_wait_cli_flag_overrides_env(monkeypatch):
|
||||
"""CLI flag --ci-wait-timeout 2 + env OPENCODE_CI_WAIT_TIMEOUT=1 → timeout 2s."""
|
||||
monkeypatch.setenv("OPENCODE_CI_WAIT_TIMEOUT", "1")
|
||||
|
||||
def _fixed_config(argv=None):
|
||||
return ps.CiPollConfig(wait_timeout=2, poll_interval=ps.CI_POLL_INTERVAL)
|
||||
|
||||
monkeypatch.setattr(ps, "_load_ci_config", _fixed_config)
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
"run_cmd",
|
||||
mock_run_cmd_seq(
|
||||
api_responses=[
|
||||
(0, '{"status": "in_progress", "conclusion": null}', ""),
|
||||
],
|
||||
),
|
||||
)
|
||||
result = ps.check_ci(46)
|
||||
assert result.status == ps.PhaseStatus.AMBIGUOUS
|
||||
assert "после 2s" in result.detail
|
||||
|
||||
|
||||
def test_ci_wait_failure_no_wait(monkeypatch):
|
||||
"""1st call completed+failure → сразу NOT_DONE, 0 sleeps (don't wait for fail)."""
|
||||
monkeypatch.setattr(
|
||||
ps,
|
||||
"run_cmd",
|
||||
mock_run_cmd_seq(
|
||||
api_responses=[
|
||||
(0, '{"status": "completed", "conclusion": "failure"}', ""),
|
||||
],
|
||||
),
|
||||
)
|
||||
result = ps.check_ci(46)
|
||||
assert result.status == ps.PhaseStatus.NOT_DONE
|
||||
assert "failure" in result.detail
|
||||
assert ps._test_sleep_calls == [] # type: ignore[attr-defined]
|
||||
|
||||
|
||||
# ── check_ci — dynamic repo full name ──────────────────────────────────────────
|
||||
|
||||
|
||||
def test_ci_uses_dynamic_repo_full_name(monkeypatch):
|
||||
"""``get_repo_full_name`` is derived from ``git remote``, not hardcoded.
|
||||
|
||||
Mocks a non-opencode remote (``slaid098/media-gen``) and asserts that the
|
||||
``gh api`` call uses ``repos/slaid098/media-gen/actions/runs`` (dynamic),
|
||||
not a hardcoded ``slaid098/opencode-config``.
|
||||
"""
|
||||
captured_args: list[list[str]] = []
|
||||
|
||||
def _capture_mock(args: list[str]) -> tuple[int, str, str]:
|
||||
captured_args.append(args)
|
||||
if tuple(args[:3]) == ("git", "remote", "get-url"):
|
||||
return (0, "https://github.com/slaid098/media-gen.git\n", "")
|
||||
if tuple(args[:3]) == ("gh", "pr", "view"):
|
||||
return (0, '{"headRefName": "feat/test-branch"}', "")
|
||||
if tuple(args[:2]) == ("gh", "api"):
|
||||
return (0, '{"status": "completed", "conclusion": "success"}', "")
|
||||
return (1, "", f"unmocked call: {args}")
|
||||
|
||||
monkeypatch.setattr(ps, "run_cmd", _capture_mock)
|
||||
result = ps.check_ci(46)
|
||||
assert result.status == ps.PhaseStatus.DONE
|
||||
|
||||
api_calls = [a for a in captured_args if a[:2] == ["gh", "api"]]
|
||||
assert len(api_calls) == 1
|
||||
assert "repos/slaid098/media-gen/actions/runs" in " ".join(api_calls[0])
|
||||
assert "slaid098/opencode-config" not in " ".join(api_calls[0])
|
||||
|
||||
|
||||
# ── _load_ci_config — priority ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_load_ci_config_defaults(monkeypatch):
|
||||
"""No env, no CLI → constants."""
|
||||
monkeypatch.delenv("OPENCODE_CI_WAIT_TIMEOUT", raising=False)
|
||||
monkeypatch.delenv("OPENCODE_CI_POLL_INTERVAL", raising=False)
|
||||
cfg = ps._load_ci_config(argv=[])
|
||||
assert cfg.wait_timeout == ps.CI_WAIT_TIMEOUT
|
||||
assert cfg.poll_interval == ps.CI_POLL_INTERVAL
|
||||
|
||||
|
||||
def test_load_ci_config_env_override(monkeypatch):
|
||||
"""Env vars override constants."""
|
||||
monkeypatch.setenv("OPENCODE_CI_WAIT_TIMEOUT", "42")
|
||||
monkeypatch.setenv("OPENCODE_CI_POLL_INTERVAL", "7")
|
||||
cfg = ps._load_ci_config(argv=[])
|
||||
assert cfg.wait_timeout == 42
|
||||
assert cfg.poll_interval == 7
|
||||
|
||||
|
||||
def test_load_ci_config_cli_overrides_env(monkeypatch):
|
||||
"""CLI flags override env vars."""
|
||||
monkeypatch.setenv("OPENCODE_CI_WAIT_TIMEOUT", "1")
|
||||
monkeypatch.setenv("OPENCODE_CI_POLL_INTERVAL", "1")
|
||||
cfg = ps._load_ci_config(argv=["--ci-wait-timeout", "2", "--ci-poll-interval", "3"])
|
||||
assert cfg.wait_timeout == 2
|
||||
assert cfg.poll_interval == 3
|
||||
|
||||
|
||||
def test_load_ci_config_env_invalid_falls_back(monkeypatch):
|
||||
"""Invalid env var value → fall back to constant."""
|
||||
monkeypatch.setenv("OPENCODE_CI_WAIT_TIMEOUT", "not-a-number")
|
||||
cfg = ps._load_ci_config(argv=[])
|
||||
assert cfg.wait_timeout == ps.CI_WAIT_TIMEOUT
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
211
tests/test_pipeline_status_tool.py
Normal file
211
tests/test_pipeline_status_tool.py
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
"""Tests for .opencode/tools/pipeline-status.ts — the pipeline_status custom tool.
|
||||
|
||||
Covers the spawnSync-based implementation that replaced the original
|
||||
``Bun.$`` spawn (issue #99 / PR-?).
|
||||
|
||||
The TS tool file is Bun-runtime code (TypeScript + ``import.meta.dir``)
|
||||
and there is no bun/tsx/esbuild on the CI runner — only node + pytest. We
|
||||
exercise the tool's ``execute()`` function via a tiny CommonJS loader
|
||||
(``tests/_ts_loader.mjs``) which:
|
||||
- strips TS-only import type annotations,
|
||||
- stubs ``@opencode-ai/plugin``'s ``tool()`` (identity) + ``tool.schema``
|
||||
(chainable zod shim),
|
||||
- replaces ``import.meta.dir`` with the real ``.opencode/tools`` directory,
|
||||
- exposes the ``execute()`` function via a JSON-stdout protocol.
|
||||
|
||||
Three modes are used by these tests:
|
||||
- ``load`` — sanity-check that the tool loads and has ``pr_number`` arg.
|
||||
- ``exec_stub`` — call execute with a stubbed spawnSync to verify:
|
||||
(a) args passed correctly,
|
||||
(b) stdout is trimmed on success,
|
||||
(c) non-zero exit returns an actionable error message.
|
||||
- ``exec_real`` — call execute against the real pipeline-status.py
|
||||
(integration test, PR #23 is a known-good reference).
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
LOADER = REPO_ROOT / "tests" / "_ts_loader.mjs"
|
||||
TS_FILE = REPO_ROOT / ".opencode" / "tools" / "pipeline-status.ts"
|
||||
|
||||
|
||||
def _gh_available() -> bool:
|
||||
"""True if `gh auth status` succeeds (local dev machine, not CI runner)."""
|
||||
try:
|
||||
return (
|
||||
subprocess.run(
|
||||
["gh", "auth", "status"],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
).returncode
|
||||
== 0
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
# Integration tests below hit the real pipeline-status.py which makes gh/git
|
||||
# calls to GitHub. The CI runner has no gh auth — pipeline-status.py returns
|
||||
# non-zero exit and our tool surfaces the error string. Skip those tests in
|
||||
# that environment; the unit tests cover the same code paths.
|
||||
_GH_OK = _gh_available()
|
||||
_SKIP_REASON = "gh CLI not authenticated — skip real pipeline-status.py call"
|
||||
|
||||
|
||||
def _run_loader(*args: str, stdin: str | None = None) -> dict:
|
||||
"""Invoke the loader and parse its JSON stdout."""
|
||||
proc = subprocess.run(
|
||||
["node", str(LOADER), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
cwd=str(REPO_ROOT),
|
||||
input=stdin,
|
||||
timeout=60,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"_ts_loader.mjs {' '.join(args)} failed (exit {proc.returncode}):\n"
|
||||
f"stdout: {proc.stdout}\nstderr: {proc.stderr}"
|
||||
)
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_loader_can_load_tool():
|
||||
"""Sanity: the TS tool loads and has the pr_number argument."""
|
||||
if not TS_FILE.exists():
|
||||
pytest.skip("pipeline-status.ts not present")
|
||||
out = _run_loader("load")
|
||||
assert "description" in out
|
||||
assert "pr_number" in out["args"]
|
||||
|
||||
|
||||
def test_execute_passes_correct_args():
|
||||
"""execute calls spawnSync with ["python3", <script path>, "<pr_number>"]."""
|
||||
# Stub spawnSync returns status 0 + empty stdout, but we capture the
|
||||
# argv it received via the loader's call log.
|
||||
out = _run_loader("exec_stub", "94", "0", "", "")
|
||||
calls = out["calls"]
|
||||
assert len(calls) == 1, f"expected 1 spawnSync call, got {len(calls)}"
|
||||
call = calls[0]
|
||||
assert call["cmd"] == "python3"
|
||||
assert call["args"][0] == str(REPO_ROOT / ".opencode" / "scripts" / "pipeline-status.py"), (
|
||||
f"script path mismatch: {call['args'][0]}"
|
||||
)
|
||||
assert call["args"][1] == "94", f"pr_number must be stringified: {call['args'][1]}"
|
||||
# String conversion is explicit in the source: String(args.pr_number).
|
||||
# An int 94 would arrive as "94" — verify by sending a 0 and checking it's "0".
|
||||
out0 = _run_loader("exec_stub", "0", "0", "", "")
|
||||
assert out0["calls"][0]["args"][1] == "0"
|
||||
|
||||
|
||||
def test_execute_passes_cwd_from_context():
|
||||
"""execute passes cwd=context.worktree to spawnSync (PR#101).
|
||||
|
||||
The TS tool wrapper must propagate ``context.worktree`` (git worktree
|
||||
root) as ``cwd`` to ``spawnSync``. Without it, the Python script
|
||||
inherits the opencode process cwd (which may NOT be a git repo — e.g.
|
||||
when opencode is launched from ``/root`` or ``/tmp``), and ``gh``
|
||||
without ``--repo`` cannot determine the repo, returning "PR #N не
|
||||
существует" for real PRs.
|
||||
|
||||
The ``_ts_loader.mjs`` exec_stub mode passes ``worktree: REPO_ROOT``
|
||||
in the ToolContext and captures the spawnSync ``opts`` kwarg in its
|
||||
call log. This test asserts ``opts.cwd == REPO_ROOT``.
|
||||
"""
|
||||
out = _run_loader("exec_stub", "94", "0", "", "")
|
||||
calls = out["calls"]
|
||||
assert len(calls) == 1, f"expected 1 spawnSync call, got {len(calls)}"
|
||||
opts = calls[0]["opts"]
|
||||
assert opts is not None, "spawnSync called without opts — expected cwd kwarg"
|
||||
assert "cwd" in opts, f"opts missing 'cwd' key — got: {opts}"
|
||||
assert opts["cwd"] == str(REPO_ROOT), (
|
||||
f"cwd must equal context.worktree ({REPO_ROOT}), got: {opts['cwd']!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_execute_trims_output():
|
||||
"""execute trims leading/trailing whitespace from the script stdout."""
|
||||
# Pass a stdout with leading/trailing whitespace + newline; the result
|
||||
# must be trimmed (issue spec: result.trim()).
|
||||
# Use a sentinel that survives shell argv passing.
|
||||
raw_stdout = " trimmed-output \n"
|
||||
out = _run_loader("exec_stub", "94", "0", raw_stdout, "")
|
||||
result = out["result"]
|
||||
assert result == "trimmed-output", f"expected trimmed output, got: {result!r}"
|
||||
|
||||
|
||||
def test_execute_non_zero_exit_returns_error():
|
||||
"""execute returns an actionable error message on non-zero exit code."""
|
||||
out = _run_loader("exec_stub", "94", "1", "", "some stderr from python")
|
||||
result = out["result"]
|
||||
assert "pipeline_status failed" in result
|
||||
assert "exit 1" in result
|
||||
assert "some stderr from python" in result
|
||||
|
||||
|
||||
def test_execute_exit_null_returns_error():
|
||||
"""execute handles spawnSync status=null (process killed by signal)."""
|
||||
# The loader converts status to int via parseInt; pass "null" and the
|
||||
# loader's stub returns it as a literal — but in JS, `parseInt("null")`
|
||||
# is NaN, and `NaN !== 0` is true, so we hit the error branch.
|
||||
# However, the real spawnSync returns status=null when the process
|
||||
# is killed by a signal. Our stub signature is (status as string) —
|
||||
# verify our tool treats any non-zero (including null/NaN) as failure.
|
||||
out = _run_loader("exec_stub", "94", "2", "", "boom")
|
||||
result = out["result"]
|
||||
assert "pipeline_status failed" in result
|
||||
assert "exit 2" in result
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _GH_OK, reason=_SKIP_REASON)
|
||||
def test_execute_real_pipeline_status_23():
|
||||
"""Integration: execute({pr_number: 23}) returns the real script output.
|
||||
|
||||
PR #23 is a known-good reference (merged, has handoff, ADR, docs-review
|
||||
marker). The output should contain "PR #23" and "Status:" or phase list.
|
||||
"""
|
||||
if not (REPO_ROOT / ".opencode" / "scripts" / "pipeline-status.py").exists():
|
||||
pytest.skip("pipeline-status.py not present")
|
||||
out = _run_loader("exec_real", "23")
|
||||
if out.get("error"):
|
||||
pytest.fail(f"execute raised: {out['error']}")
|
||||
result = out["result"]
|
||||
assert "PR #23" in result, f"expected 'PR #23' in output, got first 200: {result[:200]!r}"
|
||||
# The pipeline-status.py output uses ✅/❌ phase markers. Either one
|
||||
# confirms the script ran and returned structured output.
|
||||
assert "✅" in result or "❌" in result, (
|
||||
f"expected phase markers in output, got first 200: {result[:200]!r}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _GH_OK, reason=_SKIP_REASON)
|
||||
def test_execute_aborts_do_not_break_spawn_sync():
|
||||
"""Regression: spawnSync ignores ToolContext.abort — the original root cause.
|
||||
|
||||
Issue #99 hypothesis: Bun.$ respects AbortSignal, so plan-mode aborts
|
||||
caused the tool to throw. spawnSync is synchronous and does NOT
|
||||
register an AbortSignal listener, so it completes regardless.
|
||||
|
||||
We reproduce the aborted-signal condition by routing through the
|
||||
loader's exec_real mode (which uses a fresh, non-aborted signal), and
|
||||
verify the tool returns a non-error result. A test that passes here
|
||||
confirms the spawnSync path is abort-resilient by construction (sync
|
||||
syscalls cannot be cancelled via the event loop).
|
||||
|
||||
Note: this test is the static-expression counterpart of the runtime
|
||||
experiment documented in the PR handoff (the dynamic test via
|
||||
`opencode run --plan-mode` could not run because Gemini free-tier
|
||||
blocked the agent loop). The handoff contains the abort-vs-spawnSync
|
||||
experiment results for posterity.
|
||||
"""
|
||||
out = _run_loader("exec_real", "23")
|
||||
assert "error" not in out or not out["error"], (
|
||||
f"execute raised despite spawnSync being abort-resilient: {out.get('error')}"
|
||||
)
|
||||
assert "PR #23" in out["result"]
|
||||
110
tests/test_pipeline_status_tool.ts
Normal file
110
tests/test_pipeline_status_tool.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* Tests for .opencode/tools/pipeline-status.ts — the pipeline_status custom tool.
|
||||
*
|
||||
* These tests target the spawnSync-based implementation that replaced the
|
||||
* original `Bun.$` spawn (issue #99 — pipeline_status tool fails in plan-mode).
|
||||
*
|
||||
* Runtime note: opencode ships a standalone binary with Bun bundled inside;
|
||||
* there is no separate `bun` CLI on the host (CI runner uses node + pytest).
|
||||
* The CI runs the equivalent Python tests in `tests/test_pipeline_status_tool.py`
|
||||
* via the JS loader `tests/_ts_loader.mjs`. This file exists to document the
|
||||
* intended TS-side test cases and to be runnable under `bun test` once a
|
||||
* bun runtime is available on the host.
|
||||
*
|
||||
* Test cases (mirror tests/test_pipeline_status_tool.py):
|
||||
* - test_execute_passes_correct_args
|
||||
* - test_execute_trims_output
|
||||
* - test_execute_non_zero_exit_returns_error
|
||||
* - test_execute_real_pipeline_status_94 (integration)
|
||||
* - test_execute_aborts_do_not_break_spawn_sync (regression for plan-mode)
|
||||
*/
|
||||
|
||||
import { describe, test, expect, mock } from "bun:test" with { type: "'bun-test'" }
|
||||
import { spawnSync } from "child_process"
|
||||
import path from "path"
|
||||
|
||||
// Load the tool under test. Because the tool uses `import.meta.dir`, we
|
||||
// re-resolve the script path explicitly here for testing.
|
||||
const TOOL_SRC = path.resolve(import.meta.dir, "..", ".opencode", "tools", "pipeline-status.ts")
|
||||
// Re-import as a fresh module so the tool's default export is loaded.
|
||||
const { default: pipelineStatusTool } = await import(TOOL_SRC)
|
||||
|
||||
describe("pipeline_status tool", () => {
|
||||
test("execute passes correct args to spawnSync", async () => {
|
||||
const mockSpawn = mock((_cmd: string, _args: string[]) => ({
|
||||
status: 0,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
}))
|
||||
// Re-import the tool with spawnSync stubbed out — bun:test supports
|
||||
// `mock.module` for this. See https://bun.sh/docs/test/mocking
|
||||
mock.module("child_process", () => ({ spawnSync: mockSpawn }))
|
||||
const mod = await import(TOOL_SRC + "?t=" + Date.now())
|
||||
const t = mod.default
|
||||
await t.execute({ pr_number: 94 }, {
|
||||
sessionID: "test", messageID: "test", agent: "test",
|
||||
directory: ".", worktree: ".",
|
||||
abort: new AbortController().signal,
|
||||
metadata() {}, async ask() {},
|
||||
})
|
||||
expect(mockSpawn).toHaveBeenCalledTimes(1)
|
||||
const [cmd, args] = mockSpawn.mock.calls[0]
|
||||
expect(cmd).toBe("python3")
|
||||
expect(args[1]).toBe("94") // String(args.pr_number) — stringified
|
||||
})
|
||||
|
||||
test("execute trims output", async () => {
|
||||
mock.module("child_process", () => ({
|
||||
spawnSync: () => ({ status: 0, stdout: " trimmed \n", stderr: "" }),
|
||||
}))
|
||||
const mod = await import(TOOL_SRC + "?t=" + Date.now())
|
||||
const result = await mod.default.execute({ pr_number: 1 }, {
|
||||
sessionID: "t", messageID: "t", agent: "t",
|
||||
directory: ".", worktree: ".",
|
||||
abort: new AbortController().signal,
|
||||
metadata() {}, async ask() {},
|
||||
})
|
||||
expect(result).toBe("trimmed")
|
||||
})
|
||||
|
||||
test("execute non-zero exit returns error message", async () => {
|
||||
mock.module("child_process", () => ({
|
||||
spawnSync: () => ({ status: 1, stdout: "", stderr: "boom" }),
|
||||
}))
|
||||
const mod = await import(TOOL_SRC + "?t=" + Date.now())
|
||||
const result = await mod.default.execute({ pr_number: 1 }, {
|
||||
sessionID: "t", messageID: "t", agent: "t",
|
||||
directory: ".", worktree: ".",
|
||||
abort: new AbortController().signal,
|
||||
metadata() {}, async ask() {},
|
||||
})
|
||||
expect(result).toContain("pipeline_status failed")
|
||||
expect(result).toContain("exit 1")
|
||||
expect(result).toContain("boom")
|
||||
})
|
||||
|
||||
test("execute real pipeline-status.py 23 (integration)", async () => {
|
||||
const result = await pipelineStatusTool.execute({ pr_number: 23 }, {
|
||||
sessionID: "t", messageID: "t", agent: "t",
|
||||
directory: ".", worktree: ".",
|
||||
abort: new AbortController().signal,
|
||||
metadata() {}, async ask() {},
|
||||
})
|
||||
expect(result).toContain("PR #23")
|
||||
expect(result).toMatch(/[✅❌]/)
|
||||
})
|
||||
|
||||
test("execute ignores ToolContext.abort (plan-mode regression)", async () => {
|
||||
// Reproduce plan-mode: ToolContext.abort is aborted.
|
||||
const ctrl = new AbortController()
|
||||
ctrl.abort()
|
||||
const result = await pipelineStatusTool.execute({ pr_number: 23 }, {
|
||||
sessionID: "t", messageID: "t", agent: "t",
|
||||
directory: ".", worktree: ".",
|
||||
abort: ctrl.signal,
|
||||
metadata() {}, async ask() {},
|
||||
})
|
||||
// spawnSync ignores the AbortSignal, so the tool completes normally.
|
||||
expect(result).toContain("PR #23")
|
||||
})
|
||||
})
|
||||
58
tests/test_search.py
Normal file
58
tests/test_search.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import json
|
||||
from argparse import Namespace
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import src.memory.search as search_mod
|
||||
from src.memory.search import _cosine_sim
|
||||
|
||||
|
||||
class TestCosineSim:
|
||||
def test_identical(self) -> None:
|
||||
v = np.array([1.0, 2.0, 3.0])
|
||||
assert _cosine_sim(v, v) == pytest.approx(1.0)
|
||||
|
||||
def test_orthogonal(self) -> None:
|
||||
a = np.array([1.0, 0.0])
|
||||
b = np.array([0.0, 1.0])
|
||||
assert _cosine_sim(a, b) == pytest.approx(0.0)
|
||||
|
||||
def test_zero_vector(self) -> None:
|
||||
a = np.array([0.0, 0.0])
|
||||
b = np.array([1.0, 1.0])
|
||||
assert _cosine_sim(a, b) == pytest.approx(0.0)
|
||||
|
||||
def test_parallel(self) -> None:
|
||||
a = np.array([1.0, 2.0])
|
||||
b = np.array([2.0, 4.0])
|
||||
assert _cosine_sim(a, b) == pytest.approx(1.0)
|
||||
|
||||
def test_opposite(self) -> None:
|
||||
a = np.array([1.0, 1.0])
|
||||
b = np.array([-1.0, -1.0])
|
||||
assert _cosine_sim(a, b) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
class TestSearchOutput:
|
||||
def test_search_json_format(self, tmp_path: Path) -> None:
|
||||
index_dir = tmp_path / ".rag"
|
||||
index_dir.mkdir()
|
||||
index = {
|
||||
"files": [
|
||||
{
|
||||
"source": "test.md",
|
||||
"text": "hello world",
|
||||
"embedding": [1.0, 0.0, 0.0],
|
||||
},
|
||||
],
|
||||
}
|
||||
(index_dir / "index.json").write_text(json.dumps(index))
|
||||
|
||||
def fake_embed_texts(texts):
|
||||
return [[1.0, 0.0, 0.0]]
|
||||
|
||||
with patch.object(search_mod, "embed_texts", fake_embed_texts):
|
||||
args = Namespace(index_dir=str(index_dir), query="hello", k=5, json=True)
|
||||
search_mod.run_search(args)
|
||||
757
tests/test_spec_status.py
Normal file
757
tests/test_spec_status.py
Normal file
|
|
@ -0,0 +1,757 @@
|
|||
"""Tests for .opencode/scripts/spec-status.py — spec phase oracle.
|
||||
|
||||
All gh/git calls are mocked via monkeypatch on the module's ``run_cmd``
|
||||
helper. Filesystem checks (``docs/spec/*.md``) use ``tmp_path`` with
|
||||
``monkeypatch`` on ``ss.SPEC_DIR`` and ``ss.META_FILE`` so each test sees
|
||||
an isolated spec directory.
|
||||
|
||||
``get_repo_full_name`` is cached via ``functools.cache`` and called by
|
||||
Phase 8 (``gh issue view --repo``). The ``_clear_repo_cache`` autouse
|
||||
fixture clears the cache before and after each test; ``mock_run_cmd``
|
||||
automatically includes a ``git remote get-url origin`` mock
|
||||
(``GIT_REMOTE_MOCK``) so ``get_repo_full_name()`` works without extra
|
||||
boilerplate.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
SCRIPT_PATH = Path(__file__).resolve().parent.parent / ".opencode" / "scripts" / "spec-status.py"
|
||||
spec = importlib.util.spec_from_file_location("spec_status", SCRIPT_PATH)
|
||||
ss = importlib.util.module_from_spec(spec)
|
||||
sys.modules["spec_status"] = ss
|
||||
spec.loader.exec_module(ss)
|
||||
|
||||
GIT_REMOTE_MOCK: tuple[tuple[str, ...], tuple[int, str, str]] = (
|
||||
("git", "remote"),
|
||||
(0, "https://github.com/slaid098/opencode-config.git\n", ""),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_repo_cache():
|
||||
"""Clear ``functools.cache`` on ``get_repo_full_name`` before/after each test."""
|
||||
ss.get_repo_full_name.cache_clear()
|
||||
yield
|
||||
ss.get_repo_full_name.cache_clear()
|
||||
|
||||
|
||||
def mock_run_cmd(responses: dict[tuple, tuple[int, str, str]]):
|
||||
"""Factory: mock run_cmd matching by command prefix.
|
||||
|
||||
Keys are tuples of leading args (e.g. ("gh", "issue", "view")),
|
||||
values are (returncode, stdout, stderr).
|
||||
|
||||
Automatically includes a ``git remote get-url origin`` mock
|
||||
(``GIT_REMOTE_MOCK``) so ``get_repo_full_name()`` works without extra
|
||||
boilerplate in every test.
|
||||
"""
|
||||
merged = {GIT_REMOTE_MOCK[0]: GIT_REMOTE_MOCK[1], **responses}
|
||||
|
||||
def _mock(args: list[str]) -> tuple[int, str, str]:
|
||||
for prefix, result in merged.items():
|
||||
if tuple(args[: len(prefix)]) == tuple(prefix):
|
||||
return result
|
||||
return (1, "", f"unmocked call: {args}")
|
||||
|
||||
return _mock
|
||||
|
||||
|
||||
def _set_spec_dir(monkeypatch, tmp_path: Path) -> Path:
|
||||
"""Point ``ss.SPEC_DIR`` and ``ss.META_FILE`` at ``tmp_path/spec``.
|
||||
|
||||
Creates the directory so tests can drop files into it. Returns the
|
||||
spec dir path.
|
||||
"""
|
||||
spec_dir = tmp_path / "spec"
|
||||
spec_dir.mkdir(parents=True, exist_ok=True)
|
||||
monkeypatch.setattr(ss, "SPEC_DIR", spec_dir)
|
||||
monkeypatch.setattr(ss, "META_FILE", spec_dir / "meta.md")
|
||||
return spec_dir
|
||||
|
||||
|
||||
def _write_meta(spec_dir: Path, **fields) -> None:
|
||||
"""Write a ``meta.md`` frontmatter file with the given key/value pairs."""
|
||||
lines = ["---"]
|
||||
for k, v in fields.items():
|
||||
value = ("true" if v else "false") if isinstance(v, bool) else str(v)
|
||||
lines.append(f"{k}: {value}")
|
||||
lines.extend(["---", ""])
|
||||
(spec_dir / "meta.md").write_text("\n".join(lines))
|
||||
|
||||
|
||||
# ── parse_frontmatter ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_parse_frontmatter_empty():
|
||||
assert ss.parse_frontmatter("") == {}
|
||||
|
||||
|
||||
def test_parse_frontmatter_no_delimiters():
|
||||
assert ss.parse_frontmatter("just text\nno frontmatter") == {}
|
||||
|
||||
|
||||
def test_parse_frontmatter_basic():
|
||||
content = "---\nproject: foo\ntype: backend\n---\nbody"
|
||||
assert ss.parse_frontmatter(content) == {"project": "foo", "type": "backend"}
|
||||
|
||||
|
||||
def test_parse_frontmatter_quoted_values():
|
||||
content = '---\nproject: "foo bar"\ntype: "backend"\n---\n'
|
||||
fm = ss.parse_frontmatter(content)
|
||||
assert fm["project"] == '"foo bar"'
|
||||
assert fm["type"] == '"backend"'
|
||||
|
||||
|
||||
def test_parse_frontmatter_multiline_body():
|
||||
content = "---\nproject: foo\n---\nline1\nline2\nline3\n"
|
||||
fm = ss.parse_frontmatter(content)
|
||||
assert fm == {"project": "foo"}
|
||||
|
||||
|
||||
# ── parse_remote_url ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_parse_remote_url_https():
|
||||
assert ss.parse_remote_url("https://github.com/slaid098/opencode-config.git") == (
|
||||
"github.com",
|
||||
"slaid098",
|
||||
"opencode-config",
|
||||
)
|
||||
|
||||
|
||||
def test_parse_remote_url_https_no_git_suffix():
|
||||
assert ss.parse_remote_url("https://github.com/slaid098/opencode-config") == (
|
||||
"github.com",
|
||||
"slaid098",
|
||||
"opencode-config",
|
||||
)
|
||||
|
||||
|
||||
def test_parse_remote_url_ssh():
|
||||
assert ss.parse_remote_url("git@github.com:slaid098/opencode-config.git") == (
|
||||
"github.com",
|
||||
"slaid098",
|
||||
"opencode-config",
|
||||
)
|
||||
|
||||
|
||||
def test_parse_remote_url_ssh_no_git_suffix():
|
||||
assert ss.parse_remote_url("git@github.com:slaid098/opencode-config") == (
|
||||
"github.com",
|
||||
"slaid098",
|
||||
"opencode-config",
|
||||
)
|
||||
|
||||
|
||||
def test_parse_remote_url_invalid():
|
||||
with pytest.raises(ValueError, match="Cannot parse remote URL"):
|
||||
ss.parse_remote_url("not-a-valid-url")
|
||||
|
||||
|
||||
# ── get_repo_full_name ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_get_repo_full_name_current_repo(monkeypatch):
|
||||
ss.get_repo_full_name.cache_clear()
|
||||
monkeypatch.setattr(
|
||||
ss,
|
||||
"run_cmd",
|
||||
mock_run_cmd(
|
||||
{("git", "remote"): (0, "https://github.com/slaid098/opencode-config.git\n", "")}
|
||||
),
|
||||
)
|
||||
assert ss.get_repo_full_name() == "slaid098/opencode-config"
|
||||
ss.get_repo_full_name.cache_clear()
|
||||
|
||||
|
||||
def test_get_repo_full_name_cached(monkeypatch):
|
||||
ss.get_repo_full_name.cache_clear()
|
||||
call_count = [0]
|
||||
|
||||
def _counting_mock(args: list[str]) -> tuple[int, str, str]:
|
||||
if tuple(args[:2]) == ("git", "remote"):
|
||||
call_count[0] += 1
|
||||
return (0, "https://github.com/slaid098/opencode-config.git\n", "")
|
||||
return (1, "", f"unmocked: {args}")
|
||||
|
||||
monkeypatch.setattr(ss, "run_cmd", _counting_mock)
|
||||
assert ss.get_repo_full_name() == "slaid098/opencode-config"
|
||||
assert ss.get_repo_full_name() == "slaid098/opencode-config"
|
||||
assert call_count[0] == 1
|
||||
ss.get_repo_full_name.cache_clear()
|
||||
|
||||
|
||||
def test_get_repo_full_name_remote_error(monkeypatch):
|
||||
ss.get_repo_full_name.cache_clear()
|
||||
monkeypatch.setattr(
|
||||
ss, "run_cmd", mock_run_cmd({("git", "remote"): (1, "", "not a git repository")})
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="Cannot get git remote URL"):
|
||||
ss.get_repo_full_name()
|
||||
ss.get_repo_full_name.cache_clear()
|
||||
|
||||
|
||||
# ── _resolve_repo_root ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_resolve_repo_root_via_git(monkeypatch, tmp_path):
|
||||
fake_root = tmp_path / "some-repo"
|
||||
fake_root.mkdir()
|
||||
|
||||
class _FakeResult:
|
||||
returncode = 0
|
||||
stdout = f"{fake_root}\n"
|
||||
stderr = ""
|
||||
|
||||
def fake_run(args, **kwargs):
|
||||
if tuple(args[:3]) == ("git", "rev-parse", "--show-toplevel"):
|
||||
return _FakeResult()
|
||||
raise AssertionError(f"unmocked: {args}")
|
||||
|
||||
monkeypatch.setattr(ss.subprocess, "run", fake_run)
|
||||
root = ss._resolve_repo_root()
|
||||
assert root == fake_root.resolve()
|
||||
|
||||
|
||||
def test_resolve_repo_root_fallback_to_file(monkeypatch):
|
||||
class _FakeResult:
|
||||
returncode = 1
|
||||
stdout = ""
|
||||
stderr = "not a git repo"
|
||||
|
||||
def fake_run(args, **kwargs):
|
||||
if tuple(args[:3]) == ("git", "rev-parse", "--show-toplevel"):
|
||||
return _FakeResult()
|
||||
raise AssertionError(f"unmocked: {args}")
|
||||
|
||||
monkeypatch.setattr(ss.subprocess, "run", fake_run)
|
||||
root = ss._resolve_repo_root()
|
||||
expected = Path(ss.__file__).resolve().parent.parent.parent
|
||||
assert root == expected
|
||||
|
||||
|
||||
# ── file helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_file_filled_missing(tmp_path):
|
||||
assert not ss.file_filled(tmp_path / "nope.md")
|
||||
|
||||
|
||||
def test_file_filled_empty(tmp_path):
|
||||
f = tmp_path / "empty.md"
|
||||
f.write_text(" \n\n")
|
||||
assert not ss.file_filled(f)
|
||||
|
||||
|
||||
def test_file_filled_ok(tmp_path):
|
||||
f = tmp_path / "ok.md"
|
||||
f.write_text("content")
|
||||
assert ss.file_filled(f)
|
||||
|
||||
|
||||
def test_has_bullet_items_missing(tmp_path):
|
||||
assert not ss.has_bullet_items(tmp_path / "nope.md")
|
||||
|
||||
|
||||
def test_has_bullet_items_no_bullet(tmp_path):
|
||||
f = tmp_path / "no_bullet.md"
|
||||
f.write_text("just text\nno bullets here")
|
||||
assert not ss.has_bullet_items(f)
|
||||
|
||||
|
||||
def test_has_bullet_items_dash(tmp_path):
|
||||
f = tmp_path / "dash.md"
|
||||
f.write_text("- item one\n- item two\n")
|
||||
assert ss.has_bullet_items(f)
|
||||
|
||||
|
||||
def test_has_bullet_items_star(tmp_path):
|
||||
f = tmp_path / "star.md"
|
||||
f.write_text("* item one\n")
|
||||
assert ss.has_bullet_items(f)
|
||||
|
||||
|
||||
# ── check_detect (Phase 0) ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_check_detect_no_meta(monkeypatch, tmp_path):
|
||||
_set_spec_dir(monkeypatch, tmp_path)
|
||||
# Ensure meta.md does not exist
|
||||
assert not (tmp_path / "spec" / "meta.md").exists()
|
||||
result = ss.check_detect()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
assert "meta.md" in result.detail
|
||||
|
||||
|
||||
def test_check_detect_empty_meta(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
(spec_dir / "meta.md").write_text("")
|
||||
result = ss.check_detect()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
|
||||
|
||||
def test_check_detect_no_frontmatter(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
(spec_dir / "meta.md").write_text("just some text\nwithout frontmatter\n")
|
||||
result = ss.check_detect()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
|
||||
|
||||
def test_check_detect_no_project_key(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, type="backend", created="2026-07-20")
|
||||
result = ss.check_detect()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
assert "project" in result.detail
|
||||
|
||||
|
||||
def test_check_detect_ok(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="backend")
|
||||
result = ss.check_detect()
|
||||
assert result.status == ss.PhaseStatus.DONE
|
||||
|
||||
|
||||
# ── check_project_type (Phase 1) ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_check_project_type_empty_type(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="")
|
||||
result = ss.check_project_type()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
|
||||
|
||||
def test_check_project_type_invalid_type(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="unknown-type")
|
||||
result = ss.check_project_type()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
assert "unknown-type" in result.detail
|
||||
|
||||
|
||||
def test_check_project_type_empty_project(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="", type="backend")
|
||||
result = ss.check_project_type()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
|
||||
|
||||
def test_check_project_type_no_context_md(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="backend")
|
||||
# No context.md
|
||||
result = ss.check_project_type()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
assert "context.md" in result.detail
|
||||
|
||||
|
||||
def test_check_project_type_empty_context_md(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="backend")
|
||||
(spec_dir / "context.md").write_text(" \n")
|
||||
result = ss.check_project_type()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
|
||||
|
||||
def test_check_project_type_ok(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="backend")
|
||||
(spec_dir / "context.md").write_text("A backend project for foo\n")
|
||||
result = ss.check_project_type()
|
||||
assert result.status == ss.PhaseStatus.DONE
|
||||
assert "backend" in result.detail
|
||||
|
||||
|
||||
# ── check_stack (Phase 2) ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_check_stack_no_stack_md(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="backend")
|
||||
result = ss.check_stack()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
assert "stack.md" in result.detail
|
||||
|
||||
|
||||
def test_check_stack_missing_items(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="backend")
|
||||
(spec_dir / "stack.md").write_text("- only fastapi\n- nothing else\n")
|
||||
result = ss.check_stack()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
assert "tortoise" in result.detail
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ptype",
|
||||
[
|
||||
"backend",
|
||||
"fullstack",
|
||||
"mcp-server",
|
||||
"cli",
|
||||
"bot",
|
||||
"worker",
|
||||
],
|
||||
)
|
||||
def test_check_stack_ok_all_types(monkeypatch, tmp_path, ptype):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type=ptype)
|
||||
required = ss.STACK_REQUIRED[ptype]
|
||||
(spec_dir / "stack.md").write_text("\n".join(f"- {item}" for item in required) + "\n")
|
||||
result = ss.check_stack()
|
||||
assert result.status == ss.PhaseStatus.DONE, f"{ptype}: {result.detail}"
|
||||
|
||||
|
||||
def test_check_stack_invalid_type(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="unknown")
|
||||
result = ss.check_stack()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
assert "невалиден" in result.detail
|
||||
|
||||
|
||||
def test_check_stack_case_insensitive(monkeypatch, tmp_path):
|
||||
"""STACK_REQUIRED substring match is case-insensitive."""
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="backend")
|
||||
(spec_dir / "stack.md").write_text(
|
||||
"- FastAPI\n- Tortoise ORM\n- UV\n- Pytest\n- Ruff\n- Mypy\n"
|
||||
)
|
||||
result = ss.check_stack()
|
||||
assert result.status == ss.PhaseStatus.DONE
|
||||
|
||||
|
||||
# ── check_modules (Phase 3) ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_check_modules_no_file(monkeypatch, tmp_path):
|
||||
_set_spec_dir(monkeypatch, tmp_path)
|
||||
result = ss.check_modules()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
|
||||
|
||||
def test_check_modules_empty(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
(spec_dir / "modules.md").write_text(" \n\n")
|
||||
result = ss.check_modules()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
|
||||
|
||||
def test_check_modules_no_bullet(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
(spec_dir / "modules.md").write_text("just prose\nno bullets")
|
||||
result = ss.check_modules()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
|
||||
|
||||
def test_check_modules_ok(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
(spec_dir / "modules.md").write_text("- auth\n- users\n- posts\n")
|
||||
result = ss.check_modules()
|
||||
assert result.status == ss.PhaseStatus.DONE
|
||||
assert "3" in result.detail
|
||||
|
||||
|
||||
# ── check_db_schema (Phase 4) ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_check_db_schema_no_db_true(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="cli", no_db="true")
|
||||
# db-schema.md intentionally absent
|
||||
result = ss.check_db_schema()
|
||||
assert result.status == ss.PhaseStatus.DONE
|
||||
assert "no_db" in result.detail
|
||||
|
||||
|
||||
def test_check_db_schema_no_file(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="backend")
|
||||
result = ss.check_db_schema()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
|
||||
|
||||
def test_check_db_schema_empty(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="backend")
|
||||
(spec_dir / "db-schema.md").write_text(" \n\n")
|
||||
result = ss.check_db_schema()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
|
||||
|
||||
def test_check_db_schema_ok(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="backend")
|
||||
(spec_dir / "db-schema.md").write_text("User: id, name\nPost: id, title\n")
|
||||
result = ss.check_db_schema()
|
||||
assert result.status == ss.PhaseStatus.DONE
|
||||
|
||||
|
||||
# ── check_infra (Phase 5) ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_check_infra_no_file(monkeypatch, tmp_path):
|
||||
_set_spec_dir(monkeypatch, tmp_path)
|
||||
result = ss.check_infra()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
|
||||
|
||||
def test_check_infra_empty(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
(spec_dir / "infra.md").write_text(" \n\n")
|
||||
result = ss.check_infra()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
|
||||
|
||||
def test_check_infra_short(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
(spec_dir / "infra.md").write_text("ab")
|
||||
result = ss.check_infra()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
assert "<3" in result.detail
|
||||
|
||||
|
||||
def test_check_infra_ok(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
(spec_dir / "infra.md").write_text("Docker compose, Prefect\n")
|
||||
result = ss.check_infra()
|
||||
assert result.status == ss.PhaseStatus.DONE
|
||||
|
||||
|
||||
# ── check_roadmap (Phase 6) ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_check_roadmap_no_file(monkeypatch, tmp_path):
|
||||
_set_spec_dir(monkeypatch, tmp_path)
|
||||
result = ss.check_roadmap()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
|
||||
|
||||
def test_check_roadmap_empty(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
(spec_dir / "roadmap.md").write_text(" \n\n")
|
||||
result = ss.check_roadmap()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
|
||||
|
||||
def test_check_roadmap_no_bullet(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
(spec_dir / "roadmap.md").write_text("just prose\nno bullets")
|
||||
result = ss.check_roadmap()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
|
||||
|
||||
def test_check_roadmap_ok(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
(spec_dir / "roadmap.md").write_text("- scaffolding\n- auth module\n- users module\n")
|
||||
result = ss.check_roadmap()
|
||||
assert result.status == ss.PhaseStatus.DONE
|
||||
assert "3" in result.detail
|
||||
|
||||
|
||||
# ── check_confirm (Phase 7) ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_check_confirm_true(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="backend", confirmed="true")
|
||||
result = ss.check_confirm()
|
||||
assert result.status == ss.PhaseStatus.DONE
|
||||
|
||||
|
||||
def test_check_confirm_false(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="backend", confirmed="false")
|
||||
result = ss.check_confirm()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
|
||||
|
||||
def test_check_confirm_missing_key(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="backend")
|
||||
result = ss.check_confirm()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
|
||||
|
||||
# ── check_execute (Phase 8) ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _setup_execute_spec(spec_dir: Path, *, executed: str = "true") -> None:
|
||||
"""Write a spec where Phases 0-7 are DONE and roadmap has #N references."""
|
||||
_write_meta(
|
||||
spec_dir,
|
||||
project="foo",
|
||||
type="backend",
|
||||
confirmed="true",
|
||||
executed=executed,
|
||||
)
|
||||
(spec_dir / "roadmap.md").write_text("- #1 scaffolding\n- #2 auth\n- #3 users\n")
|
||||
|
||||
|
||||
def test_check_execute_not_executed(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="backend", executed="false")
|
||||
result = ss.check_execute()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
assert "executed" in result.detail
|
||||
|
||||
|
||||
def test_check_execute_no_roadmap_file(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="backend", executed="true")
|
||||
# No roadmap.md
|
||||
result = ss.check_execute()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
assert "roadmap.md" in result.detail
|
||||
|
||||
|
||||
def test_check_execute_no_issue_refs(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_write_meta(spec_dir, project="foo", type="backend", executed="true")
|
||||
(spec_dir / "roadmap.md").write_text("- scaffolding\n- auth\n") # No #N refs
|
||||
result = ss.check_execute()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
assert "#N" in result.detail
|
||||
|
||||
|
||||
def test_check_execute_issues_found(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_setup_execute_spec(spec_dir)
|
||||
monkeypatch.setattr(
|
||||
ss,
|
||||
"run_cmd",
|
||||
mock_run_cmd({("gh", "issue", "view"): (0, "issue body", "")}),
|
||||
)
|
||||
result = ss.check_execute()
|
||||
assert result.status == ss.PhaseStatus.DONE
|
||||
assert "3" in result.detail
|
||||
|
||||
|
||||
def test_check_execute_issues_missing(monkeypatch, tmp_path):
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_setup_execute_spec(spec_dir)
|
||||
monkeypatch.setattr(
|
||||
ss,
|
||||
"run_cmd",
|
||||
mock_run_cmd({("gh", "issue", "view"): (1, "", "not found")}),
|
||||
)
|
||||
result = ss.check_execute()
|
||||
assert result.status == ss.PhaseStatus.NOT_DONE
|
||||
assert "#1" in result.detail
|
||||
assert "#2" in result.detail
|
||||
assert "#3" in result.detail
|
||||
|
||||
|
||||
def test_check_execute_no_remote(monkeypatch, tmp_path):
|
||||
"""git remote fails → AMBIGUOUS."""
|
||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||
_setup_execute_spec(spec_dir)
|
||||
ss.get_repo_full_name.cache_clear()
|
||||
monkeypatch.setattr(
|
||||
ss,
|
||||
"run_cmd",
|
||||
mock_run_cmd({("git", "remote"): (1, "", "not a git repository")}),
|
||||
)
|
||||
result = ss.check_execute()
|
||||
assert result.status == ss.PhaseStatus.AMBIGUOUS
|
||||
assert "git remote error" in result.detail
|
||||
ss.get_repo_full_name.cache_clear()
|
||||
|
||||
|
||||
# ── find_current_phase ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_find_current_phase_all_done():
|
||||
results = [ss.PhaseResult(ss.PhaseStatus.DONE, "")] * 9
|
||||
assert ss.find_current_phase(results) is None
|
||||
|
||||
|
||||
def test_find_current_phase_first_not_done():
|
||||
results = [ss.PhaseResult(ss.PhaseStatus.NOT_DONE, "")] + [
|
||||
ss.PhaseResult(ss.PhaseStatus.DONE, "")
|
||||
] * 8
|
||||
assert ss.find_current_phase(results) == 0
|
||||
|
||||
|
||||
def test_find_current_phase_middle():
|
||||
results = (
|
||||
[ss.PhaseResult(ss.PhaseStatus.DONE, "")] * 3
|
||||
+ [ss.PhaseResult(ss.PhaseStatus.NOT_DONE, "")]
|
||||
+ [ss.PhaseResult(ss.PhaseStatus.DONE, "")] * 5
|
||||
)
|
||||
assert ss.find_current_phase(results) == 3
|
||||
|
||||
|
||||
# ── format_output ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_format_output_complete(monkeypatch):
|
||||
results = [ss.PhaseResult(ss.PhaseStatus.DONE, "ok")] * 9
|
||||
fm = {"project": "test-project"}
|
||||
monkeypatch.setattr("sys.argv", ["spec-status.py"])
|
||||
output = ss.format_output(results, fm)
|
||||
assert "Status: COMPLETE" in output
|
||||
assert "Spec: test-project" in output
|
||||
|
||||
|
||||
def test_format_output_next_action(monkeypatch):
|
||||
results = (
|
||||
[ss.PhaseResult(ss.PhaseStatus.DONE, "ok")] * 3
|
||||
+ [ss.PhaseResult(ss.PhaseStatus.NOT_DONE, "missing")]
|
||||
+ [ss.PhaseResult(ss.PhaseStatus.NOT_DONE, "")] * 5
|
||||
)
|
||||
fm = {"project": "test-project"}
|
||||
monkeypatch.setattr("sys.argv", ["spec-status.py"])
|
||||
output = ss.format_output(results, fm)
|
||||
assert "NEXT:" in output
|
||||
assert "Phase 3" in output
|
||||
|
||||
|
||||
def test_format_output_ambiguous(monkeypatch):
|
||||
results = (
|
||||
[ss.PhaseResult(ss.PhaseStatus.DONE, "ok")] * 2
|
||||
+ [ss.PhaseResult(ss.PhaseStatus.AMBIGUOUS, "remote error")]
|
||||
+ [ss.PhaseResult(ss.PhaseStatus.NOT_DONE, "")] * 6
|
||||
)
|
||||
fm = {"project": "test-project"}
|
||||
monkeypatch.setattr("sys.argv", ["spec-status.py"])
|
||||
output = ss.format_output(results, fm)
|
||||
assert "AMBIGUOUS" in output
|
||||
assert "уточните статус вручную" in output
|
||||
|
||||
|
||||
def test_format_output_unnamed_project(monkeypatch):
|
||||
results = [ss.PhaseResult(ss.PhaseStatus.NOT_DONE, "")] * 9
|
||||
fm = {}
|
||||
monkeypatch.setattr("sys.argv", ["spec-status.py"])
|
||||
output = ss.format_output(results, fm)
|
||||
assert "Spec: <unnamed>" in output
|
||||
|
||||
|
||||
def test_format_output_validate_shows_done_details(monkeypatch):
|
||||
"""--validate flag shows full detail even for DONE phases."""
|
||||
results = [ss.PhaseResult(ss.PhaseStatus.DONE, "specific detail")] * 9
|
||||
fm = {"project": "test-project"}
|
||||
monkeypatch.setattr("sys.argv", ["spec-status.py", "--validate"])
|
||||
output = ss.format_output(results, fm)
|
||||
# In non-validate mode DONE phases show "done" only; --validate shows detail.
|
||||
assert "specific detail" in output
|
||||
|
||||
|
||||
# ── main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_main_no_spec(monkeypatch, tmp_path, capsys):
|
||||
"""Running main() with no docs/spec/ → 9 ❌ lines + NEXT, exit 0."""
|
||||
_set_spec_dir(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr("sys.argv", ["spec-status.py"])
|
||||
ss.main()
|
||||
captured = capsys.readouterr()
|
||||
assert "Spec: <unnamed>" in captured.out
|
||||
assert "NEXT:" in captured.out
|
||||
assert "DETECT" in captured.out
|
||||
172
tests/test_spec_status_tool.py
Normal file
172
tests/test_spec_status_tool.py
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
"""Tests for .opencode/tools/spec-status.ts — the spec_status custom tool.
|
||||
|
||||
Mirrors ``tests/test_pipeline_status_tool.py``: exercises the tool's
|
||||
``execute()`` function via ``tests/_ts_loader.mjs`` (a node CommonJS
|
||||
sandbox that strips TS-only syntax, stubs ``@opencode-ai/plugin``, and
|
||||
replaces ``import.meta.dir`` with the real ``.opencode/tools`` directory).
|
||||
|
||||
The loader is parameterized via the ``TS_FILE`` env var (PR#109) so the
|
||||
same harness works for both ``pipeline-status.ts`` and ``spec-status.ts``
|
||||
without breaking existing callers (``TS_FILE`` unset → defaults to
|
||||
``pipeline-status.ts``). These tests set ``TS_FILE=.opencode/tools/spec-status.ts``.
|
||||
|
||||
Modes used (same as pipeline-status tool tests):
|
||||
- ``load`` — sanity-check that the tool loads and has the expected args.
|
||||
- ``exec_stub`` — call execute with a stubbed spawnSync to verify:
|
||||
(a) args passed correctly (``--validate`` flag when ``validate: true``),
|
||||
(b) stdout is trimmed on success,
|
||||
(c) non-zero exit returns an actionable error message,
|
||||
(d) ``cwd`` is propagated from ``context.worktree`` (ADR-023).
|
||||
- ``exec_real`` — call execute against the real spec-status.py
|
||||
(integration test, skipped if no gh auth — though spec-status.py itself
|
||||
does not require gh auth for Phases 0-7; only Phase 8 hits gh).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
LOADER = REPO_ROOT / "tests" / "_ts_loader.mjs"
|
||||
TS_FILE = REPO_ROOT / ".opencode" / "tools" / "spec-status.ts"
|
||||
TS_FILE_REL = ".opencode/tools/spec-status.ts"
|
||||
|
||||
|
||||
def _gh_available() -> bool:
|
||||
"""True if `gh auth status` succeeds (local dev machine, not CI runner)."""
|
||||
try:
|
||||
return (
|
||||
subprocess.run(
|
||||
["gh", "auth", "status"],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
).returncode
|
||||
== 0
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
_GH_OK = _gh_available()
|
||||
_SKIP_REASON = "gh CLI not authenticated — skip real spec-status.py call"
|
||||
|
||||
|
||||
def _run_loader(*args: str, stdin: str | None = None) -> dict:
|
||||
"""Invoke the loader with ``TS_FILE`` env set to spec-status.ts and parse JSON stdout."""
|
||||
env = {**os.environ, "TS_FILE": TS_FILE_REL}
|
||||
proc = subprocess.run(
|
||||
["node", str(LOADER), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
cwd=str(REPO_ROOT),
|
||||
input=stdin,
|
||||
timeout=60,
|
||||
env=env,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"_ts_loader.mjs {' '.join(args)} failed (exit {proc.returncode}):\n"
|
||||
f"stdout: {proc.stdout}\nstderr: {proc.stderr}"
|
||||
)
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_loader_can_load_tool():
|
||||
"""Sanity: spec-status.ts loads and has the ``validate`` arg (optional)."""
|
||||
if not TS_FILE.exists():
|
||||
pytest.skip("spec-status.ts not present")
|
||||
out = _run_loader("load")
|
||||
assert "description" in out
|
||||
# spec-status.ts declares: validate (optional boolean)
|
||||
assert "validate" in out["args"]
|
||||
|
||||
|
||||
def test_execute_no_validate_flag():
|
||||
"""execute() without ``validate: true`` calls spawnSync WITHOUT ``--validate``.
|
||||
|
||||
Default path: ``args.validate`` is undefined/falsy → ``cmdArgs = []`` →
|
||||
spawnSync argv is ``[script]`` (no ``--validate``).
|
||||
"""
|
||||
# exec_stub args: <validate_str> <stub_status> <stub_stdout> <stub_stderr>
|
||||
# validate_str="false" → parseInt(false)=NaN → falsy → no --validate
|
||||
out = _run_loader("exec_stub", "false", "0", "spec output", "")
|
||||
calls = out["calls"]
|
||||
assert len(calls) == 1, f"expected 1 spawnSync call, got {len(calls)}"
|
||||
call = calls[0]
|
||||
assert call["cmd"] == "python3"
|
||||
assert call["args"][0] == str(REPO_ROOT / ".opencode" / "scripts" / "spec-status.py"), (
|
||||
f"script path mismatch: {call['args'][0]}"
|
||||
)
|
||||
# No --validate in args (only the script path is present).
|
||||
assert "--validate" not in call["args"], f"unexpected --validate in args: {call['args']}"
|
||||
|
||||
|
||||
def test_execute_passes_validate_flag():
|
||||
"""execute() with ``validate: true`` adds ``--validate`` to spawnSync argv."""
|
||||
# validate_str="true" → Boolean(true) → cmdArgs = ["--validate"]
|
||||
out = _run_loader("exec_stub", "true", "0", "spec validate output", "")
|
||||
calls = out["calls"]
|
||||
assert len(calls) == 1, f"expected 1 spawnSync call, got {len(calls)}"
|
||||
call = calls[0]
|
||||
assert "--validate" in call["args"], f"expected --validate in args: {call['args']}"
|
||||
|
||||
|
||||
def test_execute_trims_stdout():
|
||||
"""execute trims leading/trailing whitespace from the script stdout."""
|
||||
raw_stdout = " trimmed-spec-output \n"
|
||||
out = _run_loader("exec_stub", "false", "0", raw_stdout, "")
|
||||
result = out["result"]
|
||||
assert result == "trimmed-spec-output", f"expected trimmed output, got: {result!r}"
|
||||
|
||||
|
||||
def test_execute_nonzero_exit_returns_error():
|
||||
"""execute returns an actionable error message on non-zero exit code."""
|
||||
out = _run_loader("exec_stub", "false", "1", "", "some stderr from spec-status")
|
||||
result = out["result"]
|
||||
assert "spec_status failed" in result
|
||||
assert "exit 1" in result
|
||||
assert "some stderr from spec-status" in result
|
||||
|
||||
|
||||
def test_execute_uses_cwd_from_context():
|
||||
"""execute passes ``cwd=context.worktree`` to spawnSync (ADR-023).
|
||||
|
||||
The TS tool wrapper must propagate ``context.worktree`` as ``cwd`` to
|
||||
``spawnSync`` so the Python script runs in the git worktree root
|
||||
(where ``docs/spec/`` lives), not in the opencode process cwd.
|
||||
"""
|
||||
out = _run_loader("exec_stub", "false", "0", "spec ok", "")
|
||||
calls = out["calls"]
|
||||
assert len(calls) == 1, f"expected 1 spawnSync call, got {len(calls)}"
|
||||
opts = calls[0]["opts"]
|
||||
assert opts is not None, "spawnSync called without opts — expected cwd kwarg"
|
||||
assert "cwd" in opts, f"opts missing 'cwd' key — got: {opts}"
|
||||
assert opts["cwd"] == str(REPO_ROOT), (
|
||||
f"cwd must equal context.worktree ({REPO_ROOT}), got: {opts['cwd']!r}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _GH_OK, reason=_SKIP_REASON)
|
||||
def test_execute_real_spec_status():
|
||||
"""Integration: execute() returns the real spec-status.py output.
|
||||
|
||||
spec-status.py reads ``docs/spec/`` from cwd (REPO_ROOT). In this repo
|
||||
there is no ``docs/spec/``, so all 9 phases will be ❌ — that's a
|
||||
valid output (exit 0) and confirms the script ran through the loader.
|
||||
"""
|
||||
if not (REPO_ROOT / ".opencode" / "scripts" / "spec-status.py").exists():
|
||||
pytest.skip("spec-status.py not present")
|
||||
out = _run_loader("exec_real", "false")
|
||||
if out.get("error"):
|
||||
pytest.fail(f"execute raised: {out['error']}")
|
||||
result = out["result"]
|
||||
# spec-status.py output starts with "Spec: <project>" line.
|
||||
assert "Spec:" in result, f"expected 'Spec:' in output, got: {result[:200]!r}"
|
||||
# All 9 phases are present in the output (✅ or ❌ icons).
|
||||
assert "✅" in result or "❌" in result, (
|
||||
f"expected phase markers in output, got: {result[:200]!r}"
|
||||
)
|
||||
814
uv.lock
generated
Normal file
814
uv.lock
generated
Normal file
|
|
@ -0,0 +1,814 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.15'",
|
||||
"python_full_version < '3.15'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ast-serialize"
|
||||
version = "0.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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 = "certifi"
|
||||
version = "2026.5.20"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfgv"
|
||||
version = "3.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/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.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/d0/55fe630f4cf94e3fcba868240fad8c8cdd1f764e2a932f8926347e6ec4cd/coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", size = 927741, upload-time = "2026-07-15T18:56:19.558Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/50/eb5bf42e531611a9f8d272556b1ed4de503f84a91413584094487cf69f8f/coverage-7.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1adac78e5abc7c5438f7a209c9ca69d06542f0bf481d728b6989ea80b813fdf9", size = 221587, upload-time = "2026-07-15T18:54:18.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/d1/da99af464c335d4e023a6efcd7ec30f63b88a43c93745154ab74ffb31cea/coverage-7.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b868acc62aa5de3be7a9d05c2333bf8359ca987e43f9cb30ff8fbda6a024ab73", size = 221943, upload-time = "2026-07-15T18:54:20.062Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/8a/13c42723d61ca447eafa18732e8141dd6a63f2732e1c7e1502c182dd88d7/coverage-7.15.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6f6966fc30e6f06ca8f98fb0ce51eda6b111b3ee8d066a8b1ec9e77fa06ab55d", size = 253450, upload-time = "2026-07-15T18:54:21.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/29/99021303f98fbdcb63504b4d07bea4cc025b9b2dd907c4f07c85d50a0dab/coverage-7.15.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:68af907f595ab01a78f794932ff3bdf929c316d3000810d38dbc247129e26f8b", size = 256187, upload-time = "2026-07-15T18:54:23.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/a8/fd503715ed6ca9c5d742923aa5209257340b367a867b2ced0c7d4ba8a0b9/coverage-7.15.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afa29e2eff3d5729267e2cb2fd4ce9d61c952932fb2694e34ccb5d9540c6a296", size = 257301, upload-time = "2026-07-15T18:54:25.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/40/3f4b8fb409810036ebc2857d36adc0498c6e957b5df0290c5036b2e143f1/coverage-7.15.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbf44513ceb1589e31948e20eafbde9deaface90e1a1afa5f5f77b4423d17ce6", size = 259562, upload-time = "2026-07-15T18:54:27.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/8a/9bdffbef47db77cce3d6b02a28f7e919b19f0106c4b080c2c2246040f885/coverage-7.15.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9deddf09eecb717b7f980414b43d90a5b22ff3967d2949ab29cb0aa83d9e9098", size = 253841, upload-time = "2026-07-15T18:54:29.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/1e/9031efde019d31a06646261fce6dfc5c3c74e951e27a71e5c9a424563178/coverage-7.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae901f7e55ba405c84ee1cab3d3e962e4e871e4a2bcb9c90911adbd69b42ac5a", size = 255221, upload-time = "2026-07-15T18:54:31.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/db/787acde872389fc84a9ef9d8cd1ccc658e391ab4cb5b28092a714426a394/coverage-7.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a0f47002c6eeb7c280228467a4cb0cc15ca2103a8421b986b2d3ec04a0f9bd8b", size = 253366, upload-time = "2026-07-15T18:54:32.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/9b/6f57bc4b93c842eef1695f8cdaf2318e35e7ba54f5ba80d84be213ab7858/coverage-7.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd7a5beb7af3e864a13b1f0fb26efd3695da43ef0daf71e586adfffaf34d5b2", size = 257434, upload-time = "2026-07-15T18:54:34.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/26/b3186a21b2acc83e451118978905c81c7072c3333707804db09a78c096a2/coverage-7.15.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97a5c5457a9fb1d6c4e06cfb5dc835871fbfb6a6a51addc9e925bdeff5ef7440", size = 252935, upload-time = "2026-07-15T18:54:36.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/c2/c9f3376b2e717ea69ed7a6e9a5fcab968fb0b290db6cf4bd9a1fc7541b75/coverage-7.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0901cfe6c13bcd2302da4f83e884555d2a22bda6e4c476f09ef204ba20ca536e", size = 254807, upload-time = "2026-07-15T18:54:38.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/e1/dfc15401f4a8aaeb486e1ba3e9e3c40522a6e38bd0ecf0b3f29cb8082957/coverage-7.15.2-cp312-cp312-win32.whl", hash = "sha256:b171bdd71cb7ff792bf32e376173b0ace7e7963e7e57c58dfc42063a6a7174cd", size = 223641, upload-time = "2026-07-15T18:54:40.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/40/81b6d809d320cd366ec5bdf8176575e897dcb8efe7fb4b489ef9e93e4d13/coverage-7.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:582edc45c2040543fef83341be23c43024a3ab3ae0c2d8bc498a06282905ad40", size = 224172, upload-time = "2026-07-15T18:54:41.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/28/9f14ec438149f7de557f45518f09b4a7917b795cc37083aa7db482693f8c/coverage-7.15.2-cp312-cp312-win_arm64.whl", hash = "sha256:a638db90c61cd219aeee65e83a24fdaa57269a741ae0cf773309208ac862cee3", size = 223556, upload-time = "2026-07-15T18:54:43.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/d5/f8c838e6b7282976f7c918884b792df7a0c42c5bba5d99c60ad2d221d56d/coverage-7.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8", size = 221606, upload-time = "2026-07-15T18:54:45.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/37/97c926376364f66298cc44893b89cdf17b8bc406376497c4061ae4b8a8ff/coverage-7.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1", size = 221982, upload-time = "2026-07-15T18:54:47.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/30/a36050a6e83c2135ee0776f452ca3948224befc6d7f26acecc082d0c106a/coverage-7.15.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578", size = 252972, upload-time = "2026-07-15T18:54:49.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/d3/06b5f1daf95f0f15ab05bd75f26ba5f3c8b33d0bb72f3aaa3cf41d1bad3a/coverage-7.15.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1", size = 255569, upload-time = "2026-07-15T18:54:51.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/1c/9afb3f8de2b8d36960391c48559a2e3ff96594b58099f115921549ea8d0d/coverage-7.15.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6", size = 256806, upload-time = "2026-07-15T18:54:53.145Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/d8/b989f96061a5e32d82fddd1b1b9ff48a7c8f8ae7606f0e80fd9de54b1e33/coverage-7.15.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7", size = 258936, upload-time = "2026-07-15T18:54:55.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/fa/f99771f5110457c7b511c1935ca49ddf288218eaa84322e028b9334146ae/coverage-7.15.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d", size = 253178, upload-time = "2026-07-15T18:54:57.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/96/c098a6044d119c751ceede7be91035fa8310170ec24a6523aff72f0a5793/coverage-7.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026", size = 254934, upload-time = "2026-07-15T18:54:59.41Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/a2/1457b3a7a50c8d77500103b97a046db863e2f59a1cf6d2f814595f349885/coverage-7.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa", size = 252898, upload-time = "2026-07-15T18:55:01.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/0e/76958874c471ecfcdde0d2b2747bb2c61bdbf34a40636f4ce9db9923e643/coverage-7.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d", size = 257056, upload-time = "2026-07-15T18:55:03.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/7c/3d7c4e3bf58baa40327dc7edc2272b17cf02299366d52763db1b0ca1556a/coverage-7.15.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b", size = 252718, upload-time = "2026-07-15T18:55:05.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/b8/1cecffed9ce14fb25be9ba42d37b6bb61485c9a3ddd43cd3dde36b6087d8/coverage-7.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188", size = 254490, upload-time = "2026-07-15T18:55:06.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/2c/42984561bc7f4c045dca67516a0c50ee5ef8d84352dbeb5559dc86c4823e/coverage-7.15.2-cp313-cp313-win32.whl", hash = "sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050", size = 223647, upload-time = "2026-07-15T18:55:08.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/9f/39c7c9245efc583beddf89a87683574e663ed93637f3afb6cd7b88405676/coverage-7.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c", size = 224190, upload-time = "2026-07-15T18:55:10.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/de/3a2883cf8a213659280ef4b403059e17a9acaeb7fc7fd4105e1226ff2e6d/coverage-7.15.2-cp313-cp313-win_arm64.whl", hash = "sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b", size = 223583, upload-time = "2026-07-15T18:55:12.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/5f/aed265fd7a3551a394f36dfe41868aee709b7f95db4052205b4ad1563ac3/coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", size = 221650, upload-time = "2026-07-15T18:55:14.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/2c/222ba12a545189017120f8eddfc1a0bd4616b47d5d4a8d99421edb2fe4c6/coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", size = 221988, upload-time = "2026-07-15T18:55:16.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/38/304b5877ab46e6c290b4292cfcf3fe28245f0e5597cad7f6acc91fc7e0a4/coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", size = 253029, upload-time = "2026-07-15T18:55:18.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/58/821b533b8db9e44cf1d8a97bd525149ced40dde1d0093da02cb78e715244/coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", size = 255536, upload-time = "2026-07-15T18:55:21.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/f2/7aa06604c389d32ea7f0a6a988359a7eafc3cd3f8e7bc2e88cd2fdf0b877/coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", size = 256881, upload-time = "2026-07-15T18:55:23.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/4f/1ef342339c7916d0096bc5888cc0f653882cc7bc8f897d5cb89143287c9b/coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", size = 259196, upload-time = "2026-07-15T18:55:25.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/f4/7ed055d7a9c5ec13b161773a115a5ccc6b0081d568c31fad830806306cc7/coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", size = 253036, upload-time = "2026-07-15T18:55:27.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/79/ea82cca18c242a3a38b6c017da39726aa62dcb64aa635abf79b92009975c/coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", size = 254887, upload-time = "2026-07-15T18:55:29.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/ba/a136db3c0d9562b00e10b72540dbf3a33cd3bc5b95060c9308e247494623/coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", size = 252852, upload-time = "2026-07-15T18:55:31.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/17/ea334246b16b7d059953fad6fdefa11e33c68efbd3fe37b1098120a1fac2/coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", size = 257128, upload-time = "2026-07-15T18:55:33.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/c3/074fb66d46d607855f710876b117cbda562c5ab08363528e78820449f937/coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", size = 252668, upload-time = "2026-07-15T18:55:35.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/c1/f620850ada9b36435921c9a3a8057013422b1d964eb4bf37fe138724d192/coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", size = 254325, upload-time = "2026-07-15T18:55:37.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/31/a729ca3689404493af82ef8e6ff70bd88bdda8da89aeef6ca9b387aeb2b4/coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", size = 223844, upload-time = "2026-07-15T18:55:39.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/83/5d809dc808fb1698c671f3e372259bb9158e64b7ea526fc6ab7de64de9fe/coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", size = 224331, upload-time = "2026-07-15T18:55:41.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/4e/35e488548e952795829e129995c4174df33bf432b591d1aa42c8d9e4e7ad/coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", size = 223760, upload-time = "2026-07-15T18:55:43.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/49/dd2c86cd6374038f6e415fb5bfb86db5218553209c081384a020369dee79/coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", size = 222384, upload-time = "2026-07-15T18:55:45.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/74/173ff17a1c0808e5a438f549f6f145d5ac7528f2791310b63523e3200ac7/coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", size = 222647, upload-time = "2026-07-15T18:55:47.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/f8/b8cba872162356fb44ac79c10309d987206a4461e32072fc29228dad7331/coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", size = 264013, upload-time = "2026-07-15T18:55:49.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/67/a807a7586d0b8cae485308ddd55756f0806c92f8e0b411bacbf23c48edf3/coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a", size = 266135, upload-time = "2026-07-15T18:55:51.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/67/cd78771dc985f7e4ebdcc82b1a96d9a932af9e806f01f2f91a89f4c72e80/coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", size = 268555, upload-time = "2026-07-15T18:55:54.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/3e/10134cf81275188c58568f324fc74aedff32c63ca4d5bbc513a91944a6f0/coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", size = 269674, upload-time = "2026-07-15T18:55:56.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/4a/771b77de446cba985dc414bbc5844bd21604da05dbc044286df8318a48a7/coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", size = 263101, upload-time = "2026-07-15T18:55:58.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/b5/70a7011da15f4071943361183aefa27847f3e3aec4fd335f1cb3d3a622b1/coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", size = 266007, upload-time = "2026-07-15T18:56:00.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/0d/f9547e804ce7ad49646ffeffac26699510efbe6c0f751b66fdc960c4e825/coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", size = 263611, upload-time = "2026-07-15T18:56:02.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/59/f576a396659c0efd351f5c1544f67c3560e89c7761cabf7f65e412beeda5/coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", size = 267344, upload-time = "2026-07-15T18:56:04.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/5d/c2e4fce3579c0cb635024293f1a32bbe26df101b3e3a69f22243d1352b6c/coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", size = 262456, upload-time = "2026-07-15T18:56:06.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/dd/956287d69436b66094bc4b57ac2da71e43bfd2a5524e958900b9f582fcf8/coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", size = 264771, upload-time = "2026-07-15T18:56:08.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/5a/6f979530c2734c575de77cf58f5f28d51f7123a94b5030fd9156fe5f363c/coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", size = 224151, upload-time = "2026-07-15T18:56:10.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/7e/27f6b2a74d484742f4017553e710b01e396b23d809df3e95ca0bb9a2824b/coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", size = 224981, upload-time = "2026-07-15T18:56:12.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/48/284863423aa474240f6842bd00d680da22f4e6ea2e466618ef7c9c9e69a9/coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", size = 224294, upload-time = "2026-07-15T18:56:15.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375, upload-time = "2026-07-15T18:56:17.305Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.30.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/b7/e82e2bf111ba6ea135062e556f2ed7f4ff1c17a6c385e95ffddd99885535/filelock-3.30.1.tar.gz", hash = "sha256:6ad1b1ec6a5e2c91e02979b7d1096e5e964d83ad8a55679bacd1d96f917fda6e", size = 176008, upload-time = "2026-07-16T16:59:22.201Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/90/5e/6060a5791d3d5b54aae21c510eb2f76a51e1b28c2122cc1e39b12237e2e7/filelock-3.30.1-py3-none-any.whl", hash = "sha256:69b98bab51bed4c86b141ef338f85ca07937573e8a7798f7fe6a31ccd94b9338", size = 93698, upload-time = "2026-07-16T16:59:20.727Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.19"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.17"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/28/99c51f664567218d824af024c0251650fb27e4ca066df188dab0769c5b91/idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f", size = 196048, upload-time = "2026-05-28T14:32:38.55Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/a7/f76514cc40ad6234098ecdebda08732d75964776c51a42845b7da10649e2/idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c", size = 65316, upload-time = "2026-05-28T14:32:37.035Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "librt"
|
||||
version = "0.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/packages/f0/f4/b2933ddae222dac338476abb872641169a5cfed2c2bb5444a5b07b32b0c3/librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0", size = 150990, upload-time = "2026-07-08T12:25:02.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/ef/db98f744ca50e6efc9c95c70ee49b77aefac31f6a3fc7c83754a42d6a74f/librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5", size = 155238, upload-time = "2026-07-08T12:25:03.681Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/e7/a197e7bc72baf2c61ce7fdc6906a5054dc05bd8da0819aa894e4857bf87e/librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9", size = 503073, upload-time = "2026-07-08T12:25:05.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/e7/7887712e27da7c1ab80fcabb1de6eb24243964f6557cae530d4b70706dbd/librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b", size = 496528, upload-time = "2026-07-08T12:25:06.26Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/f0/f2283385bb6b950b26a1410f4ce51ec27231e0b3a4b925c46366d218b198/librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03", size = 531786, upload-time = "2026-07-08T12:25:07.658Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/11/69ac3b54766ffba5fd7e5acebfb048d66dbe1f9f2d14516c2b3edc59cf87/librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e", size = 524393, upload-time = "2026-07-08T12:25:09.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/5f/d72f95fd444a926a3c14b4e24979474116988dd57a45be242077c45d3c22/librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd", size = 543026, upload-time = "2026-07-08T12:25:10.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/08/dcd9993ad192737a004ba263d549f8ea605b326b952e7d6205c7d4170b76/librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348", size = 546829, upload-time = "2026-07-08T12:25:11.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/d5/6d9bb2f54e4109a956b7128836529653eb9d740f784bc47ed10a02c1000e/librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7", size = 535700, upload-time = "2026-07-08T12:25:13.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/f2/10946922503858a359492fa27f13e86228bde702116a740ac7b3cd185f24/librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82", size = 573566, upload-time = "2026-07-08T12:25:14.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/a8/94f00e3c99479a18088af3685ea016c42f3c7d5d1964d8dbb40c08d7f1aa/librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3", size = 106099, upload-time = "2026-07-08T12:25:16.159Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/7b/2da9c74c1ed25a89cc4e1c8e007ea2eb4a0f1fafa3e70d757fe3242c5c5c/librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa", size = 126934, upload-time = "2026-07-08T12:25:17.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/65/aead61bbf3b5358593f9d4779d2a0e88eaf6ec191a6342dde36dd1df6371/librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1", size = 112236, upload-time = "2026-07-08T12:25:18.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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://files.pythonhosted.org/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 = "mando"
|
||||
version = "0.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/35/24/cd70d5ae6d35962be752feccb7dca80b5e0c2d450e995b16abd6275f3296/mando-0.7.1.tar.gz", hash = "sha256:18baa999b4b613faefb00eac4efadcf14f510b59b924b66e08289aa1de8c3500", size = 37868, upload-time = "2022-02-24T08:12:27.316Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/f0/834e479e47e499b6478e807fb57b31cc2db696c4db30557bb6f5aea4a90b/mando-0.7.1-py2.py3-none-any.whl", hash = "sha256:26ef1d70928b6057ee3ca12583d73c63e05c49de8972d620c278a7b206581a8a", size = 28149, upload-time = "2022-02-24T08:12:25.24Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ast-serialize" },
|
||||
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "mypy-extensions" },
|
||||
{ name = "pathspec" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/12/af/4e516a05d3ca2eb9283e9ec45b2c02225c1514dd6da49fd3c9eaa6639370/mypy-2.3.0.tar.gz", hash = "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e", size = 3988104, upload-time = "2026-07-13T11:34:53.387Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/94/0e7e592619e2133596a47cdd642534b0456545c218430bd3b9d8fefdd1b1/mypy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5", size = 15026523, upload-time = "2026-07-13T11:34:49.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/d2/1e1731df090a857df2807177a4626863e5ac0f0256513c35780efe53986f/mypy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c", size = 14032189, upload-time = "2026-07-13T11:33:57.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/95/cab921f4a806e171f34113e6181dd23c55358ccf6a80741269ef594a410e/mypy-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491", size = 14198696, upload-time = "2026-07-13T11:32:12.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/80/e6d008bb19fe446e3662d85e0e2717bf9f2d611a2164fb29d6e067dbf46c/mypy-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7", size = 15286904, upload-time = "2026-07-13T11:34:27.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/83/94397c9293608a364aa03e8084fb34ede4ae976a260384b9b52929308135/mypy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3", size = 15528342, upload-time = "2026-07-13T11:34:07.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/96/d8b37d819adec6cfccfb1fd3afc1735d94717ddeafb45536db9c6943e09b/mypy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c", size = 11218346, upload-time = "2026-07-13T11:28:27.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/cd/cd9f725b19b19e5b530a154cf9bcf9e94279c5d55b3c34fb42b3aa48ea1b/mypy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595", size = 10204525, upload-time = "2026-07-13T11:31:02.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/ae/f7d056eb0294586a572d0d0d89580ec633c064db520f11d37d5a2fb833bd/mypy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97", size = 14947298, upload-time = "2026-07-13T11:27:47.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/d5/db3e7af01e7844d21662c6ddc1f7825ec7cb4053f0391ac02faf3638396f/mypy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac", size = 13950768, upload-time = "2026-07-13T11:27:57.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/fb/43c031f0190513d1ec248ed037eceb742ddd2a4d74bbf406658a28173837/mypy-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6", size = 14151586, upload-time = "2026-07-13T11:29:18.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/c3/f8b2ffc60883084da91be51af58e88a7ffd4ff9795acb7d902ff88d31eb1/mypy-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b", size = 15227411, upload-time = "2026-07-13T11:30:29.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/2e/16b917fc7adcf03f1aadddfc93aab804ffb234b1ab09c0ffd6d92a5d34a2/mypy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2", size = 15478790, upload-time = "2026-07-13T11:33:14.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/88/aaa65a93c73d0cdae7e42f8adb302bf6885bb281302084f99d0290a35347/mypy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757", size = 11234919, upload-time = "2026-07-13T11:33:39.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/19/b40de63f1a80e63bc2d40f0679a6a8dbd34e95176c8122119bdf406aa552/mypy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1", size = 10201510, upload-time = "2026-07-13T11:31:52.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/58/fa0ae047da911f540284009b4f44b96fe09d83c076d7c103e9d645f46303/mypy-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db", size = 14941909, upload-time = "2026-07-13T11:32:34.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/14/2ba1d61452d7c2a7fe12741e8d374e52b183476b07aa7f9e2a0d02b0720a/mypy-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762", size = 13967581, upload-time = "2026-07-13T11:30:00.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/5a/483fb9e5ffbbb1a28dccc7b0a13d141b17ac769b6c9f488c0a0c63698962/mypy-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef", size = 14168807, upload-time = "2026-07-13T11:28:48.6Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/77/70d7a10732063beb74ad713682cf871e88f5c5fa39bfc8beff8a524bf9cb/mypy-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b", size = 15200144, upload-time = "2026-07-13T11:31:25.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/72/766218ac783be4fdfcd699b90037b63017348a3e86fb2c1fbfb18302637d/mypy-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff", size = 15460389, upload-time = "2026-07-13T11:29:29.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/4e/8a9db7411ecb8ec0cb1fd05dba432f28bafffcd38b4e887714a4a0506689/mypy-2.3.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4", size = 7753664, upload-time = "2026-07-13T11:29:08.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/4c/c3f8bfd6ed0e5e38b5a244403b27f821d433443df5a15a278417c10a3a3c/mypy-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f", size = 11417237, upload-time = "2026-07-13T11:33:47.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/00/89a32eaf5ccf174bc4f90db0eaea5d70636c01b8d49f384bdab2e8834390/mypy-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7", size = 10389252, upload-time = "2026-07-13T11:31:43.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/56/104f93d69aa9f339b6b9d3b0a7faa699b8b466c942cf3ae86cc2a2ec0915/mypy-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373", size = 16385495, upload-time = "2026-07-13T11:29:49.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/03/f1d2123313f55efafdd27706960f43a771c62f1b68426c76043f3ab9ebf3/mypy-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556", size = 15098155, upload-time = "2026-07-13T11:30:40.301Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/5d/d5f9200399b445e81726c4f23becee33f233aee81c72680b1ef3a258b641/mypy-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88", size = 15514155, upload-time = "2026-07-13T11:34:38.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/ce/69977c555f08faa3190cfde44189b89dbd56861b1ab97aa18fc5f3a2e4a3/mypy-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe", size = 16766351, upload-time = "2026-07-13T11:33:29.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/92/6648b6caa3ab9e00f9ac0c2a78307805f873dd48139b24a6f6f7c3667bbf/mypy-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60", size = 17043490, upload-time = "2026-07-13T11:30:53.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/ab/0dc91d80f3f016634c68d451f294a97320fe903a9b6f90b9e57b3f7f1717/mypy-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654", size = 12146869, upload-time = "2026-07-13T11:29:38.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/b5/4c964d02634ba81f4d1c84838e5c5b18ab06d13ed568960f5d6318495ccc/mypy-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5", size = 10965113, upload-time = "2026-07-13T11:28:07.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/fa/fdc54fe583ba3cafbcedfb70eeeaf03849f75b1827a07096c7bd996f582d/mypy-2.3.0-py3-none-any.whl", hash = "sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88", size = 2753292, upload-time = "2026-07-13T11:33:18.48Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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 = "nodeenv"
|
||||
version = "1.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/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.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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 = "platformdirs"
|
||||
version = "4.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "4.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cfgv" },
|
||||
{ name = "identify" },
|
||||
{ name = "nodeenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "virtualenv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/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 = "pytest"
|
||||
version = "9.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "7.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "coverage" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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 = "pytest-timeout"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-discovery"
|
||||
version = "1.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "filelock" },
|
||||
{ name = "platformdirs" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/81/58c70036dffeccb7fe7d79d6260c69f7a28272bbd3909c29a01ea9422744/python_discovery-1.4.4.tar.gz", hash = "sha256:5cad33982d412c1f3ffb8f9ca4ea292c9680bca3942451d30b69c37fce53a4a3", size = 72212, upload-time = "2026-07-08T23:06:50.691Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl", hash = "sha256:abebe9120b43453b68c908acfb1e72a19d1a959ed2cb620ad38fc57d08056dbe", size = 34181, upload-time = "2026-07-08T23:06:49.402Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "radon"
|
||||
version = "6.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama" },
|
||||
{ name = "mando" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/6d/98e61600febf6bd929cf04154537c39dc577ce414bafbfc24a286c4fa76d/radon-6.0.1.tar.gz", hash = "sha256:d1ac0053943a893878940fedc8b19ace70386fc9c9bf0a09229a44125ebf45b5", size = 1874992, upload-time = "2023-03-26T06:24:38.868Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl", hash = "sha256:632cc032364a6f8bb1010a2f6a12d0f14bc7e5ede76585ef29dc0cecf4cd8859", size = 52784, upload-time = "2023-03-26T06:24:33.949Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.34.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "second-brain"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "numpy" },
|
||||
{ name = "tenacity" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "mypy" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "pytest-timeout" },
|
||||
{ name = "ruff" },
|
||||
{ name = "xenon" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "httpx" },
|
||||
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10" },
|
||||
{ name = "numpy" },
|
||||
{ name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.7" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
|
||||
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" },
|
||||
{ name = "pytest-timeout", marker = "extra == 'dev'", specifier = ">=2.2" },
|
||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.5" },
|
||||
{ name = "tenacity" },
|
||||
{ name = "xenon", marker = "extra == 'dev'", specifier = ">=0.9" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/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 = "tenacity"
|
||||
version = "9.1.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "21.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "distlib" },
|
||||
{ name = "filelock" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "python-discovery" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/34/d9/b477fddb68840b570af8b22afe9b035cbc277b5fb7b33dea390617a8b10f/virtualenv-21.6.1.tar.gz", hash = "sha256:15f978b7cd329f24855ff4a0c4b4899cc7678589f49adbdcbbb4d3232e641128", size = 5526620, upload-time = "2026-07-10T19:33:53.312Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl", hash = "sha256:afe991df855715a2b2f60edfcc0107ef95a79fdfd8cb4cdaa71603d1c12e463b", size = 5506392, upload-time = "2026-07-10T19:33:51.629Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xenon"
|
||||
version = "0.9.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyyaml" },
|
||||
{ name = "radon" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c4/7c/2b341eaeec69d514b635ea18481885a956d196a74322a4b0942ef0c31691/xenon-0.9.3.tar.gz", hash = "sha256:4a7538d8ba08aa5d79055fb3e0b2393c0bd6d7d16a4ab0fcdef02ef1f10a43fa", size = 9883, upload-time = "2024-10-21T10:27:53.722Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/5d/29ff8665b129cafd147d90b86e92babee32e116e3c84447107da3e77f8fb/xenon-0.9.3-py2.py3-none-any.whl", hash = "sha256:6e2c2c251cc5e9d01fe984e623499b13b2140fcbf74d6c03a613fa43a9347097", size = 8966, upload-time = "2024-10-21T10:27:51.121Z" },
|
||||
]
|
||||
Loading…
Add table
Reference in a new issue