test: add 106 unit + smoke tests, 93% coverage

- pytest + respx + pytest-cov added as dev dependencies
- tests/smoke/test_registration.py: verify all 66 tools registered and unique
- tests/unit/: 12 files covering every tool — HTTP method, URL, params/body, errors
- tests/config.test.json + conftest.py: zero-network test setup via GITEA_CONFIG
- CLAUDE.md: developer guide with testing philosophy, pyramid, and commands
- server/gitea_mcp.py: get_commit, list_pr_files tools added
- README.md: tool count corrected to 66
This commit is contained in:
2026-06-08 09:09:44 +03:00
parent 50cd8e85e6
commit a6162a855c
23 changed files with 1722 additions and 4 deletions
+179
View File
@@ -0,0 +1,179 @@
# CLAUDE.md — Developer Guide
## Project overview
MCP server exposing Gitea REST API as 66 tools. Single source file: `server/gitea_mcp.py`.
Config in `config.json` (gitignored). Run via `uv run server/gitea_mcp.py`.
## Commit style
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, `test:`
- No `Co-Authored-By` or any AI attribution lines in commit messages
## Adding tools
Each tool is a `@mcp.tool()` function. Pattern:
```python
@mcp.tool()
def my_tool(owner: str, repo: str, instance: str = "") -> Any:
"""One-line docstring — becomes the tool description shown to the LLM."""
return GET(instance, f"/repos/{owner}/{repo}/something")
```
Rules:
- `instance: str = ""` is always the last parameter
- Use `_strip({...})` to drop `None` values before sending
- URL-encode path segments that may contain slashes via `_b(value)`
- Update the tool count in `README.md` after adding tools
---
## Testing
### Philosophy
The server has no business logic — every tool is a thin mapping from MCP arguments to a
Gitea REST call. Tests therefore focus on **contract correctness**:
- correct HTTP method and URL
- correct request body / query parameters
- correct handling of API error responses
- all tools are registered and publicly discoverable
We do **not** test Gitea's own behavior — that is covered by Gitea's own test suite.
### Test pyramid
```
┌──────────────────────────────┐
│ Integration (opt-in) │ real Gitea via Docker Compose
├──────────────────────────────┤
│ Unit — per-tool contracts │ pytest + respx (httpx mock)
├──────────────────────────────┤
│ Smoke — registration check │ import + introspect registered tools
└──────────────────────────────┘
```
### Tooling
| Tool | Purpose |
|------|---------|
| `pytest` | test runner |
| `respx` | mock `httpx.Client` at the transport level — no monkey-patching |
| `pytest-cov` | coverage reporting |
Install dev dependencies:
```bash
uv add --dev pytest respx pytest-cov
```
### Directory layout
```
tests/
├── conftest.py # shared fixtures (mock transport, config override)
├── smoke/
│ └── test_registration.py # all tools are registered, count matches README
└── unit/
├── test_meta.py
├── test_repos.py
├── test_branches.py
├── test_files.py
├── test_issues.py
├── test_labels_milestones.py
├── test_pull_requests.py
├── test_releases.py
├── test_webhooks.py
├── test_users_orgs.py
├── test_commits.py
└── test_notifications.py
```
### What each unit test must cover
For every tool function, write at minimum:
1. **Happy path** — correct HTTP method, URL pattern, and response forwarded as-is
2. **Parameter mapping** — optional params omitted when empty, present when set
3. **Error propagation** — non-2xx status raises `RuntimeError` with the status code
```python
# Example pattern
import respx, httpx, pytest
from server.gitea_mcp import get_commit
@respx.mock
def test_get_commit_happy_path():
route = respx.get(
"https://gitea.example.com/api/v1/repos/owner/repo/git/commits/abc123"
).mock(return_value=httpx.Response(200, json={"sha": "abc123"}))
result = get_commit("owner", "repo", "abc123")
assert route.called
assert result["sha"] == "abc123"
@respx.mock
def test_get_commit_api_error():
respx.get(...).mock(return_value=httpx.Response(404, json={"message": "not found"}))
with pytest.raises(RuntimeError, match="404"):
get_commit("owner", "repo", "deadbeef")
```
### Smoke test: tool registration
```python
# tests/smoke/test_registration.py
from server.gitea_mcp import mcp
EXPECTED_TOOL_COUNT = 66 # update when adding tools
def test_all_tools_registered():
tools = mcp.list_tools() # or equivalent FastMCP introspection
assert len(tools) == EXPECTED_TOOL_COUNT
```
Update `EXPECTED_TOOL_COUNT` whenever you add or remove tools. If the count drifts from
the README table, the smoke test catches it.
### Running tests
```bash
# All tests
uv run pytest
# With coverage
uv run pytest --cov=server --cov-report=term-missing
# Smoke only
uv run pytest tests/smoke/
# One module
uv run pytest tests/unit/test_commits.py -v
```
### Coverage target
- **Unit tests**: 90 % line coverage on `server/gitea_mcp.py`
- **Smoke tests**: 100 % tool registration (no gaps)
- Integration tests are opt-in (require `GITEA_TEST_URL` + `GITEA_TEST_TOKEN` env vars)
### Integration tests (opt-in)
Integration tests live in `tests/integration/` and are skipped unless the environment
variables `GITEA_TEST_URL` and `GITEA_TEST_TOKEN` are set. Use a dedicated test
organisation/user on a throwaway Gitea instance — tests create and delete real resources.
```python
import pytest, os
pytestmark = pytest.mark.skipif(
not os.environ.get("GITEA_TEST_URL"),
reason="integration tests require GITEA_TEST_URL"
)
```
For local development, a Docker Compose file (`docker-compose.test.yml`) can spin up a
fresh Gitea instance automatically.
+4 -4
View File
@@ -1,6 +1,6 @@
# gitea-platform-mcp # gitea-platform-mcp
Full Gitea platform management — 50 tools for repositories, issues, PRs, branches, files, releases, webhooks, and organizations. Full Gitea platform management — 66 tools for repositories, issues, PRs, branches, files, releases, webhooks, and organizations.
## Setup (one-time) ## Setup (one-time)
@@ -55,7 +55,7 @@ After installing, ask: "проверь подключение к Gitea" — Clau
- `uv` installed (https://docs.astral.sh/uv/) - `uv` installed (https://docs.astral.sh/uv/)
- Gitea instance accessible from your computer - Gitea instance accessible from your computer
## Tools (50) ## Tools (66)
| Category | Tools | | Category | Tools |
|----------|-------| |----------|-------|
@@ -65,9 +65,9 @@ After installing, ask: "проверь подключение к Gitea" — Clau
| Files | list_directory, get_file, create_file, update_file, delete_file | | Files | list_directory, get_file, create_file, update_file, delete_file |
| Issues | list_issues, get_issue, create_issue, update_issue, close_issue, reopen_issue, list_issue_comments, add_issue_comment, edit_issue_comment, delete_issue_comment | | Issues | list_issues, get_issue, create_issue, update_issue, close_issue, reopen_issue, list_issue_comments, add_issue_comment, edit_issue_comment, delete_issue_comment |
| Labels & Milestones | list_labels, create_label, add_issue_labels, list_milestones, create_milestone | | Labels & Milestones | list_labels, create_label, add_issue_labels, list_milestones, create_milestone |
| Pull Requests | list_pull_requests, get_pull_request, create_pull_request, update_pull_request, merge_pull_request, get_pr_diff, list_pr_reviews, create_pr_review | | Pull Requests | list_pull_requests, get_pull_request, create_pull_request, update_pull_request, merge_pull_request, get_pr_diff, list_pr_files, list_pr_reviews, create_pr_review |
| Releases | list_releases, get_latest_release, create_release, delete_release | | Releases | list_releases, get_latest_release, create_release, delete_release |
| Webhooks | list_repo_webhooks, create_repo_webhook, delete_repo_webhook | | Webhooks | list_repo_webhooks, create_repo_webhook, delete_repo_webhook |
| Users & Orgs | get_user, search_users, list_my_orgs, get_org, list_org_repos, list_org_members, list_org_teams | | Users & Orgs | get_user, search_users, list_my_orgs, get_org, list_org_repos, list_org_members, list_org_teams |
| Commits | list_commits, compare_branches, list_repo_contributors | | Commits | list_commits, get_commit, compare_branches, list_repo_contributors |
| Notifications | list_notifications, mark_all_notifications_read | | Notifications | list_notifications, mark_all_notifications_read |
+10
View File
@@ -8,5 +8,15 @@ dependencies = [
"httpx>=0.27", "httpx>=0.27",
] ]
[dependency-groups]
dev = [
"pytest>=8.0",
"respx>=0.21",
"pytest-cov>=5.0",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.uv] [tool.uv]
package = false package = false
+24
View File
@@ -484,6 +484,19 @@ def get_pr_diff(owner: str, repo: str, index: int, instance: str = "") -> Any:
raise RuntimeError(f"Gitea API {r.status_code}: {r.text}") raise RuntimeError(f"Gitea API {r.status_code}: {r.text}")
return {"diff": r.text} return {"diff": r.text}
@mcp.tool()
def list_pr_files(
owner: str, repo: str, index: int,
limit: int = 50, page: int = 1,
whitespace: str = "",
instance: str = "",
) -> Any:
"""List files changed in a pull request. whitespace: ignore-all|ignore-change|ignore-eol|show-all."""
return GET(instance, f"/repos/{owner}/{repo}/pulls/{index}/files", _strip({
"limit": limit, "page": page,
"whitespace": whitespace or None,
}))
@mcp.tool() @mcp.tool()
def list_pr_reviews(owner: str, repo: str, index: int, instance: str = "") -> Any: def list_pr_reviews(owner: str, repo: str, index: int, instance: str = "") -> Any:
"""List reviews on a pull request.""" """List reviews on a pull request."""
@@ -617,6 +630,17 @@ def list_commits(
"limit": limit, "page": page, "limit": limit, "page": page,
})) }))
@mcp.tool()
def get_commit(
owner: str, repo: str, sha: str,
stat: bool = True, files: bool = True, verification: bool = False,
instance: str = "",
) -> Any:
"""Get a single commit by SHA or ref. stat=True includes diff stats, files=True includes changed files list."""
return GET(instance, f"/repos/{owner}/{repo}/git/commits/{_b(sha)}", _strip({
"stat": stat, "files": files, "verification": verification or None,
}))
@mcp.tool() @mcp.tool()
def compare_branches(owner: str, repo: str, base: str, head: str, instance: str = "") -> Any: def compare_branches(owner: str, repo: str, base: str, head: str, instance: str = "") -> Any:
"""Compare two branches, tags, or commits.""" """Compare two branches, tags, or commits."""
View File
+9
View File
@@ -0,0 +1,9 @@
{
"instances": {
"default": {
"url": "https://gitea.test",
"token": "test-token-abc123"
}
},
"default": "default"
}
+6
View File
@@ -0,0 +1,6 @@
import os
from pathlib import Path
# Must be set at module level, before any import of server.gitea_mcp,
# so find_config() picks up the test credentials instead of the real config.json.
os.environ.setdefault("GITEA_CONFIG", str(Path(__file__).parent / "config.test.json"))
View File
+27
View File
@@ -0,0 +1,27 @@
import asyncio
from server.gitea_mcp import mcp
EXPECTED_TOOL_COUNT = 66
def test_all_tools_registered():
tools = asyncio.run(mcp.list_tools())
names = sorted(t.name for t in tools)
assert len(tools) == EXPECTED_TOOL_COUNT, (
f"Expected {EXPECTED_TOOL_COUNT} tools, found {len(tools)}.\n"
f"Tools: {names}"
)
def test_tool_names_unique():
tools = asyncio.run(mcp.list_tools())
names = [t.name for t in tools]
assert len(names) == len(set(names)), (
f"Duplicate tool names: {[n for n in names if names.count(n) > 1]}"
)
def test_all_tools_have_description():
tools = asyncio.run(mcp.list_tools())
missing = [t.name for t in tools if not t.description]
assert not missing, f"Tools without description: {missing}"
View File
+89
View File
@@ -0,0 +1,89 @@
import json
import pytest
import httpx
import respx
from server.gitea_mcp import list_branches, get_branch, create_branch, delete_branch, list_tags
BASE = "https://gitea.test/api/v1"
@respx.mock
def test_list_branches():
route = respx.get(f"{BASE}/repos/owner/repo/branches").mock(
return_value=httpx.Response(200, json=[{"name": "main"}])
)
result = list_branches("owner", "repo")
assert route.called
assert result[0]["name"] == "main"
assert dict(route.calls[0].request.url.params)["limit"] == "50"
@respx.mock
def test_get_branch():
route = respx.get(f"{BASE}/repos/owner/repo/branches/main").mock(
return_value=httpx.Response(200, json={"name": "main"})
)
result = get_branch("owner", "repo", "main")
assert route.called
assert result["name"] == "main"
@respx.mock
def test_get_branch_with_slash_in_name():
route = respx.get(f"{BASE}/repos/owner/repo/branches/feature%2Ffoo").mock(
return_value=httpx.Response(200, json={"name": "feature/foo"})
)
result = get_branch("owner", "repo", "feature/foo")
assert route.called
@respx.mock
def test_get_branch_not_found():
respx.get(f"{BASE}/repos/owner/repo/branches/missing").mock(
return_value=httpx.Response(404, json={"message": "not found"})
)
with pytest.raises(RuntimeError, match="404"):
get_branch("owner", "repo", "missing")
@respx.mock
def test_create_branch():
route = respx.post(f"{BASE}/repos/owner/repo/branches").mock(
return_value=httpx.Response(201, json={"name": "feature/new"})
)
result = create_branch("owner", "repo", "feature/new")
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["new_branch_name"] == "feature/new"
assert "old_branch_name" not in body
@respx.mock
def test_create_branch_from_specific():
route = respx.post(f"{BASE}/repos/owner/repo/branches").mock(
return_value=httpx.Response(201, json={"name": "hotfix"})
)
create_branch("owner", "repo", "hotfix", old_branch_name="main")
body = json.loads(route.calls[0].request.content)
assert body["old_branch_name"] == "main"
@respx.mock
def test_delete_branch():
route = respx.delete(f"{BASE}/repos/owner/repo/branches/old-branch").mock(
return_value=httpx.Response(204)
)
result = delete_branch("owner", "repo", "old-branch")
assert route.called
assert result == {"success": True}
@respx.mock
def test_list_tags():
route = respx.get(f"{BASE}/repos/owner/repo/tags").mock(
return_value=httpx.Response(200, json=[{"name": "v1.0.0"}])
)
result = list_tags("owner", "repo")
assert route.called
assert result[0]["name"] == "v1.0.0"
assert dict(route.calls[0].request.url.params)["limit"] == "20"
+94
View File
@@ -0,0 +1,94 @@
import pytest
import httpx
import respx
from server.gitea_mcp import list_commits, get_commit, compare_branches, list_repo_contributors
BASE = "https://gitea.test/api/v1"
@respx.mock
def test_list_commits_defaults():
route = respx.get(f"{BASE}/repos/owner/repo/commits").mock(
return_value=httpx.Response(200, json=[{"sha": "abc123"}])
)
result = list_commits("owner", "repo")
assert route.called
params = dict(route.calls[0].request.url.params)
assert params["limit"] == "20"
assert params["page"] == "1"
assert "sha" not in params
assert "path" not in params
@respx.mock
def test_list_commits_on_branch_and_path():
route = respx.get(f"{BASE}/repos/owner/repo/commits").mock(
return_value=httpx.Response(200, json=[])
)
list_commits("owner", "repo", sha="develop", path="server/gitea_mcp.py")
params = dict(route.calls[0].request.url.params)
assert params["sha"] == "develop"
assert params["path"] == "server/gitea_mcp.py"
@respx.mock
def test_get_commit():
route = respx.get(f"{BASE}/repos/owner/repo/git/commits/abc123").mock(
return_value=httpx.Response(200, json={"sha": "abc123", "commit": {"message": "fix bug"}})
)
result = get_commit("owner", "repo", "abc123")
assert route.called
assert result["sha"] == "abc123"
params = dict(route.calls[0].request.url.params)
assert params["stat"] == "true"
assert params["files"] == "true"
@respx.mock
def test_get_commit_without_stat():
route = respx.get(f"{BASE}/repos/owner/repo/git/commits/deadbeef").mock(
return_value=httpx.Response(200, json={"sha": "deadbeef"})
)
get_commit("owner", "repo", "deadbeef", stat=False, files=False)
params = dict(route.calls[0].request.url.params)
assert params["stat"] == "false"
assert params["files"] == "false"
@respx.mock
def test_get_commit_not_found():
respx.get(f"{BASE}/repos/owner/repo/git/commits/notacommit").mock(
return_value=httpx.Response(404, json={"message": "not found"})
)
with pytest.raises(RuntimeError, match="404"):
get_commit("owner", "repo", "notacommit")
@respx.mock
def test_compare_branches():
route = respx.get(f"{BASE}/repos/owner/repo/compare/main...develop").mock(
return_value=httpx.Response(200, json={"total_commits": 3, "commits": []})
)
result = compare_branches("owner", "repo", "main", "develop")
assert route.called
assert result["total_commits"] == 3
@respx.mock
def test_compare_branches_with_slashes():
route = respx.get(f"{BASE}/repos/owner/repo/compare/release%2F1.0...feature%2Fnew").mock(
return_value=httpx.Response(200, json={"total_commits": 1})
)
result = compare_branches("owner", "repo", "release/1.0", "feature/new")
assert route.called
@respx.mock
def test_list_repo_contributors():
route = respx.get(f"{BASE}/repos/owner/repo/contributors").mock(
return_value=httpx.Response(200, json=[{"login": "alice", "contributions": 42}])
)
result = list_repo_contributors("owner", "repo")
assert route.called
assert result[0]["login"] == "alice"
assert dict(route.calls[0].request.url.params)["limit"] == "20"
+108
View File
@@ -0,0 +1,108 @@
import base64
import json
import pytest
import httpx
import respx
from server.gitea_mcp import list_directory, get_file, create_file, update_file, delete_file
BASE = "https://gitea.test/api/v1"
@respx.mock
def test_list_directory_root():
route = respx.get(f"{BASE}/repos/owner/repo/contents/").mock(
return_value=httpx.Response(200, json=[{"name": "README.md", "type": "file"}])
)
result = list_directory("owner", "repo")
assert route.called
assert result[0]["name"] == "README.md"
@respx.mock
def test_list_directory_with_ref():
route = respx.get(f"{BASE}/repos/owner/repo/contents/src").mock(
return_value=httpx.Response(200, json=[])
)
list_directory("owner", "repo", path="src", ref="develop")
params = dict(route.calls[0].request.url.params)
assert params["ref"] == "develop"
@respx.mock
def test_get_file():
encoded = base64.b64encode(b"hello world").decode()
route = respx.get(f"{BASE}/repos/owner/repo/contents/README.md").mock(
return_value=httpx.Response(200, json={"name": "README.md", "content": encoded, "sha": "abc"})
)
result = get_file("owner", "repo", "README.md")
assert route.called
assert result["content"] == encoded
assert result["sha"] == "abc"
@respx.mock
def test_get_file_with_ref():
route = respx.get(f"{BASE}/repos/owner/repo/contents/README.md").mock(
return_value=httpx.Response(200, json={"name": "README.md"})
)
get_file("owner", "repo", "README.md", ref="v1.0")
params = dict(route.calls[0].request.url.params)
assert params["ref"] == "v1.0"
@respx.mock
def test_get_file_not_found():
respx.get(f"{BASE}/repos/owner/repo/contents/missing.txt").mock(
return_value=httpx.Response(404, json={"message": "not found"})
)
with pytest.raises(RuntimeError, match="404"):
get_file("owner", "repo", "missing.txt")
@respx.mock
def test_create_file():
route = respx.post(f"{BASE}/repos/owner/repo/contents/hello.txt").mock(
return_value=httpx.Response(201, json={"content": {"name": "hello.txt"}})
)
result = create_file("owner", "repo", "hello.txt", "hello world", "add hello")
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["content"] == base64.b64encode(b"hello world").decode()
assert body["message"] == "add hello"
assert "branch" not in body
@respx.mock
def test_create_file_on_branch():
route = respx.post(f"{BASE}/repos/owner/repo/contents/file.py").mock(
return_value=httpx.Response(201, json={"content": {"name": "file.py"}})
)
create_file("owner", "repo", "file.py", "print('hi')", "add file", branch="develop")
body = json.loads(route.calls[0].request.content)
assert body["branch"] == "develop"
@respx.mock
def test_update_file():
new_content = "updated content"
route = respx.put(f"{BASE}/repos/owner/repo/contents/README.md").mock(
return_value=httpx.Response(200, json={"content": {"name": "README.md"}})
)
update_file("owner", "repo", "README.md", new_content, "update readme", sha="deadbeef")
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["content"] == base64.b64encode(new_content.encode()).decode()
assert body["sha"] == "deadbeef"
assert body["message"] == "update readme"
@respx.mock
def test_delete_file():
route = respx.delete(f"{BASE}/repos/owner/repo/contents/old.txt").mock(
return_value=httpx.Response(200, json={"commit": {"sha": "abc"}})
)
result = delete_file("owner", "repo", "old.txt", "remove old file", sha="abc123")
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["message"] == "remove old file"
assert body["sha"] == "abc123"
+149
View File
@@ -0,0 +1,149 @@
import json
import pytest
import httpx
import respx
from server.gitea_mcp import (
list_issues, get_issue, create_issue, update_issue, close_issue, reopen_issue,
list_issue_comments, add_issue_comment, edit_issue_comment, delete_issue_comment,
)
BASE = "https://gitea.test/api/v1"
@respx.mock
def test_list_issues_defaults():
route = respx.get(f"{BASE}/repos/owner/repo/issues").mock(
return_value=httpx.Response(200, json=[])
)
list_issues("owner", "repo")
params = dict(route.calls[0].request.url.params)
assert params["state"] == "open"
assert params["type"] == "issues"
assert params["limit"] == "20"
@respx.mock
def test_list_issues_with_filters():
route = respx.get(f"{BASE}/repos/owner/repo/issues").mock(
return_value=httpx.Response(200, json=[])
)
list_issues("owner", "repo", state="closed", labels="bug", assignee="alice")
params = dict(route.calls[0].request.url.params)
assert params["state"] == "closed"
assert params["labels"] == "bug"
assert params["assignee"] == "alice"
@respx.mock
def test_get_issue():
route = respx.get(f"{BASE}/repos/owner/repo/issues/42").mock(
return_value=httpx.Response(200, json={"number": 42, "title": "Bug"})
)
result = get_issue("owner", "repo", 42)
assert route.called
assert result["number"] == 42
@respx.mock
def test_get_issue_not_found():
respx.get(f"{BASE}/repos/owner/repo/issues/999").mock(
return_value=httpx.Response(404, json={"message": "not found"})
)
with pytest.raises(RuntimeError, match="404"):
get_issue("owner", "repo", 999)
@respx.mock
def test_create_issue():
route = respx.post(f"{BASE}/repos/owner/repo/issues").mock(
return_value=httpx.Response(201, json={"number": 1, "title": "New bug"})
)
result = create_issue("owner", "repo", "New bug", body="description")
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["title"] == "New bug"
assert body["body"] == "description"
@respx.mock
def test_create_issue_with_assignees_and_labels():
route = respx.post(f"{BASE}/repos/owner/repo/issues").mock(
return_value=httpx.Response(201, json={"number": 2})
)
create_issue("owner", "repo", "Task", assignees=["alice"], labels=[1, 2])
body = json.loads(route.calls[0].request.content)
assert body["assignees"] == ["alice"]
assert body["labels"] == [1, 2]
@respx.mock
def test_update_issue():
route = respx.patch(f"{BASE}/repos/owner/repo/issues/5").mock(
return_value=httpx.Response(200, json={"number": 5})
)
update_issue("owner", "repo", 5, title="Updated title")
body = json.loads(route.calls[0].request.content)
assert body["title"] == "Updated title"
assert "body" not in body
@respx.mock
def test_close_issue():
route = respx.patch(f"{BASE}/repos/owner/repo/issues/3").mock(
return_value=httpx.Response(200, json={"state": "closed"})
)
result = close_issue("owner", "repo", 3)
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["state"] == "closed"
@respx.mock
def test_reopen_issue():
route = respx.patch(f"{BASE}/repos/owner/repo/issues/3").mock(
return_value=httpx.Response(200, json={"state": "open"})
)
reopen_issue("owner", "repo", 3)
body = json.loads(route.calls[0].request.content)
assert body["state"] == "open"
@respx.mock
def test_list_issue_comments():
route = respx.get(f"{BASE}/repos/owner/repo/issues/7/comments").mock(
return_value=httpx.Response(200, json=[{"id": 1, "body": "hi"}])
)
result = list_issue_comments("owner", "repo", 7)
assert route.called
assert result[0]["id"] == 1
@respx.mock
def test_add_issue_comment():
route = respx.post(f"{BASE}/repos/owner/repo/issues/7/comments").mock(
return_value=httpx.Response(201, json={"id": 10, "body": "LGTM"})
)
result = add_issue_comment("owner", "repo", 7, "LGTM")
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["body"] == "LGTM"
@respx.mock
def test_edit_issue_comment():
route = respx.patch(f"{BASE}/repos/owner/repo/issues/comments/42").mock(
return_value=httpx.Response(200, json={"id": 42})
)
edit_issue_comment("owner", "repo", 42, "edited body")
body = json.loads(route.calls[0].request.content)
assert body["body"] == "edited body"
@respx.mock
def test_delete_issue_comment():
route = respx.delete(f"{BASE}/repos/owner/repo/issues/comments/42").mock(
return_value=httpx.Response(204)
)
result = delete_issue_comment("owner", "repo", 42)
assert route.called
assert result == {"success": True}
+105
View File
@@ -0,0 +1,105 @@
import json
import pytest
import httpx
import respx
from server.gitea_mcp import (
list_labels, create_label, add_issue_labels, list_milestones, create_milestone,
)
BASE = "https://gitea.test/api/v1"
@respx.mock
def test_list_labels():
route = respx.get(f"{BASE}/repos/owner/repo/labels").mock(
return_value=httpx.Response(200, json=[{"id": 1, "name": "bug", "color": "#ee0701"}])
)
result = list_labels("owner", "repo")
assert route.called
assert result[0]["name"] == "bug"
@respx.mock
def test_create_label():
route = respx.post(f"{BASE}/repos/owner/repo/labels").mock(
return_value=httpx.Response(201, json={"id": 5, "name": "enhancement"})
)
result = create_label("owner", "repo", "enhancement", "#84b6eb")
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["name"] == "enhancement"
assert body["color"] == "#84b6eb"
@respx.mock
def test_create_label_with_description():
route = respx.post(f"{BASE}/repos/owner/repo/labels").mock(
return_value=httpx.Response(201, json={"id": 6})
)
create_label("owner", "repo", "wontfix", "#ffffff", description="Not going to fix")
body = json.loads(route.calls[0].request.content)
assert body["description"] == "Not going to fix"
@respx.mock
def test_create_label_error():
respx.post(f"{BASE}/repos/owner/repo/labels").mock(
return_value=httpx.Response(422, json={"message": "validation failed"})
)
with pytest.raises(RuntimeError, match="422"):
create_label("owner", "repo", "bad", "notacolor")
@respx.mock
def test_add_issue_labels():
route = respx.post(f"{BASE}/repos/owner/repo/issues/3/labels").mock(
return_value=httpx.Response(200, json=[{"id": 1}])
)
result = add_issue_labels("owner", "repo", 3, [1, 2])
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["labels"] == [1, 2]
@respx.mock
def test_list_milestones():
route = respx.get(f"{BASE}/repos/owner/repo/milestones").mock(
return_value=httpx.Response(200, json=[{"id": 1, "title": "v1.0"}])
)
result = list_milestones("owner", "repo")
assert route.called
assert result[0]["title"] == "v1.0"
assert dict(route.calls[0].request.url.params)["state"] == "open"
@respx.mock
def test_list_milestones_closed():
route = respx.get(f"{BASE}/repos/owner/repo/milestones").mock(
return_value=httpx.Response(200, json=[])
)
list_milestones("owner", "repo", state="closed")
assert dict(route.calls[0].request.url.params)["state"] == "closed"
@respx.mock
def test_create_milestone():
route = respx.post(f"{BASE}/repos/owner/repo/milestones").mock(
return_value=httpx.Response(201, json={"id": 2, "title": "v2.0"})
)
result = create_milestone("owner", "repo", "v2.0")
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["title"] == "v2.0"
assert "description" not in body
assert "due_on" not in body
@respx.mock
def test_create_milestone_with_due_date():
route = respx.post(f"{BASE}/repos/owner/repo/milestones").mock(
return_value=httpx.Response(201, json={"id": 3})
)
create_milestone("owner", "repo", "v3.0", description="Major release", due_on="2025-12-31T00:00:00Z")
body = json.loads(route.calls[0].request.content)
assert body["description"] == "Major release"
assert body["due_on"] == "2025-12-31T00:00:00Z"
+46
View File
@@ -0,0 +1,46 @@
import pytest
import httpx
import respx
from server.gitea_mcp import list_instances, get_server_info, get_current_user
BASE = "https://gitea.test/api/v1"
def test_list_instances():
result = list_instances()
assert result["default"] == "default"
assert "default" in result["instances"]
@respx.mock
def test_get_server_info():
route = respx.get(f"{BASE}/version").mock(
return_value=httpx.Response(200, json={"version": "1.22.0"})
)
result = get_server_info()
assert route.called
assert result == {"version": "1.22.0"}
@respx.mock
def test_get_server_info_error():
respx.get(f"{BASE}/version").mock(return_value=httpx.Response(500, json={"message": "internal error"}))
with pytest.raises(RuntimeError, match="500"):
get_server_info()
@respx.mock
def test_get_current_user():
route = respx.get(f"{BASE}/user").mock(
return_value=httpx.Response(200, json={"login": "alice", "id": 1})
)
result = get_current_user()
assert route.called
assert result["login"] == "alice"
@respx.mock
def test_get_current_user_error():
respx.get(f"{BASE}/user").mock(return_value=httpx.Response(401, json={"message": "unauthorized"}))
with pytest.raises(RuntimeError, match="401"):
get_current_user()
+47
View File
@@ -0,0 +1,47 @@
import json
import pytest
import httpx
import respx
from server.gitea_mcp import list_notifications, mark_all_notifications_read
BASE = "https://gitea.test/api/v1"
@respx.mock
def test_list_notifications_defaults():
route = respx.get(f"{BASE}/notifications").mock(
return_value=httpx.Response(200, json=[{"id": 1, "unread": True}])
)
result = list_notifications()
assert route.called
params = dict(route.calls[0].request.url.params)
assert params["limit"] == "20"
assert params["all"] == "false"
@respx.mock
def test_list_notifications_include_read():
route = respx.get(f"{BASE}/notifications").mock(
return_value=httpx.Response(200, json=[])
)
list_notifications(include_read=True)
params = dict(route.calls[0].request.url.params)
assert params["all"] == "true"
@respx.mock
def test_list_notifications_error():
respx.get(f"{BASE}/notifications").mock(
return_value=httpx.Response(401, json={"message": "unauthorized"})
)
with pytest.raises(RuntimeError, match="401"):
list_notifications()
@respx.mock
def test_mark_all_notifications_read():
route = respx.put(f"{BASE}/notifications").mock(
return_value=httpx.Response(205)
)
result = mark_all_notifications_read()
assert route.called
+180
View File
@@ -0,0 +1,180 @@
import json
import pytest
import httpx
import respx
from server.gitea_mcp import (
list_pull_requests, get_pull_request, create_pull_request, update_pull_request,
merge_pull_request, get_pr_diff, list_pr_files, list_pr_reviews, create_pr_review,
)
BASE = "https://gitea.test/api/v1"
@respx.mock
def test_list_pull_requests():
route = respx.get(f"{BASE}/repos/owner/repo/pulls").mock(
return_value=httpx.Response(200, json=[])
)
list_pull_requests("owner", "repo")
params = dict(route.calls[0].request.url.params)
assert params["state"] == "open"
assert params["limit"] == "20"
@respx.mock
def test_list_pull_requests_closed():
route = respx.get(f"{BASE}/repos/owner/repo/pulls").mock(
return_value=httpx.Response(200, json=[])
)
list_pull_requests("owner", "repo", state="closed", limit=5)
params = dict(route.calls[0].request.url.params)
assert params["state"] == "closed"
assert params["limit"] == "5"
@respx.mock
def test_get_pull_request():
route = respx.get(f"{BASE}/repos/owner/repo/pulls/7").mock(
return_value=httpx.Response(200, json={"number": 7, "title": "Add feature"})
)
result = get_pull_request("owner", "repo", 7)
assert route.called
assert result["number"] == 7
@respx.mock
def test_create_pull_request():
route = respx.post(f"{BASE}/repos/owner/repo/pulls").mock(
return_value=httpx.Response(201, json={"number": 10})
)
result = create_pull_request("owner", "repo", "Add feature", "feature/x", "main")
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["title"] == "Add feature"
assert body["head"] == "feature/x"
assert body["base"] == "main"
assert "body" not in body
@respx.mock
def test_create_pull_request_with_body_and_labels():
route = respx.post(f"{BASE}/repos/owner/repo/pulls").mock(
return_value=httpx.Response(201, json={"number": 11})
)
create_pull_request(
"owner", "repo", "Fix bug", "fix/crash", "main",
body="Fixes crash on startup", labels=[3],
)
body = json.loads(route.calls[0].request.content)
assert body["body"] == "Fixes crash on startup"
assert body["labels"] == [3]
@respx.mock
def test_update_pull_request():
route = respx.patch(f"{BASE}/repos/owner/repo/pulls/5").mock(
return_value=httpx.Response(200, json={"number": 5})
)
update_pull_request("owner", "repo", 5, title="New title")
body = json.loads(route.calls[0].request.content)
assert body["title"] == "New title"
assert "body" not in body
@respx.mock
def test_merge_pull_request_defaults():
route = respx.post(f"{BASE}/repos/owner/repo/pulls/3/merge").mock(
return_value=httpx.Response(200, json={"success": True})
)
merge_pull_request("owner", "repo", 3)
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["Do"] == "merge"
assert body["delete_branch_after_merge"] is False
@respx.mock
def test_merge_pull_request_squash_with_delete():
route = respx.post(f"{BASE}/repos/owner/repo/pulls/4/merge").mock(
return_value=httpx.Response(200, json={"success": True})
)
merge_pull_request("owner", "repo", 4, merge_style="squash", delete_branch_after_merge=True)
body = json.loads(route.calls[0].request.content)
assert body["Do"] == "squash"
assert body["delete_branch_after_merge"] is True
@respx.mock
def test_get_pr_diff():
diff_text = "diff --git a/file.py b/file.py\n+added line\n"
route = respx.get(f"{BASE}/repos/owner/repo/pulls/2.diff").mock(
return_value=httpx.Response(200, text=diff_text)
)
result = get_pr_diff("owner", "repo", 2)
assert route.called
assert result["diff"] == diff_text
@respx.mock
def test_get_pr_diff_error():
respx.get(f"{BASE}/repos/owner/repo/pulls/99.diff").mock(
return_value=httpx.Response(404, text="not found")
)
with pytest.raises(RuntimeError, match="404"):
get_pr_diff("owner", "repo", 99)
@respx.mock
def test_list_pr_files():
route = respx.get(f"{BASE}/repos/owner/repo/pulls/5/files").mock(
return_value=httpx.Response(200, json=[{"filename": "server/gitea_mcp.py", "status": "modified"}])
)
result = list_pr_files("owner", "repo", 5)
assert route.called
assert result[0]["filename"] == "server/gitea_mcp.py"
params = dict(route.calls[0].request.url.params)
assert params["limit"] == "50"
assert params["page"] == "1"
@respx.mock
def test_list_pr_files_with_whitespace():
route = respx.get(f"{BASE}/repos/owner/repo/pulls/6/files").mock(
return_value=httpx.Response(200, json=[])
)
list_pr_files("owner", "repo", 6, whitespace="ignore-all")
params = dict(route.calls[0].request.url.params)
assert params["whitespace"] == "ignore-all"
@respx.mock
def test_list_pr_reviews():
route = respx.get(f"{BASE}/repos/owner/repo/pulls/8/reviews").mock(
return_value=httpx.Response(200, json=[{"id": 1, "state": "APPROVED"}])
)
result = list_pr_reviews("owner", "repo", 8)
assert route.called
assert result[0]["state"] == "APPROVED"
@respx.mock
def test_create_pr_review_approved():
route = respx.post(f"{BASE}/repos/owner/repo/pulls/9/reviews").mock(
return_value=httpx.Response(200, json={"id": 5, "state": "APPROVED"})
)
result = create_pr_review("owner", "repo", 9, "APPROVED", body="Looks good!")
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["event"] == "APPROVED"
assert body["body"] == "Looks good!"
@respx.mock
def test_create_pr_review_request_changes():
route = respx.post(f"{BASE}/repos/owner/repo/pulls/10/reviews").mock(
return_value=httpx.Response(200, json={"id": 6, "state": "REQUEST_CHANGES"})
)
create_pr_review("owner", "repo", 10, "REQUEST_CHANGES")
body = json.loads(route.calls[0].request.content)
assert body["event"] == "REQUEST_CHANGES"
assert "body" not in body
+79
View File
@@ -0,0 +1,79 @@
import json
import pytest
import httpx
import respx
from server.gitea_mcp import list_releases, get_latest_release, create_release, delete_release
BASE = "https://gitea.test/api/v1"
@respx.mock
def test_list_releases():
route = respx.get(f"{BASE}/repos/owner/repo/releases").mock(
return_value=httpx.Response(200, json=[{"id": 1, "tag_name": "v1.0.0"}])
)
result = list_releases("owner", "repo")
assert route.called
assert result[0]["tag_name"] == "v1.0.0"
assert dict(route.calls[0].request.url.params)["limit"] == "10"
@respx.mock
def test_get_latest_release():
route = respx.get(f"{BASE}/repos/owner/repo/releases/latest").mock(
return_value=httpx.Response(200, json={"id": 3, "tag_name": "v2.0.0"})
)
result = get_latest_release("owner", "repo")
assert route.called
assert result["tag_name"] == "v2.0.0"
@respx.mock
def test_get_latest_release_not_found():
respx.get(f"{BASE}/repos/owner/repo/releases/latest").mock(
return_value=httpx.Response(404, json={"message": "not found"})
)
with pytest.raises(RuntimeError, match="404"):
get_latest_release("owner", "repo")
@respx.mock
def test_create_release():
route = respx.post(f"{BASE}/repos/owner/repo/releases").mock(
return_value=httpx.Response(201, json={"id": 5, "tag_name": "v3.0.0"})
)
result = create_release("owner", "repo", "v3.0.0", "Release 3.0.0")
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["tag_name"] == "v3.0.0"
assert body["name"] == "Release 3.0.0"
assert body["draft"] is False
assert body["prerelease"] is False
assert "body" not in body
@respx.mock
def test_create_release_draft_with_notes():
route = respx.post(f"{BASE}/repos/owner/repo/releases").mock(
return_value=httpx.Response(201, json={"id": 6})
)
create_release(
"owner", "repo", "v4.0.0-beta", "Beta release",
body="## Changes\n- new feature", draft=True, prerelease=True,
target_commitish="develop",
)
body = json.loads(route.calls[0].request.content)
assert body["body"] == "## Changes\n- new feature"
assert body["draft"] is True
assert body["prerelease"] is True
assert body["target_commitish"] == "develop"
@respx.mock
def test_delete_release():
route = respx.delete(f"{BASE}/repos/owner/repo/releases/5").mock(
return_value=httpx.Response(204)
)
result = delete_release("owner", "repo", 5)
assert route.called
assert result == {"success": True}
+151
View File
@@ -0,0 +1,151 @@
import json
import pytest
import httpx
import respx
from server.gitea_mcp import (
list_my_repos, search_repos, get_repo, create_repo, create_org_repo,
update_repo, delete_repo, fork_repo, set_repo_topics,
)
BASE = "https://gitea.test/api/v1"
@respx.mock
def test_list_my_repos_defaults():
route = respx.get(f"{BASE}/user/repos").mock(return_value=httpx.Response(200, json=[]))
list_my_repos()
assert route.called
params = dict(route.calls[0].request.url.params)
assert params["limit"] == "50"
assert params["page"] == "1"
@respx.mock
def test_list_my_repos_custom_page():
route = respx.get(f"{BASE}/user/repos").mock(return_value=httpx.Response(200, json=[]))
list_my_repos(limit=10, page=3)
assert route.called
params = dict(route.calls[0].request.url.params)
assert params["limit"] == "10"
assert params["page"] == "3"
@respx.mock
def test_search_repos():
route = respx.get(f"{BASE}/repos/search").mock(
return_value=httpx.Response(200, json={"data": []})
)
result = search_repos("myquery")
assert route.called
params = dict(route.calls[0].request.url.params)
assert params["q"] == "myquery"
assert params["limit"] == "20"
@respx.mock
def test_get_repo():
route = respx.get(f"{BASE}/repos/owner/myrepo").mock(
return_value=httpx.Response(200, json={"name": "myrepo", "full_name": "owner/myrepo"})
)
result = get_repo("owner", "myrepo")
assert route.called
assert result["name"] == "myrepo"
@respx.mock
def test_get_repo_not_found():
respx.get(f"{BASE}/repos/owner/missing").mock(
return_value=httpx.Response(404, json={"message": "not found"})
)
with pytest.raises(RuntimeError, match="404"):
get_repo("owner", "missing")
@respx.mock
def test_create_repo_defaults():
route = respx.post(f"{BASE}/user/repos").mock(
return_value=httpx.Response(201, json={"name": "newrepo"})
)
result = create_repo("newrepo")
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["name"] == "newrepo"
assert body["private"] is True
assert body["auto_init"] is True
assert "description" not in body
@respx.mock
def test_create_repo_with_options():
route = respx.post(f"{BASE}/user/repos").mock(
return_value=httpx.Response(201, json={"name": "pub"})
)
create_repo("pub", description="A public repo", private=False, gitignores="Python")
body = json.loads(route.calls[0].request.content)
assert body["description"] == "A public repo"
assert body["private"] is False
assert body["gitignores"] == "Python"
@respx.mock
def test_create_org_repo():
route = respx.post(f"{BASE}/orgs/myorg/repos").mock(
return_value=httpx.Response(201, json={"name": "orgrepo"})
)
result = create_org_repo("myorg", "orgrepo")
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["name"] == "orgrepo"
@respx.mock
def test_update_repo():
route = respx.patch(f"{BASE}/repos/owner/repo").mock(
return_value=httpx.Response(200, json={"name": "repo"})
)
update_repo("owner", "repo", description="updated")
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["description"] == "updated"
@respx.mock
def test_delete_repo():
route = respx.delete(f"{BASE}/repos/owner/repo").mock(
return_value=httpx.Response(204)
)
result = delete_repo("owner", "repo")
assert route.called
assert result == {"success": True}
@respx.mock
def test_fork_repo():
route = respx.post(f"{BASE}/repos/upstream/repo/forks").mock(
return_value=httpx.Response(202, json={"name": "repo"})
)
fork_repo("upstream", "repo")
assert route.called
@respx.mock
def test_fork_repo_to_org():
route = respx.post(f"{BASE}/repos/upstream/repo/forks").mock(
return_value=httpx.Response(202, json={"name": "myfork"})
)
fork_repo("upstream", "repo", organization="myorg", new_repo_name="myfork")
body = json.loads(route.calls[0].request.content)
assert body["organization"] == "myorg"
assert body["name"] == "myfork"
@respx.mock
def test_set_repo_topics():
route = respx.put(f"{BASE}/repos/owner/repo/topics").mock(
return_value=httpx.Response(204)
)
result = set_repo_topics("owner", "repo", ["python", "mcp"])
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["topics"] == ["python", "mcp"]
assert result == {"success": True}
+90
View File
@@ -0,0 +1,90 @@
import pytest
import httpx
import respx
from server.gitea_mcp import (
get_user, search_users, list_my_orgs, get_org,
list_org_repos, list_org_members, list_org_teams,
)
BASE = "https://gitea.test/api/v1"
@respx.mock
def test_get_user():
route = respx.get(f"{BASE}/users/alice").mock(
return_value=httpx.Response(200, json={"login": "alice", "id": 42})
)
result = get_user("alice")
assert route.called
assert result["login"] == "alice"
@respx.mock
def test_get_user_not_found():
respx.get(f"{BASE}/users/nobody").mock(
return_value=httpx.Response(404, json={"message": "not found"})
)
with pytest.raises(RuntimeError, match="404"):
get_user("nobody")
@respx.mock
def test_search_users():
route = respx.get(f"{BASE}/users/search").mock(
return_value=httpx.Response(200, json={"data": [{"login": "alice"}]})
)
result = search_users("ali")
assert route.called
params = dict(route.calls[0].request.url.params)
assert params["q"] == "ali"
assert params["limit"] == "10"
@respx.mock
def test_list_my_orgs():
route = respx.get(f"{BASE}/user/orgs").mock(
return_value=httpx.Response(200, json=[{"name": "myorg"}])
)
result = list_my_orgs()
assert route.called
assert result[0]["name"] == "myorg"
@respx.mock
def test_get_org():
route = respx.get(f"{BASE}/orgs/myorg").mock(
return_value=httpx.Response(200, json={"name": "myorg", "full_name": "My Org"})
)
result = get_org("myorg")
assert route.called
assert result["name"] == "myorg"
@respx.mock
def test_list_org_repos():
route = respx.get(f"{BASE}/orgs/myorg/repos").mock(
return_value=httpx.Response(200, json=[{"name": "repo1"}])
)
result = list_org_repos("myorg")
assert route.called
assert dict(route.calls[0].request.url.params)["limit"] == "50"
@respx.mock
def test_list_org_members():
route = respx.get(f"{BASE}/orgs/myorg/members").mock(
return_value=httpx.Response(200, json=[{"login": "bob"}])
)
result = list_org_members("myorg")
assert route.called
assert result[0]["login"] == "bob"
@respx.mock
def test_list_org_teams():
route = respx.get(f"{BASE}/orgs/myorg/teams").mock(
return_value=httpx.Response(200, json=[{"id": 1, "name": "developers"}])
)
result = list_org_teams("myorg")
assert route.called
assert result[0]["name"] == "developers"
+68
View File
@@ -0,0 +1,68 @@
import json
import pytest
import httpx
import respx
from server.gitea_mcp import list_repo_webhooks, create_repo_webhook, delete_repo_webhook
BASE = "https://gitea.test/api/v1"
@respx.mock
def test_list_repo_webhooks():
route = respx.get(f"{BASE}/repos/owner/repo/hooks").mock(
return_value=httpx.Response(200, json=[{"id": 1, "type": "gitea"}])
)
result = list_repo_webhooks("owner", "repo")
assert route.called
assert result[0]["id"] == 1
@respx.mock
def test_create_repo_webhook_minimal():
route = respx.post(f"{BASE}/repos/owner/repo/hooks").mock(
return_value=httpx.Response(201, json={"id": 2})
)
create_repo_webhook("owner", "repo", "https://example.com/hook", ["push"])
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["type"] == "gitea"
assert body["config"]["url"] == "https://example.com/hook"
assert body["config"]["content_type"] == "json"
assert "secret" not in body["config"]
assert body["events"] == ["push"]
assert body["active"] is True
@respx.mock
def test_create_repo_webhook_with_secret():
route = respx.post(f"{BASE}/repos/owner/repo/hooks").mock(
return_value=httpx.Response(201, json={"id": 3})
)
create_repo_webhook(
"owner", "repo", "https://ci.example.com/hook",
["push", "pull_request", "issues"],
secret="mysecret", active=False,
)
body = json.loads(route.calls[0].request.content)
assert body["config"]["secret"] == "mysecret"
assert body["events"] == ["push", "pull_request", "issues"]
assert body["active"] is False
@respx.mock
def test_delete_repo_webhook():
route = respx.delete(f"{BASE}/repos/owner/repo/hooks/7").mock(
return_value=httpx.Response(204)
)
result = delete_repo_webhook("owner", "repo", 7)
assert route.called
assert result == {"success": True}
@respx.mock
def test_create_repo_webhook_error():
respx.post(f"{BASE}/repos/owner/repo/hooks").mock(
return_value=httpx.Response(422, json={"message": "invalid url"})
)
with pytest.raises(RuntimeError, match="422"):
create_repo_webhook("owner", "repo", "not-a-url", ["push"])
Generated
+257
View File
@@ -159,6 +159,124 @@ 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" }, { 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.14.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/92/69/0d2ef01ff4b8fcecd4cba920d11e92fa4f96ae412441d3b56a90a258e69b/coverage-7.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e3680291c4a1d0dadfa84a2c459576a4af5133abb617905714339a0c73138cf", size = 219722, upload-time = "2026-05-26T20:38:14.002Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ae/9afdeaa31b9d9ce98124b6abf8bb49119bf71aecae04f8567c189d91299f/coverage-7.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5274669f37f2343635a347b91a60777621341ab3378e9c6ac9335eee704bddf", size = 220240, upload-time = "2026-05-26T20:38:17.424Z" },
{ url = "https://files.pythonhosted.org/packages/51/69/c998589871df7ea7dba865cc5ee32b5a3e1d47ba6c68ef91104c7c46fa5e/coverage-7.14.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe5a5fec635799ef33428f1e5e61bafa45a92a96190ba731561ba558ccc214d", size = 246981, upload-time = "2026-05-26T20:38:19.266Z" },
{ url = "https://files.pythonhosted.org/packages/fc/10/1c7d04c13040dac531d21b712bbe08f902e6dd9b58f5d77875c4d030f8f2/coverage-7.14.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:62a9f70b52e0b5a95cfef4a5c5641b06983cadc5e538a3feeb5c00211f523ac2", size = 248812, upload-time = "2026-05-26T20:38:20.75Z" },
{ url = "https://files.pythonhosted.org/packages/c1/65/2a38a4607ef27cadcfbcee034dba5830ae2569f90144a0f4c7dbf47d30b0/coverage-7.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c18ebc343e15be53049b3a2dce38fe82d58f37e20ab9094b3a39c0aa4f6bb47", size = 250675, upload-time = "2026-05-26T20:38:22.159Z" },
{ url = "https://files.pythonhosted.org/packages/c9/a2/a446ed9752a4a59b79e0fb6cbb319f6facb2183045c0725462625e66f87e/coverage-7.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b84ffdf877644e7096aa936991efeed873f7f3df57b9cd001312b7668ab08550", size = 252590, upload-time = "2026-05-26T20:38:23.63Z" },
{ url = "https://files.pythonhosted.org/packages/9e/fd/e81fbd7ba752365546e9842b1cbdaad3d6919d2a522c590aef16a281ec5e/coverage-7.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e854312c4103f2ad4c0dc023b69b77ebfd2c89db5f86c4c94dc2353f9a92167e", size = 247691, upload-time = "2026-05-26T20:38:25.057Z" },
{ url = "https://files.pythonhosted.org/packages/53/35/f3c26fdaae9ea937d154ca4d372e5ea0a4167ff70d36c6074ac2eacb2f83/coverage-7.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c643734307300234fafa36bf2a040a7235f8f177ea1fd6ec1423aea6fb7b929f", size = 248716, upload-time = "2026-05-26T20:38:26.406Z" },
{ url = "https://files.pythonhosted.org/packages/2e/14/940b6c49551fd343e8507ee2b0ba7af5d0aa04ed5bf768285cb7c72a9884/coverage-7.14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84ac9499e48700399a5dd0ea7085b5091961fec52c68d66b4ec0d3cf7f4441b1", size = 246721, upload-time = "2026-05-26T20:38:28.282Z" },
{ url = "https://files.pythonhosted.org/packages/aa/2c/40fc0634186c28292a662dff578866b3913983d6c375a3c2a74020938719/coverage-7.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7f02d09f70776579b926d889a4c9c235070a1f47c40458aeaca563fae5acfdb5", size = 250533, upload-time = "2026-05-26T20:38:29.753Z" },
{ url = "https://files.pythonhosted.org/packages/de/e3/2c26bf1e811f9df991ff2a9bdddebdd13ee0665d564df7d05979f9146297/coverage-7.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ce66d8e46da2bb5ee313a745cbd2e391d319176c1f7a9451bfcd3a2fb920859b", size = 246990, upload-time = "2026-05-26T20:38:31.516Z" },
{ url = "https://files.pythonhosted.org/packages/a8/b0/060260ef56bd92363ebdce0c7095ce422b06e69aae71828efeca473ab1ca/coverage-7.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c912c259304cfb5ee584481cfb7ce1ff932b4d61e6c9140b8f19cb7b5ed82332", size = 247593, upload-time = "2026-05-26T20:38:33.065Z" },
{ url = "https://files.pythonhosted.org/packages/63/f3/501502046efeb0d6d94b5ca54941d95f1184183dd6bdb7f283985783bb4a/coverage-7.14.1-cp310-cp310-win32.whl", hash = "sha256:1238cb94638e610e972c60dac68e813f868dc7d6e982535270558443058d9d59", size = 222330, upload-time = "2026-05-26T20:38:35.36Z" },
{ url = "https://files.pythonhosted.org/packages/a0/5d/1bf99f2c558f128faf7906817ccbdb576ba815d3b41ce2ac1719b70a3663/coverage-7.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:fc459e5d73be2d6332fcfe8dbf3d8994671fe33c700f4565988ecfa511547253", size = 223261, upload-time = "2026-05-26T20:38:37.196Z" },
{ url = "https://files.pythonhosted.org/packages/7d/d7/477ad149490e6cb849f28abea1dabb9c823cea72e7500c81b4240ce619c0/coverage-7.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:478b5bcd63c2e1357c5c7e16c070690df7b07f676b1c114d7b93e533c664309f", size = 219848, upload-time = "2026-05-26T20:38:38.715Z" },
{ url = "https://files.pythonhosted.org/packages/91/82/a5eb47257c50601bb7b9a9d2857c67b7a3a85ad74180eb2c98bb1fbe0ce5/coverage-7.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a24a81f9715ee42ef59a316cc11611c98fe23920f7c81861315c9f3ff4a230f4", size = 220354, upload-time = "2026-05-26T20:38:40.232Z" },
{ url = "https://files.pythonhosted.org/packages/43/8b/78419b5391a5cb706b6544390507e469d83ffc9a8248b02c4011aceb9365/coverage-7.14.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:196a13319ad88d6d8ef5ab489ec4f44ddde2143c0c7d5b27786f6c3ffd56a7e1", size = 250771, upload-time = "2026-05-26T20:38:41.782Z" },
{ url = "https://files.pythonhosted.org/packages/77/63/e77aaacd491182210d639636b7a8bba23ffffa9b82aa3762da9431855fa9/coverage-7.14.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d452fd08b5c72c5167c93e6867b5c08500bd40f2a21e1e854a500550b6cc36f", size = 252683, upload-time = "2026-05-26T20:38:43.305Z" },
{ url = "https://files.pythonhosted.org/packages/65/1c/a022e3cfbec2ac241640003cb3a817e161d9c7f5aa9b49173756cdc03204/coverage-7.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23bf7fa51ac02e07fc7c96849b82946da47ae862dc8f86d183b2a4864fc38129", size = 254791, upload-time = "2026-05-26T20:38:45.361Z" },
{ url = "https://files.pythonhosted.org/packages/61/d6/967e408aca4c1ceb88cb0cc677169110ae7f5995fb5eaf5fb1f5a1bb8f5d/coverage-7.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcaa50684dcaadfa599ac48f81103c756d791cfd85c97203d2217c593d48b860", size = 256748, upload-time = "2026-05-26T20:38:46.91Z" },
{ url = "https://files.pythonhosted.org/packages/b8/be/869188f7fe28638078ec479331ace6dc5f7b40b7153eb616f47ab79404d8/coverage-7.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ea1c034f95c9b056e856b794630b17f9fa3d57e4800ff1e503d3be0f9c9078c", size = 250907, upload-time = "2026-05-26T20:38:48.493Z" },
{ url = "https://files.pythonhosted.org/packages/07/aa/adb7d3b4278d690e68703abcd76ab1b948242e3668d921711551b78f9ddb/coverage-7.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e057326434e441306226fbeb5d1aaf14a2637efe97ba668306635835f32ad7", size = 252483, upload-time = "2026-05-26T20:38:50.074Z" },
{ url = "https://files.pythonhosted.org/packages/43/61/331c74103c62dcb0c4b9b3a0de9a61aca016208b0a90f109592a9f9ecc28/coverage-7.14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59baf88468dbc8d63b1887afd92bda52e40bb1561696e5819670601403810cec", size = 250545, upload-time = "2026-05-26T20:38:51.613Z" },
{ url = "https://files.pythonhosted.org/packages/f6/b6/c5dae3c104d89be04828f61810e6b3473825482e4c288cc4ed04553e08ae/coverage-7.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d75f892b3ab73ba11cab5442cce7b3e168fd64162b16f0e1e0d09c508edef", size = 254310, upload-time = "2026-05-26T20:38:53.503Z" },
{ url = "https://files.pythonhosted.org/packages/ad/a1/2b9d5863e3b83c01ad8199e3c597802fbb3a9dc90b058885804c20296d31/coverage-7.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3a56abc20a472baf0304c455721bc601477440d28ecfde8a03dde79ede07e0df", size = 250266, upload-time = "2026-05-26T20:38:55.414Z" },
{ url = "https://files.pythonhosted.org/packages/7f/5e/0e511fbdb269359be26fe678a1c3fa1f2aa2a01573cc3f54268c8d6d4797/coverage-7.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a3cb83d1552c0cd1b4906655b6a33fd4a8473229633a901c6b73bf86914dee9", size = 251174, upload-time = "2026-05-26T20:38:57.141Z" },
{ url = "https://files.pythonhosted.org/packages/85/10/e55307b622b3dd9671cb321824502dc10f93e72f2802b9946159a8edadeb/coverage-7.14.1-cp311-cp311-win32.whl", hash = "sha256:10274a1fbeb8ec5d72966e17bb198a3104257aca4ac09d98667c5f8aca8c8548", size = 222354, upload-time = "2026-05-26T20:38:58.727Z" },
{ url = "https://files.pythonhosted.org/packages/71/cf/107421693cfb71e4f1ca5bf70443f64d4161878068d07a3e51c7ad21d17b/coverage-7.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:87ebdf787d4888e3f3f2d523eadc6e18c6d18c6d0eb173801a189641627fb37e", size = 223290, upload-time = "2026-05-26T20:39:00.413Z" },
{ url = "https://files.pythonhosted.org/packages/b8/1d/3e3644585eb29e9dafefb19555078529a4d7cce12bd21929664eea989277/coverage-7.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:dd34767fa19848d35659ffc0a75314f58c7af3f1cd87ec521e8292a1238398a3", size = 221953, upload-time = "2026-05-26T20:39:02.159Z" },
{ url = "https://files.pythonhosted.org/packages/3d/b7/bdbb725ba02c5b42825b200c940f38b7a54fcad24627b7192f78f8110d76/coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c", size = 220022, upload-time = "2026-05-26T20:39:03.702Z" },
{ url = "https://files.pythonhosted.org/packages/72/81/fdc0898a55c6219223291ec1a1fe89966ef212ce82276aa0899df84b5de0/coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c", size = 220379, upload-time = "2026-05-26T20:39:05.381Z" },
{ url = "https://files.pythonhosted.org/packages/de/72/de048c4a25e13bce59ac6a339351c10bdf2515e07459afcdaf04dc3143a2/coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b", size = 251888, upload-time = "2026-05-26T20:39:07.367Z" },
{ url = "https://files.pythonhosted.org/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6", size = 254624, upload-time = "2026-05-26T20:39:09.037Z" },
{ url = "https://files.pythonhosted.org/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37", size = 255739, upload-time = "2026-05-26T20:39:10.889Z" },
{ url = "https://files.pythonhosted.org/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad", size = 257998, upload-time = "2026-05-26T20:39:12.722Z" },
{ url = "https://files.pythonhosted.org/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84", size = 252296, upload-time = "2026-05-26T20:39:14.259Z" },
{ url = "https://files.pythonhosted.org/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54", size = 253658, upload-time = "2026-05-26T20:39:15.923Z" },
{ url = "https://files.pythonhosted.org/packages/99/6f/ca6ad067364b337ef997802115e7ecad2abd2248b05471464b0dea02b4d4/coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7", size = 251803, upload-time = "2026-05-26T20:39:17.537Z" },
{ url = "https://files.pythonhosted.org/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9", size = 255873, upload-time = "2026-05-26T20:39:19.414Z" },
{ url = "https://files.pythonhosted.org/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02", size = 251372, upload-time = "2026-05-26T20:39:21.169Z" },
{ url = "https://files.pythonhosted.org/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a", size = 253245, upload-time = "2026-05-26T20:39:23.097Z" },
{ url = "https://files.pythonhosted.org/packages/34/62/70a9024672a5f6910517d9628c52c9afbdd3cf8f46426af52bb148a56fff/coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1", size = 222567, upload-time = "2026-05-26T20:39:24.868Z" },
{ url = "https://files.pythonhosted.org/packages/f6/81/8b7cd386839b039ebe1855733b9f9449a8dec5d79564018234f185a7fa70/coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e", size = 223372, upload-time = "2026-05-26T20:39:26.603Z" },
{ url = "https://files.pythonhosted.org/packages/ae/ba/b44d472022f620d289d95fa830143235c0c36461c6f2437ea8d51e5481ed/coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a", size = 221989, upload-time = "2026-05-26T20:39:28.242Z" },
{ url = "https://files.pythonhosted.org/packages/8a/9e/5f6d56327c62b185225d145191c607e07515294a0aa6338e58805cd4a5ac/coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793", size = 220044, upload-time = "2026-05-26T20:39:29.902Z" },
{ url = "https://files.pythonhosted.org/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d", size = 220412, upload-time = "2026-05-26T20:39:31.561Z" },
{ url = "https://files.pythonhosted.org/packages/27/c9/385bde0bf7ed0f4bf3a7ee5367060a86b5d218718cfd6fb943c0f836b34f/coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247", size = 251412, upload-time = "2026-05-26T20:39:33.337Z" },
{ url = "https://files.pythonhosted.org/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d", size = 254008, upload-time = "2026-05-26T20:39:35.009Z" },
{ url = "https://files.pythonhosted.org/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b", size = 255241, upload-time = "2026-05-26T20:39:36.71Z" },
{ url = "https://files.pythonhosted.org/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be", size = 257373, upload-time = "2026-05-26T20:39:38.412Z" },
{ url = "https://files.pythonhosted.org/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43", size = 251635, upload-time = "2026-05-26T20:39:40.268Z" },
{ url = "https://files.pythonhosted.org/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901", size = 253373, upload-time = "2026-05-26T20:39:42.145Z" },
{ url = "https://files.pythonhosted.org/packages/6e/2c/0396562c32deaebe7be51d865b3a41e9a87d7561acafe1a28f53b07e019a/coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff", size = 251341, upload-time = "2026-05-26T20:39:43.907Z" },
{ url = "https://files.pythonhosted.org/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4", size = 255497, upload-time = "2026-05-26T20:39:46.166Z" },
{ url = "https://files.pythonhosted.org/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d", size = 251159, upload-time = "2026-05-26T20:39:48.03Z" },
{ url = "https://files.pythonhosted.org/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33", size = 252934, upload-time = "2026-05-26T20:39:49.872Z" },
{ url = "https://files.pythonhosted.org/packages/eb/fd/11c928cd6bdffc7074bb5965c173d9ebf517fb00205e1da524b98d29ef92/coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c", size = 222584, upload-time = "2026-05-26T20:39:51.68Z" },
{ url = "https://files.pythonhosted.org/packages/6f/92/fb416fc26d340dcba19518c418d6048e913186e17243982c5e435e41fa7a/coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416", size = 223394, upload-time = "2026-05-26T20:39:53.472Z" },
{ url = "https://files.pythonhosted.org/packages/73/c6/02d56e3867972f77d5036de924643f26c056e848f00452cafb4dbc3c29b4/coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42", size = 222015, upload-time = "2026-05-26T20:39:55.374Z" },
{ url = "https://files.pythonhosted.org/packages/4d/9e/fcc77914050df73f7662fa1f00902774c79c075a8388ab334074574bf77e/coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d", size = 220733, upload-time = "2026-05-26T20:39:57.189Z" },
{ url = "https://files.pythonhosted.org/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5", size = 221086, upload-time = "2026-05-26T20:39:59.019Z" },
{ url = "https://files.pythonhosted.org/packages/c8/c5/8701645574e11881f2f47d8930f98bc48b5d43b25eb5b4430dfc4a2f9f48/coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52", size = 262381, upload-time = "2026-05-26T20:40:00.822Z" },
{ url = "https://files.pythonhosted.org/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a", size = 264458, upload-time = "2026-05-26T20:40:02.506Z" },
{ url = "https://files.pythonhosted.org/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a", size = 266884, upload-time = "2026-05-26T20:40:04.421Z" },
{ url = "https://files.pythonhosted.org/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2", size = 268022, upload-time = "2026-05-26T20:40:06.298Z" },
{ url = "https://files.pythonhosted.org/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e", size = 261631, upload-time = "2026-05-26T20:40:08.226Z" },
{ url = "https://files.pythonhosted.org/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d", size = 264443, upload-time = "2026-05-26T20:40:10.137Z" },
{ url = "https://files.pythonhosted.org/packages/f1/d8/5603a88a7c5913a6b54f6cb1a8c46f7b39cbb30f27cd3f492908da09b2d7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb", size = 262069, upload-time = "2026-05-26T20:40:11.999Z" },
{ url = "https://files.pythonhosted.org/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d", size = 265780, upload-time = "2026-05-26T20:40:13.858Z" },
{ url = "https://files.pythonhosted.org/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69", size = 260970, upload-time = "2026-05-26T20:40:16.142Z" },
{ url = "https://files.pythonhosted.org/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54", size = 263157, upload-time = "2026-05-26T20:40:18.382Z" },
{ url = "https://files.pythonhosted.org/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1", size = 223259, upload-time = "2026-05-26T20:40:20.381Z" },
{ url = "https://files.pythonhosted.org/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce", size = 224320, upload-time = "2026-05-26T20:40:22.316Z" },
{ url = "https://files.pythonhosted.org/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1", size = 222577, upload-time = "2026-05-26T20:40:24.894Z" },
{ url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" },
{ url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" },
{ url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" },
{ url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" },
{ url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" },
{ url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" },
{ url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" },
{ url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" },
{ url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" },
{ url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" },
{ url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" },
{ url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" },
{ url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" },
{ url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" },
{ url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" },
{ url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" },
{ url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" },
{ url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" },
{ url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" },
{ url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" },
{ url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" },
{ url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" },
{ url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" },
{ url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" },
{ url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" },
{ url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" },
{ url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" },
{ url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" },
{ url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" },
{ url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" },
{ url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" },
]
[package.optional-dependencies]
toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "48.0.0" version = "48.0.0"
@@ -240,12 +358,26 @@ dependencies = [
{ name = "mcp", extra = ["cli"] }, { name = "mcp", extra = ["cli"] },
] ]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "respx" },
]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "httpx", specifier = ">=0.27" }, { name = "httpx", specifier = ">=0.27" },
{ name = "mcp", extras = ["cli"], specifier = ">=1.0" }, { name = "mcp", extras = ["cli"], specifier = ">=1.0" },
] ]
[package.metadata.requires-dev]
dev = [
{ name = "pytest", specifier = ">=8.0" },
{ name = "pytest-cov", specifier = ">=5.0" },
{ name = "respx", specifier = ">=0.21" },
]
[[package]] [[package]]
name = "h11" name = "h11"
version = "0.16.0" version = "0.16.0"
@@ -301,6 +433,15 @@ 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" }, { 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]] [[package]]
name = "jsonschema" name = "jsonschema"
version = "4.26.0" version = "4.26.0"
@@ -381,6 +522,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
] ]
[[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 = "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]] [[package]]
name = "pycparser" name = "pycparser"
version = "3.0" version = "3.0"
@@ -561,6 +720,38 @@ crypto = [
{ name = "cryptography" }, { name = "cryptography" },
] ]
[[package]]
name = "pytest"
version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
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", extra = ["toml"] },
{ 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]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.2.2" version = "1.2.2"
@@ -616,6 +807,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
] ]
[[package]]
name = "respx"
version = "0.23.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/98/4e55c9c486404ec12373708d015ebce157966965a5ebe7f28ff2c784d41b/respx-0.23.1.tar.gz", hash = "sha256:242dcc6ce6b5b9bf621f5870c82a63997e8e82bc7c947f9ffe272b8f3dd5a780", size = 29243, upload-time = "2026-04-08T14:37:16.008Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/4a/221da6ca167db45693d8d26c7dc79ccfc978a440251bf6721c9aaf251ac0/respx-0.23.1-py2.py3-none-any.whl", hash = "sha256:b18004b029935384bccfa6d7d9d74b4ec9af73a081cc28600fffc0447f4b8c1a", size = 25557, upload-time = "2026-04-08T14:37:14.613Z" },
]
[[package]] [[package]]
name = "rich" name = "rich"
version = "15.0.0" version = "15.0.0"
@@ -929,6 +1132,60 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1c/54/196d0c1db10af76baa4f64894448505d60d3cdf70ef92cbb35f46a4e4c71/starlette-1.2.1-py3-none-any.whl", hash = "sha256:4de0082d08c8f6764a85a54cf1120d6939507a19905c7768acad2a9f875d2b89", size = 73350, upload-time = "2026-05-31T01:07:50.09Z" }, { url = "https://files.pythonhosted.org/packages/1c/54/196d0c1db10af76baa4f64894448505d60d3cdf70ef92cbb35f46a4e4c71/starlette-1.2.1-py3-none-any.whl", hash = "sha256:4de0082d08c8f6764a85a54cf1120d6939507a19905c7768acad2a9f875d2b89", size = 73350, upload-time = "2026-05-31T01:07:50.09Z" },
] ]
[[package]]
name = "tomli"
version = "2.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
{ url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
{ url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
{ url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
{ url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
{ url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
{ url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
{ url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
{ url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
]
[[package]] [[package]]
name = "typer" name = "typer"
version = "0.26.5" version = "0.26.5"