Python client for the DevNomads API (transport, DNS, ACME)
Project description
devnomads
Python client library for the DevNomads API: HTTP transport, DNS zone/record management, and ACME certificate issuance.
It is the shared foundation of the DevNomads command-line tools - dnctl
and dnsync - exposed so you can build against the same primitives
directly instead of reimplementing them.
Layers
The package is split by dependency weight, lightest first. httpx is the
only hard dependency; import what you need.
devnomads.api- HTTP transport: authentication, retries withRetry-After, Laravel envelope unwrapping, and a typed error hierarchy.devnomads.dns- DNS helpers on top of the transport: zone discovery, rrset merging, A/AAAA records, and TXT/ACME-challenge handling.devnomads.acme- ACME client for certificate issuance over DNS-01 and HTTP-01. Requires theacmeextra (acme,josepy,cryptography,dnspython).
Installation
pip install devnomads # api + dns (httpx only)
pip install devnomads[acme] # + the ACME client and its dependencies
devnomads.acme only imports cleanly with the acme extra installed;
without it, importing the module raises an ImportError that names the
extra. Requires Python 3.10 or newer.
Authentication
Build a client from the environment, or pass credentials explicitly:
from devnomads.api import Client
# Resolved from the environment and credentials file.
client = Client.from_environment()
# Or explicit:
client = Client("https://api.devnomads.nl", "dn_xxx")
Client is a context manager and reuses one underlying connection pool:
with Client.from_environment() as client:
...
The API key is resolved in this order:
- an explicit value passed to
Client/resolve; DN_API_KEY(or theDEVNOMADS_API_KEYalias);- the
api_keyof the selected profile in an INI credentials file.
The credentials file is taken from DN_CREDENTIALS_FILE if set, otherwise
the first of /etc/devnomads/credentials and ~/.config/dnctl/credentials
(the file dnctl configure writes). The profile is DN_PROFILE, else
default. This matches the resolution used by dnctl and dnsync, so a
host already configured for those works unchanged.
DNS
Dns wraps a Client with zone-scoped operations. Zone discovery is
done by listing accessible zones and picking the longest matching suffix,
so a deep host resolves to its flat parent zone automatically.
from devnomads.api import Client
from devnomads.dns import Dns
with Client.from_environment() as client:
dns = Dns(client)
# Merge-aware, idempotent TXT updates (used for ACME challenges).
dns.set_txt("_acme-challenge.example.com", "token-value")
dns.unset_txt("_acme-challenge.example.com", "token-value")
# Generic records (content sent verbatim).
dns.replace_records("host.example.com", "A", ["192.0.2.1"], ttl=300)
dns.delete_records("host.example.com", "A")
zone = dns.find_zone("_acme-challenge.deep.host.example.com")
set_txt/unset_txt return a TxtResult(zone, values, changed);
set_txt merges with any values already present (so a wildcard and its
base domain can validate the same name concurrently) and makes no request
when the value is already there. unset_txt deletes the rrset once its
last value is gone.
The devnomads.dns module also exports name helpers - challenge_name,
fqdn, zone_id, quote_txt, unquote_txt - and the ZoneNotFound
error.
ACME
AcmeClient issues certificates from an ACME CA (Let's Encrypt by
default) over DNS-01, HTTP-01, or a mix of both within one order. It
manages the account key, builds the CSR, verifies DNS propagation against
the authoritative nameservers, and selects the preferred chain.
from devnomads.api import Client
from devnomads.dns import Dns
from devnomads.acme import AcmeClient, DevNomadsDnsProvider, generate_key
acme = AcmeClient(
"/etc/devnomads/account.key",
contact_email="ops@example.com",
)
domain_key = generate_key("ec256")
# DNS-01, including wildcards:
with Client.from_environment() as client:
provider = DevNomadsDnsProvider(Dns(client))
leaf, fullchain, chain, key_pem = acme.obtain_certificate(
"example.com",
"dns-01",
domain_key,
sans=["*.example.com"],
dns_provider=provider,
)
obtain_certificate returns
(cert_pem, fullchain_pem, chain_pem, domain_key_pem) as strings (the key
as bytes).
HTTP-01
Answer HTTP-01 challenges either from the bundled in-memory server or by writing files under an existing web root:
from devnomads.acme import StandaloneSolver, WebrootSolver
# Standalone: bind :80 and serve challenges directly.
with StandaloneSolver(port=80) as solver:
acme.obtain_certificate(
"example.com", "http-01", domain_key, http01_solver=solver
)
# Webroot: write files for an existing server to serve.
acme.obtain_certificate(
"example.com",
"http-01",
domain_key,
http01_solver=WebrootSolver("/var/www/html"),
)
Mixed challenges
Pass a {identifier: challenge_type} mapping to use different challenge
types per name in a single order - e.g. HTTP-01 for the base domain and
DNS-01 for its wildcard:
acme.obtain_certificate(
"example.com",
{"example.com": "http-01", "*.example.com": "dns-01"},
domain_key,
sans=["*.example.com"],
dns_provider=provider,
http01_solver=solver,
)
To target staging, pass
directory_url="https://acme-staging-v02.api.letsencrypt.org/directory".
Implement devnomads.acme.DnsProvider to drive a DNS backend other than
DevNomads.
Errors
The library never prints or exits; operations return values and failures
raise devnomads.api.DevNomadsError subclasses, so the caller decides how
to present them.
from devnomads.api import DevNomadsError, AuthError
try:
dns.set_txt(name, value)
except AuthError:
... # 401/403: missing, invalid, or unauthorized key
except DevNomadsError as exc:
... # ApiError, ConfigError, DnsError, AcmeError, ...
ApiError carries status and detail. Note that Client.request
returns None (rather than raising) for a 404, so a missing resource
reads as absence.
Logging
Progress and warnings go through the standard logging module under the
devnomads logger (devnomads.api, devnomads.acme). It is silent
unless you configure logging:
import logging
logging.getLogger("devnomads").setLevel(logging.INFO)
Development
uv sync --dev --extra acme
uv run pytest
uv run black . && uv run flake8 src tests
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file devnomads-0.1.0.tar.gz.
File metadata
- Download URL: devnomads-0.1.0.tar.gz
- Upload date:
- Size: 140.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Alpine Linux","version":"3.23.4","id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f9f6621274deaab02e8877224244242e941d5ef2a874e1a54f948936f52a0119
|
|
| MD5 |
fc38a9d8847bdee9da6175a7da44fd4f
|
|
| BLAKE2b-256 |
2f803aba34568453c54cb51976247f79d16b32bfc806db39216860fac532157b
|
File details
Details for the file devnomads-0.1.0-py3-none-any.whl.
File metadata
- Download URL: devnomads-0.1.0-py3-none-any.whl
- Upload date:
- Size: 26.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Alpine Linux","version":"3.23.4","id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c1d324d5839db4c3c80ee4e52052db2327004f0f203f84d6248285074ff5143a
|
|
| MD5 |
5b2d7e21b93c9989ef4ce8611cdd2de5
|
|
| BLAKE2b-256 |
cdc322b25add5985eabc2fabe9dfe48ba05bde839940bb50c911f8dce3662116
|