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.1.tar.gz (140.0 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.1-py3-none-any.whl (26.2 kB view details)

Uploaded Python 3

File details

Details for the file devnomads-0.1.1.tar.gz.

File metadata

  • Download URL: devnomads-0.1.1.tar.gz
  • Upload date:
  • Size: 140.0 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.1.tar.gz
Algorithm Hash digest
SHA256 af9c9824b76c500b6c4ab5b2370ac349a96a3155cfa5f9b5f19d55b1755df1d2
MD5 b6bebadd266aa1b1b0188950b8df1e41
BLAKE2b-256 35f4ac9d338d723b582bdb2fe7cdd1f5f4e27b7bc07c05c6568dcd0b7cd11c82

See more details on using hashes here.

File details

Details for the file devnomads-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: devnomads-0.1.1-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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e79be5f739ed741582bbc8fa8c07bb98d0b8b1a529f9357e41868f81b67ca22e
MD5 cc9dc6d6623a824202fd16301d7a1448
BLAKE2b-256 4699ff8c7f7497150b1b77481af6bf466d1e4d62ad6a1426395d513f2d865948

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