devnomads 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. devnomads-0.1.0/.flake8 +4 -0
  2. devnomads-0.1.0/.gitignore +10 -0
  3. devnomads-0.1.0/.gitlab-ci.yml +42 -0
  4. devnomads-0.1.0/PKG-INFO +243 -0
  5. devnomads-0.1.0/README.md +225 -0
  6. devnomads-0.1.0/pyproject.toml +53 -0
  7. devnomads-0.1.0/src/devnomads/__init__.py +17 -0
  8. devnomads-0.1.0/src/devnomads/acme/__init__.py +49 -0
  9. devnomads-0.1.0/src/devnomads/acme/challenge_server.py +103 -0
  10. devnomads-0.1.0/src/devnomads/acme/client.py +364 -0
  11. devnomads-0.1.0/src/devnomads/acme/dns01.py +42 -0
  12. devnomads-0.1.0/src/devnomads/acme/errors.py +9 -0
  13. devnomads-0.1.0/src/devnomads/acme/http01.py +91 -0
  14. devnomads-0.1.0/src/devnomads/acme/keys.py +104 -0
  15. devnomads-0.1.0/src/devnomads/acme/verify.py +109 -0
  16. devnomads-0.1.0/src/devnomads/api/__init__.py +28 -0
  17. devnomads-0.1.0/src/devnomads/api/client.py +197 -0
  18. devnomads-0.1.0/src/devnomads/api/credentials.py +164 -0
  19. devnomads-0.1.0/src/devnomads/api/errors.py +32 -0
  20. devnomads-0.1.0/src/devnomads/dns/__init__.py +31 -0
  21. devnomads-0.1.0/src/devnomads/dns/errors.py +13 -0
  22. devnomads-0.1.0/src/devnomads/dns/names.py +75 -0
  23. devnomads-0.1.0/src/devnomads/dns/zones.py +211 -0
  24. devnomads-0.1.0/src/devnomads/py.typed +0 -0
  25. devnomads-0.1.0/tests/conftest.py +29 -0
  26. devnomads-0.1.0/tests/test_acme_challenges.py +85 -0
  27. devnomads-0.1.0/tests/test_acme_client.py +64 -0
  28. devnomads-0.1.0/tests/test_acme_keys.py +70 -0
  29. devnomads-0.1.0/tests/test_client.py +91 -0
  30. devnomads-0.1.0/tests/test_credentials.py +99 -0
  31. devnomads-0.1.0/tests/test_dns.py +187 -0
  32. devnomads-0.1.0/tests/test_names.py +70 -0
  33. devnomads-0.1.0/uv.lock +2234 -0
@@ -0,0 +1,4 @@
1
+ [flake8]
2
+ # E501 (line length) is handled by black; E203/W503 conflict with black.
3
+ extend-ignore = E203,E501,E704,W503
4
+ exclude = .git,__pycache__,build,dist,.venv
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ .mypy_cache/
8
+ .pytest_cache/
9
+ .coverage
10
+ htmlcov/
@@ -0,0 +1,42 @@
1
+ stages:
2
+ - check
3
+ - test
4
+ - publish
5
+
6
+ default:
7
+ image: ghcr.io/astral-sh/uv:python3.14-alpine
8
+ before_script:
9
+ # dev tools + the acme extra, so linting and tests cover devnomads.acme.
10
+ - uv sync --dev --extra acme
11
+
12
+ check:
13
+ stage: check
14
+ script:
15
+ - uv run black --check .
16
+ - uv run isort --check-only .
17
+ - uv run flake8 src tests
18
+ - uv run bandit -r src
19
+ - uv run semgrep scan --quiet --error --config p/python src
20
+ - uv run mypy src
21
+
22
+ test:
23
+ stage: test
24
+ parallel:
25
+ matrix:
26
+ - PYTHON: ["3.10", "3.14"]
27
+ image: ghcr.io/astral-sh/uv:python$PYTHON-alpine
28
+ script:
29
+ - uv run pytest -v
30
+
31
+ # Runs on version tags (v0.1.0). PYPI_TOKEN (masked) and
32
+ # PYPI_PUBLISH_URL are CI variables pointing at pypi.org; the
33
+ # PYPI_TEST_* variants hold the test.pypi.org equivalents - swap them
34
+ # in to rehearse a release without publishing for real.
35
+ publish:
36
+ stage: publish
37
+ rules:
38
+ - if: $CI_COMMIT_TAG =~ /^v/
39
+ before_script: []
40
+ script:
41
+ - uv build
42
+ - uv publish --publish-url "$PYPI_PUBLISH_URL" --token "$PYPI_TOKEN"
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.4
2
+ Name: devnomads
3
+ Version: 0.1.0
4
+ Summary: Python client for the DevNomads API (transport, DNS, ACME)
5
+ Project-URL: Homepage, https://devnomads.nl
6
+ Project-URL: Source, https://gitlab.infrapod.nl/infrapod/devnomads-libs
7
+ Author-email: Loek Geleijn <support@devnomads.nl>
8
+ License: MIT
9
+ Keywords: acme,api,devnomads,dns,powerdns
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: httpx>=0.27
12
+ Provides-Extra: acme
13
+ Requires-Dist: acme>=2.11; extra == 'acme'
14
+ Requires-Dist: cryptography>=42; extra == 'acme'
15
+ Requires-Dist: dnspython>=2.6; extra == 'acme'
16
+ Requires-Dist: josepy>=1.14; extra == 'acme'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # devnomads
20
+
21
+ Python client library for the [DevNomads](https://devnomads.nl) API:
22
+ HTTP transport, DNS zone/record management, and ACME certificate
23
+ issuance.
24
+
25
+ It is the shared foundation of the DevNomads command-line tools - `dnctl`
26
+ and `dnsync` - exposed so you can build against the same primitives
27
+ directly instead of reimplementing them.
28
+
29
+ ## Layers
30
+
31
+ The package is split by dependency weight, lightest first. `httpx` is the
32
+ only hard dependency; import what you need.
33
+
34
+ - **`devnomads.api`** - HTTP transport: authentication, retries with
35
+ `Retry-After`, Laravel envelope unwrapping, and a typed error hierarchy.
36
+ - **`devnomads.dns`** - DNS helpers on top of the transport: zone
37
+ discovery, rrset merging, A/AAAA records, and TXT/ACME-challenge
38
+ handling.
39
+ - **`devnomads.acme`** - ACME client for certificate issuance over DNS-01
40
+ and HTTP-01. Requires the `acme` extra (`acme`, `josepy`,
41
+ `cryptography`, `dnspython`).
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install devnomads # api + dns (httpx only)
47
+ pip install devnomads[acme] # + the ACME client and its dependencies
48
+ ```
49
+
50
+ `devnomads.acme` only imports cleanly with the `acme` extra installed;
51
+ without it, importing the module raises an `ImportError` that names the
52
+ extra. Requires Python 3.10 or newer.
53
+
54
+ ## Authentication
55
+
56
+ Build a client from the environment, or pass credentials explicitly:
57
+
58
+ ```python
59
+ from devnomads.api import Client
60
+
61
+ # Resolved from the environment and credentials file.
62
+ client = Client.from_environment()
63
+
64
+ # Or explicit:
65
+ client = Client("https://api.devnomads.nl", "dn_xxx")
66
+ ```
67
+
68
+ `Client` is a context manager and reuses one underlying connection pool:
69
+
70
+ ```python
71
+ with Client.from_environment() as client:
72
+ ...
73
+ ```
74
+
75
+ The API key is resolved in this order:
76
+
77
+ 1. an explicit value passed to `Client`/`resolve`;
78
+ 2. `DN_API_KEY` (or the `DEVNOMADS_API_KEY` alias);
79
+ 3. the `api_key` of the selected profile in an INI credentials file.
80
+
81
+ The credentials file is taken from `DN_CREDENTIALS_FILE` if set, otherwise
82
+ the first of `/etc/devnomads/credentials` and `~/.config/dnctl/credentials`
83
+ (the file `dnctl configure` writes). The profile is `DN_PROFILE`, else
84
+ `default`. This matches the resolution used by `dnctl` and `dnsync`, so a
85
+ host already configured for those works unchanged.
86
+
87
+ ## DNS
88
+
89
+ `Dns` wraps a `Client` with zone-scoped operations. Zone discovery is
90
+ done by listing accessible zones and picking the longest matching suffix,
91
+ so a deep host resolves to its flat parent zone automatically.
92
+
93
+ ```python
94
+ from devnomads.api import Client
95
+ from devnomads.dns import Dns
96
+
97
+ with Client.from_environment() as client:
98
+ dns = Dns(client)
99
+
100
+ # Merge-aware, idempotent TXT updates (used for ACME challenges).
101
+ dns.set_txt("_acme-challenge.example.com", "token-value")
102
+ dns.unset_txt("_acme-challenge.example.com", "token-value")
103
+
104
+ # Generic records (content sent verbatim).
105
+ dns.replace_records("host.example.com", "A", ["192.0.2.1"], ttl=300)
106
+ dns.delete_records("host.example.com", "A")
107
+
108
+ zone = dns.find_zone("_acme-challenge.deep.host.example.com")
109
+ ```
110
+
111
+ `set_txt`/`unset_txt` return a `TxtResult(zone, values, changed)`;
112
+ `set_txt` merges with any values already present (so a wildcard and its
113
+ base domain can validate the same name concurrently) and makes no request
114
+ when the value is already there. `unset_txt` deletes the rrset once its
115
+ last value is gone.
116
+
117
+ The `devnomads.dns` module also exports name helpers - `challenge_name`,
118
+ `fqdn`, `zone_id`, `quote_txt`, `unquote_txt` - and the `ZoneNotFound`
119
+ error.
120
+
121
+ ## ACME
122
+
123
+ `AcmeClient` issues certificates from an ACME CA (Let's Encrypt by
124
+ default) over DNS-01, HTTP-01, or a mix of both within one order. It
125
+ manages the account key, builds the CSR, verifies DNS propagation against
126
+ the authoritative nameservers, and selects the preferred chain.
127
+
128
+ ```python
129
+ from devnomads.api import Client
130
+ from devnomads.dns import Dns
131
+ from devnomads.acme import AcmeClient, DevNomadsDnsProvider, generate_key
132
+
133
+ acme = AcmeClient(
134
+ "/etc/devnomads/account.key",
135
+ contact_email="ops@example.com",
136
+ )
137
+ domain_key = generate_key("ec256")
138
+
139
+ # DNS-01, including wildcards:
140
+ with Client.from_environment() as client:
141
+ provider = DevNomadsDnsProvider(Dns(client))
142
+ leaf, fullchain, chain, key_pem = acme.obtain_certificate(
143
+ "example.com",
144
+ "dns-01",
145
+ domain_key,
146
+ sans=["*.example.com"],
147
+ dns_provider=provider,
148
+ )
149
+ ```
150
+
151
+ `obtain_certificate` returns
152
+ `(cert_pem, fullchain_pem, chain_pem, domain_key_pem)` as strings (the key
153
+ as bytes).
154
+
155
+ ### HTTP-01
156
+
157
+ Answer HTTP-01 challenges either from the bundled in-memory server or by
158
+ writing files under an existing web root:
159
+
160
+ ```python
161
+ from devnomads.acme import StandaloneSolver, WebrootSolver
162
+
163
+ # Standalone: bind :80 and serve challenges directly.
164
+ with StandaloneSolver(port=80) as solver:
165
+ acme.obtain_certificate(
166
+ "example.com", "http-01", domain_key, http01_solver=solver
167
+ )
168
+
169
+ # Webroot: write files for an existing server to serve.
170
+ acme.obtain_certificate(
171
+ "example.com",
172
+ "http-01",
173
+ domain_key,
174
+ http01_solver=WebrootSolver("/var/www/html"),
175
+ )
176
+ ```
177
+
178
+ ### Mixed challenges
179
+
180
+ Pass a `{identifier: challenge_type}` mapping to use different challenge
181
+ types per name in a single order - e.g. HTTP-01 for the base domain and
182
+ DNS-01 for its wildcard:
183
+
184
+ ```python
185
+ acme.obtain_certificate(
186
+ "example.com",
187
+ {"example.com": "http-01", "*.example.com": "dns-01"},
188
+ domain_key,
189
+ sans=["*.example.com"],
190
+ dns_provider=provider,
191
+ http01_solver=solver,
192
+ )
193
+ ```
194
+
195
+ To target staging, pass
196
+ `directory_url="https://acme-staging-v02.api.letsencrypt.org/directory"`.
197
+ Implement `devnomads.acme.DnsProvider` to drive a DNS backend other than
198
+ DevNomads.
199
+
200
+ ## Errors
201
+
202
+ The library never prints or exits; operations return values and failures
203
+ raise `devnomads.api.DevNomadsError` subclasses, so the caller decides how
204
+ to present them.
205
+
206
+ ```python
207
+ from devnomads.api import DevNomadsError, AuthError
208
+
209
+ try:
210
+ dns.set_txt(name, value)
211
+ except AuthError:
212
+ ... # 401/403: missing, invalid, or unauthorized key
213
+ except DevNomadsError as exc:
214
+ ... # ApiError, ConfigError, DnsError, AcmeError, ...
215
+ ```
216
+
217
+ `ApiError` carries `status` and `detail`. Note that `Client.request`
218
+ returns `None` (rather than raising) for a 404, so a missing resource
219
+ reads as absence.
220
+
221
+ ## Logging
222
+
223
+ Progress and warnings go through the standard `logging` module under the
224
+ `devnomads` logger (`devnomads.api`, `devnomads.acme`). It is silent
225
+ unless you configure logging:
226
+
227
+ ```python
228
+ import logging
229
+
230
+ logging.getLogger("devnomads").setLevel(logging.INFO)
231
+ ```
232
+
233
+ ## Development
234
+
235
+ ```bash
236
+ uv sync --dev --extra acme
237
+ uv run pytest
238
+ uv run black . && uv run flake8 src tests
239
+ ```
240
+
241
+ ## License
242
+
243
+ MIT
@@ -0,0 +1,225 @@
1
+ # devnomads
2
+
3
+ Python client library for the [DevNomads](https://devnomads.nl) API:
4
+ HTTP transport, DNS zone/record management, and ACME certificate
5
+ issuance.
6
+
7
+ It is the shared foundation of the DevNomads command-line tools - `dnctl`
8
+ and `dnsync` - exposed so you can build against the same primitives
9
+ directly instead of reimplementing them.
10
+
11
+ ## Layers
12
+
13
+ The package is split by dependency weight, lightest first. `httpx` is the
14
+ only hard dependency; import what you need.
15
+
16
+ - **`devnomads.api`** - HTTP transport: authentication, retries with
17
+ `Retry-After`, Laravel envelope unwrapping, and a typed error hierarchy.
18
+ - **`devnomads.dns`** - DNS helpers on top of the transport: zone
19
+ discovery, rrset merging, A/AAAA records, and TXT/ACME-challenge
20
+ handling.
21
+ - **`devnomads.acme`** - ACME client for certificate issuance over DNS-01
22
+ and HTTP-01. Requires the `acme` extra (`acme`, `josepy`,
23
+ `cryptography`, `dnspython`).
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install devnomads # api + dns (httpx only)
29
+ pip install devnomads[acme] # + the ACME client and its dependencies
30
+ ```
31
+
32
+ `devnomads.acme` only imports cleanly with the `acme` extra installed;
33
+ without it, importing the module raises an `ImportError` that names the
34
+ extra. Requires Python 3.10 or newer.
35
+
36
+ ## Authentication
37
+
38
+ Build a client from the environment, or pass credentials explicitly:
39
+
40
+ ```python
41
+ from devnomads.api import Client
42
+
43
+ # Resolved from the environment and credentials file.
44
+ client = Client.from_environment()
45
+
46
+ # Or explicit:
47
+ client = Client("https://api.devnomads.nl", "dn_xxx")
48
+ ```
49
+
50
+ `Client` is a context manager and reuses one underlying connection pool:
51
+
52
+ ```python
53
+ with Client.from_environment() as client:
54
+ ...
55
+ ```
56
+
57
+ The API key is resolved in this order:
58
+
59
+ 1. an explicit value passed to `Client`/`resolve`;
60
+ 2. `DN_API_KEY` (or the `DEVNOMADS_API_KEY` alias);
61
+ 3. the `api_key` of the selected profile in an INI credentials file.
62
+
63
+ The credentials file is taken from `DN_CREDENTIALS_FILE` if set, otherwise
64
+ the first of `/etc/devnomads/credentials` and `~/.config/dnctl/credentials`
65
+ (the file `dnctl configure` writes). The profile is `DN_PROFILE`, else
66
+ `default`. This matches the resolution used by `dnctl` and `dnsync`, so a
67
+ host already configured for those works unchanged.
68
+
69
+ ## DNS
70
+
71
+ `Dns` wraps a `Client` with zone-scoped operations. Zone discovery is
72
+ done by listing accessible zones and picking the longest matching suffix,
73
+ so a deep host resolves to its flat parent zone automatically.
74
+
75
+ ```python
76
+ from devnomads.api import Client
77
+ from devnomads.dns import Dns
78
+
79
+ with Client.from_environment() as client:
80
+ dns = Dns(client)
81
+
82
+ # Merge-aware, idempotent TXT updates (used for ACME challenges).
83
+ dns.set_txt("_acme-challenge.example.com", "token-value")
84
+ dns.unset_txt("_acme-challenge.example.com", "token-value")
85
+
86
+ # Generic records (content sent verbatim).
87
+ dns.replace_records("host.example.com", "A", ["192.0.2.1"], ttl=300)
88
+ dns.delete_records("host.example.com", "A")
89
+
90
+ zone = dns.find_zone("_acme-challenge.deep.host.example.com")
91
+ ```
92
+
93
+ `set_txt`/`unset_txt` return a `TxtResult(zone, values, changed)`;
94
+ `set_txt` merges with any values already present (so a wildcard and its
95
+ base domain can validate the same name concurrently) and makes no request
96
+ when the value is already there. `unset_txt` deletes the rrset once its
97
+ last value is gone.
98
+
99
+ The `devnomads.dns` module also exports name helpers - `challenge_name`,
100
+ `fqdn`, `zone_id`, `quote_txt`, `unquote_txt` - and the `ZoneNotFound`
101
+ error.
102
+
103
+ ## ACME
104
+
105
+ `AcmeClient` issues certificates from an ACME CA (Let's Encrypt by
106
+ default) over DNS-01, HTTP-01, or a mix of both within one order. It
107
+ manages the account key, builds the CSR, verifies DNS propagation against
108
+ the authoritative nameservers, and selects the preferred chain.
109
+
110
+ ```python
111
+ from devnomads.api import Client
112
+ from devnomads.dns import Dns
113
+ from devnomads.acme import AcmeClient, DevNomadsDnsProvider, generate_key
114
+
115
+ acme = AcmeClient(
116
+ "/etc/devnomads/account.key",
117
+ contact_email="ops@example.com",
118
+ )
119
+ domain_key = generate_key("ec256")
120
+
121
+ # DNS-01, including wildcards:
122
+ with Client.from_environment() as client:
123
+ provider = DevNomadsDnsProvider(Dns(client))
124
+ leaf, fullchain, chain, key_pem = acme.obtain_certificate(
125
+ "example.com",
126
+ "dns-01",
127
+ domain_key,
128
+ sans=["*.example.com"],
129
+ dns_provider=provider,
130
+ )
131
+ ```
132
+
133
+ `obtain_certificate` returns
134
+ `(cert_pem, fullchain_pem, chain_pem, domain_key_pem)` as strings (the key
135
+ as bytes).
136
+
137
+ ### HTTP-01
138
+
139
+ Answer HTTP-01 challenges either from the bundled in-memory server or by
140
+ writing files under an existing web root:
141
+
142
+ ```python
143
+ from devnomads.acme import StandaloneSolver, WebrootSolver
144
+
145
+ # Standalone: bind :80 and serve challenges directly.
146
+ with StandaloneSolver(port=80) as solver:
147
+ acme.obtain_certificate(
148
+ "example.com", "http-01", domain_key, http01_solver=solver
149
+ )
150
+
151
+ # Webroot: write files for an existing server to serve.
152
+ acme.obtain_certificate(
153
+ "example.com",
154
+ "http-01",
155
+ domain_key,
156
+ http01_solver=WebrootSolver("/var/www/html"),
157
+ )
158
+ ```
159
+
160
+ ### Mixed challenges
161
+
162
+ Pass a `{identifier: challenge_type}` mapping to use different challenge
163
+ types per name in a single order - e.g. HTTP-01 for the base domain and
164
+ DNS-01 for its wildcard:
165
+
166
+ ```python
167
+ acme.obtain_certificate(
168
+ "example.com",
169
+ {"example.com": "http-01", "*.example.com": "dns-01"},
170
+ domain_key,
171
+ sans=["*.example.com"],
172
+ dns_provider=provider,
173
+ http01_solver=solver,
174
+ )
175
+ ```
176
+
177
+ To target staging, pass
178
+ `directory_url="https://acme-staging-v02.api.letsencrypt.org/directory"`.
179
+ Implement `devnomads.acme.DnsProvider` to drive a DNS backend other than
180
+ DevNomads.
181
+
182
+ ## Errors
183
+
184
+ The library never prints or exits; operations return values and failures
185
+ raise `devnomads.api.DevNomadsError` subclasses, so the caller decides how
186
+ to present them.
187
+
188
+ ```python
189
+ from devnomads.api import DevNomadsError, AuthError
190
+
191
+ try:
192
+ dns.set_txt(name, value)
193
+ except AuthError:
194
+ ... # 401/403: missing, invalid, or unauthorized key
195
+ except DevNomadsError as exc:
196
+ ... # ApiError, ConfigError, DnsError, AcmeError, ...
197
+ ```
198
+
199
+ `ApiError` carries `status` and `detail`. Note that `Client.request`
200
+ returns `None` (rather than raising) for a 404, so a missing resource
201
+ reads as absence.
202
+
203
+ ## Logging
204
+
205
+ Progress and warnings go through the standard `logging` module under the
206
+ `devnomads` logger (`devnomads.api`, `devnomads.acme`). It is silent
207
+ unless you configure logging:
208
+
209
+ ```python
210
+ import logging
211
+
212
+ logging.getLogger("devnomads").setLevel(logging.INFO)
213
+ ```
214
+
215
+ ## Development
216
+
217
+ ```bash
218
+ uv sync --dev --extra acme
219
+ uv run pytest
220
+ uv run black . && uv run flake8 src tests
221
+ ```
222
+
223
+ ## License
224
+
225
+ MIT
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "devnomads"
7
+ version = "0.1.0"
8
+ description = "Python client for the DevNomads API (transport, DNS, ACME)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Loek Geleijn", email = "support@devnomads.nl" }]
13
+ keywords = ["devnomads", "dns", "acme", "powerdns", "api"]
14
+ dependencies = ["httpx>=0.27"]
15
+
16
+ [project.optional-dependencies]
17
+ # The ACME client pulls in the only heavy dependencies in the project, so
18
+ # it lives behind an extra: `pip install devnomads[acme]`. Without it,
19
+ # `import devnomads.acme` raises a clear, actionable ImportError.
20
+ acme = ["acme>=2.11", "josepy>=1.14", "cryptography>=42", "dnspython>=2.6"]
21
+
22
+ [project.urls]
23
+ Homepage = "https://devnomads.nl"
24
+ Source = "https://gitlab.infrapod.nl/infrapod/devnomads-libs"
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "pytest>=8",
29
+ "respx>=0.21",
30
+ "black>=24",
31
+ "isort>=5",
32
+ "flake8>=7",
33
+ "mypy>=1.10",
34
+ "bandit>=1.7",
35
+ "semgrep>=1.50",
36
+ "pre-commit>=3",
37
+ ]
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/devnomads"]
41
+
42
+ [tool.black]
43
+ line-length = 88
44
+ target-version = ["py310"]
45
+
46
+ [tool.isort]
47
+ profile = "black"
48
+
49
+ [tool.mypy]
50
+ python_version = "3.10"
51
+ warn_unused_ignores = true
52
+ warn_redundant_casts = true
53
+ ignore_missing_imports = true
@@ -0,0 +1,17 @@
1
+ """Python client for the DevNomads API.
2
+
3
+ Three layers, smallest dependency footprint first:
4
+
5
+ * :mod:`devnomads.api` - HTTP transport (auth, retries, error handling)
6
+ and, in time, the generated API SDK. Depends only on ``httpx``.
7
+ * :mod:`devnomads.dns` - DNS zone/record helpers (zone discovery, rrset
8
+ merging, TXT/challenge handling) on top of the transport.
9
+ * :mod:`devnomads.acme` - ACME client (DNS-01 and HTTP-01). Requires the
10
+ ``acme`` extra: ``pip install devnomads[acme]``.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ __all__ = ["__version__"]
@@ -0,0 +1,49 @@
1
+ """ACME client for the DevNomads tooling (DNS-01 and HTTP-01 issuance).
2
+
3
+ Requires the ``acme`` extra::
4
+
5
+ pip install devnomads[acme]
6
+
7
+ Without it, importing this package raises a clear ImportError pointing at
8
+ the extra rather than a bare "No module named 'acme'".
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ # The heavy third-party dependencies (acme, josepy, cryptography, dnspython)
14
+ # only ship with the [acme] extra. Catch their absence and re-raise with an
15
+ # actionable message; let any unrelated ImportError surface unchanged.
16
+ _EXTRA_MODULES = {"acme", "josepy", "cryptography", "dns"}
17
+
18
+ try:
19
+ from .challenge_server import ChallengeServer
20
+ from .client import DEFAULT_DIRECTORY_URL, AcmeClient
21
+ from .dns01 import DevNomadsDnsProvider, DnsProvider
22
+ from .errors import AcmeError
23
+ from .http01 import Http01Solver, StandaloneSolver, WebrootSolver
24
+ from .keys import build_csr, generate_key, load_or_create_account_key, serialize_key
25
+ from .verify import verify_txt_record
26
+ except ImportError as exc: # pragma: no cover - exercised via packaging
27
+ if (exc.name or "").split(".")[0] in _EXTRA_MODULES:
28
+ raise ImportError(
29
+ "devnomads.acme requires the 'acme' extra; install it with: "
30
+ "pip install devnomads[acme]"
31
+ ) from exc
32
+ raise
33
+
34
+ __all__ = [
35
+ "AcmeClient",
36
+ "DEFAULT_DIRECTORY_URL",
37
+ "AcmeError",
38
+ "DnsProvider",
39
+ "DevNomadsDnsProvider",
40
+ "Http01Solver",
41
+ "WebrootSolver",
42
+ "StandaloneSolver",
43
+ "ChallengeServer",
44
+ "build_csr",
45
+ "generate_key",
46
+ "serialize_key",
47
+ "load_or_create_account_key",
48
+ "verify_txt_record",
49
+ ]