Async first.
Pydantic types.
Native asyncio, Pydantic v2 models for every response, httpx under the hood. Works with FastAPI, Django, Flask, or plain scripts.
pip, uv, or poetry.
Your call.
Zero native dependencies. Pure Python with httpx for HTTP and Pydantic v2 for model validation. Ships with py.typed for mypy and pyright.
async with.
Context managed.
The async client uses httpx under the hood with connection pooling, automatic retries, and proper resource cleanup via async context manager.
Keyword arguments.
Pythonic.
Every method uses keyword arguments with sensible defaults. Return types are Pydantic models with full autocomplete in PyCharm and VS Code.
Bulk ops.
Atomic.
Create, update, and delete multiple records in a single atomic transaction. All succeed or all roll back. Progress callback included.
from relays import BulkOp
result = await client.records.bulk(
zone="example.com",
operations=[
BulkOp.create(type="A", name="app1",
content="10.0.0.1", ttl=300),
BulkOp.create(type="A", name="app2",
content="10.0.0.2", ttl=300),
BulkOp.create(type="AAAA", name="app1",
content="2001:db8::1", ttl=300),
BulkOp.delete(record_id="rec_old"),
],
)
print(f"Created: {result.created}")
print(f"Deleted: {result.deleted}")
print(f"Errors: {result.errors}")Pydantic v2.
Not dicts.
Every response is a Pydantic BaseModel. Validated, serializable, and fully typed. Use model_dump() for JSON, or access fields directly.
from relays.models import DnsRecord, Zone
# Every field is typed and validated
record: DnsRecord = await client.records.get(
zone="example.com",
record_id="rec_8f3c",
)
record.type # Literal["A","AAAA","CNAME",...]
record.name # str
record.content # str
record.ttl # int
record.created_at # datetime
record.dnssec # DnssecStatus
# Serialize to dict / JSON
data = record.model_dump()
json = record.model_dump_json()
# Works with FastAPI response models
@app.get("/record/{id}", response_model=DnsRecord)
async def get_record(id: str) -> DnsRecord:
return await client.records.get("example.com", id)