Architecture¶
donazopy is a small, layered CLI. This page maps the package, traces the data flow of a typical command, summarizes the typing conventions, and indexes the 12-chapter design spec.
Package layout¶
src/donazopy/
├── __init__.py # package version (re-exported as donazopy.__version__)
├── __main__.py # console-script entry point (-> main())
├── cli.py # the Donazopy class: every CLI command is a method
├── models.py # ProviderCapability, ProviderSpec dataclasses
├── target.py # Target dataclass + parse_target / resolve_provider_key
├── zonefile.py # zone-file engine: parse / normalize / filter / diff / safe-write
└── providers/
├── __init__.py
├── base.py # capabilities, credential loading, ProviderError types,
│ # DNSHostingProvider / RegistrarProvider protocols
├── registry.py # operational-provider registry + adapter factories
├── cloudflare.py # operational adapter (CloudflareProvider)
├── godaddy.py # operational adapter (GoDaddyProvider)
├── ionos.py # operational adapter (IonosProvider)
├── joker.py # operational adapter (JokerProvider)
└── <provider>.py # documented-only ProviderSpec stubs (aws, azure, namecheap, …)
Tests mirror src/ under tests/ (test_cli.py, test_zonefile.py,
test_cloudflare_provider.py, test_godaddy_provider.py, test_ionos_provider.py,
test_joker_provider.py, test_registry.py, test_provider_base.py,
test_package.py).
Layers¶
- CLI layer (
cli.py,__main__.py) —Donazopyis a plain class whose methods are commands;fire.Fire(Donazopy)turns it into a CLI. Methods do argument plumbing only: parse the target, resolve the provider, call the engine/adapter, return a JSON-serializable value. - Target layer (
target.py) — turns a[provider/][domain][:type][:host][:value]string into a typedTarget, and resolves which operational provider to use when the prefix is omitted. Also decides whether a string is a local file path (fordiff). - Provider layer (
providers/) —registry.pyknows which providers are operational and constructs adapters;base.pydefines the capability constants, credential loading (python-dotenv+ env, redacted status), theProviderErrorhierarchy, and theDNSHostingProvider/RegistrarProviderprotocols; eachproviders/<key>.pyeither implements an adapter (cloudflare.py,godaddy.py,ionos.py,joker.py) or just declares aProviderSpec. - Zone engine (
zonefile.py) — pure, network-free. Parses BIND text withdnspython, normalizes records, filters them, diffs two record sets, and writes files safely (never overwriting without permission).
Data flow of a command¶
donazopy export cloudflare/example.com --output=out.zone --skip-ns --dotenv-path=.env:
CLI (Donazopy.export)
└─ parse_target("cloudflare/example.com") → Target(provider="cloudflare", domain="example.com", …)
└─ resolve_provider_key(target, operational_keys) → "cloudflare"
└─ get_provider("cloudflare") → ProviderSpec
└─ require_provider_credentials(spec, dotenv_path=.env)
└─ dotenv_environment(...) → merged {.env, environ}, raises if CLOUDFLARE_API_TOKEN missing
└─ create_dns_provider("cloudflare", creds) → CloudflareProvider
└─ CloudflareProvider.export_zone("example.com")
└─ GET /zones?name=example.com → zone id
└─ GET /zones/{id}/dns_records/export → BIND text
└─ zonefile.filter_zone_text(text, origin, skip_ns=True)
└─ parse → records → drop NS (keep apex SOA) → re-serialize canonical
└─ zonefile.write_text_safely(Path("out.zone"), text, overwrite=False)
└─ raises if out.zone exists and --overwrite not given
└─ return the zone text → Fire prints it
donazopy diff a.zone cloudflare/example.com --origin=example.com. follows the
same shape but each side is resolved independently — a local path is read and
parsed, a target is fetched via an adapter — and the two normalized record sets
are passed to diff_zone_records.
Typing and boundary conventions¶
- Parse, don't validate. Raw zone text and raw provider JSON are converted
at the boundary into typed values (
NormalizedRecord,Target,ProviderSpec,CredentialStatus). Internal code works with those, not strings/dicts. - Frozen dataclasses with
slots.ProviderSpec,ProviderCapability,Target,NormalizedRecord,ZoneChange,ZoneDiff,CredentialStatus,LoadedCredentialsare all immutable. - Errors as typed exceptions, raised at the edge.
TargetError,ZoneFileError,ProviderError(withProviderCredentialError,ProviderAPIError). Fire turns an uncaught exception into a non-zero exit with a traceback. Protocols for adapters.DNSHostingProvider/RegistrarProviderareruntime_checkableprotocols, so adapters are structurally typed — no base class to inherit.- Provider isolation. One module per provider; nothing outside
providers/<key>.pyandregistry.pyknows provider-specific details. - No secrets in output. Credential status is redacted; adapters must not put tokens into error messages or logs.
Strict typing is configured: mypy --strict and Pyright standard mode over
src and tests. Ruff (line length 120) lints with E,W,F,I,B,C4,UP,SIM.
Specification chapters¶
The full design lives in spec/00-toc.md plus spec/01.md … spec/12.md. The
implemented code follows it partially today (the zone engine, the provider
protocol, and the Cloudflare adapter are done; the broader write/migration and
delegation workflows are specified but not yet exposed).
| Chapter | Title | TL;DR |
|---|---|---|
| 01 | Vision and Scope | A zone-file-first CLI for DNS zones, provider records, and registrar delegation — an audit/migration/backup/sync tool, not a magic layer. The first release scaffolds the registry and ships the local engine + verified adapters only. |
| 02 | Domain Model | Zones, records, providers, capabilities, credentials, sync plans, verification results. Provider-neutral record model; capability checks fail early; plans are the unit of review. |
| 03 | Zone File Engine | dnspython is the authoritative parser/serializer. Require an origin when it can't be inferred; normalize names/TTLs/classes; stable serialization; explicit SOA-serial handling; diff produces a plan with conflicts surfaced as warnings/errors. |
| 04 | Provider Architecture | One module per provider, explicit capabilities, small adapter surface. API behavior must be confirmed against official docs before live writes; researched placeholders may exist but must not claim tested write support. |
| 05 | Credential and Configuration Model | Credentials from env vars or ignored local config; never committed/printed/logged. Each adapter declares its credential names; missing credentials fail before network calls; redaction is tested. |
| 06 | CLI Experience | Fire-based, discoverable commands mapping to common workflows. Output names the files/providers/zones involved; dry-run/plan output is human-readable; write commands need explicit confirmation. |
| 07 | Read, Export, and Dump Workflows | Read provider state and dump it to stable BIND zone files for backup/audit/migration. Prefer native zone export when complete and documented, then re-validate it through the same engine. |
| 08 | Write, Import, and Sync Workflows | Writes are plan-first: read local + provider state, compute creates/updates/deletes/no-ops/warnings, block unsafe changes by default, then apply in a safe order; export-before-import for native importers; idempotent re-applies. |
| 09 | Nameserver and Registrar Workflows | Delegation is a registrar/parent-zone concern — editing zone-file NS records is not sufficient. A reassignment plan reads current NS, validates target syntax/glue, updates via the domain API, then verifies via RDAP/WHOIS/DNS. DNS-only providers refuse registrar ops. |
| 10 | Safety, Validation, and Observability | Validate before writes, dry-run for destructive ops, block unsupported capabilities, confirm unsafe changes; categorize provider errors; redact secrets; verification failures must not look like successful applies. |
| 11 | Testing Strategy | Deterministic local zone tests → mocked provider contract tests (auth, bodies, pagination, errors, rate limits, idempotency) → opt-in, gated live tests on disposable zones. |
| 12 | Implementation Roadmap | Build in safety-first slices: scaffold → zone engine → provider protocols → high-confidence providers (Cloudflare, IONOS, Joker, Route 53, DNSimple, one DNS-only) → migration/delegation → provider expansion. Each phase leaves a working CLI and passing tests. |
See also¶
- Providers — the provider model in detail and how to add one.
- Zone files — the zone engine.
- Contributing — the development workflow.