Initial commit: Vultr API v2 Python wrapper with FastAPI server

- vultr_api/: Python library wrapping Vultr API v2
  - 17 resource modules (instances, dns, firewall, vpc, etc.)
  - Pagination support, error handling

- server/: FastAPI REST server
  - All API endpoints exposed via HTTP
  - X-API-Key header authentication
  - Swagger docs at /docs

- Podman quadlet config for systemd deployment

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
HWANG BYUNGHA
2026-01-22 01:08:17 +09:00
commit 184054c6c1
48 changed files with 6058 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
# Environment variables
.env
# Python
__pycache__/
*.py[cod]
*.egg-info/
.venv/
venv/
dist/
build/
# IDE
.idea/
.vscode/
*.swp
.claude/

68
CLAUDE.md Normal file
View File

@@ -0,0 +1,68 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a Python wrapper library for the Vultr API v2. It provides a typed, object-oriented interface for managing Vultr cloud infrastructure including compute instances, DNS, firewalls, VPCs, and more.
## Development Setup
```bash
# Install in development mode
cd ~/vultr-api
pip install -e .
# Or add to PYTHONPATH if requests is already installed
export PYTHONPATH=~/vultr-api:$PYTHONPATH
# Install dev dependencies for testing
pip install -e ".[dev]"
```
## Running Tests
```bash
# Run all tests
pytest
# Run with coverage
pytest --cov=vultr_api
# Run a single test file
pytest tests/test_instances.py
```
## Architecture
### Client Pattern
- `VultrClient` (client.py) is the main entry point that holds the API key and HTTP session
- Resources are initialized as attributes on the client (e.g., `client.instances`, `client.dns`)
- All HTTP requests flow through `VultrClient._request()` which handles auth, errors, and JSON parsing
### Resource Pattern
- Each API domain has a resource class in `vultr_api/resources/`
- All resources inherit from `BaseResource` which provides access to the parent client
- Resources call `self.client.get()`, `self.client.post()`, etc. for API operations
- Pagination is handled by `VultrClient.paginate()` - resources expose this via `list_all()` methods
### Key Resources
| Resource | File | Description |
|----------|------|-------------|
| `account` | account.py | Account info, ACL (IP access control) |
| `instances` | instances.py | Cloud compute VMs, IPv4/IPv6, VPC attachment |
| `dns` | dns.py | DNS domains and records |
| `firewall` | firewall.py | Firewall groups and rules |
| `vpc` | vpc.py | VPC and VPC 2.0 networks |
### Error Handling
- `VultrAPIError` exception contains `message`, `status_code`, and raw `response`
- Import from `vultr_api.client import VultrAPIError`
## API Authentication
The library expects a Vultr API key passed to `VultrClient(api_key="...")`. For examples, use the `VULTR_API_KEY` environment variable.
## API Reference
Official Vultr API v2 documentation: https://www.vultr.com/api/

256
README.md Normal file
View File

@@ -0,0 +1,256 @@
# Vultr API v2 Python Wrapper
Vultr API v2를 위한 Python 클라이언트 라이브러리입니다.
## 설치
```bash
cd ~/vultr-api
pip install -e .
```
또는 requests가 설치되어 있다면 직접 import 가능:
```bash
export PYTHONPATH=~/vultr-api:$PYTHONPATH
```
## 사용법
### 기본 설정
```python
from vultr_api import VultrClient
# API 키로 클라이언트 생성
client = VultrClient(api_key="your-api-key")
```
### 계정 정보
```python
# 계정 정보 조회
account = client.account.get()
print(f"잔액: ${account['balance']}")
print(f"이메일: {account['email']}")
```
### ACL (IP 접근 제어) - 중요!
```python
# 현재 ACL 조회
acl = client.account.get_acl()
print(f"ACL 활성화: {acl.get('acl_enabled')}")
print(f"허용된 IP: {acl.get('acls', [])}")
# IP 추가
client.account.add_acl("203.0.113.50/32")
# IP 목록으로 설정
client.account.set_acl([
"203.0.113.50/32",
"10.0.0.0/8",
"192.168.1.0/24"
])
# IP 제거
client.account.remove_acl("10.0.0.0/8")
# ACL 비활성화 (모든 IP 허용)
client.account.clear_acl()
```
### 인스턴스 관리
```python
# 인스턴스 목록 조회
instances = client.instances.list()
for instance in instances.get("instances", []):
print(f"{instance['label']}: {instance['main_ip']}")
# 인스턴스 생성
new_instance = client.instances.create(
region="nrt", # 도쿄
plan="vc2-1c-1gb", # 1 vCPU, 1GB RAM
os_id=1743, # Ubuntu 22.04
label="my-server",
enable_ipv6=True,
sshkey_id=["ssh-key-id"],
tags=["web", "production"]
)
print(f"인스턴스 생성됨: {new_instance['id']}")
# 인스턴스 상세 조회
instance = client.instances.get("instance-id")
# 인스턴스 제어
client.instances.start("instance-id")
client.instances.stop("instance-id")
client.instances.reboot("instance-id")
# 인스턴스 삭제
client.instances.delete("instance-id")
```
### DNS 관리
```python
# 도메인 목록
domains = client.dns.list_domains()
# 도메인 생성
domain = client.dns.create_domain("example.com", ip="203.0.113.50")
# 레코드 추가
client.dns.create_a_record("example.com", "www", "203.0.113.50")
client.dns.create_aaaa_record("example.com", "www", "2001:db8::1")
client.dns.create_cname_record("example.com", "blog", "www.example.com")
client.dns.create_mx_record("example.com", "@", "mail.example.com", priority=10)
client.dns.create_txt_record("example.com", "@", "v=spf1 include:_spf.example.com ~all")
# 레코드 목록
records = client.dns.list_records("example.com")
# 레코드 삭제
client.dns.delete_record("example.com", "record-id")
```
### 방화벽 관리
```python
# 방화벽 그룹 생성
group = client.firewall.create_group(description="Web Servers")
group_id = group["id"]
# 규칙 추가
client.firewall.allow_ssh(group_id) # SSH (22)
client.firewall.allow_http(group_id) # HTTP (80)
client.firewall.allow_https(group_id) # HTTPS (443)
client.firewall.allow_ping(group_id) # ICMP
# 커스텀 규칙
client.firewall.create_rule(
firewall_group_id=group_id,
ip_type="v4",
protocol="tcp",
port="3000:3999", # 포트 범위
subnet="10.0.0.0",
subnet_size=8,
notes="Internal services"
)
# 인스턴스에 방화벽 적용
client.instances.update("instance-id", firewall_group_id=group_id)
```
### SSH 키 관리
```python
# SSH 키 목록
keys = client.ssh_keys.list()
# SSH 키 추가
key = client.ssh_keys.create(
name="my-laptop",
ssh_key="ssh-rsa AAAAB3NzaC1yc2EAAAA..."
)
# SSH 키 삭제
client.ssh_keys.delete("key-id")
```
### VPC 관리
```python
# VPC 생성
vpc = client.vpc.create(
region="nrt",
description="Private Network",
v4_subnet="10.10.0.0",
v4_subnet_mask=24
)
# VPC 2.0 생성
vpc2 = client.vpc.create_vpc2(
region="nrt",
description="VPC 2.0 Network",
ip_block="10.20.0.0",
prefix_length=24
)
# 인스턴스에 VPC 연결
client.instances.attach_vpc("instance-id", "vpc-id")
```
### 스냅샷 관리
```python
# 스냅샷 생성
snapshot = client.snapshots.create(
instance_id="instance-id",
description="Before upgrade"
)
# 스냅샷 목록
snapshots = client.snapshots.list()
# 스냅샷으로 복원
client.instances.restore_snapshot("instance-id", "snapshot-id")
```
### 플랜 및 리전 조회
```python
# 사용 가능한 플랜
plans = client.plans.list()
for plan in plans.get("plans", []):
print(f"{plan['id']}: {plan['vcpu_count']}vCPU, {plan['ram']}MB, ${plan['monthly_cost']}/월")
# 리전 목록
regions = client.regions.list()
for region in regions.get("regions", []):
print(f"{region['id']}: {region['city']}, {region['country']}")
# OS 목록
os_list = client.os.list()
for os_info in os_list.get("os", []):
print(f"{os_info['id']}: {os_info['name']}")
```
## 지원하는 리소스
| 리소스 | 설명 |
|--------|------|
| `account` | 계정 정보, ACL 관리 |
| `instances` | 클라우드 인스턴스 |
| `bare_metal` | 베어메탈 서버 |
| `dns` | DNS 도메인/레코드 |
| `firewall` | 방화벽 그룹/규칙 |
| `ssh_keys` | SSH 키 |
| `startup_scripts` | 시작 스크립트 |
| `snapshots` | 스냅샷 |
| `backups` | 백업 |
| `block_storage` | 블록 스토리지 |
| `reserved_ips` | 예약 IP |
| `vpc` | VPC, VPC 2.0 |
| `load_balancers` | 로드 밸런서 |
| `plans` | 플랜 목록 |
| `regions` | 리전 목록 |
| `os` | OS 목록 |
## 에러 처리
```python
from vultr_api.client import VultrAPIError
try:
client.instances.get("invalid-id")
except VultrAPIError as e:
print(f"API 오류: {e.message}")
print(f"상태 코드: {e.status_code}")
```
## API 참고
- [Vultr API v2 문서](https://www.vultr.com/api/)
- [Vultr API Postman Collection](https://www.postman.com/vultr-api/vultr-api-v2/documentation/soddyfe/vultr-api-v2)

119
examples/acl_management.py Normal file
View File

@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""
Vultr ACL (IP 접근 제어) 관리 예제
IP를 등록해야 API에 접근할 수 있을 때 사용하는 예제입니다.
"""
import os
import sys
import argparse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from vultr_api import VultrClient
from vultr_api.client import VultrAPIError
def get_client():
api_key = os.environ.get("VULTR_API_KEY")
if not api_key:
print("VULTR_API_KEY 환경 변수를 설정해주세요")
sys.exit(1)
return VultrClient(api_key=api_key)
def list_acl(client):
"""현재 ACL 목록 조회"""
acl = client.account.get_acl()
print(f"ACL 활성화: {acl.get('acl_enabled', False)}")
acls = acl.get("acls", [])
if acls:
print("허용된 IP/CIDR:")
for ip in acls:
print(f" - {ip}")
else:
print("허용된 IP 없음 (모든 IP 허용)")
def add_ip(client, ip):
"""IP 추가"""
print(f"IP 추가 중: {ip}")
client.account.add_acl(ip)
print("완료!")
list_acl(client)
def remove_ip(client, ip):
"""IP 제거"""
print(f"IP 제거 중: {ip}")
client.account.remove_acl(ip)
print("완료!")
list_acl(client)
def set_ips(client, ips):
"""IP 목록 설정"""
print(f"IP 목록 설정 중: {ips}")
client.account.set_acl(ips)
print("완료!")
list_acl(client)
def clear_acl(client):
"""ACL 초기화"""
print("ACL 초기화 중 (모든 IP 허용)...")
client.account.clear_acl()
print("완료!")
list_acl(client)
def main():
parser = argparse.ArgumentParser(description="Vultr ACL 관리")
subparsers = parser.add_subparsers(dest="command", help="명령")
# list 명령
subparsers.add_parser("list", help="ACL 목록 조회")
# add 명령
add_parser = subparsers.add_parser("add", help="IP 추가")
add_parser.add_argument("ip", help="추가할 IP/CIDR (예: 192.168.1.1/32)")
# remove 명령
remove_parser = subparsers.add_parser("remove", help="IP 제거")
remove_parser.add_argument("ip", help="제거할 IP/CIDR")
# set 명령
set_parser = subparsers.add_parser("set", help="IP 목록 설정")
set_parser.add_argument("ips", nargs="+", help="설정할 IP/CIDR 목록")
# clear 명령
subparsers.add_parser("clear", help="ACL 초기화 (모든 IP 허용)")
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
try:
client = get_client()
if args.command == "list":
list_acl(client)
elif args.command == "add":
add_ip(client, args.ip)
elif args.command == "remove":
remove_ip(client, args.ip)
elif args.command == "set":
set_ips(client, args.ips)
elif args.command == "clear":
clear_acl(client)
except VultrAPIError as e:
print(f"API 오류: {e.message}")
sys.exit(1)
if __name__ == "__main__":
main()

105
examples/basic_usage.py Normal file
View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""
Vultr API 기본 사용 예제
"""
import os
import sys
# vultr_api 패키지 경로 추가 (개발용)
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from vultr_api import VultrClient
from vultr_api.client import VultrAPIError
def main():
# API 키 설정 (환경 변수에서 가져오기)
api_key = os.environ.get("VULTR_API_KEY")
if not api_key:
print("VULTR_API_KEY 환경 변수를 설정해주세요")
print("export VULTR_API_KEY='your-api-key'")
sys.exit(1)
# 클라이언트 생성
client = VultrClient(api_key=api_key)
try:
# 1. 계정 정보 조회
print("=" * 50)
print("계정 정보")
print("=" * 50)
account = client.account.get()
print(f"이메일: {account.get('email')}")
print(f"이름: {account.get('name')}")
print(f"잔액: ${account.get('balance')}")
print(f"보류 요금: ${account.get('pending_charges')}")
print()
# 2. ACL 정보 조회
print("=" * 50)
print("ACL (IP 접근 제어)")
print("=" * 50)
acl = client.account.get_acl()
print(f"ACL 활성화: {acl.get('acl_enabled', False)}")
acls = acl.get("acls", [])
if acls:
print("허용된 IP:")
for ip in acls:
print(f" - {ip}")
else:
print("허용된 IP: 없음 (모든 IP 허용)")
print()
# 3. 인스턴스 목록
print("=" * 50)
print("인스턴스 목록")
print("=" * 50)
instances_resp = client.instances.list()
instances = instances_resp.get("instances", [])
if instances:
for inst in instances:
status = inst.get("status", "unknown")
power = inst.get("power_status", "unknown")
print(f"- {inst.get('label', 'No Label')}")
print(f" ID: {inst.get('id')}")
print(f" IP: {inst.get('main_ip')}")
print(f" 리전: {inst.get('region')}")
print(f" 상태: {status} ({power})")
print(f" 플랜: {inst.get('plan')}")
print()
else:
print("인스턴스가 없습니다")
print()
# 4. 리전 목록 (일부만)
print("=" * 50)
print("사용 가능한 리전 (일부)")
print("=" * 50)
regions_resp = client.regions.list(per_page=10)
regions = regions_resp.get("regions", [])
for region in regions[:5]:
print(f"- {region.get('id')}: {region.get('city')}, {region.get('country')}")
print()
# 5. 플랜 목록 (일부만)
print("=" * 50)
print("사용 가능한 플랜 (일부)")
print("=" * 50)
plans_resp = client.plans.list(per_page=10)
plans = plans_resp.get("plans", [])
for plan in plans[:5]:
print(f"- {plan.get('id')}")
print(f" CPU: {plan.get('vcpu_count')}vCPU, RAM: {plan.get('ram')}MB")
print(f" 가격: ${plan.get('monthly_cost')}/월")
print()
except VultrAPIError as e:
print(f"API 오류 발생: {e.message}")
if e.status_code:
print(f"상태 코드: {e.status_code}")
sys.exit(1)
if __name__ == "__main__":
main()

44
pyproject.toml Normal file
View File

@@ -0,0 +1,44 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "vultr-api"
version = "1.0.0"
description = "Vultr API v2 Python Wrapper"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "MIT"}
authors = [
{name = "Admin"}
]
keywords = ["vultr", "api", "cloud", "vps"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"requests>=2.25.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
]
[project.urls]
"Homepage" = "https://github.com/admin/vultr-api"
"Bug Tracker" = "https://github.com/admin/vultr-api/issues"
[tool.setuptools.packages.find]
where = ["."]
include = ["vultr_api*"]

18
server/Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM python:3.12-slim
WORKDIR /app
# Install dependencies
RUN pip install --no-cache-dir fastapi uvicorn requests pydantic
# Copy vultr_api library
COPY vultr_api/ /app/vultr_api/
# Copy server code
COPY server/ /app/
# Expose port
EXPOSE 8000
# Run server
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

20
server/deps.py Normal file
View File

@@ -0,0 +1,20 @@
"""Dependencies for FastAPI"""
import os
from fastapi import HTTPException, Security
from fastapi.security import APIKeyHeader
from vultr_api import VultrClient
API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)
VULTR_API_KEY = os.environ.get("VULTR_API_KEY")
def get_client(api_key: str = Security(API_KEY_HEADER)) -> VultrClient:
"""Get VultrClient instance with API key from header or environment"""
key = api_key or VULTR_API_KEY
if not key:
raise HTTPException(
status_code=401,
detail="API key required. Set X-API-Key header or VULTR_API_KEY env var"
)
return VultrClient(api_key=key)

58
server/main.py Normal file
View File

@@ -0,0 +1,58 @@
"""
Vultr API REST Server
FastAPI wrapper for vultr-api library
"""
from fastapi import FastAPI
from contextlib import asynccontextmanager
from routers import (
account, instances, dns, firewall, ssh_keys,
startup_scripts, snapshots, block_storage, reserved_ips,
vpc, load_balancers, bare_metal, backups, plans, regions, os_api
)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan handler"""
print("Vultr API Server starting...")
yield
print("Vultr API Server shutting down...")
app = FastAPI(
title="Vultr API Server",
description="REST API server wrapping Vultr API v2",
version="1.0.0",
lifespan=lifespan,
)
# Include routers
app.include_router(account.router, prefix="/account", tags=["Account"])
app.include_router(instances.router, prefix="/instances", tags=["Instances"])
app.include_router(dns.router, prefix="/dns", tags=["DNS"])
app.include_router(firewall.router, prefix="/firewall", tags=["Firewall"])
app.include_router(ssh_keys.router, prefix="/ssh-keys", tags=["SSH Keys"])
app.include_router(startup_scripts.router, prefix="/startup-scripts", tags=["Startup Scripts"])
app.include_router(snapshots.router, prefix="/snapshots", tags=["Snapshots"])
app.include_router(block_storage.router, prefix="/block-storage", tags=["Block Storage"])
app.include_router(reserved_ips.router, prefix="/reserved-ips", tags=["Reserved IPs"])
app.include_router(vpc.router, prefix="/vpc", tags=["VPC"])
app.include_router(load_balancers.router, prefix="/load-balancers", tags=["Load Balancers"])
app.include_router(bare_metal.router, prefix="/bare-metal", tags=["Bare Metal"])
app.include_router(backups.router, prefix="/backups", tags=["Backups"])
app.include_router(plans.router, prefix="/plans", tags=["Plans"])
app.include_router(regions.router, prefix="/regions", tags=["Regions"])
app.include_router(os_api.router, prefix="/os", tags=["Operating Systems"])
@app.get("/", tags=["Health"])
async def root():
"""Health check endpoint"""
return {"status": "ok", "service": "vultr-api-server"}
@app.get("/health", tags=["Health"])
async def health():
"""Health check endpoint"""
return {"status": "healthy"}

View File

@@ -0,0 +1,38 @@
"""API Routers"""
from . import (
account,
instances,
dns,
firewall,
ssh_keys,
startup_scripts,
snapshots,
block_storage,
reserved_ips,
vpc,
load_balancers,
bare_metal,
backups,
plans,
regions,
os_api,
)
__all__ = [
"account",
"instances",
"dns",
"firewall",
"ssh_keys",
"startup_scripts",
"snapshots",
"block_storage",
"reserved_ips",
"vpc",
"load_balancers",
"bare_metal",
"backups",
"plans",
"regions",
"os_api",
]

38
server/routers/account.py Normal file
View File

@@ -0,0 +1,38 @@
"""Account router"""
from fastapi import APIRouter, Depends
from typing import List
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
@router.get("")
async def get_account(client: VultrClient = Depends(get_client)):
"""Get account information"""
return client.account.get()
@router.get("/acl")
async def get_acl(client: VultrClient = Depends(get_client)):
"""Get account ACL (IP access control list)"""
return client.account.get_acl()
@router.put("/acl")
async def set_acl(acls: List[str], client: VultrClient = Depends(get_client)):
"""Set account ACL"""
return client.account.set_acl(acls)
@router.post("/acl")
async def add_acl(ip: str, client: VultrClient = Depends(get_client)):
"""Add IP to ACL"""
return client.account.add_acl(ip)
@router.delete("/acl/{ip}")
async def remove_acl(ip: str, client: VultrClient = Depends(get_client)):
"""Remove IP from ACL"""
return client.account.remove_acl(ip)

34
server/routers/backups.py Normal file
View File

@@ -0,0 +1,34 @@
"""Backups router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
@router.get("")
async def list_backups(
instance_id: Optional[str] = None,
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all backups"""
return client.backups.list(instance_id=instance_id, per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_backups(
instance_id: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all backups (auto-paginated)"""
return {"backups": client.backups.list_all(instance_id=instance_id)}
@router.get("/{backup_id}")
async def get_backup(backup_id: str, client: VultrClient = Depends(get_client)):
"""Get backup details"""
return client.backups.get(backup_id)

View File

@@ -0,0 +1,118 @@
"""Bare Metal router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional, List
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateBareMetalRequest(BaseModel):
region: str
plan: str
os_id: Optional[int] = None
snapshot_id: Optional[str] = None
app_id: Optional[int] = None
image_id: Optional[str] = None
script_id: Optional[str] = None
ssh_key_ids: Optional[List[str]] = None
enable_ipv6: Optional[bool] = None
hostname: Optional[str] = None
label: Optional[str] = None
tags: Optional[List[str]] = None
class UpdateBareMetalRequest(BaseModel):
label: Optional[str] = None
tags: Optional[List[str]] = None
enable_ipv6: Optional[bool] = None
@router.get("")
async def list_bare_metal(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all bare metal servers"""
return client.bare_metal.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_bare_metal(client: VultrClient = Depends(get_client)):
"""List all bare metal servers (auto-paginated)"""
return {"bare_metals": client.bare_metal.list_all()}
@router.get("/{baremetal_id}")
async def get_bare_metal(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""Get bare metal server details"""
return client.bare_metal.get(baremetal_id)
@router.post("")
async def create_bare_metal(req: CreateBareMetalRequest, client: VultrClient = Depends(get_client)):
"""Create a bare metal server"""
return client.bare_metal.create(**req.model_dump(exclude_none=True))
@router.patch("/{baremetal_id}")
async def update_bare_metal(baremetal_id: str, req: UpdateBareMetalRequest, client: VultrClient = Depends(get_client)):
"""Update a bare metal server"""
return client.bare_metal.update(baremetal_id, **req.model_dump(exclude_none=True))
@router.delete("/{baremetal_id}")
async def delete_bare_metal(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""Delete a bare metal server"""
return client.bare_metal.delete(baremetal_id)
@router.post("/{baremetal_id}/start")
async def start_bare_metal(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""Start a bare metal server"""
return client.bare_metal.start(baremetal_id)
@router.post("/{baremetal_id}/halt")
async def halt_bare_metal(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""Halt a bare metal server"""
return client.bare_metal.halt(baremetal_id)
@router.post("/{baremetal_id}/reboot")
async def reboot_bare_metal(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""Reboot a bare metal server"""
return client.bare_metal.reboot(baremetal_id)
@router.post("/{baremetal_id}/reinstall")
async def reinstall_bare_metal(baremetal_id: str, hostname: Optional[str] = None, client: VultrClient = Depends(get_client)):
"""Reinstall a bare metal server"""
return client.bare_metal.reinstall(baremetal_id, hostname=hostname)
@router.get("/{baremetal_id}/ipv4")
async def list_ipv4(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""List IPv4 addresses for a bare metal server"""
return client.bare_metal.list_ipv4(baremetal_id)
@router.get("/{baremetal_id}/ipv6")
async def list_ipv6(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""List IPv6 addresses for a bare metal server"""
return client.bare_metal.list_ipv6(baremetal_id)
@router.get("/{baremetal_id}/bandwidth")
async def get_bandwidth(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""Get bandwidth usage for a bare metal server"""
return client.bare_metal.bandwidth(baremetal_id)
@router.get("/{baremetal_id}/user-data")
async def get_user_data(baremetal_id: str, client: VultrClient = Depends(get_client)):
"""Get user data for a bare metal server"""
return client.bare_metal.get_user_data(baremetal_id)

View File

@@ -0,0 +1,78 @@
"""Block Storage router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateBlockStorageRequest(BaseModel):
region: str
size_gb: int
label: Optional[str] = None
block_type: Optional[str] = None
class UpdateBlockStorageRequest(BaseModel):
size_gb: Optional[int] = None
label: Optional[str] = None
class AttachBlockStorageRequest(BaseModel):
instance_id: str
live: Optional[bool] = True
@router.get("")
async def list_block_storage(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all block storage volumes"""
return client.block_storage.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_block_storage(client: VultrClient = Depends(get_client)):
"""List all block storage volumes (auto-paginated)"""
return {"blocks": client.block_storage.list_all()}
@router.get("/{block_id}")
async def get_block_storage(block_id: str, client: VultrClient = Depends(get_client)):
"""Get block storage details"""
return client.block_storage.get(block_id)
@router.post("")
async def create_block_storage(req: CreateBlockStorageRequest, client: VultrClient = Depends(get_client)):
"""Create a block storage volume"""
return client.block_storage.create(**req.model_dump(exclude_none=True))
@router.patch("/{block_id}")
async def update_block_storage(block_id: str, req: UpdateBlockStorageRequest, client: VultrClient = Depends(get_client)):
"""Update a block storage volume"""
return client.block_storage.update(block_id, **req.model_dump(exclude_none=True))
@router.delete("/{block_id}")
async def delete_block_storage(block_id: str, client: VultrClient = Depends(get_client)):
"""Delete a block storage volume"""
return client.block_storage.delete(block_id)
@router.post("/{block_id}/attach")
async def attach_block_storage(block_id: str, req: AttachBlockStorageRequest, client: VultrClient = Depends(get_client)):
"""Attach block storage to an instance"""
return client.block_storage.attach(block_id, instance_id=req.instance_id, live=req.live)
@router.post("/{block_id}/detach")
async def detach_block_storage(block_id: str, live: bool = True, client: VultrClient = Depends(get_client)):
"""Detach block storage from an instance"""
return client.block_storage.detach(block_id, live=live)

155
server/routers/dns.py Normal file
View File

@@ -0,0 +1,155 @@
"""DNS router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateDomainRequest(BaseModel):
domain: str
ip: Optional[str] = None
class CreateRecordRequest(BaseModel):
type: str
name: str
data: str
ttl: Optional[int] = 300
priority: Optional[int] = None
class UpdateRecordRequest(BaseModel):
name: Optional[str] = None
data: Optional[str] = None
ttl: Optional[int] = None
priority: Optional[int] = None
@router.get("/domains")
async def list_domains(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all DNS domains"""
return client.dns.list_domains(per_page=per_page, cursor=cursor)
@router.get("/domains/all")
async def list_all_domains(client: VultrClient = Depends(get_client)):
"""List all DNS domains (auto-paginated)"""
return {"domains": client.dns.list_all_domains()}
@router.get("/domains/{domain}")
async def get_domain(domain: str, client: VultrClient = Depends(get_client)):
"""Get DNS domain details"""
return client.dns.get_domain(domain)
@router.post("/domains")
async def create_domain(req: CreateDomainRequest, client: VultrClient = Depends(get_client)):
"""Create a DNS domain"""
return client.dns.create_domain(req.domain, ip=req.ip)
@router.delete("/domains/{domain}")
async def delete_domain(domain: str, client: VultrClient = Depends(get_client)):
"""Delete a DNS domain"""
return client.dns.delete_domain(domain)
@router.get("/domains/{domain}/soa")
async def get_soa(domain: str, client: VultrClient = Depends(get_client)):
"""Get SOA record for a domain"""
return client.dns.get_soa(domain)
@router.get("/domains/{domain}/dnssec")
async def get_dnssec(domain: str, client: VultrClient = Depends(get_client)):
"""Get DNSSEC info for a domain"""
return client.dns.get_dnssec(domain)
# Records
@router.get("/domains/{domain}/records")
async def list_records(
domain: str,
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List DNS records for a domain"""
return client.dns.list_records(domain, per_page=per_page, cursor=cursor)
@router.get("/domains/{domain}/records/all")
async def list_all_records(domain: str, client: VultrClient = Depends(get_client)):
"""List all DNS records (auto-paginated)"""
return {"records": client.dns.list_all_records(domain)}
@router.get("/domains/{domain}/records/{record_id}")
async def get_record(domain: str, record_id: str, client: VultrClient = Depends(get_client)):
"""Get a DNS record"""
return client.dns.get_record(domain, record_id)
@router.post("/domains/{domain}/records")
async def create_record(domain: str, req: CreateRecordRequest, client: VultrClient = Depends(get_client)):
"""Create a DNS record"""
return client.dns.create_record(
domain,
type=req.type,
name=req.name,
data=req.data,
ttl=req.ttl,
priority=req.priority
)
@router.patch("/domains/{domain}/records/{record_id}")
async def update_record(domain: str, record_id: str, req: UpdateRecordRequest, client: VultrClient = Depends(get_client)):
"""Update a DNS record"""
return client.dns.update_record(domain, record_id, **req.model_dump(exclude_none=True))
@router.delete("/domains/{domain}/records/{record_id}")
async def delete_record(domain: str, record_id: str, client: VultrClient = Depends(get_client)):
"""Delete a DNS record"""
return client.dns.delete_record(domain, record_id)
# Convenience endpoints
@router.post("/domains/{domain}/records/a")
async def create_a_record(domain: str, name: str, ip: str, ttl: int = 300, client: VultrClient = Depends(get_client)):
"""Create an A record"""
return client.dns.create_a_record(domain, name, ip, ttl)
@router.post("/domains/{domain}/records/aaaa")
async def create_aaaa_record(domain: str, name: str, ip: str, ttl: int = 300, client: VultrClient = Depends(get_client)):
"""Create an AAAA record"""
return client.dns.create_aaaa_record(domain, name, ip, ttl)
@router.post("/domains/{domain}/records/cname")
async def create_cname_record(domain: str, name: str, target: str, ttl: int = 300, client: VultrClient = Depends(get_client)):
"""Create a CNAME record"""
return client.dns.create_cname_record(domain, name, target, ttl)
@router.post("/domains/{domain}/records/txt")
async def create_txt_record(domain: str, name: str, data: str, ttl: int = 300, client: VultrClient = Depends(get_client)):
"""Create a TXT record"""
return client.dns.create_txt_record(domain, name, data, ttl)
@router.post("/domains/{domain}/records/mx")
async def create_mx_record(domain: str, name: str, data: str, priority: int = 10, ttl: int = 300, client: VultrClient = Depends(get_client)):
"""Create an MX record"""
return client.dns.create_mx_record(domain, name, data, priority, ttl)

124
server/routers/firewall.py Normal file
View File

@@ -0,0 +1,124 @@
"""Firewall router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateGroupRequest(BaseModel):
description: Optional[str] = None
class CreateRuleRequest(BaseModel):
ip_type: str # v4 or v6
protocol: str # tcp, udp, icmp, gre, esp, ah
subnet: str
subnet_size: int
port: Optional[str] = None
source: Optional[str] = None
notes: Optional[str] = None
@router.get("/groups")
async def list_groups(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all firewall groups"""
return client.firewall.list_groups(per_page=per_page, cursor=cursor)
@router.get("/groups/all")
async def list_all_groups(client: VultrClient = Depends(get_client)):
"""List all firewall groups (auto-paginated)"""
return {"firewall_groups": client.firewall.list_all_groups()}
@router.get("/groups/{group_id}")
async def get_group(group_id: str, client: VultrClient = Depends(get_client)):
"""Get firewall group details"""
return client.firewall.get_group(group_id)
@router.post("/groups")
async def create_group(req: CreateGroupRequest, client: VultrClient = Depends(get_client)):
"""Create a firewall group"""
return client.firewall.create_group(description=req.description)
@router.patch("/groups/{group_id}")
async def update_group(group_id: str, description: str, client: VultrClient = Depends(get_client)):
"""Update a firewall group"""
return client.firewall.update_group(group_id, description=description)
@router.delete("/groups/{group_id}")
async def delete_group(group_id: str, client: VultrClient = Depends(get_client)):
"""Delete a firewall group"""
return client.firewall.delete_group(group_id)
# Rules
@router.get("/groups/{group_id}/rules")
async def list_rules(
group_id: str,
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List firewall rules for a group"""
return client.firewall.list_rules(group_id, per_page=per_page, cursor=cursor)
@router.get("/groups/{group_id}/rules/all")
async def list_all_rules(group_id: str, client: VultrClient = Depends(get_client)):
"""List all firewall rules (auto-paginated)"""
return {"firewall_rules": client.firewall.list_all_rules(group_id)}
@router.get("/groups/{group_id}/rules/{rule_id}")
async def get_rule(group_id: str, rule_id: int, client: VultrClient = Depends(get_client)):
"""Get a firewall rule"""
return client.firewall.get_rule(group_id, rule_id)
@router.post("/groups/{group_id}/rules")
async def create_rule(group_id: str, req: CreateRuleRequest, client: VultrClient = Depends(get_client)):
"""Create a firewall rule"""
return client.firewall.create_rule(group_id, **req.model_dump(exclude_none=True))
@router.delete("/groups/{group_id}/rules/{rule_id}")
async def delete_rule(group_id: str, rule_id: int, client: VultrClient = Depends(get_client)):
"""Delete a firewall rule"""
return client.firewall.delete_rule(group_id, rule_id)
# Convenience endpoints
@router.post("/groups/{group_id}/rules/allow-ssh")
async def allow_ssh(group_id: str, source: str = "0.0.0.0/0", client: VultrClient = Depends(get_client)):
"""Allow SSH (port 22) from source"""
return client.firewall.allow_ssh(group_id, source=source)
@router.post("/groups/{group_id}/rules/allow-http")
async def allow_http(group_id: str, source: str = "0.0.0.0/0", client: VultrClient = Depends(get_client)):
"""Allow HTTP (port 80) from source"""
return client.firewall.allow_http(group_id, source=source)
@router.post("/groups/{group_id}/rules/allow-https")
async def allow_https(group_id: str, source: str = "0.0.0.0/0", client: VultrClient = Depends(get_client)):
"""Allow HTTPS (port 443) from source"""
return client.firewall.allow_https(group_id, source=source)
@router.post("/groups/{group_id}/rules/allow-ping")
async def allow_ping(group_id: str, source: str = "0.0.0.0/0", client: VultrClient = Depends(get_client)):
"""Allow ICMP ping from source"""
return client.firewall.allow_ping(group_id, source=source)

151
server/routers/instances.py Normal file
View File

@@ -0,0 +1,151 @@
"""Instances router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional, List
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateInstanceRequest(BaseModel):
region: str
plan: str
os_id: Optional[int] = None
iso_id: Optional[str] = None
snapshot_id: Optional[str] = None
app_id: Optional[int] = None
image_id: Optional[str] = None
script_id: Optional[str] = None
ssh_key_ids: Optional[List[str]] = None
backups: Optional[str] = None
enable_ipv6: Optional[bool] = None
hostname: Optional[str] = None
label: Optional[str] = None
tags: Optional[List[str]] = None
firewall_group_id: Optional[str] = None
vpc_id: Optional[str] = None
class UpdateInstanceRequest(BaseModel):
label: Optional[str] = None
tags: Optional[List[str]] = None
firewall_group_id: Optional[str] = None
enable_ipv6: Optional[bool] = None
backups: Optional[str] = None
@router.get("")
async def list_instances(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all instances"""
return client.instances.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_instances(client: VultrClient = Depends(get_client)):
"""List all instances (auto-paginated)"""
return {"instances": client.instances.list_all()}
@router.get("/{instance_id}")
async def get_instance(instance_id: str, client: VultrClient = Depends(get_client)):
"""Get instance details"""
return client.instances.get(instance_id)
@router.post("")
async def create_instance(req: CreateInstanceRequest, client: VultrClient = Depends(get_client)):
"""Create a new instance"""
return client.instances.create(**req.model_dump(exclude_none=True))
@router.patch("/{instance_id}")
async def update_instance(instance_id: str, req: UpdateInstanceRequest, client: VultrClient = Depends(get_client)):
"""Update an instance"""
return client.instances.update(instance_id, **req.model_dump(exclude_none=True))
@router.delete("/{instance_id}")
async def delete_instance(instance_id: str, client: VultrClient = Depends(get_client)):
"""Delete an instance"""
return client.instances.delete(instance_id)
@router.post("/{instance_id}/start")
async def start_instance(instance_id: str, client: VultrClient = Depends(get_client)):
"""Start an instance"""
return client.instances.start(instance_id)
@router.post("/{instance_id}/stop")
async def stop_instance(instance_id: str, client: VultrClient = Depends(get_client)):
"""Stop an instance (halt)"""
return client.instances.halt(instance_id)
@router.post("/{instance_id}/reboot")
async def reboot_instance(instance_id: str, client: VultrClient = Depends(get_client)):
"""Reboot an instance"""
return client.instances.reboot(instance_id)
@router.post("/{instance_id}/reinstall")
async def reinstall_instance(instance_id: str, hostname: Optional[str] = None, client: VultrClient = Depends(get_client)):
"""Reinstall an instance"""
return client.instances.reinstall(instance_id, hostname=hostname)
@router.get("/{instance_id}/ipv4")
async def list_ipv4(instance_id: str, client: VultrClient = Depends(get_client)):
"""List IPv4 addresses for an instance"""
return client.instances.list_ipv4(instance_id)
@router.get("/{instance_id}/ipv6")
async def list_ipv6(instance_id: str, client: VultrClient = Depends(get_client)):
"""List IPv6 addresses for an instance"""
return client.instances.list_ipv6(instance_id)
@router.get("/{instance_id}/bandwidth")
async def get_bandwidth(instance_id: str, client: VultrClient = Depends(get_client)):
"""Get bandwidth usage for an instance"""
return client.instances.bandwidth(instance_id)
@router.get("/{instance_id}/neighbors")
async def get_neighbors(instance_id: str, client: VultrClient = Depends(get_client)):
"""Get instance neighbors"""
return client.instances.neighbors(instance_id)
@router.get("/{instance_id}/user-data")
async def get_user_data(instance_id: str, client: VultrClient = Depends(get_client)):
"""Get instance user data"""
return client.instances.get_user_data(instance_id)
@router.post("/{instance_id}/backup")
async def create_backup(instance_id: str, client: VultrClient = Depends(get_client)):
"""Create a backup of an instance"""
return client.instances.create_backup(instance_id)
@router.post("/{instance_id}/restore")
async def restore_instance(
instance_id: str,
backup_id: Optional[str] = None,
snapshot_id: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""Restore an instance from backup or snapshot"""
if backup_id:
return client.instances.restore_backup(instance_id, backup_id)
elif snapshot_id:
return client.instances.restore_snapshot(instance_id, snapshot_id)
return {"error": "backup_id or snapshot_id required"}

View File

@@ -0,0 +1,108 @@
"""Load Balancers router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional, List
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class ForwardingRule(BaseModel):
frontend_protocol: str
frontend_port: int
backend_protocol: str
backend_port: int
class HealthCheck(BaseModel):
protocol: str
port: int
path: Optional[str] = None
check_interval: Optional[int] = None
response_timeout: Optional[int] = None
unhealthy_threshold: Optional[int] = None
healthy_threshold: Optional[int] = None
class CreateLoadBalancerRequest(BaseModel):
region: str
label: Optional[str] = None
balancing_algorithm: Optional[str] = None # roundrobin or leastconn
forwarding_rules: Optional[List[ForwardingRule]] = None
health_check: Optional[HealthCheck] = None
instances: Optional[List[str]] = None
ssl_redirect: Optional[bool] = None
proxy_protocol: Optional[bool] = None
class UpdateLoadBalancerRequest(BaseModel):
label: Optional[str] = None
balancing_algorithm: Optional[str] = None
ssl_redirect: Optional[bool] = None
proxy_protocol: Optional[bool] = None
@router.get("")
async def list_load_balancers(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all load balancers"""
return client.load_balancers.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_load_balancers(client: VultrClient = Depends(get_client)):
"""List all load balancers (auto-paginated)"""
return {"load_balancers": client.load_balancers.list_all()}
@router.get("/{lb_id}")
async def get_load_balancer(lb_id: str, client: VultrClient = Depends(get_client)):
"""Get load balancer details"""
return client.load_balancers.get(lb_id)
@router.post("")
async def create_load_balancer(req: CreateLoadBalancerRequest, client: VultrClient = Depends(get_client)):
"""Create a load balancer"""
data = req.model_dump(exclude_none=True)
if 'forwarding_rules' in data:
data['forwarding_rules'] = [r for r in data['forwarding_rules']]
if 'health_check' in data:
data['health_check'] = dict(data['health_check'])
return client.load_balancers.create(**data)
@router.patch("/{lb_id}")
async def update_load_balancer(lb_id: str, req: UpdateLoadBalancerRequest, client: VultrClient = Depends(get_client)):
"""Update a load balancer"""
return client.load_balancers.update(lb_id, **req.model_dump(exclude_none=True))
@router.delete("/{lb_id}")
async def delete_load_balancer(lb_id: str, client: VultrClient = Depends(get_client)):
"""Delete a load balancer"""
return client.load_balancers.delete(lb_id)
# Forwarding rules
@router.get("/{lb_id}/forwarding-rules")
async def list_forwarding_rules(lb_id: str, client: VultrClient = Depends(get_client)):
"""List forwarding rules for a load balancer"""
return client.load_balancers.list_forwarding_rules(lb_id)
@router.post("/{lb_id}/forwarding-rules")
async def create_forwarding_rule(lb_id: str, req: ForwardingRule, client: VultrClient = Depends(get_client)):
"""Create a forwarding rule"""
return client.load_balancers.create_forwarding_rule(lb_id, **req.model_dump())
@router.delete("/{lb_id}/forwarding-rules/{rule_id}")
async def delete_forwarding_rule(lb_id: str, rule_id: str, client: VultrClient = Depends(get_client)):
"""Delete a forwarding rule"""
return client.load_balancers.delete_forwarding_rule(lb_id, rule_id)

24
server/routers/os_api.py Normal file
View File

@@ -0,0 +1,24 @@
"""Operating Systems router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
@router.get("")
async def list_os(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all operating systems"""
return client.os.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_os(client: VultrClient = Depends(get_client)):
"""List all operating systems (auto-paginated)"""
return {"os": client.os.list_all()}

44
server/routers/plans.py Normal file
View File

@@ -0,0 +1,44 @@
"""Plans router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
@router.get("")
async def list_plans(
plan_type: Optional[str] = None, # vc2, vhf, vdc, etc.
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all cloud compute plans"""
return client.plans.list(plan_type=plan_type, per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_plans(
plan_type: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all cloud compute plans (auto-paginated)"""
return {"plans": client.plans.list_all(plan_type=plan_type)}
@router.get("/bare-metal")
async def list_bare_metal_plans(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all bare metal plans"""
return client.plans.list_bare_metal(per_page=per_page, cursor=cursor)
@router.get("/bare-metal/all")
async def list_all_bare_metal_plans(client: VultrClient = Depends(get_client)):
"""List all bare metal plans (auto-paginated)"""
return {"plans_metal": client.plans.list_all_bare_metal()}

34
server/routers/regions.py Normal file
View File

@@ -0,0 +1,34 @@
"""Regions router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
@router.get("")
async def list_regions(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all regions"""
return client.regions.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_regions(client: VultrClient = Depends(get_client)):
"""List all regions (auto-paginated)"""
return {"regions": client.regions.list_all()}
@router.get("/{region_id}/availability")
async def list_region_availability(
region_id: str,
plan_type: Optional[str] = None, # vc2, vhf, vdc, etc.
client: VultrClient = Depends(get_client)
):
"""List available plans in a region"""
return client.regions.list_availability(region_id, plan_type=plan_type)

View File

@@ -0,0 +1,86 @@
"""Reserved IPs router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateReservedIPRequest(BaseModel):
region: str
ip_type: str = "v4" # v4 or v6
label: Optional[str] = None
class UpdateReservedIPRequest(BaseModel):
label: str
class AttachReservedIPRequest(BaseModel):
instance_id: str
class ConvertReservedIPRequest(BaseModel):
ip_address: str
label: Optional[str] = None
@router.get("")
async def list_reserved_ips(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all reserved IPs"""
return client.reserved_ips.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_reserved_ips(client: VultrClient = Depends(get_client)):
"""List all reserved IPs (auto-paginated)"""
return {"reserved_ips": client.reserved_ips.list_all()}
@router.get("/{reserved_ip}")
async def get_reserved_ip(reserved_ip: str, client: VultrClient = Depends(get_client)):
"""Get reserved IP details"""
return client.reserved_ips.get(reserved_ip)
@router.post("")
async def create_reserved_ip(req: CreateReservedIPRequest, client: VultrClient = Depends(get_client)):
"""Create a reserved IP"""
return client.reserved_ips.create(**req.model_dump(exclude_none=True))
@router.patch("/{reserved_ip}")
async def update_reserved_ip(reserved_ip: str, req: UpdateReservedIPRequest, client: VultrClient = Depends(get_client)):
"""Update a reserved IP"""
return client.reserved_ips.update(reserved_ip, label=req.label)
@router.delete("/{reserved_ip}")
async def delete_reserved_ip(reserved_ip: str, client: VultrClient = Depends(get_client)):
"""Delete a reserved IP"""
return client.reserved_ips.delete(reserved_ip)
@router.post("/{reserved_ip}/attach")
async def attach_reserved_ip(reserved_ip: str, req: AttachReservedIPRequest, client: VultrClient = Depends(get_client)):
"""Attach reserved IP to an instance"""
return client.reserved_ips.attach(reserved_ip, instance_id=req.instance_id)
@router.post("/{reserved_ip}/detach")
async def detach_reserved_ip(reserved_ip: str, client: VultrClient = Depends(get_client)):
"""Detach reserved IP from an instance"""
return client.reserved_ips.detach(reserved_ip)
@router.post("/convert")
async def convert_to_reserved_ip(req: ConvertReservedIPRequest, client: VultrClient = Depends(get_client)):
"""Convert an instance IP to a reserved IP"""
return client.reserved_ips.convert(ip_address=req.ip_address, label=req.label)

View File

@@ -0,0 +1,59 @@
"""Snapshots router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateSnapshotRequest(BaseModel):
instance_id: str
description: Optional[str] = None
class CreateSnapshotFromURLRequest(BaseModel):
url: str
description: Optional[str] = None
@router.get("")
async def list_snapshots(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all snapshots"""
return client.snapshots.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_snapshots(client: VultrClient = Depends(get_client)):
"""List all snapshots (auto-paginated)"""
return {"snapshots": client.snapshots.list_all()}
@router.get("/{snapshot_id}")
async def get_snapshot(snapshot_id: str, client: VultrClient = Depends(get_client)):
"""Get snapshot details"""
return client.snapshots.get(snapshot_id)
@router.post("")
async def create_snapshot(req: CreateSnapshotRequest, client: VultrClient = Depends(get_client)):
"""Create a snapshot from an instance"""
return client.snapshots.create(instance_id=req.instance_id, description=req.description)
@router.post("/from-url")
async def create_snapshot_from_url(req: CreateSnapshotFromURLRequest, client: VultrClient = Depends(get_client)):
"""Create a snapshot from a URL"""
return client.snapshots.create_from_url(url=req.url, description=req.description)
@router.delete("/{snapshot_id}")
async def delete_snapshot(snapshot_id: str, client: VultrClient = Depends(get_client)):
"""Delete a snapshot"""
return client.snapshots.delete(snapshot_id)

View File

@@ -0,0 +1,59 @@
"""SSH Keys router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateSSHKeyRequest(BaseModel):
name: str
ssh_key: str
class UpdateSSHKeyRequest(BaseModel):
name: Optional[str] = None
ssh_key: Optional[str] = None
@router.get("")
async def list_ssh_keys(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all SSH keys"""
return client.ssh_keys.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_ssh_keys(client: VultrClient = Depends(get_client)):
"""List all SSH keys (auto-paginated)"""
return {"ssh_keys": client.ssh_keys.list_all()}
@router.get("/{ssh_key_id}")
async def get_ssh_key(ssh_key_id: str, client: VultrClient = Depends(get_client)):
"""Get SSH key details"""
return client.ssh_keys.get(ssh_key_id)
@router.post("")
async def create_ssh_key(req: CreateSSHKeyRequest, client: VultrClient = Depends(get_client)):
"""Create an SSH key"""
return client.ssh_keys.create(name=req.name, ssh_key=req.ssh_key)
@router.patch("/{ssh_key_id}")
async def update_ssh_key(ssh_key_id: str, req: UpdateSSHKeyRequest, client: VultrClient = Depends(get_client)):
"""Update an SSH key"""
return client.ssh_keys.update(ssh_key_id, **req.model_dump(exclude_none=True))
@router.delete("/{ssh_key_id}")
async def delete_ssh_key(ssh_key_id: str, client: VultrClient = Depends(get_client)):
"""Delete an SSH key"""
return client.ssh_keys.delete(ssh_key_id)

View File

@@ -0,0 +1,61 @@
"""Startup Scripts router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateScriptRequest(BaseModel):
name: str
script: str
type: Optional[str] = "boot" # boot or pxe
class UpdateScriptRequest(BaseModel):
name: Optional[str] = None
script: Optional[str] = None
type: Optional[str] = None
@router.get("")
async def list_startup_scripts(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all startup scripts"""
return client.startup_scripts.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_startup_scripts(client: VultrClient = Depends(get_client)):
"""List all startup scripts (auto-paginated)"""
return {"startup_scripts": client.startup_scripts.list_all()}
@router.get("/{script_id}")
async def get_startup_script(script_id: str, client: VultrClient = Depends(get_client)):
"""Get startup script details"""
return client.startup_scripts.get(script_id)
@router.post("")
async def create_startup_script(req: CreateScriptRequest, client: VultrClient = Depends(get_client)):
"""Create a startup script"""
return client.startup_scripts.create(name=req.name, script=req.script, type=req.type)
@router.patch("/{script_id}")
async def update_startup_script(script_id: str, req: UpdateScriptRequest, client: VultrClient = Depends(get_client)):
"""Update a startup script"""
return client.startup_scripts.update(script_id, **req.model_dump(exclude_none=True))
@router.delete("/{script_id}")
async def delete_startup_script(script_id: str, client: VultrClient = Depends(get_client)):
"""Delete a startup script"""
return client.startup_scripts.delete(script_id)

136
server/routers/vpc.py Normal file
View File

@@ -0,0 +1,136 @@
"""VPC router"""
from fastapi import APIRouter, Depends, Query
from typing import Optional, List
from pydantic import BaseModel
from vultr_api import VultrClient
from deps import get_client
router = APIRouter()
class CreateVPCRequest(BaseModel):
region: str
description: Optional[str] = None
v4_subnet: Optional[str] = None
v4_subnet_mask: Optional[int] = None
class UpdateVPCRequest(BaseModel):
description: str
class CreateVPC2Request(BaseModel):
region: str
description: Optional[str] = None
ip_block: Optional[str] = None
prefix_length: Optional[int] = None
class AttachVPC2Request(BaseModel):
nodes: List[dict] # [{"id": "instance_id", "ip_address": "10.0.0.1"}, ...]
# VPC 1.0
@router.get("")
async def list_vpcs(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all VPCs"""
return client.vpc.list(per_page=per_page, cursor=cursor)
@router.get("/all")
async def list_all_vpcs(client: VultrClient = Depends(get_client)):
"""List all VPCs (auto-paginated)"""
return {"vpcs": client.vpc.list_all()}
@router.get("/{vpc_id}")
async def get_vpc(vpc_id: str, client: VultrClient = Depends(get_client)):
"""Get VPC details"""
return client.vpc.get(vpc_id)
@router.post("")
async def create_vpc(req: CreateVPCRequest, client: VultrClient = Depends(get_client)):
"""Create a VPC"""
return client.vpc.create(**req.model_dump(exclude_none=True))
@router.patch("/{vpc_id}")
async def update_vpc(vpc_id: str, req: UpdateVPCRequest, client: VultrClient = Depends(get_client)):
"""Update a VPC"""
return client.vpc.update(vpc_id, description=req.description)
@router.delete("/{vpc_id}")
async def delete_vpc(vpc_id: str, client: VultrClient = Depends(get_client)):
"""Delete a VPC"""
return client.vpc.delete(vpc_id)
# VPC 2.0
@router.get("/v2/list")
async def list_vpc2s(
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List all VPC 2.0 networks"""
return client.vpc.list_vpc2(per_page=per_page, cursor=cursor)
@router.get("/v2/all")
async def list_all_vpc2s(client: VultrClient = Depends(get_client)):
"""List all VPC 2.0 networks (auto-paginated)"""
return {"vpcs": client.vpc.list_all_vpc2()}
@router.get("/v2/{vpc_id}")
async def get_vpc2(vpc_id: str, client: VultrClient = Depends(get_client)):
"""Get VPC 2.0 details"""
return client.vpc.get_vpc2(vpc_id)
@router.post("/v2")
async def create_vpc2(req: CreateVPC2Request, client: VultrClient = Depends(get_client)):
"""Create a VPC 2.0 network"""
return client.vpc.create_vpc2(**req.model_dump(exclude_none=True))
@router.patch("/v2/{vpc_id}")
async def update_vpc2(vpc_id: str, description: str, client: VultrClient = Depends(get_client)):
"""Update a VPC 2.0 network"""
return client.vpc.update_vpc2(vpc_id, description=description)
@router.delete("/v2/{vpc_id}")
async def delete_vpc2(vpc_id: str, client: VultrClient = Depends(get_client)):
"""Delete a VPC 2.0 network"""
return client.vpc.delete_vpc2(vpc_id)
@router.get("/v2/{vpc_id}/nodes")
async def list_vpc2_nodes(
vpc_id: str,
per_page: int = Query(25, le=500),
cursor: Optional[str] = None,
client: VultrClient = Depends(get_client)
):
"""List nodes attached to a VPC 2.0"""
return client.vpc.list_vpc2_nodes(vpc_id, per_page=per_page, cursor=cursor)
@router.post("/v2/{vpc_id}/nodes/attach")
async def attach_vpc2_nodes(vpc_id: str, req: AttachVPC2Request, client: VultrClient = Depends(get_client)):
"""Attach nodes to a VPC 2.0"""
return client.vpc.attach_vpc2_nodes(vpc_id, nodes=req.nodes)
@router.post("/v2/{vpc_id}/nodes/detach")
async def detach_vpc2_nodes(vpc_id: str, req: AttachVPC2Request, client: VultrClient = Depends(get_client)):
"""Detach nodes from a VPC 2.0"""
return client.vpc.detach_vpc2_nodes(vpc_id, nodes=req.nodes)

814
uv.lock generated Normal file
View File

@@ -0,0 +1,814 @@
version = 1
revision = 3
requires-python = ">=3.8"
resolution-markers = [
"python_full_version >= '3.10'",
"python_full_version == '3.9.*'",
"python_full_version < '3.9'",
]
[[package]]
name = "certifi"
version = "2026.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" },
{ url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" },
{ url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" },
{ url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" },
{ url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" },
{ url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" },
{ url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" },
{ url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" },
{ url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" },
{ url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" },
{ url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" },
{ url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" },
{ url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" },
{ url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" },
{ url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" },
{ url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
{ url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
{ url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
{ url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
{ url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
{ url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
{ url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
{ url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
{ url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
{ url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
{ url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
{ url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
{ url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
{ url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
{ url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
{ url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
{ url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
{ url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
{ url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
{ url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
{ url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
{ url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
{ url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
{ url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
{ url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
{ url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
{ url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
{ url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
{ url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
{ url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
{ url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
{ url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4e/3926a1c11f0433791985727965263f788af00db3482d89a7545ca5ecc921/charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", size = 198599, upload-time = "2025-10-14T04:41:53.213Z" },
{ url = "https://files.pythonhosted.org/packages/ec/7c/b92d1d1dcffc34592e71ea19c882b6709e43d20fa498042dea8b815638d7/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", size = 143090, upload-time = "2025-10-14T04:41:54.385Z" },
{ url = "https://files.pythonhosted.org/packages/84/ce/61a28d3bb77281eb24107b937a497f3c43089326d27832a63dcedaab0478/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", size = 139490, upload-time = "2025-10-14T04:41:55.551Z" },
{ url = "https://files.pythonhosted.org/packages/c0/bd/c9e59a91b2061c6f8bb98a150670cb16d4cd7c4ba7d11ad0cdf789155f41/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", size = 155334, upload-time = "2025-10-14T04:41:56.724Z" },
{ url = "https://files.pythonhosted.org/packages/bf/37/f17ae176a80f22ff823456af91ba3bc59df308154ff53aef0d39eb3d3419/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", size = 152823, upload-time = "2025-10-14T04:41:58.236Z" },
{ url = "https://files.pythonhosted.org/packages/bf/fa/cf5bb2409a385f78750e78c8d2e24780964976acdaaed65dbd6083ae5b40/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", size = 147618, upload-time = "2025-10-14T04:41:59.409Z" },
{ url = "https://files.pythonhosted.org/packages/9b/63/579784a65bc7de2d4518d40bb8f1870900163e86f17f21fd1384318c459d/charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", size = 145516, upload-time = "2025-10-14T04:42:00.579Z" },
{ url = "https://files.pythonhosted.org/packages/a3/a9/94ec6266cd394e8f93a4d69cca651d61bf6ac58d2a0422163b30c698f2c7/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", size = 145266, upload-time = "2025-10-14T04:42:01.684Z" },
{ url = "https://files.pythonhosted.org/packages/09/14/d6626eb97764b58c2779fa7928fa7d1a49adb8ce687c2dbba4db003c1939/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", size = 139559, upload-time = "2025-10-14T04:42:02.902Z" },
{ url = "https://files.pythonhosted.org/packages/09/01/ddbe6b01313ba191dbb0a43c7563bc770f2448c18127f9ea4b119c44dff0/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", size = 156653, upload-time = "2025-10-14T04:42:04.005Z" },
{ url = "https://files.pythonhosted.org/packages/95/c8/d05543378bea89296e9af4510b44c704626e191da447235c8fdedfc5b7b2/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", size = 145644, upload-time = "2025-10-14T04:42:05.211Z" },
{ url = "https://files.pythonhosted.org/packages/72/01/2866c4377998ef8a1f6802f6431e774a4c8ebe75b0a6e569ceec55c9cbfb/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", size = 153964, upload-time = "2025-10-14T04:42:06.341Z" },
{ url = "https://files.pythonhosted.org/packages/4a/66/66c72468a737b4cbd7851ba2c522fe35c600575fbeac944460b4fd4a06fe/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", size = 148777, upload-time = "2025-10-14T04:42:07.535Z" },
{ url = "https://files.pythonhosted.org/packages/50/94/d0d56677fdddbffa8ca00ec411f67bb8c947f9876374ddc9d160d4f2c4b3/charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", size = 98687, upload-time = "2025-10-14T04:42:08.678Z" },
{ url = "https://files.pythonhosted.org/packages/00/64/c3bc303d1b586480b1c8e6e1e2191a6d6dd40255244e5cf16763dcec52e6/charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", size = 106115, upload-time = "2025-10-14T04:42:09.793Z" },
{ url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" },
{ url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" },
{ url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" },
{ url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" },
{ url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" },
{ url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" },
{ url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" },
{ url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" },
{ url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" },
{ url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" },
{ url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" },
{ url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" },
{ url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" },
{ url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" },
{ url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" },
{ url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "coverage"
version = "7.6.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
]
sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791, upload-time = "2024-08-04T19:45:30.9Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690, upload-time = "2024-08-04T19:43:07.695Z" },
{ url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127, upload-time = "2024-08-04T19:43:10.15Z" },
{ url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654, upload-time = "2024-08-04T19:43:12.405Z" },
{ url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598, upload-time = "2024-08-04T19:43:14.078Z" },
{ url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732, upload-time = "2024-08-04T19:43:16.632Z" },
{ url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816, upload-time = "2024-08-04T19:43:19.049Z" },
{ url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325, upload-time = "2024-08-04T19:43:21.246Z" },
{ url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418, upload-time = "2024-08-04T19:43:22.945Z" },
{ url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343, upload-time = "2024-08-04T19:43:25.121Z" },
{ url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136, upload-time = "2024-08-04T19:43:26.851Z" },
{ url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796, upload-time = "2024-08-04T19:43:29.115Z" },
{ url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244, upload-time = "2024-08-04T19:43:31.285Z" },
{ url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279, upload-time = "2024-08-04T19:43:33.581Z" },
{ url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859, upload-time = "2024-08-04T19:43:35.301Z" },
{ url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549, upload-time = "2024-08-04T19:43:37.578Z" },
{ url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477, upload-time = "2024-08-04T19:43:39.92Z" },
{ url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134, upload-time = "2024-08-04T19:43:41.453Z" },
{ url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910, upload-time = "2024-08-04T19:43:43.037Z" },
{ url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348, upload-time = "2024-08-04T19:43:44.787Z" },
{ url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230, upload-time = "2024-08-04T19:43:46.707Z" },
{ url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983, upload-time = "2024-08-04T19:43:49.082Z" },
{ url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221, upload-time = "2024-08-04T19:43:52.15Z" },
{ url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342, upload-time = "2024-08-04T19:43:53.746Z" },
{ url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371, upload-time = "2024-08-04T19:43:55.993Z" },
{ url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455, upload-time = "2024-08-04T19:43:57.618Z" },
{ url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924, upload-time = "2024-08-04T19:44:00.012Z" },
{ url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252, upload-time = "2024-08-04T19:44:01.713Z" },
{ url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897, upload-time = "2024-08-04T19:44:03.898Z" },
{ url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606, upload-time = "2024-08-04T19:44:05.532Z" },
{ url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373, upload-time = "2024-08-04T19:44:07.079Z" },
{ url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007, upload-time = "2024-08-04T19:44:09.453Z" },
{ url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269, upload-time = "2024-08-04T19:44:11.045Z" },
{ url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886, upload-time = "2024-08-04T19:44:12.83Z" },
{ url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037, upload-time = "2024-08-04T19:44:15.393Z" },
{ url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038, upload-time = "2024-08-04T19:44:17.466Z" },
{ url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690, upload-time = "2024-08-04T19:44:19.336Z" },
{ url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765, upload-time = "2024-08-04T19:44:20.994Z" },
{ url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611, upload-time = "2024-08-04T19:44:22.616Z" },
{ url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671, upload-time = "2024-08-04T19:44:24.418Z" },
{ url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368, upload-time = "2024-08-04T19:44:26.276Z" },
{ url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758, upload-time = "2024-08-04T19:44:29.028Z" },
{ url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035, upload-time = "2024-08-04T19:44:30.673Z" },
{ url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839, upload-time = "2024-08-04T19:44:32.412Z" },
{ url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569, upload-time = "2024-08-04T19:44:34.547Z" },
{ url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927, upload-time = "2024-08-04T19:44:36.313Z" },
{ url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401, upload-time = "2024-08-04T19:44:38.155Z" },
{ url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301, upload-time = "2024-08-04T19:44:39.883Z" },
{ url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598, upload-time = "2024-08-04T19:44:41.59Z" },
{ url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307, upload-time = "2024-08-04T19:44:43.301Z" },
{ url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453, upload-time = "2024-08-04T19:44:45.677Z" },
{ url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674, upload-time = "2024-08-04T19:44:47.694Z" },
{ url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101, upload-time = "2024-08-04T19:44:49.32Z" },
{ url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554, upload-time = "2024-08-04T19:44:51.631Z" },
{ url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440, upload-time = "2024-08-04T19:44:53.464Z" },
{ url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889, upload-time = "2024-08-04T19:44:55.165Z" },
{ url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142, upload-time = "2024-08-04T19:44:57.269Z" },
{ url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805, upload-time = "2024-08-04T19:44:59.033Z" },
{ url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655, upload-time = "2024-08-04T19:45:01.398Z" },
{ url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296, upload-time = "2024-08-04T19:45:03.819Z" },
{ url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137, upload-time = "2024-08-04T19:45:06.25Z" },
{ url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688, upload-time = "2024-08-04T19:45:08.358Z" },
{ url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120, upload-time = "2024-08-04T19:45:11.526Z" },
{ url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249, upload-time = "2024-08-04T19:45:13.202Z" },
{ url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237, upload-time = "2024-08-04T19:45:14.961Z" },
{ url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311, upload-time = "2024-08-04T19:45:16.924Z" },
{ url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453, upload-time = "2024-08-04T19:45:18.672Z" },
{ url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958, upload-time = "2024-08-04T19:45:20.63Z" },
{ url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938, upload-time = "2024-08-04T19:45:23.062Z" },
{ url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352, upload-time = "2024-08-04T19:45:25.042Z" },
{ url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153, upload-time = "2024-08-04T19:45:27.079Z" },
{ url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926, upload-time = "2024-08-04T19:45:28.875Z" },
]
[package.optional-dependencies]
toml = [
{ name = "tomli", marker = "python_full_version < '3.9'" },
]
[[package]]
name = "coverage"
version = "7.10.7"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version == '3.9.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" },
{ url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" },
{ url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" },
{ url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" },
{ url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" },
{ url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" },
{ url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" },
{ url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" },
{ url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" },
{ url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" },
{ url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" },
{ url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" },
{ url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" },
{ url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" },
{ url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" },
{ url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" },
{ url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" },
{ url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" },
{ url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" },
{ url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" },
{ url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" },
{ url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" },
{ url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" },
{ url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" },
{ url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" },
{ url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" },
{ url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" },
{ url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" },
{ url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" },
{ url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" },
{ url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" },
{ url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" },
{ url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" },
{ url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" },
{ url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" },
{ url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" },
{ url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" },
{ url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" },
{ url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" },
{ url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" },
{ url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" },
{ url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" },
{ url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" },
{ url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" },
{ url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" },
{ url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" },
{ url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" },
{ url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" },
{ url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" },
{ url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" },
{ url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" },
{ url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" },
{ url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" },
{ url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" },
{ url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" },
{ url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" },
{ url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" },
{ url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" },
{ url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" },
{ url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" },
{ url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" },
{ url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" },
{ url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" },
{ url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" },
{ url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" },
{ url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" },
{ url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" },
{ url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" },
{ url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" },
{ url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" },
{ url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" },
{ url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" },
{ url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" },
{ url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" },
{ url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" },
{ url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" },
{ url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" },
{ url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" },
{ url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" },
{ url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" },
{ url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" },
{ url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" },
{ url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" },
{ url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" },
{ url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" },
{ url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" },
{ url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" },
{ url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" },
{ url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" },
{ url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" },
{ url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" },
{ url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" },
{ url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" },
{ url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" },
{ url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" },
{ url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" },
{ url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" },
{ url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" },
{ url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" },
{ url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" },
{ url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" },
{ url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" },
{ url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" },
]
[package.optional-dependencies]
toml = [
{ name = "tomli", marker = "python_full_version == '3.9.*'" },
]
[[package]]
name = "coverage"
version = "7.13.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
]
sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" },
{ url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" },
{ url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" },
{ url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" },
{ url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" },
{ url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" },
{ url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" },
{ url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" },
{ url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" },
{ url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" },
{ url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" },
{ url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" },
{ url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" },
{ url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" },
{ url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" },
{ url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" },
{ url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" },
{ url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" },
{ url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" },
{ url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" },
{ url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" },
{ url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" },
{ url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" },
{ url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" },
{ url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" },
{ url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" },
{ url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" },
{ url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" },
{ url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" },
{ url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" },
{ url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" },
{ url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" },
{ url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" },
{ url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" },
{ url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" },
{ url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" },
{ url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" },
{ url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" },
{ url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" },
{ url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" },
{ url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" },
{ url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" },
{ url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" },
{ url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" },
{ url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" },
{ url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" },
{ url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" },
{ url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" },
{ url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" },
{ url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" },
{ url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" },
{ url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" },
{ url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" },
{ url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" },
{ url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" },
{ url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" },
{ url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" },
{ url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" },
{ url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" },
{ url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" },
{ url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" },
{ url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" },
{ url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" },
{ url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" },
{ url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" },
{ url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" },
{ url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" },
{ url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" },
{ url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" },
{ url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" },
{ url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" },
{ url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" },
{ url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" },
{ url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" },
{ url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" },
{ url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" },
{ url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" },
{ url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" },
{ url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" },
{ url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" },
{ url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" },
{ url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" },
{ url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" },
{ url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" },
{ url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" },
{ url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" },
{ url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" },
{ url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" },
{ url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" },
{ url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" },
]
[package.optional-dependencies]
toml = [
{ name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" },
]
[[package]]
name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "iniconfig"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version == '3.9.*'",
"python_full_version < '3.9'",
]
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
]
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pluggy"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
]
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
"python_full_version == '3.9.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pytest"
version = "8.3.5"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
]
dependencies = [
{ name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.9'" },
{ name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
{ name = "packaging", marker = "python_full_version < '3.9'" },
{ name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
{ name = "tomli", marker = "python_full_version < '3.9'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
]
[[package]]
name = "pytest"
version = "8.4.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version == '3.9.*'",
]
dependencies = [
{ name = "colorama", marker = "python_full_version == '3.9.*' and sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version == '3.9.*'" },
{ name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
{ name = "packaging", marker = "python_full_version == '3.9.*'" },
{ name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
{ name = "pygments", marker = "python_full_version == '3.9.*'" },
{ name = "tomli", marker = "python_full_version == '3.9.*'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
]
[[package]]
name = "pytest"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
]
dependencies = [
{ name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version == '3.10.*'" },
{ name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "packaging", marker = "python_full_version >= '3.10'" },
{ name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "pygments", marker = "python_full_version >= '3.10'" },
{ name = "tomli", marker = "python_full_version == '3.10.*'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
name = "pytest-cov"
version = "5.0.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
]
dependencies = [
{ name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.9'" },
{ name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042, upload-time = "2024-03-24T20:16:34.856Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990, upload-time = "2024-03-24T20:16:32.444Z" },
]
[[package]]
name = "pytest-cov"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
"python_full_version == '3.9.*'",
]
dependencies = [
{ name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version == '3.9.*'" },
{ name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" },
{ name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
{ name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]
[[package]]
name = "requests"
version = "2.32.4"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
]
dependencies = [
{ name = "certifi", marker = "python_full_version < '3.9'" },
{ name = "charset-normalizer", marker = "python_full_version < '3.9'" },
{ name = "idna", marker = "python_full_version < '3.9'" },
{ name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
]
[[package]]
name = "requests"
version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
"python_full_version == '3.9.*'",
]
dependencies = [
{ name = "certifi", marker = "python_full_version >= '3.9'" },
{ name = "charset-normalizer", marker = "python_full_version >= '3.9'" },
{ name = "idna", marker = "python_full_version >= '3.9'" },
{ name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
[[package]]
name = "tomli"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
{ url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
{ url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
{ url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
{ url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
{ url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
{ url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
{ url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
{ url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
{ url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
{ url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
{ url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
{ url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
{ url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
{ url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
{ url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
{ url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
{ url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
{ url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
{ url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
{ url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
{ url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
{ url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
{ url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
{ url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
{ url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
{ url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
{ url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
{ url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
{ url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
{ url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
{ url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
{ url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
{ url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
{ url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
{ url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
{ url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
{ url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
{ url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
{ url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
{ url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
{ url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
{ url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
{ url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
{ url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
]
[[package]]
name = "typing-extensions"
version = "4.13.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
]
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
"python_full_version == '3.9.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "urllib3"
version = "2.2.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
]
sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677, upload-time = "2024-09-12T10:52:18.401Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338, upload-time = "2024-09-12T10:52:16.589Z" },
]
[[package]]
name = "urllib3"
version = "2.6.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
"python_full_version == '3.9.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
]
[[package]]
name = "vultr-api"
version = "1.0.0"
source = { editable = "." }
dependencies = [
{ name = "requests", version = "2.32.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
{ name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
]
[package.optional-dependencies]
dev = [
{ name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
{ name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "pytest-cov", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
{ name = "pytest-cov", version = "7.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
]
[package.metadata]
requires-dist = [
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" },
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" },
{ name = "requests", specifier = ">=2.25.0" },
]
provides-extras = ["dev"]

View File

@@ -0,0 +1,16 @@
[Unit]
Description=Vultr API Server
After=network-online.target
[Container]
Image=localhost/vultr-api-server:latest
ContainerName=vultr-api-server
PublishPort=8001:8000
AutoUpdate=local
[Service]
Restart=always
TimeoutStartSec=300
[Install]
WantedBy=default.target

8
vultr_api/__init__.py Normal file
View File

@@ -0,0 +1,8 @@
"""
Vultr API v2 Python Wrapper
"""
from .client import VultrClient
__version__ = "1.0.0"
__all__ = ["VultrClient"]

214
vultr_api/client.py Normal file
View File

@@ -0,0 +1,214 @@
"""
Vultr API v2 Client
"""
import requests
from typing import Optional, Dict, Any, List
from urllib.parse import urljoin
from .resources.account import AccountResource
from .resources.instances import InstancesResource
from .resources.dns import DNSResource
from .resources.firewall import FirewallResource
from .resources.ssh_keys import SSHKeysResource
from .resources.startup_scripts import StartupScriptsResource
from .resources.snapshots import SnapshotsResource
from .resources.block_storage import BlockStorageResource
from .resources.reserved_ips import ReservedIPsResource
from .resources.vpc import VPCResource
from .resources.load_balancers import LoadBalancersResource
from .resources.bare_metal import BareMetalResource
from .resources.plans import PlansResource
from .resources.regions import RegionsResource
from .resources.os import OSResource
from .resources.backups import BackupsResource
class VultrAPIError(Exception):
"""Vultr API Error"""
def __init__(self, message: str, status_code: int = None, response: dict = None):
self.message = message
self.status_code = status_code
self.response = response
super().__init__(self.message)
class VultrClient:
"""
Vultr API v2 Client
Usage:
client = VultrClient(api_key="your-api-key")
# Get account info
account = client.account.get()
# List instances
instances = client.instances.list()
# Create instance
instance = client.instances.create(
region="ewr",
plan="vc2-1c-1gb",
os_id=215
)
"""
BASE_URL = "https://api.vultr.com/v2/"
def __init__(self, api_key: str, timeout: int = 30):
"""
Initialize Vultr API client
Args:
api_key: Vultr API key (get from https://my.vultr.com/settings/#settingsapi)
timeout: Request timeout in seconds
"""
self.api_key = api_key
self.timeout = timeout
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
})
# Initialize resources
self.account = AccountResource(self)
self.instances = InstancesResource(self)
self.dns = DNSResource(self)
self.firewall = FirewallResource(self)
self.ssh_keys = SSHKeysResource(self)
self.startup_scripts = StartupScriptsResource(self)
self.snapshots = SnapshotsResource(self)
self.block_storage = BlockStorageResource(self)
self.reserved_ips = ReservedIPsResource(self)
self.vpc = VPCResource(self)
self.load_balancers = LoadBalancersResource(self)
self.bare_metal = BareMetalResource(self)
self.plans = PlansResource(self)
self.regions = RegionsResource(self)
self.os = OSResource(self)
self.backups = BackupsResource(self)
def _request(
self,
method: str,
endpoint: str,
params: Optional[Dict] = None,
data: Optional[Dict] = None,
) -> Optional[Dict[str, Any]]:
"""
Make API request
Args:
method: HTTP method (GET, POST, PATCH, DELETE)
endpoint: API endpoint (e.g., "instances")
params: Query parameters
data: Request body data
Returns:
Response JSON or None for 204 responses
"""
url = urljoin(self.BASE_URL, endpoint)
try:
response = self.session.request(
method=method,
url=url,
params=params,
json=data,
timeout=self.timeout,
)
if response.status_code == 204:
return None
if response.status_code >= 400:
error_data = None
try:
error_data = response.json()
except:
pass
error_msg = f"API Error: {response.status_code}"
if error_data and "error" in error_data:
error_msg = error_data["error"]
raise VultrAPIError(
message=error_msg,
status_code=response.status_code,
response=error_data,
)
if response.text:
return response.json()
return None
except requests.RequestException as e:
raise VultrAPIError(f"Request failed: {str(e)}")
def get(self, endpoint: str, params: Optional[Dict] = None) -> Optional[Dict]:
"""GET request"""
return self._request("GET", endpoint, params=params)
def post(self, endpoint: str, data: Optional[Dict] = None) -> Optional[Dict]:
"""POST request"""
return self._request("POST", endpoint, data=data)
def patch(self, endpoint: str, data: Optional[Dict] = None) -> Optional[Dict]:
"""PATCH request"""
return self._request("PATCH", endpoint, data=data)
def put(self, endpoint: str, data: Optional[Dict] = None) -> Optional[Dict]:
"""PUT request"""
return self._request("PUT", endpoint, data=data)
def delete(self, endpoint: str) -> None:
"""DELETE request"""
self._request("DELETE", endpoint)
def paginate(
self,
endpoint: str,
key: str,
params: Optional[Dict] = None,
per_page: int = 100,
) -> List[Dict]:
"""
Paginate through all results
Args:
endpoint: API endpoint
key: Response key containing items (e.g., "instances")
params: Additional query parameters
per_page: Items per page (max 500)
Returns:
List of all items
"""
params = params or {}
params["per_page"] = per_page
all_items = []
cursor = None
while True:
if cursor:
params["cursor"] = cursor
response = self.get(endpoint, params=params)
if not response:
break
items = response.get(key, [])
all_items.extend(items)
meta = response.get("meta", {})
links = meta.get("links", {})
cursor = links.get("next", "")
if not cursor:
break
return all_items

View File

@@ -0,0 +1,41 @@
"""
Vultr API Resources
"""
from .base import BaseResource
from .account import AccountResource
from .instances import InstancesResource
from .dns import DNSResource
from .firewall import FirewallResource
from .ssh_keys import SSHKeysResource
from .startup_scripts import StartupScriptsResource
from .snapshots import SnapshotsResource
from .block_storage import BlockStorageResource
from .reserved_ips import ReservedIPsResource
from .vpc import VPCResource
from .load_balancers import LoadBalancersResource
from .bare_metal import BareMetalResource
from .plans import PlansResource
from .regions import RegionsResource
from .os import OSResource
from .backups import BackupsResource
__all__ = [
"BaseResource",
"AccountResource",
"InstancesResource",
"DNSResource",
"FirewallResource",
"SSHKeysResource",
"StartupScriptsResource",
"SnapshotsResource",
"BlockStorageResource",
"ReservedIPsResource",
"VPCResource",
"LoadBalancersResource",
"BareMetalResource",
"PlansResource",
"RegionsResource",
"OSResource",
"BackupsResource",
]

View File

@@ -0,0 +1,115 @@
"""
Account Resource
Includes account info and ACL (Access Control List) management
"""
from typing import Dict, List, Optional
from .base import BaseResource
class AccountResource(BaseResource):
"""
Account and ACL management
Usage:
# Get account info
account = client.account.get()
# Get bandwidth
bandwidth = client.account.get_bandwidth()
# ACL (IP access control)
acl = client.account.get_acl()
client.account.set_acl(acls=["192.168.1.1/32", "10.0.0.0/8"])
"""
def get(self) -> Dict:
"""
Get account information
Returns:
Account info including name, email, balance, etc.
"""
response = self.client.get("account")
return response.get("account", {})
def get_bandwidth(self) -> Dict:
"""
Get account bandwidth usage
Returns:
Bandwidth usage info
"""
return self.client.get("account/bandwidth")
# ACL (Access Control List) methods
def get_acl(self) -> Dict:
"""
Get API access control list
Returns:
ACL info including enabled status and list of allowed IPs/CIDRs
"""
return self.client.get("account/acl")
def set_acl(self, acls: List[str]) -> Dict:
"""
Update API access control list
Args:
acls: List of IP addresses or CIDR ranges to allow.
Pass empty list to disable ACL.
Returns:
Updated ACL info
Example:
# Allow specific IPs
client.account.set_acl(["192.168.1.100/32", "10.0.0.0/8"])
# Disable ACL (allow all)
client.account.set_acl([])
"""
return self.client.post("account/acl", {"acls": acls})
def add_acl(self, ip: str) -> Dict:
"""
Add an IP/CIDR to the ACL
Args:
ip: IP address or CIDR to add (e.g., "192.168.1.1/32")
Returns:
Updated ACL info
"""
current = self.get_acl()
acls = current.get("acls", [])
if ip not in acls:
acls.append(ip)
return self.set_acl(acls)
def remove_acl(self, ip: str) -> Dict:
"""
Remove an IP/CIDR from the ACL
Args:
ip: IP address or CIDR to remove
Returns:
Updated ACL info
"""
current = self.get_acl()
acls = current.get("acls", [])
if ip in acls:
acls.remove(ip)
return self.set_acl(acls)
def clear_acl(self) -> Dict:
"""
Clear all ACL entries (disable ACL)
Returns:
Updated ACL info
"""
return self.set_acl([])

View File

@@ -0,0 +1,65 @@
"""
Backups Resource
Backup management
"""
from typing import Dict, List
from .base import BaseResource
class BackupsResource(BaseResource):
"""
Backup management
Usage:
# List backups
backups = client.backups.list()
# Get backup details
backup = client.backups.get("backup-id")
"""
def list(
self,
instance_id: str = None,
per_page: int = 100,
cursor: str = None
) -> Dict:
"""
List backups
Args:
instance_id: Filter by instance ID
per_page: Items per page
cursor: Pagination cursor
Returns:
Dict with 'backups' list and 'meta' pagination
"""
params = {"per_page": per_page}
if instance_id:
params["instance_id"] = instance_id
if cursor:
params["cursor"] = cursor
return self.client.get("backups", params=params)
def list_all(self, instance_id: str = None) -> List[Dict]:
"""List all backups (auto-paginate)"""
params = {}
if instance_id:
params["instance_id"] = instance_id
return self.client.paginate("backups", "backups", params=params)
def get(self, backup_id: str) -> Dict:
"""
Get backup details
Args:
backup_id: Backup ID
Returns:
Backup details
"""
response = self.client.get(f"backups/{backup_id}")
return response.get("backup", {})

View File

@@ -0,0 +1,291 @@
"""
Bare Metal Resource
Bare metal server management
"""
from typing import Dict, List
from .base import BaseResource
class BareMetalResource(BaseResource):
"""
Bare metal server management
Usage:
# List bare metal servers
servers = client.bare_metal.list()
# Create server
server = client.bare_metal.create(
region="ewr",
plan="vbm-4c-32gb",
os_id=215
)
"""
def list(
self,
per_page: int = 100,
cursor: str = None,
tag: str = None,
label: str = None,
main_ip: str = None,
region: str = None,
) -> Dict:
"""
List bare metal servers
Returns:
Dict with 'bare_metals' list and 'meta' pagination
"""
params = {"per_page": per_page}
if cursor:
params["cursor"] = cursor
if tag:
params["tag"] = tag
if label:
params["label"] = label
if main_ip:
params["main_ip"] = main_ip
if region:
params["region"] = region
return self.client.get("bare-metals", params=params)
def list_all(self, **kwargs) -> List[Dict]:
"""List all bare metal servers (auto-paginate)"""
return self.client.paginate("bare-metals", "bare_metals", params=kwargs)
def get(self, baremetal_id: str) -> Dict:
"""
Get bare metal server details
Args:
baremetal_id: Bare metal ID
Returns:
Server details
"""
response = self.client.get(f"bare-metals/{baremetal_id}")
return response.get("bare_metal", {})
def create(
self,
region: str,
plan: str,
os_id: int = None,
script_id: str = None,
snapshot_id: str = None,
enable_ipv6: bool = False,
label: str = None,
sshkey_id: List[str] = None,
app_id: int = None,
image_id: str = None,
user_data: str = None,
activation_email: bool = False,
hostname: str = None,
tag: str = None,
tags: List[str] = None,
reserved_ipv4: str = None,
persistent_pxe: bool = False,
attach_vpc2: List[str] = None,
) -> Dict:
"""
Create a bare metal server
Args:
region: Region ID
plan: Plan ID
os_id: Operating System ID
script_id: Startup script ID
snapshot_id: Snapshot ID
enable_ipv6: Enable IPv6
label: Server label
sshkey_id: SSH key IDs
app_id: Application ID
image_id: Custom image ID
user_data: Cloud-init user data
activation_email: Send activation email
hostname: Server hostname
tag: Deprecated, use tags
tags: Tags
reserved_ipv4: Reserved IPv4 to assign
persistent_pxe: Enable persistent PXE
attach_vpc2: VPC 2.0 IDs to attach
Returns:
Created server details
"""
data = {"region": region, "plan": plan}
if os_id is not None:
data["os_id"] = os_id
if script_id:
data["script_id"] = script_id
if snapshot_id:
data["snapshot_id"] = snapshot_id
if enable_ipv6:
data["enable_ipv6"] = enable_ipv6
if label:
data["label"] = label
if sshkey_id:
data["sshkey_id"] = sshkey_id
if app_id is not None:
data["app_id"] = app_id
if image_id:
data["image_id"] = image_id
if user_data:
data["user_data"] = user_data
if activation_email:
data["activation_email"] = activation_email
if hostname:
data["hostname"] = hostname
if tag:
data["tag"] = tag
if tags:
data["tags"] = tags
if reserved_ipv4:
data["reserved_ipv4"] = reserved_ipv4
if persistent_pxe:
data["persistent_pxe"] = persistent_pxe
if attach_vpc2:
data["attach_vpc2"] = attach_vpc2
response = self.client.post("bare-metals", data)
return response.get("bare_metal", {})
def update(
self,
baremetal_id: str,
user_data: str = None,
label: str = None,
tag: str = None,
tags: List[str] = None,
os_id: int = None,
app_id: int = None,
image_id: str = None,
enable_ipv6: bool = None,
) -> Dict:
"""
Update a bare metal server
Args:
baremetal_id: Bare metal ID
(other args same as create)
Returns:
Updated server details
"""
data = {}
if user_data is not None:
data["user_data"] = user_data
if label is not None:
data["label"] = label
if tag is not None:
data["tag"] = tag
if tags is not None:
data["tags"] = tags
if os_id is not None:
data["os_id"] = os_id
if app_id is not None:
data["app_id"] = app_id
if image_id is not None:
data["image_id"] = image_id
if enable_ipv6 is not None:
data["enable_ipv6"] = enable_ipv6
response = self.client.patch(f"bare-metals/{baremetal_id}", data)
return response.get("bare_metal", {})
def delete(self, baremetal_id: str) -> None:
"""
Delete a bare metal server
Args:
baremetal_id: Bare metal ID to delete
"""
self.client.delete(f"bare-metals/{baremetal_id}")
def start(self, baremetal_id: str) -> None:
"""Start a bare metal server"""
self.client.post(f"bare-metals/{baremetal_id}/start")
def halt(self, baremetal_id: str) -> None:
"""Halt a bare metal server"""
self.client.post(f"bare-metals/{baremetal_id}/halt")
def reboot(self, baremetal_id: str) -> None:
"""Reboot a bare metal server"""
self.client.post(f"bare-metals/{baremetal_id}/reboot")
def reinstall(self, baremetal_id: str, hostname: str = None) -> Dict:
"""
Reinstall a bare metal server
Args:
baremetal_id: Bare metal ID
hostname: New hostname (optional)
Returns:
Server details
"""
data = {}
if hostname:
data["hostname"] = hostname
response = self.client.post(f"bare-metals/{baremetal_id}/reinstall", data)
return response.get("bare_metal", {})
def get_bandwidth(self, baremetal_id: str) -> Dict:
"""Get bandwidth usage"""
return self.client.get(f"bare-metals/{baremetal_id}/bandwidth")
def get_user_data(self, baremetal_id: str) -> Dict:
"""Get user data"""
return self.client.get(f"bare-metals/{baremetal_id}/user-data")
def get_upgrades(self, baremetal_id: str, upgrade_type: str = None) -> Dict:
"""Get available upgrades"""
params = {}
if upgrade_type:
params["type"] = upgrade_type
return self.client.get(f"bare-metals/{baremetal_id}/upgrades", params=params)
def get_vnc(self, baremetal_id: str) -> Dict:
"""Get VNC URL"""
return self.client.get(f"bare-metals/{baremetal_id}/vnc")
# IPv4 management
def list_ipv4(self, baremetal_id: str) -> List[Dict]:
"""List IPv4 addresses"""
response = self.client.get(f"bare-metals/{baremetal_id}/ipv4")
return response.get("ipv4s", [])
def list_ipv6(self, baremetal_id: str) -> List[Dict]:
"""List IPv6 addresses"""
response = self.client.get(f"bare-metals/{baremetal_id}/ipv6")
return response.get("ipv6s", [])
# VPC 2.0
def list_vpc2(self, baremetal_id: str) -> List[Dict]:
"""List attached VPC 2.0 networks"""
response = self.client.get(f"bare-metals/{baremetal_id}/vpc2")
return response.get("vpcs", [])
def attach_vpc2(
self,
baremetal_id: str,
vpc_id: str,
ip_address: str = None
) -> None:
"""Attach VPC 2.0 to bare metal"""
data = {"vpc_id": vpc_id}
if ip_address:
data["ip_address"] = ip_address
self.client.post(f"bare-metals/{baremetal_id}/vpc2/attach", data)
def detach_vpc2(self, baremetal_id: str, vpc_id: str) -> None:
"""Detach VPC 2.0 from bare metal"""
self.client.post(f"bare-metals/{baremetal_id}/vpc2/detach", {"vpc_id": vpc_id})

View File

@@ -0,0 +1,15 @@
"""
Base Resource class
"""
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..client import VultrClient
class BaseResource:
"""Base class for API resources"""
def __init__(self, client: "VultrClient"):
self.client = client

View File

@@ -0,0 +1,135 @@
"""
Block Storage Resource
Block storage management
"""
from typing import Dict, List
from .base import BaseResource
class BlockStorageResource(BaseResource):
"""
Block storage management
Usage:
# List block storage
blocks = client.block_storage.list()
# Create block storage
block = client.block_storage.create(
region="ewr",
size_gb=50,
label="my-storage"
)
# Attach to instance
client.block_storage.attach(block_id="block-id", instance_id="instance-id")
"""
def list(self, per_page: int = 100, cursor: str = None) -> Dict:
"""
List block storage volumes
Returns:
Dict with 'blocks' list and 'meta' pagination
"""
params = {"per_page": per_page}
if cursor:
params["cursor"] = cursor
return self.client.get("blocks", params=params)
def list_all(self) -> List[Dict]:
"""List all block storage volumes (auto-paginate)"""
return self.client.paginate("blocks", "blocks")
def get(self, block_id: str) -> Dict:
"""
Get block storage details
Args:
block_id: Block storage ID
Returns:
Block storage details
"""
response = self.client.get(f"blocks/{block_id}")
return response.get("block", {})
def create(
self,
region: str,
size_gb: int,
label: str = None,
block_type: str = None
) -> Dict:
"""
Create block storage
Args:
region: Region ID
size_gb: Size in GB (10-40000)
label: Label
block_type: Storage type ("high_perf" or "storage_opt")
Returns:
Created block storage details
"""
data = {"region": region, "size_gb": size_gb}
if label:
data["label"] = label
if block_type:
data["block_type"] = block_type
response = self.client.post("blocks", data)
return response.get("block", {})
def update(self, block_id: str, label: str = None, size_gb: int = None) -> None:
"""
Update block storage
Args:
block_id: Block ID
label: New label
size_gb: New size (can only increase)
"""
data = {}
if label:
data["label"] = label
if size_gb:
data["size_gb"] = size_gb
self.client.patch(f"blocks/{block_id}", data)
def delete(self, block_id: str) -> None:
"""
Delete block storage
Args:
block_id: Block ID to delete
"""
self.client.delete(f"blocks/{block_id}")
def attach(self, block_id: str, instance_id: str, live: bool = False) -> None:
"""
Attach block storage to instance
Args:
block_id: Block storage ID
instance_id: Instance ID to attach to
live: Live attach (no reboot)
"""
self.client.post(f"blocks/{block_id}/attach", {
"instance_id": instance_id,
"live": live
})
def detach(self, block_id: str, live: bool = False) -> None:
"""
Detach block storage from instance
Args:
block_id: Block storage ID
live: Live detach (no reboot)
"""
self.client.post(f"blocks/{block_id}/detach", {"live": live})

348
vultr_api/resources/dns.py Normal file
View File

@@ -0,0 +1,348 @@
"""
DNS Resource
DNS domain and record management
"""
from typing import Dict, List, Optional
from .base import BaseResource
class DNSResource(BaseResource):
"""
DNS domain and record management
Usage:
# List domains
domains = client.dns.list_domains()
# Create domain
domain = client.dns.create_domain("example.com")
# List records
records = client.dns.list_records("example.com")
# Create A record
record = client.dns.create_record(
domain="example.com",
name="www",
record_type="A",
data="192.168.1.1",
ttl=300
)
"""
# Domain management
def list_domains(self, per_page: int = 100, cursor: str = None) -> Dict:
"""
List all DNS domains
Args:
per_page: Number of items per page
cursor: Pagination cursor
Returns:
Dict with 'domains' list and 'meta' pagination
"""
params = {"per_page": per_page}
if cursor:
params["cursor"] = cursor
return self.client.get("domains", params=params)
def list_all_domains(self) -> List[Dict]:
"""List all domains (auto-paginate)"""
return self.client.paginate("domains", "domains")
def get_domain(self, domain: str) -> Dict:
"""
Get domain details
Args:
domain: Domain name (e.g., "example.com")
Returns:
Domain details
"""
response = self.client.get(f"domains/{domain}")
return response.get("domain", {})
def create_domain(
self,
domain: str,
ip: str = None,
dns_sec: str = "disabled"
) -> Dict:
"""
Create a DNS domain
Args:
domain: Domain name
ip: Default IP address for A record (optional)
dns_sec: DNSSEC status ("enabled" or "disabled")
Returns:
Created domain details
"""
data = {"domain": domain, "dns_sec": dns_sec}
if ip:
data["ip"] = ip
response = self.client.post("domains", data)
return response.get("domain", {})
def update_domain(self, domain: str, dns_sec: str) -> None:
"""
Update domain DNSSEC setting
Args:
domain: Domain name
dns_sec: DNSSEC status ("enabled" or "disabled")
"""
self.client.put(f"domains/{domain}", {"dns_sec": dns_sec})
def delete_domain(self, domain: str) -> None:
"""
Delete a DNS domain
Args:
domain: Domain name to delete
"""
self.client.delete(f"domains/{domain}")
def get_soa(self, domain: str) -> Dict:
"""
Get SOA record info
Args:
domain: Domain name
Returns:
SOA record details
"""
return self.client.get(f"domains/{domain}/soa")
def update_soa(
self,
domain: str,
nsprimary: str = None,
email: str = None
) -> None:
"""
Update SOA record
Args:
domain: Domain name
nsprimary: Primary nameserver
email: Admin email
"""
data = {}
if nsprimary:
data["nsprimary"] = nsprimary
if email:
data["email"] = email
self.client.patch(f"domains/{domain}/soa", data)
def get_dnssec(self, domain: str) -> Dict:
"""
Get DNSSEC info
Args:
domain: Domain name
Returns:
DNSSEC details
"""
return self.client.get(f"domains/{domain}/dnssec")
# Record management
def list_records(
self,
domain: str,
per_page: int = 100,
cursor: str = None
) -> Dict:
"""
List DNS records for a domain
Args:
domain: Domain name
per_page: Number of items per page
cursor: Pagination cursor
Returns:
Dict with 'records' list and 'meta' pagination
"""
params = {"per_page": per_page}
if cursor:
params["cursor"] = cursor
return self.client.get(f"domains/{domain}/records", params=params)
def list_all_records(self, domain: str) -> List[Dict]:
"""List all records for domain (auto-paginate)"""
return self.client.paginate(f"domains/{domain}/records", "records")
def get_record(self, domain: str, record_id: str) -> Dict:
"""
Get a specific DNS record
Args:
domain: Domain name
record_id: Record ID
Returns:
Record details
"""
response = self.client.get(f"domains/{domain}/records/{record_id}")
return response.get("record", {})
def create_record(
self,
domain: str,
name: str,
record_type: str,
data: str,
ttl: int = 300,
priority: int = None
) -> Dict:
"""
Create a DNS record
Args:
domain: Domain name
name: Record name (subdomain or "@" for root)
record_type: Record type (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA, SSHFP)
data: Record data/value
ttl: Time to live in seconds (default 300)
priority: Priority (required for MX and SRV)
Returns:
Created record details
Example:
# A record
client.dns.create_record("example.com", "www", "A", "192.168.1.1")
# MX record
client.dns.create_record("example.com", "@", "MX", "mail.example.com", priority=10)
# TXT record (SPF)
client.dns.create_record("example.com", "@", "TXT", "v=spf1 include:_spf.example.com ~all")
"""
record_data = {
"name": name,
"type": record_type,
"data": data,
"ttl": ttl
}
if priority is not None:
record_data["priority"] = priority
response = self.client.post(f"domains/{domain}/records", record_data)
return response.get("record", {})
def update_record(
self,
domain: str,
record_id: str,
name: str = None,
data: str = None,
ttl: int = None,
priority: int = None
) -> None:
"""
Update a DNS record
Args:
domain: Domain name
record_id: Record ID to update
name: New record name
data: New record data
ttl: New TTL
priority: New priority
"""
record_data = {}
if name is not None:
record_data["name"] = name
if data is not None:
record_data["data"] = data
if ttl is not None:
record_data["ttl"] = ttl
if priority is not None:
record_data["priority"] = priority
self.client.patch(f"domains/{domain}/records/{record_id}", record_data)
def delete_record(self, domain: str, record_id: str) -> None:
"""
Delete a DNS record
Args:
domain: Domain name
record_id: Record ID to delete
"""
self.client.delete(f"domains/{domain}/records/{record_id}")
# Convenience methods
def create_a_record(
self,
domain: str,
name: str,
ip: str,
ttl: int = 300
) -> Dict:
"""Create an A record"""
return self.create_record(domain, name, "A", ip, ttl)
def create_aaaa_record(
self,
domain: str,
name: str,
ip: str,
ttl: int = 300
) -> Dict:
"""Create an AAAA record"""
return self.create_record(domain, name, "AAAA", ip, ttl)
def create_cname_record(
self,
domain: str,
name: str,
target: str,
ttl: int = 300
) -> Dict:
"""Create a CNAME record"""
return self.create_record(domain, name, "CNAME", target, ttl)
def create_mx_record(
self,
domain: str,
name: str,
mail_server: str,
priority: int = 10,
ttl: int = 300
) -> Dict:
"""Create an MX record"""
return self.create_record(domain, name, "MX", mail_server, ttl, priority)
def create_txt_record(
self,
domain: str,
name: str,
text: str,
ttl: int = 300
) -> Dict:
"""Create a TXT record"""
return self.create_record(domain, name, "TXT", text, ttl)
def create_ns_record(
self,
domain: str,
name: str,
nameserver: str,
ttl: int = 300
) -> Dict:
"""Create an NS record"""
return self.create_record(domain, name, "NS", nameserver, ttl)

View File

@@ -0,0 +1,306 @@
"""
Firewall Resource
Firewall group and rule management
"""
from typing import Dict, List, Optional
from .base import BaseResource
class FirewallResource(BaseResource):
"""
Firewall group and rule management
Usage:
# List firewall groups
groups = client.firewall.list_groups()
# Create firewall group
group = client.firewall.create_group(description="Web servers")
# Add rule
rule = client.firewall.create_rule(
firewall_group_id="group-id",
ip_type="v4",
protocol="tcp",
port="80",
subnet="0.0.0.0",
subnet_size=0
)
"""
# Firewall groups
def list_groups(self, per_page: int = 100, cursor: str = None) -> Dict:
"""
List firewall groups
Returns:
Dict with 'firewall_groups' list and 'meta' pagination
"""
params = {"per_page": per_page}
if cursor:
params["cursor"] = cursor
return self.client.get("firewalls", params=params)
def list_all_groups(self) -> List[Dict]:
"""List all firewall groups (auto-paginate)"""
return self.client.paginate("firewalls", "firewall_groups")
def get_group(self, firewall_group_id: str) -> Dict:
"""
Get firewall group details
Args:
firewall_group_id: Firewall group ID
Returns:
Firewall group details
"""
response = self.client.get(f"firewalls/{firewall_group_id}")
return response.get("firewall_group", {})
def create_group(self, description: str = None) -> Dict:
"""
Create a firewall group
Args:
description: Group description
Returns:
Created firewall group
"""
data = {}
if description:
data["description"] = description
response = self.client.post("firewalls", data)
return response.get("firewall_group", {})
def update_group(self, firewall_group_id: str, description: str) -> None:
"""
Update firewall group description
Args:
firewall_group_id: Group ID
description: New description
"""
self.client.put(f"firewalls/{firewall_group_id}", {"description": description})
def delete_group(self, firewall_group_id: str) -> None:
"""
Delete a firewall group
Args:
firewall_group_id: Group ID to delete
"""
self.client.delete(f"firewalls/{firewall_group_id}")
# Firewall rules
def list_rules(
self,
firewall_group_id: str,
per_page: int = 100,
cursor: str = None
) -> Dict:
"""
List rules in a firewall group
Args:
firewall_group_id: Group ID
Returns:
Dict with 'firewall_rules' list and 'meta' pagination
"""
params = {"per_page": per_page}
if cursor:
params["cursor"] = cursor
return self.client.get(
f"firewalls/{firewall_group_id}/rules",
params=params
)
def list_all_rules(self, firewall_group_id: str) -> List[Dict]:
"""List all rules in a firewall group (auto-paginate)"""
return self.client.paginate(
f"firewalls/{firewall_group_id}/rules",
"firewall_rules"
)
def get_rule(self, firewall_group_id: str, rule_id: str) -> Dict:
"""
Get a firewall rule
Args:
firewall_group_id: Group ID
rule_id: Rule ID
Returns:
Rule details
"""
response = self.client.get(
f"firewalls/{firewall_group_id}/rules/{rule_id}"
)
return response.get("firewall_rule", {})
def create_rule(
self,
firewall_group_id: str,
ip_type: str,
protocol: str,
port: str,
subnet: str,
subnet_size: int,
source: str = None,
notes: str = None
) -> Dict:
"""
Create a firewall rule
Args:
firewall_group_id: Group ID
ip_type: IP type ("v4" or "v6")
protocol: Protocol ("icmp", "tcp", "udp", "gre", "esp", "ah")
port: Port number, range (e.g., "8000:9000"), or empty for all
subnet: IP address or subnet
subnet_size: CIDR size (0-32 for v4, 0-128 for v6)
source: Source type ("", "cloudflare") - empty for custom subnet
notes: Rule notes
Returns:
Created rule details
Example:
# Allow SSH from anywhere
client.firewall.create_rule(
firewall_group_id="group-id",
ip_type="v4",
protocol="tcp",
port="22",
subnet="0.0.0.0",
subnet_size=0,
notes="SSH"
)
# Allow HTTP from Cloudflare
client.firewall.create_rule(
firewall_group_id="group-id",
ip_type="v4",
protocol="tcp",
port="80",
subnet="",
subnet_size=0,
source="cloudflare",
notes="HTTP via Cloudflare"
)
"""
data = {
"ip_type": ip_type,
"protocol": protocol,
"port": port,
"subnet": subnet,
"subnet_size": subnet_size,
}
if source:
data["source"] = source
if notes:
data["notes"] = notes
response = self.client.post(
f"firewalls/{firewall_group_id}/rules",
data
)
return response.get("firewall_rule", {})
def delete_rule(self, firewall_group_id: str, rule_id: str) -> None:
"""
Delete a firewall rule
Args:
firewall_group_id: Group ID
rule_id: Rule ID to delete
"""
self.client.delete(f"firewalls/{firewall_group_id}/rules/{rule_id}")
# Convenience methods
def allow_ssh(
self,
firewall_group_id: str,
ip_type: str = "v4",
subnet: str = "0.0.0.0",
subnet_size: int = 0
) -> Dict:
"""
Create SSH allow rule
Args:
firewall_group_id: Group ID
ip_type: "v4" or "v6"
subnet: Source subnet (default: anywhere)
subnet_size: CIDR size (default: 0 for anywhere)
Returns:
Created rule
"""
return self.create_rule(
firewall_group_id=firewall_group_id,
ip_type=ip_type,
protocol="tcp",
port="22",
subnet=subnet,
subnet_size=subnet_size,
notes="SSH"
)
def allow_http(
self,
firewall_group_id: str,
ip_type: str = "v4",
subnet: str = "0.0.0.0",
subnet_size: int = 0
) -> Dict:
"""Create HTTP allow rule"""
return self.create_rule(
firewall_group_id=firewall_group_id,
ip_type=ip_type,
protocol="tcp",
port="80",
subnet=subnet,
subnet_size=subnet_size,
notes="HTTP"
)
def allow_https(
self,
firewall_group_id: str,
ip_type: str = "v4",
subnet: str = "0.0.0.0",
subnet_size: int = 0
) -> Dict:
"""Create HTTPS allow rule"""
return self.create_rule(
firewall_group_id=firewall_group_id,
ip_type=ip_type,
protocol="tcp",
port="443",
subnet=subnet,
subnet_size=subnet_size,
notes="HTTPS"
)
def allow_ping(
self,
firewall_group_id: str,
ip_type: str = "v4"
) -> Dict:
"""Create ICMP (ping) allow rule"""
return self.create_rule(
firewall_group_id=firewall_group_id,
ip_type=ip_type,
protocol="icmp",
port="",
subnet="0.0.0.0" if ip_type == "v4" else "::",
subnet_size=0,
notes="ICMP/Ping"
)

View File

@@ -0,0 +1,536 @@
"""
Instances Resource
Cloud compute instance management
"""
from typing import Dict, List, Optional
from .base import BaseResource
class InstancesResource(BaseResource):
"""
Cloud compute instances management
Usage:
# List all instances
instances = client.instances.list()
# Get specific instance
instance = client.instances.get("instance-id")
# Create instance
instance = client.instances.create(
region="ewr",
plan="vc2-1c-1gb",
os_id=215,
label="my-server"
)
# Delete instance
client.instances.delete("instance-id")
"""
def list(
self,
per_page: int = 100,
cursor: str = None,
tag: str = None,
label: str = None,
main_ip: str = None,
region: str = None,
) -> Dict:
"""
List all instances
Args:
per_page: Number of items per page (max 500)
cursor: Pagination cursor
tag: Filter by tag
label: Filter by label
main_ip: Filter by main IP
region: Filter by region
Returns:
Dict with 'instances' list and 'meta' pagination info
"""
params = {"per_page": per_page}
if cursor:
params["cursor"] = cursor
if tag:
params["tag"] = tag
if label:
params["label"] = label
if main_ip:
params["main_ip"] = main_ip
if region:
params["region"] = region
return self.client.get("instances", params=params)
def list_all(self, **kwargs) -> List[Dict]:
"""
List all instances (auto-paginate)
Returns:
List of all instances
"""
return self.client.paginate("instances", "instances", params=kwargs)
def get(self, instance_id: str) -> Dict:
"""
Get instance details
Args:
instance_id: Instance ID
Returns:
Instance details
"""
response = self.client.get(f"instances/{instance_id}")
return response.get("instance", {})
def create(
self,
region: str,
plan: str,
os_id: int = None,
iso_id: str = None,
snapshot_id: str = None,
app_id: int = None,
image_id: str = None,
ipxe_chain_url: str = None,
script_id: str = None,
enable_ipv6: bool = False,
disable_public_ipv4: bool = False,
attach_private_network: List[str] = None,
attach_vpc: List[str] = None,
attach_vpc2: List[str] = None,
label: str = None,
sshkey_id: List[str] = None,
backups: str = None,
ddos_protection: bool = False,
activation_email: bool = False,
hostname: str = None,
tag: str = None,
tags: List[str] = None,
firewall_group_id: str = None,
reserved_ipv4: str = None,
user_data: str = None,
) -> Dict:
"""
Create a new instance
Args:
region: Region ID (e.g., "ewr", "lax", "nrt")
plan: Plan ID (e.g., "vc2-1c-1gb")
os_id: Operating System ID
iso_id: ISO ID for custom install
snapshot_id: Snapshot ID to restore from
app_id: Application ID
image_id: Custom image ID
ipxe_chain_url: iPXE chain URL
script_id: Startup script ID
enable_ipv6: Enable IPv6
disable_public_ipv4: Disable public IPv4
attach_private_network: Private network IDs to attach
attach_vpc: VPC IDs to attach
attach_vpc2: VPC 2.0 IDs to attach
label: Instance label
sshkey_id: SSH key IDs to add
backups: Backup schedule ("enabled" or "disabled")
ddos_protection: Enable DDoS protection
activation_email: Send activation email
hostname: Server hostname
tag: Deprecated, use tags
tags: Tags for the instance
firewall_group_id: Firewall group ID
reserved_ipv4: Reserved IPv4 to assign
user_data: Cloud-init user data (base64)
Returns:
Created instance details
"""
data = {"region": region, "plan": plan}
# Add optional parameters
if os_id is not None:
data["os_id"] = os_id
if iso_id:
data["iso_id"] = iso_id
if snapshot_id:
data["snapshot_id"] = snapshot_id
if app_id is not None:
data["app_id"] = app_id
if image_id:
data["image_id"] = image_id
if ipxe_chain_url:
data["ipxe_chain_url"] = ipxe_chain_url
if script_id:
data["script_id"] = script_id
if enable_ipv6:
data["enable_ipv6"] = enable_ipv6
if disable_public_ipv4:
data["disable_public_ipv4"] = disable_public_ipv4
if attach_private_network:
data["attach_private_network"] = attach_private_network
if attach_vpc:
data["attach_vpc"] = attach_vpc
if attach_vpc2:
data["attach_vpc2"] = attach_vpc2
if label:
data["label"] = label
if sshkey_id:
data["sshkey_id"] = sshkey_id
if backups:
data["backups"] = backups
if ddos_protection:
data["ddos_protection"] = ddos_protection
if activation_email:
data["activation_email"] = activation_email
if hostname:
data["hostname"] = hostname
if tag:
data["tag"] = tag
if tags:
data["tags"] = tags
if firewall_group_id:
data["firewall_group_id"] = firewall_group_id
if reserved_ipv4:
data["reserved_ipv4"] = reserved_ipv4
if user_data:
data["user_data"] = user_data
response = self.client.post("instances", data)
return response.get("instance", {})
def update(
self,
instance_id: str,
app_id: int = None,
image_id: str = None,
backups: str = None,
firewall_group_id: str = None,
enable_ipv6: bool = None,
os_id: int = None,
user_data: str = None,
tag: str = None,
tags: List[str] = None,
label: str = None,
ddos_protection: bool = None,
) -> Dict:
"""
Update an instance
Args:
instance_id: Instance ID to update
(other args same as create)
Returns:
Updated instance details
"""
data = {}
if app_id is not None:
data["app_id"] = app_id
if image_id is not None:
data["image_id"] = image_id
if backups is not None:
data["backups"] = backups
if firewall_group_id is not None:
data["firewall_group_id"] = firewall_group_id
if enable_ipv6 is not None:
data["enable_ipv6"] = enable_ipv6
if os_id is not None:
data["os_id"] = os_id
if user_data is not None:
data["user_data"] = user_data
if tag is not None:
data["tag"] = tag
if tags is not None:
data["tags"] = tags
if label is not None:
data["label"] = label
if ddos_protection is not None:
data["ddos_protection"] = ddos_protection
response = self.client.patch(f"instances/{instance_id}", data)
return response.get("instance", {})
def delete(self, instance_id: str) -> None:
"""
Delete an instance
Args:
instance_id: Instance ID to delete
"""
self.client.delete(f"instances/{instance_id}")
def start(self, instance_id: str) -> None:
"""Start an instance"""
self.client.post(f"instances/{instance_id}/start")
def stop(self, instance_id: str) -> None:
"""Stop an instance (alias for halt)"""
self.halt(instance_id)
def halt(self, instance_id: str) -> None:
"""Halt an instance"""
self.client.post(f"instances/{instance_id}/halt")
def reboot(self, instance_id: str) -> None:
"""Reboot an instance"""
self.client.post(f"instances/{instance_id}/reboot")
def reinstall(self, instance_id: str, hostname: str = None) -> Dict:
"""
Reinstall an instance
Args:
instance_id: Instance ID
hostname: New hostname (optional)
Returns:
Instance details
"""
data = {}
if hostname:
data["hostname"] = hostname
response = self.client.post(f"instances/{instance_id}/reinstall", data)
return response.get("instance", {})
def get_bandwidth(self, instance_id: str) -> Dict:
"""Get instance bandwidth usage"""
return self.client.get(f"instances/{instance_id}/bandwidth")
def get_neighbors(self, instance_id: str) -> Dict:
"""Get instance neighbors (shared host)"""
return self.client.get(f"instances/{instance_id}/neighbors")
def get_user_data(self, instance_id: str) -> Dict:
"""Get instance user data"""
return self.client.get(f"instances/{instance_id}/user-data")
def get_upgrades(self, instance_id: str, upgrade_type: str = None) -> Dict:
"""
Get available upgrades
Args:
instance_id: Instance ID
upgrade_type: Filter by type ("all", "applications", "os", "plans")
Returns:
Available upgrades
"""
params = {}
if upgrade_type:
params["type"] = upgrade_type
return self.client.get(f"instances/{instance_id}/upgrades", params=params)
# IPv4 management
def list_ipv4(self, instance_id: str) -> List[Dict]:
"""List IPv4 addresses"""
response = self.client.get(f"instances/{instance_id}/ipv4")
return response.get("ipv4s", [])
def create_ipv4(self, instance_id: str, reboot: bool = True) -> Dict:
"""
Create additional IPv4
Args:
instance_id: Instance ID
reboot: Reboot instance to apply (default True)
Returns:
New IPv4 info
"""
response = self.client.post(
f"instances/{instance_id}/ipv4",
{"reboot": reboot}
)
return response.get("ipv4", {})
def delete_ipv4(self, instance_id: str, ipv4: str) -> None:
"""Delete an IPv4 address"""
self.client.delete(f"instances/{instance_id}/ipv4/{ipv4}")
def create_ipv4_reverse(
self,
instance_id: str,
ip: str,
reverse: str
) -> None:
"""Set IPv4 reverse DNS"""
self.client.post(
f"instances/{instance_id}/ipv4/reverse",
{"ip": ip, "reverse": reverse}
)
def set_default_ipv4_reverse(self, instance_id: str, ip: str) -> None:
"""Set default reverse DNS for IPv4"""
self.client.post(
f"instances/{instance_id}/ipv4/reverse/default",
{"ip": ip}
)
# IPv6 management
def list_ipv6(self, instance_id: str) -> List[Dict]:
"""List IPv6 addresses"""
response = self.client.get(f"instances/{instance_id}/ipv6")
return response.get("ipv6s", [])
def create_ipv6_reverse(
self,
instance_id: str,
ip: str,
reverse: str
) -> None:
"""Set IPv6 reverse DNS"""
self.client.post(
f"instances/{instance_id}/ipv6/reverse",
{"ip": ip, "reverse": reverse}
)
def list_ipv6_reverse(self, instance_id: str) -> List[Dict]:
"""List IPv6 reverse DNS entries"""
response = self.client.get(f"instances/{instance_id}/ipv6/reverse")
return response.get("reverse_ipv6s", [])
def delete_ipv6_reverse(self, instance_id: str, ipv6: str) -> None:
"""Delete IPv6 reverse DNS"""
self.client.delete(f"instances/{instance_id}/ipv6/reverse/{ipv6}")
# Backup management
def get_backup_schedule(self, instance_id: str) -> Dict:
"""Get backup schedule"""
return self.client.get(f"instances/{instance_id}/backup-schedule")
def set_backup_schedule(
self,
instance_id: str,
backup_type: str,
hour: int = None,
dow: int = None,
dom: int = None,
) -> None:
"""
Set backup schedule
Args:
instance_id: Instance ID
backup_type: Schedule type ("daily", "weekly", "monthly", "daily_alt_even", "daily_alt_odd")
hour: Hour of day (0-23)
dow: Day of week (1-7, for weekly)
dom: Day of month (1-28, for monthly)
"""
data = {"type": backup_type}
if hour is not None:
data["hour"] = hour
if dow is not None:
data["dow"] = dow
if dom is not None:
data["dom"] = dom
self.client.post(f"instances/{instance_id}/backup-schedule", data)
def restore_backup(self, instance_id: str, backup_id: str) -> Dict:
"""
Restore instance from backup
Args:
instance_id: Instance ID
backup_id: Backup ID to restore
Returns:
Restore status
"""
return self.client.post(
f"instances/{instance_id}/restore",
{"backup_id": backup_id}
)
def restore_snapshot(self, instance_id: str, snapshot_id: str) -> Dict:
"""
Restore instance from snapshot
Args:
instance_id: Instance ID
snapshot_id: Snapshot ID to restore
Returns:
Restore status
"""
return self.client.post(
f"instances/{instance_id}/restore",
{"snapshot_id": snapshot_id}
)
# VPC management
def list_vpcs(self, instance_id: str) -> List[Dict]:
"""List VPCs attached to instance"""
response = self.client.get(f"instances/{instance_id}/vpcs")
return response.get("vpcs", [])
def attach_vpc(self, instance_id: str, vpc_id: str) -> None:
"""Attach VPC to instance"""
self.client.post(
f"instances/{instance_id}/vpcs/attach",
{"vpc_id": vpc_id}
)
def detach_vpc(self, instance_id: str, vpc_id: str) -> None:
"""Detach VPC from instance"""
self.client.post(
f"instances/{instance_id}/vpcs/detach",
{"vpc_id": vpc_id}
)
# VPC 2.0 management
def list_vpc2(self, instance_id: str) -> List[Dict]:
"""List VPC 2.0 networks attached to instance"""
response = self.client.get(f"instances/{instance_id}/vpc2")
return response.get("vpcs", [])
def attach_vpc2(
self,
instance_id: str,
vpc_id: str,
ip_address: str = None
) -> None:
"""
Attach VPC 2.0 to instance
Args:
instance_id: Instance ID
vpc_id: VPC 2.0 ID
ip_address: Specific IP to assign (optional)
"""
data = {"vpc_id": vpc_id}
if ip_address:
data["ip_address"] = ip_address
self.client.post(f"instances/{instance_id}/vpc2/attach", data)
def detach_vpc2(self, instance_id: str, vpc_id: str) -> None:
"""Detach VPC 2.0 from instance"""
self.client.post(
f"instances/{instance_id}/vpc2/detach",
{"vpc_id": vpc_id}
)
# ISO management
def attach_iso(self, instance_id: str, iso_id: str) -> Dict:
"""Attach ISO to instance"""
return self.client.post(
f"instances/{instance_id}/iso/attach",
{"iso_id": iso_id}
)
def detach_iso(self, instance_id: str) -> Dict:
"""Detach ISO from instance"""
return self.client.post(f"instances/{instance_id}/iso/detach")
def get_iso_status(self, instance_id: str) -> Dict:
"""Get ISO attach status"""
return self.client.get(f"instances/{instance_id}/iso")

View File

@@ -0,0 +1,266 @@
"""
Load Balancers Resource
Load balancer management
"""
from typing import Dict, List
from .base import BaseResource
class LoadBalancersResource(BaseResource):
"""
Load balancer management
Usage:
# List load balancers
lbs = client.load_balancers.list()
# Create load balancer
lb = client.load_balancers.create(
region="ewr",
label="my-lb",
forwarding_rules=[{
"frontend_protocol": "http",
"frontend_port": 80,
"backend_protocol": "http",
"backend_port": 80
}]
)
"""
def list(self, per_page: int = 100, cursor: str = None) -> Dict:
"""
List load balancers
Returns:
Dict with 'load_balancers' list and 'meta' pagination
"""
params = {"per_page": per_page}
if cursor:
params["cursor"] = cursor
return self.client.get("load-balancers", params=params)
def list_all(self) -> List[Dict]:
"""List all load balancers (auto-paginate)"""
return self.client.paginate("load-balancers", "load_balancers")
def get(self, load_balancer_id: str) -> Dict:
"""
Get load balancer details
Args:
load_balancer_id: Load balancer ID
Returns:
Load balancer details
"""
response = self.client.get(f"load-balancers/{load_balancer_id}")
return response.get("load_balancer", {})
def create(
self,
region: str,
forwarding_rules: List[Dict],
label: str = None,
balancing_algorithm: str = "roundrobin",
ssl_redirect: bool = False,
proxy_protocol: bool = False,
health_check: Dict = None,
sticky_session: Dict = None,
ssl: Dict = None,
instances: List[str] = None,
firewall_rules: List[Dict] = None,
vpc: str = None,
) -> Dict:
"""
Create a load balancer
Args:
region: Region ID
forwarding_rules: List of forwarding rules
label: Load balancer label
balancing_algorithm: "roundrobin" or "leastconn"
ssl_redirect: Redirect HTTP to HTTPS
proxy_protocol: Enable proxy protocol
health_check: Health check config
sticky_session: Sticky session config
ssl: SSL config
instances: List of instance IDs to attach
firewall_rules: List of firewall rules
vpc: VPC ID
Returns:
Created load balancer details
Example forwarding_rules:
[{
"frontend_protocol": "http",
"frontend_port": 80,
"backend_protocol": "http",
"backend_port": 80
}]
Example health_check:
{
"protocol": "http",
"port": 80,
"path": "/health",
"check_interval": 15,
"response_timeout": 5,
"unhealthy_threshold": 5,
"healthy_threshold": 5
}
"""
data = {
"region": region,
"forwarding_rules": forwarding_rules,
"balancing_algorithm": balancing_algorithm,
"ssl_redirect": ssl_redirect,
"proxy_protocol": proxy_protocol,
}
if label:
data["label"] = label
if health_check:
data["health_check"] = health_check
if sticky_session:
data["sticky_session"] = sticky_session
if ssl:
data["ssl"] = ssl
if instances:
data["instances"] = instances
if firewall_rules:
data["firewall_rules"] = firewall_rules
if vpc:
data["vpc"] = vpc
response = self.client.post("load-balancers", data)
return response.get("load_balancer", {})
def update(
self,
load_balancer_id: str,
label: str = None,
balancing_algorithm: str = None,
ssl_redirect: bool = None,
proxy_protocol: bool = None,
health_check: Dict = None,
forwarding_rules: List[Dict] = None,
sticky_session: Dict = None,
ssl: Dict = None,
instances: List[str] = None,
firewall_rules: List[Dict] = None,
vpc: str = None,
) -> None:
"""
Update a load balancer
Args:
load_balancer_id: Load balancer ID
(other args same as create)
"""
data = {}
if label is not None:
data["label"] = label
if balancing_algorithm is not None:
data["balancing_algorithm"] = balancing_algorithm
if ssl_redirect is not None:
data["ssl_redirect"] = ssl_redirect
if proxy_protocol is not None:
data["proxy_protocol"] = proxy_protocol
if health_check is not None:
data["health_check"] = health_check
if forwarding_rules is not None:
data["forwarding_rules"] = forwarding_rules
if sticky_session is not None:
data["sticky_session"] = sticky_session
if ssl is not None:
data["ssl"] = ssl
if instances is not None:
data["instances"] = instances
if firewall_rules is not None:
data["firewall_rules"] = firewall_rules
if vpc is not None:
data["vpc"] = vpc
self.client.patch(f"load-balancers/{load_balancer_id}", data)
def delete(self, load_balancer_id: str) -> None:
"""
Delete a load balancer
Args:
load_balancer_id: Load balancer ID to delete
"""
self.client.delete(f"load-balancers/{load_balancer_id}")
# Forwarding rules
def list_forwarding_rules(self, load_balancer_id: str) -> List[Dict]:
"""List forwarding rules"""
response = self.client.get(
f"load-balancers/{load_balancer_id}/forwarding-rules"
)
return response.get("forwarding_rules", [])
def create_forwarding_rule(
self,
load_balancer_id: str,
frontend_protocol: str,
frontend_port: int,
backend_protocol: str,
backend_port: int
) -> Dict:
"""Create a forwarding rule"""
data = {
"frontend_protocol": frontend_protocol,
"frontend_port": frontend_port,
"backend_protocol": backend_protocol,
"backend_port": backend_port,
}
response = self.client.post(
f"load-balancers/{load_balancer_id}/forwarding-rules",
data
)
return response.get("forwarding_rule", {})
def get_forwarding_rule(
self,
load_balancer_id: str,
rule_id: str
) -> Dict:
"""Get a forwarding rule"""
response = self.client.get(
f"load-balancers/{load_balancer_id}/forwarding-rules/{rule_id}"
)
return response.get("forwarding_rule", {})
def delete_forwarding_rule(
self,
load_balancer_id: str,
rule_id: str
) -> None:
"""Delete a forwarding rule"""
self.client.delete(
f"load-balancers/{load_balancer_id}/forwarding-rules/{rule_id}"
)
# Firewall rules
def list_firewall_rules(self, load_balancer_id: str) -> List[Dict]:
"""List firewall rules"""
response = self.client.get(
f"load-balancers/{load_balancer_id}/firewall-rules"
)
return response.get("firewall_rules", [])
def get_firewall_rule(
self,
load_balancer_id: str,
rule_id: str
) -> Dict:
"""Get a firewall rule"""
response = self.client.get(
f"load-balancers/{load_balancer_id}/firewall-rules/{rule_id}"
)
return response.get("firewall_rule", {})

34
vultr_api/resources/os.py Normal file
View File

@@ -0,0 +1,34 @@
"""
OS Resource
Operating system listings
"""
from typing import Dict, List
from .base import BaseResource
class OSResource(BaseResource):
"""
Operating system listings
Usage:
# List all OS options
os_list = client.os.list()
"""
def list(self, per_page: int = 100, cursor: str = None) -> Dict:
"""
List available operating systems
Returns:
Dict with 'os' list and 'meta' pagination
"""
params = {"per_page": per_page}
if cursor:
params["cursor"] = cursor
return self.client.get("os", params=params)
def list_all(self) -> List[Dict]:
"""List all operating systems (auto-paginate)"""
return self.client.paginate("os", "os")

View File

@@ -0,0 +1,75 @@
"""
Plans Resource
Available plan listings
"""
from typing import Dict, List
from .base import BaseResource
class PlansResource(BaseResource):
"""
Plan listings
Usage:
# List cloud compute plans
plans = client.plans.list()
# List bare metal plans
bm_plans = client.plans.list_bare_metal()
"""
def list(
self,
plan_type: str = None,
per_page: int = 100,
cursor: str = None,
os: str = None
) -> Dict:
"""
List cloud compute plans
Args:
plan_type: Filter by type ("all", "vc2", "vhf", "vdc", "voc", "voc-g", "voc-c", "voc-m", "voc-s", "vcg")
per_page: Items per page
cursor: Pagination cursor
os: Filter by supported OS ("windows")
Returns:
Dict with 'plans' list and 'meta' pagination
"""
params = {"per_page": per_page}
if plan_type:
params["type"] = plan_type
if cursor:
params["cursor"] = cursor
if os:
params["os"] = os
return self.client.get("plans", params=params)
def list_all(self, plan_type: str = None, os: str = None) -> List[Dict]:
"""List all plans (auto-paginate)"""
params = {}
if plan_type:
params["type"] = plan_type
if os:
params["os"] = os
return self.client.paginate("plans", "plans", params=params)
def list_bare_metal(self, per_page: int = 100, cursor: str = None) -> Dict:
"""
List bare metal plans
Returns:
Dict with 'plans' list and 'meta' pagination
"""
params = {"per_page": per_page}
if cursor:
params["cursor"] = cursor
return self.client.get("plans-metal", params=params)
def list_all_bare_metal(self) -> List[Dict]:
"""List all bare metal plans (auto-paginate)"""
return self.client.paginate("plans-metal", "plans")

View File

@@ -0,0 +1,53 @@
"""
Regions Resource
Available region listings
"""
from typing import Dict, List
from .base import BaseResource
class RegionsResource(BaseResource):
"""
Region listings
Usage:
# List all regions
regions = client.regions.list()
# List plans available in a region
plans = client.regions.list_availability("ewr")
"""
def list(self, per_page: int = 100, cursor: str = None) -> Dict:
"""
List all regions
Returns:
Dict with 'regions' list and 'meta' pagination
"""
params = {"per_page": per_page}
if cursor:
params["cursor"] = cursor
return self.client.get("regions", params=params)
def list_all(self) -> List[Dict]:
"""List all regions (auto-paginate)"""
return self.client.paginate("regions", "regions")
def list_availability(self, region_id: str, plan_type: str = None) -> Dict:
"""
List available plans in a region
Args:
region_id: Region ID (e.g., "ewr", "lax", "nrt")
plan_type: Filter by type ("vc2", "vhf", "vdc")
Returns:
Dict with 'available_plans' list
"""
params = {}
if plan_type:
params["type"] = plan_type
return self.client.get(f"regions/{region_id}/availability", params=params)

View File

@@ -0,0 +1,135 @@
"""
Reserved IPs Resource
Reserved IP address management
"""
from typing import Dict, List
from .base import BaseResource
class ReservedIPsResource(BaseResource):
"""
Reserved IP address management
Usage:
# List reserved IPs
ips = client.reserved_ips.list()
# Create reserved IP
ip = client.reserved_ips.create(region="ewr", ip_type="v4")
# Attach to instance
client.reserved_ips.attach(reserved_ip="id", instance_id="instance-id")
"""
def list(self, per_page: int = 100, cursor: str = None) -> Dict:
"""
List reserved IPs
Returns:
Dict with 'reserved_ips' list and 'meta' pagination
"""
params = {"per_page": per_page}
if cursor:
params["cursor"] = cursor
return self.client.get("reserved-ips", params=params)
def list_all(self) -> List[Dict]:
"""List all reserved IPs (auto-paginate)"""
return self.client.paginate("reserved-ips", "reserved_ips")
def get(self, reserved_ip: str) -> Dict:
"""
Get reserved IP details
Args:
reserved_ip: Reserved IP ID
Returns:
Reserved IP details
"""
response = self.client.get(f"reserved-ips/{reserved_ip}")
return response.get("reserved_ip", {})
def create(
self,
region: str,
ip_type: str,
label: str = None
) -> Dict:
"""
Create a reserved IP
Args:
region: Region ID
ip_type: IP type ("v4" or "v6")
label: Label
Returns:
Created reserved IP details
"""
data = {"region": region, "ip_type": ip_type}
if label:
data["label"] = label
response = self.client.post("reserved-ips", data)
return response.get("reserved_ip", {})
def update(self, reserved_ip: str, label: str) -> None:
"""
Update reserved IP label
Args:
reserved_ip: Reserved IP ID
label: New label
"""
self.client.patch(f"reserved-ips/{reserved_ip}", {"label": label})
def delete(self, reserved_ip: str) -> None:
"""
Delete a reserved IP
Args:
reserved_ip: Reserved IP ID to delete
"""
self.client.delete(f"reserved-ips/{reserved_ip}")
def attach(self, reserved_ip: str, instance_id: str) -> None:
"""
Attach reserved IP to instance
Args:
reserved_ip: Reserved IP ID
instance_id: Instance ID to attach to
"""
self.client.post(f"reserved-ips/{reserved_ip}/attach", {
"instance_id": instance_id
})
def detach(self, reserved_ip: str) -> None:
"""
Detach reserved IP from instance
Args:
reserved_ip: Reserved IP ID
"""
self.client.post(f"reserved-ips/{reserved_ip}/detach")
def convert(self, ip_address: str, label: str = None) -> Dict:
"""
Convert instance IP to reserved IP
Args:
ip_address: IP address to convert
label: Label for reserved IP
Returns:
Created reserved IP details
"""
data = {"ip_address": ip_address}
if label:
data["label"] = label
response = self.client.post("reserved-ips/convert", data)
return response.get("reserved_ip", {})

View File

@@ -0,0 +1,108 @@
"""
Snapshots Resource
Snapshot management
"""
from typing import Dict, List
from .base import BaseResource
class SnapshotsResource(BaseResource):
"""
Snapshot management
Usage:
# List snapshots
snapshots = client.snapshots.list()
# Create snapshot from instance
snapshot = client.snapshots.create(instance_id="instance-id", description="My snapshot")
# Create from URL
snapshot = client.snapshots.create_from_url(url="https://example.com/image.raw")
"""
def list(self, per_page: int = 100, cursor: str = None) -> Dict:
"""
List snapshots
Returns:
Dict with 'snapshots' list and 'meta' pagination
"""
params = {"per_page": per_page}
if cursor:
params["cursor"] = cursor
return self.client.get("snapshots", params=params)
def list_all(self) -> List[Dict]:
"""List all snapshots (auto-paginate)"""
return self.client.paginate("snapshots", "snapshots")
def get(self, snapshot_id: str) -> Dict:
"""
Get snapshot details
Args:
snapshot_id: Snapshot ID
Returns:
Snapshot details
"""
response = self.client.get(f"snapshots/{snapshot_id}")
return response.get("snapshot", {})
def create(self, instance_id: str, description: str = None) -> Dict:
"""
Create a snapshot from an instance
Args:
instance_id: Instance ID to snapshot
description: Snapshot description
Returns:
Created snapshot details
"""
data = {"instance_id": instance_id}
if description:
data["description"] = description
response = self.client.post("snapshots", data)
return response.get("snapshot", {})
def create_from_url(self, url: str, description: str = None) -> Dict:
"""
Create a snapshot from a URL
Args:
url: URL to raw disk image
description: Snapshot description
Returns:
Created snapshot details
"""
data = {"url": url}
if description:
data["description"] = description
response = self.client.post("snapshots/create-from-url", data)
return response.get("snapshot", {})
def update(self, snapshot_id: str, description: str) -> None:
"""
Update snapshot description
Args:
snapshot_id: Snapshot ID
description: New description
"""
self.client.put(f"snapshots/{snapshot_id}", {"description": description})
def delete(self, snapshot_id: str) -> None:
"""
Delete a snapshot
Args:
snapshot_id: Snapshot ID to delete
"""
self.client.delete(f"snapshots/{snapshot_id}")

View File

@@ -0,0 +1,99 @@
"""
SSH Keys Resource
SSH key management
"""
from typing import Dict, List
from .base import BaseResource
class SSHKeysResource(BaseResource):
"""
SSH key management
Usage:
# List keys
keys = client.ssh_keys.list()
# Create key
key = client.ssh_keys.create(
name="my-key",
ssh_key="ssh-rsa AAAAB3..."
)
# Delete key
client.ssh_keys.delete("key-id")
"""
def list(self, per_page: int = 100, cursor: str = None) -> Dict:
"""
List SSH keys
Returns:
Dict with 'ssh_keys' list and 'meta' pagination
"""
params = {"per_page": per_page}
if cursor:
params["cursor"] = cursor
return self.client.get("ssh-keys", params=params)
def list_all(self) -> List[Dict]:
"""List all SSH keys (auto-paginate)"""
return self.client.paginate("ssh-keys", "ssh_keys")
def get(self, ssh_key_id: str) -> Dict:
"""
Get SSH key details
Args:
ssh_key_id: SSH key ID
Returns:
SSH key details
"""
response = self.client.get(f"ssh-keys/{ssh_key_id}")
return response.get("ssh_key", {})
def create(self, name: str, ssh_key: str) -> Dict:
"""
Create an SSH key
Args:
name: Key name
ssh_key: Public key content
Returns:
Created SSH key details
"""
response = self.client.post("ssh-keys", {
"name": name,
"ssh_key": ssh_key
})
return response.get("ssh_key", {})
def update(self, ssh_key_id: str, name: str = None, ssh_key: str = None) -> None:
"""
Update an SSH key
Args:
ssh_key_id: Key ID
name: New name
ssh_key: New public key content
"""
data = {}
if name:
data["name"] = name
if ssh_key:
data["ssh_key"] = ssh_key
self.client.patch(f"ssh-keys/{ssh_key_id}", data)
def delete(self, ssh_key_id: str) -> None:
"""
Delete an SSH key
Args:
ssh_key_id: Key ID to delete
"""
self.client.delete(f"ssh-keys/{ssh_key_id}")

View File

@@ -0,0 +1,112 @@
"""
Startup Scripts Resource
Startup script management
"""
from typing import Dict, List
from .base import BaseResource
class StartupScriptsResource(BaseResource):
"""
Startup script management
Usage:
# List scripts
scripts = client.startup_scripts.list()
# Create script
script = client.startup_scripts.create(
name="my-script",
script="#!/bin/bash\\necho 'Hello'",
script_type="boot"
)
# Delete script
client.startup_scripts.delete("script-id")
"""
def list(self, per_page: int = 100, cursor: str = None) -> Dict:
"""
List startup scripts
Returns:
Dict with 'startup_scripts' list and 'meta' pagination
"""
params = {"per_page": per_page}
if cursor:
params["cursor"] = cursor
return self.client.get("startup-scripts", params=params)
def list_all(self) -> List[Dict]:
"""List all startup scripts (auto-paginate)"""
return self.client.paginate("startup-scripts", "startup_scripts")
def get(self, startup_script_id: str) -> Dict:
"""
Get startup script details
Args:
startup_script_id: Script ID
Returns:
Script details
"""
response = self.client.get(f"startup-scripts/{startup_script_id}")
return response.get("startup_script", {})
def create(
self,
name: str,
script: str,
script_type: str = "boot"
) -> Dict:
"""
Create a startup script
Args:
name: Script name
script: Script content (base64 or plain text)
script_type: Type ("boot" or "pxe")
Returns:
Created script details
"""
response = self.client.post("startup-scripts", {
"name": name,
"script": script,
"type": script_type
})
return response.get("startup_script", {})
def update(
self,
startup_script_id: str,
name: str = None,
script: str = None
) -> None:
"""
Update a startup script
Args:
startup_script_id: Script ID
name: New name
script: New script content
"""
data = {}
if name:
data["name"] = name
if script:
data["script"] = script
self.client.patch(f"startup-scripts/{startup_script_id}", data)
def delete(self, startup_script_id: str) -> None:
"""
Delete a startup script
Args:
startup_script_id: Script ID to delete
"""
self.client.delete(f"startup-scripts/{startup_script_id}")

220
vultr_api/resources/vpc.py Normal file
View File

@@ -0,0 +1,220 @@
"""
VPC Resource
VPC (Virtual Private Cloud) management - both v1 and v2
"""
from typing import Dict, List
from .base import BaseResource
class VPCResource(BaseResource):
"""
VPC management (v1 and v2)
Usage:
# List VPCs
vpcs = client.vpc.list()
# Create VPC
vpc = client.vpc.create(
region="ewr",
description="My VPC",
v4_subnet="10.0.0.0",
v4_subnet_mask=24
)
# VPC 2.0
vpc2 = client.vpc.create_vpc2(region="ewr", description="My VPC 2.0")
"""
# VPC v1
def list(self, per_page: int = 100, cursor: str = None) -> Dict:
"""
List VPCs
Returns:
Dict with 'vpcs' list and 'meta' pagination
"""
params = {"per_page": per_page}
if cursor:
params["cursor"] = cursor
return self.client.get("vpcs", params=params)
def list_all(self) -> List[Dict]:
"""List all VPCs (auto-paginate)"""
return self.client.paginate("vpcs", "vpcs")
def get(self, vpc_id: str) -> Dict:
"""
Get VPC details
Args:
vpc_id: VPC ID
Returns:
VPC details
"""
response = self.client.get(f"vpcs/{vpc_id}")
return response.get("vpc", {})
def create(
self,
region: str,
description: str = None,
v4_subnet: str = None,
v4_subnet_mask: int = None
) -> Dict:
"""
Create a VPC
Args:
region: Region ID
description: VPC description
v4_subnet: IPv4 subnet (e.g., "10.0.0.0")
v4_subnet_mask: Subnet mask (e.g., 24)
Returns:
Created VPC details
"""
data = {"region": region}
if description:
data["description"] = description
if v4_subnet:
data["v4_subnet"] = v4_subnet
if v4_subnet_mask:
data["v4_subnet_mask"] = v4_subnet_mask
response = self.client.post("vpcs", data)
return response.get("vpc", {})
def update(self, vpc_id: str, description: str) -> None:
"""
Update VPC description
Args:
vpc_id: VPC ID
description: New description
"""
self.client.put(f"vpcs/{vpc_id}", {"description": description})
def delete(self, vpc_id: str) -> None:
"""
Delete a VPC
Args:
vpc_id: VPC ID to delete
"""
self.client.delete(f"vpcs/{vpc_id}")
# VPC 2.0
def list_vpc2(self, per_page: int = 100, cursor: str = None) -> Dict:
"""
List VPC 2.0 networks
Returns:
Dict with 'vpcs' list and 'meta' pagination
"""
params = {"per_page": per_page}
if cursor:
params["cursor"] = cursor
return self.client.get("vpc2", params=params)
def list_all_vpc2(self) -> List[Dict]:
"""List all VPC 2.0 networks (auto-paginate)"""
return self.client.paginate("vpc2", "vpcs")
def get_vpc2(self, vpc_id: str) -> Dict:
"""
Get VPC 2.0 details
Args:
vpc_id: VPC 2.0 ID
Returns:
VPC 2.0 details
"""
response = self.client.get(f"vpc2/{vpc_id}")
return response.get("vpc", {})
def create_vpc2(
self,
region: str,
description: str = None,
ip_block: str = None,
prefix_length: int = None
) -> Dict:
"""
Create a VPC 2.0 network
Args:
region: Region ID
description: VPC description
ip_block: IP block (e.g., "10.0.0.0")
prefix_length: Prefix length (e.g., 24)
Returns:
Created VPC 2.0 details
"""
data = {"region": region}
if description:
data["description"] = description
if ip_block:
data["ip_block"] = ip_block
if prefix_length:
data["prefix_length"] = prefix_length
response = self.client.post("vpc2", data)
return response.get("vpc", {})
def update_vpc2(self, vpc_id: str, description: str) -> None:
"""
Update VPC 2.0 description
Args:
vpc_id: VPC 2.0 ID
description: New description
"""
self.client.put(f"vpc2/{vpc_id}", {"description": description})
def delete_vpc2(self, vpc_id: str) -> None:
"""
Delete a VPC 2.0 network
Args:
vpc_id: VPC 2.0 ID to delete
"""
self.client.delete(f"vpc2/{vpc_id}")
def list_vpc2_nodes(self, vpc_id: str) -> List[Dict]:
"""
List nodes attached to VPC 2.0
Args:
vpc_id: VPC 2.0 ID
Returns:
List of attached nodes
"""
response = self.client.get(f"vpc2/{vpc_id}/nodes")
return response.get("nodes", [])
def attach_vpc2_nodes(self, vpc_id: str, nodes: List[Dict]) -> None:
"""
Attach nodes to VPC 2.0
Args:
vpc_id: VPC 2.0 ID
nodes: List of nodes [{"id": "instance-id", "ip_address": "10.0.0.5"}, ...]
"""
self.client.post(f"vpc2/{vpc_id}/nodes/attach", {"nodes": nodes})
def detach_vpc2_nodes(self, vpc_id: str, nodes: List[str]) -> None:
"""
Detach nodes from VPC 2.0
Args:
vpc_id: VPC 2.0 ID
nodes: List of node IDs
"""
self.client.post(f"vpc2/{vpc_id}/nodes/detach", {"nodes": nodes})