Skip to main content

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 with Retry-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 the acme extra (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:

  1. an explicit value passed to Client/resolve;
  2. DN_API_KEY (or the DEVNOMADS_API_KEY alias);
  3. the api_key of 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

devnomads-0.1.0.tar.gz (140.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

devnomads-0.1.0-py3-none-any.whl (26.2 kB view details)

Uploaded Python 3

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

Hashes for devnomads-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f9f6621274deaab02e8877224244242e941d5ef2a874e1a54f948936f52a0119
MD5 fc38a9d8847bdee9da6175a7da44fd4f
BLAKE2b-256 2f803aba34568453c54cb51976247f79d16b32bfc806db39216860fac532157b

See more details on using hashes here.

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

Hashes for devnomads-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c1d324d5839db4c3c80ee4e52052db2327004f0f203f84d6248285074ff5143a
MD5 5b2d7e21b93c9989ef4ce8611cdd2de5
BLAKE2b-256 cdc322b25add5985eabc2fabe9dfe48ba05bde839940bb50c911f8dce3662116

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page