npmctl-digitalocean 0.3.6__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.
@@ -0,0 +1 @@
1
+ Apache-2.0
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: npmctl-digitalocean
3
+ Version: 0.3.6
4
+ Summary: DigitalOcean DNS provider extension for npmctl.
5
+ Keywords: digitalocean,dns,nginx-proxy-manager,npmctl
6
+ Author: Jacob Stewart
7
+ Author-email: Jacob Stewart <jacob@swarmauri.com>
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 1 - Planning
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Internet :: Name Service (DNS)
20
+ Classifier: Topic :: System :: Systems Administration
21
+ Requires-Dist: requests>=2.32.0
22
+ Requires-Python: >=3.10, <3.15
23
+ Project-URL: Homepage, https://github.com/groupsum/npmctl
24
+ Project-URL: Repository, https://github.com/groupsum/npmctl
25
+ Project-URL: Documentation, https://github.com/groupsum/npmctl/tree/master/packages/npmctl-digitalocean
26
+ Project-URL: Issues, https://github.com/groupsum/npmctl/issues
27
+ Description-Content-Type: text/markdown
28
+
29
+ <h1 align="center">npmctl-digitalocean</h1>
30
+
31
+ <p align="center"><strong>DigitalOcean DNS provider plugin for npmctl</strong></p>
32
+
33
+ <p align="center">
34
+ Extend <code>npmctl</code> with DigitalOcean-backed DNS record management for declarative workflows, provider discovery, and DNS-aware automation.
35
+ </p>
36
+
37
+ <p align="center">
38
+ <a href="https://pypi.org/project/npmctl-digitalocean/"><img src="https://img.shields.io/pypi/v/npmctl-digitalocean.svg" alt="PyPI version"></a>
39
+ <a href="https://pypi.org/project/npmctl-digitalocean/"><img src="https://img.shields.io/pypi/pyversions/npmctl-digitalocean.svg" alt="Python versions"></a>
40
+ <a href="https://github.com/groupsum/npmctl/actions/workflows/ci.yml"><img src="https://github.com/groupsum/npmctl/actions/workflows/ci.yml/badge.svg?branch=master" alt="CI"></a>
41
+ <a href="https://github.com/groupsum/npmctl/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="Apache 2.0 License"></a>
42
+ </p>
43
+
44
+ <p align="center">
45
+ <a href="https://hits.sh/github.com/groupsum/npmctl/blob/master/packages/npmctl-digitalocean/README.md/"><img src="https://hits.sh/github.com/groupsum/npmctl/blob/master/packages/npmctl-digitalocean/README.md.svg?label=npmctl-digitalocean%20package%20hits" alt="npmctl-digitalocean package hits"></a>
46
+ <a href="https://pepy.tech/projects/npmctl-digitalocean"><img src="https://static.pepy.tech/badge/npmctl-digitalocean" alt="npmctl-digitalocean downloads"></a>
47
+ </p>
48
+
49
+ <p align="center">
50
+ <img src="https://raw.githubusercontent.com/groupsum/npmctl/master/docs/images/marketing/npmctl-architecture-infographic.png" alt="npmctl architecture infographic">
51
+ </p>
52
+
53
+ `npmctl-digitalocean` is the DigitalOcean DNS provider package for `npmctl`. Install it when you want desired-state DNS records or DNS diagnostics to resolve through DigitalOcean instead of using only the base `npmctl` package.
54
+
55
+ ## Supported Python Versions
56
+
57
+ `npmctl-digitalocean` supports Python `3.10`, `3.11`, `3.12`, `3.13`, and `3.14`.
58
+
59
+ ## Why npmctl-digitalocean
60
+
61
+ - Adds DigitalOcean DNS provider discovery to `npmctl`
62
+ - Lets DNS workflows live beside proxy and certificate desired state
63
+ - Keeps DigitalOcean tokens out of the core CLI package
64
+ - Supports operator diagnostics through `npmctl dns doctor`
65
+ - Provides client helpers for DigitalOcean A and CNAME record workflows
66
+
67
+ ## FAQ
68
+
69
+ ### What is npmctl-digitalocean?
70
+
71
+ **Answer:** `npmctl-digitalocean` is a plugin package that teaches `npmctl` how to talk to the DigitalOcean Domain Records API for DNS record operations and DNS provider diagnostics.
72
+
73
+ ### When do I need npmctl-digitalocean?
74
+
75
+ **Answer:** You need `npmctl-digitalocean` when your `npmctl` workflow includes DigitalOcean-managed DNS records or when you want `npmctl` to validate DigitalOcean DNS connectivity and credentials.
76
+
77
+ ### Does npmctl-digitalocean work without npmctl?
78
+
79
+ **Answer:** No. `npmctl-digitalocean` is an extension package for `npmctl`, not a standalone CLI.
80
+
81
+ ### Can npmctl-digitalocean set A and CNAME records?
82
+
83
+ **Answer:** Yes. DigitalOcean's Domain Records API supports A and CNAME records, and this package exposes helpers for create, update, and delete operations.
84
+
85
+ ### What credentials are required?
86
+
87
+ **Answer:** DigitalOcean API access requires `DIGITALOCEAN_TOKEN`. For diagnostics, the token needs domain read permissions. For record changes, it needs write access to target domain records.
88
+
89
+ ## Install
90
+
91
+ Install the base CLI and the DigitalOcean provider package together:
92
+
93
+ ```bash
94
+ pipx install npmctl
95
+ pipx inject npmctl npmctl-digitalocean
96
+ npmctl plugins list
97
+ ```
98
+
99
+ With `uv`:
100
+
101
+ ```bash
102
+ uv tool install npmctl
103
+ uv tool install npmctl-digitalocean
104
+ npmctl plugins list
105
+ ```
106
+
107
+ Inside a virtual environment:
108
+
109
+ ```bash
110
+ python -m venv .venv
111
+ . .venv/bin/activate
112
+ python -m pip install npmctl npmctl-digitalocean
113
+ npmctl plugins list
114
+ ```
115
+
116
+ ## Configure DigitalOcean
117
+
118
+ Set the required environment variable:
119
+
120
+ ```bash
121
+ export DIGITALOCEAN_TOKEN=your-digitalocean-token
122
+ ```
123
+
124
+ Optional for tests or alternate endpoints:
125
+
126
+ ```bash
127
+ export DIGITALOCEAN_API_BASE_URL=https://api.digitalocean.com
128
+ ```
129
+
130
+ ## Verify Plugin Discovery
131
+
132
+ Check that `npmctl` can discover the provider:
133
+
134
+ ```bash
135
+ npmctl plugins list
136
+ npmctl dns doctor --provider digitalocean
137
+ ```
138
+
139
+ ## Minimal DNS Workflow
140
+
141
+ Once the provider is installed and configured, `npmctl` can validate or diagnose DigitalOcean-backed DNS behavior through the base CLI:
142
+
143
+ ```bash
144
+ npmctl dns providers
145
+ npmctl dns zones --provider digitalocean
146
+ npmctl dns records --provider digitalocean --zone example.com
147
+ ```
148
+
149
+ ## DigitalOcean API Surface
150
+
151
+ The provider follows the DigitalOcean Domains and Domain Records API:
152
+
153
+ - `GET /v2/domains`: discover domains managed in the account.
154
+ - `GET /v2/domains/{domain_name}/records`: list DNS records for one domain.
155
+ - `POST /v2/domains/{domain_name}/records`: create A, CNAME, and other supported records.
156
+ - `PUT /v2/domains/{domain_name}/records/{domain_record_id}`: update a record.
157
+ - `DELETE /v2/domains/{domain_name}/records/{domain_record_id}`: delete a record.
158
+
159
+ ## Programmatic Record Operations
160
+
161
+ ```python
162
+ from npmctl_digitalocean import DigitalOceanClient, DigitalOceanConfig
163
+
164
+ client = DigitalOceanClient(DigitalOceanConfig.from_env())
165
+ record = client.create_record("example.com", type="A", name="www", value="192.0.2.10", ttl=300)
166
+ client.update_record("example.com", int(record.record_id), type="A", name="www", value="192.0.2.11", ttl=300)
167
+ client.delete_record("example.com", int(record.record_id))
168
+ ```
169
+
170
+ CNAME records use `type="CNAME"` and place the target host in `value`.
171
+
172
+ ## Safety Notes
173
+
174
+ - DigitalOcean record `name` is relative to the zone; use `@` for the root where applicable.
175
+ - Keep `DIGITALOCEAN_TOKEN` out of desired-state files and logs.
176
+ - Use account and token scoping to avoid mutating foreign-owned DNS.
177
+ - Use npmctl owner metadata for desired DNS records so future apply support can remain owner-scoped.
178
+
179
+ ## More Documentation
180
+
181
+ - Related PyPI package: https://pypi.org/project/npmctl/
182
+ - Repository: https://github.com/groupsum/npmctl
183
+ - DNS provider docs: https://github.com/groupsum/npmctl/tree/master/docs/dns-providers.md
@@ -0,0 +1,155 @@
1
+ <h1 align="center">npmctl-digitalocean</h1>
2
+
3
+ <p align="center"><strong>DigitalOcean DNS provider plugin for npmctl</strong></p>
4
+
5
+ <p align="center">
6
+ Extend <code>npmctl</code> with DigitalOcean-backed DNS record management for declarative workflows, provider discovery, and DNS-aware automation.
7
+ </p>
8
+
9
+ <p align="center">
10
+ <a href="https://pypi.org/project/npmctl-digitalocean/"><img src="https://img.shields.io/pypi/v/npmctl-digitalocean.svg" alt="PyPI version"></a>
11
+ <a href="https://pypi.org/project/npmctl-digitalocean/"><img src="https://img.shields.io/pypi/pyversions/npmctl-digitalocean.svg" alt="Python versions"></a>
12
+ <a href="https://github.com/groupsum/npmctl/actions/workflows/ci.yml"><img src="https://github.com/groupsum/npmctl/actions/workflows/ci.yml/badge.svg?branch=master" alt="CI"></a>
13
+ <a href="https://github.com/groupsum/npmctl/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="Apache 2.0 License"></a>
14
+ </p>
15
+
16
+ <p align="center">
17
+ <a href="https://hits.sh/github.com/groupsum/npmctl/blob/master/packages/npmctl-digitalocean/README.md/"><img src="https://hits.sh/github.com/groupsum/npmctl/blob/master/packages/npmctl-digitalocean/README.md.svg?label=npmctl-digitalocean%20package%20hits" alt="npmctl-digitalocean package hits"></a>
18
+ <a href="https://pepy.tech/projects/npmctl-digitalocean"><img src="https://static.pepy.tech/badge/npmctl-digitalocean" alt="npmctl-digitalocean downloads"></a>
19
+ </p>
20
+
21
+ <p align="center">
22
+ <img src="https://raw.githubusercontent.com/groupsum/npmctl/master/docs/images/marketing/npmctl-architecture-infographic.png" alt="npmctl architecture infographic">
23
+ </p>
24
+
25
+ `npmctl-digitalocean` is the DigitalOcean DNS provider package for `npmctl`. Install it when you want desired-state DNS records or DNS diagnostics to resolve through DigitalOcean instead of using only the base `npmctl` package.
26
+
27
+ ## Supported Python Versions
28
+
29
+ `npmctl-digitalocean` supports Python `3.10`, `3.11`, `3.12`, `3.13`, and `3.14`.
30
+
31
+ ## Why npmctl-digitalocean
32
+
33
+ - Adds DigitalOcean DNS provider discovery to `npmctl`
34
+ - Lets DNS workflows live beside proxy and certificate desired state
35
+ - Keeps DigitalOcean tokens out of the core CLI package
36
+ - Supports operator diagnostics through `npmctl dns doctor`
37
+ - Provides client helpers for DigitalOcean A and CNAME record workflows
38
+
39
+ ## FAQ
40
+
41
+ ### What is npmctl-digitalocean?
42
+
43
+ **Answer:** `npmctl-digitalocean` is a plugin package that teaches `npmctl` how to talk to the DigitalOcean Domain Records API for DNS record operations and DNS provider diagnostics.
44
+
45
+ ### When do I need npmctl-digitalocean?
46
+
47
+ **Answer:** You need `npmctl-digitalocean` when your `npmctl` workflow includes DigitalOcean-managed DNS records or when you want `npmctl` to validate DigitalOcean DNS connectivity and credentials.
48
+
49
+ ### Does npmctl-digitalocean work without npmctl?
50
+
51
+ **Answer:** No. `npmctl-digitalocean` is an extension package for `npmctl`, not a standalone CLI.
52
+
53
+ ### Can npmctl-digitalocean set A and CNAME records?
54
+
55
+ **Answer:** Yes. DigitalOcean's Domain Records API supports A and CNAME records, and this package exposes helpers for create, update, and delete operations.
56
+
57
+ ### What credentials are required?
58
+
59
+ **Answer:** DigitalOcean API access requires `DIGITALOCEAN_TOKEN`. For diagnostics, the token needs domain read permissions. For record changes, it needs write access to target domain records.
60
+
61
+ ## Install
62
+
63
+ Install the base CLI and the DigitalOcean provider package together:
64
+
65
+ ```bash
66
+ pipx install npmctl
67
+ pipx inject npmctl npmctl-digitalocean
68
+ npmctl plugins list
69
+ ```
70
+
71
+ With `uv`:
72
+
73
+ ```bash
74
+ uv tool install npmctl
75
+ uv tool install npmctl-digitalocean
76
+ npmctl plugins list
77
+ ```
78
+
79
+ Inside a virtual environment:
80
+
81
+ ```bash
82
+ python -m venv .venv
83
+ . .venv/bin/activate
84
+ python -m pip install npmctl npmctl-digitalocean
85
+ npmctl plugins list
86
+ ```
87
+
88
+ ## Configure DigitalOcean
89
+
90
+ Set the required environment variable:
91
+
92
+ ```bash
93
+ export DIGITALOCEAN_TOKEN=your-digitalocean-token
94
+ ```
95
+
96
+ Optional for tests or alternate endpoints:
97
+
98
+ ```bash
99
+ export DIGITALOCEAN_API_BASE_URL=https://api.digitalocean.com
100
+ ```
101
+
102
+ ## Verify Plugin Discovery
103
+
104
+ Check that `npmctl` can discover the provider:
105
+
106
+ ```bash
107
+ npmctl plugins list
108
+ npmctl dns doctor --provider digitalocean
109
+ ```
110
+
111
+ ## Minimal DNS Workflow
112
+
113
+ Once the provider is installed and configured, `npmctl` can validate or diagnose DigitalOcean-backed DNS behavior through the base CLI:
114
+
115
+ ```bash
116
+ npmctl dns providers
117
+ npmctl dns zones --provider digitalocean
118
+ npmctl dns records --provider digitalocean --zone example.com
119
+ ```
120
+
121
+ ## DigitalOcean API Surface
122
+
123
+ The provider follows the DigitalOcean Domains and Domain Records API:
124
+
125
+ - `GET /v2/domains`: discover domains managed in the account.
126
+ - `GET /v2/domains/{domain_name}/records`: list DNS records for one domain.
127
+ - `POST /v2/domains/{domain_name}/records`: create A, CNAME, and other supported records.
128
+ - `PUT /v2/domains/{domain_name}/records/{domain_record_id}`: update a record.
129
+ - `DELETE /v2/domains/{domain_name}/records/{domain_record_id}`: delete a record.
130
+
131
+ ## Programmatic Record Operations
132
+
133
+ ```python
134
+ from npmctl_digitalocean import DigitalOceanClient, DigitalOceanConfig
135
+
136
+ client = DigitalOceanClient(DigitalOceanConfig.from_env())
137
+ record = client.create_record("example.com", type="A", name="www", value="192.0.2.10", ttl=300)
138
+ client.update_record("example.com", int(record.record_id), type="A", name="www", value="192.0.2.11", ttl=300)
139
+ client.delete_record("example.com", int(record.record_id))
140
+ ```
141
+
142
+ CNAME records use `type="CNAME"` and place the target host in `value`.
143
+
144
+ ## Safety Notes
145
+
146
+ - DigitalOcean record `name` is relative to the zone; use `@` for the root where applicable.
147
+ - Keep `DIGITALOCEAN_TOKEN` out of desired-state files and logs.
148
+ - Use account and token scoping to avoid mutating foreign-owned DNS.
149
+ - Use npmctl owner metadata for desired DNS records so future apply support can remain owner-scoped.
150
+
151
+ ## More Documentation
152
+
153
+ - Related PyPI package: https://pypi.org/project/npmctl/
154
+ - Repository: https://github.com/groupsum/npmctl
155
+ - DNS provider docs: https://github.com/groupsum/npmctl/tree/master/docs/dns-providers.md
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "npmctl-digitalocean"
3
+ version = "0.3.6"
4
+ description = "DigitalOcean DNS provider extension for npmctl."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10,<3.15"
7
+ license = "Apache-2.0"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
10
+ classifiers = [
11
+ "Development Status :: 1 - Planning",
12
+ "Intended Audience :: System Administrators",
13
+ "License :: OSI Approved :: Apache Software License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3.14",
20
+ "Topic :: Internet :: Name Service (DNS)",
21
+ "Topic :: System :: Systems Administration",
22
+ ]
23
+ keywords = ["digitalocean", "dns", "nginx-proxy-manager", "npmctl"]
24
+ dependencies = [
25
+ "requests>=2.32.0",
26
+ ]
27
+
28
+ [project.entry-points."npmctl.dns_providers"]
29
+ digitalocean = "npmctl_digitalocean.provider:DigitalOceanDnsProvider"
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/groupsum/npmctl"
33
+ Repository = "https://github.com/groupsum/npmctl"
34
+ Documentation = "https://github.com/groupsum/npmctl/tree/master/packages/npmctl-digitalocean"
35
+ Issues = "https://github.com/groupsum/npmctl/issues"
36
+
37
+ [build-system]
38
+ requires = ["uv-build>=0.11.8,<0.12"]
39
+ build-backend = "uv_build"
@@ -0,0 +1,7 @@
1
+ """DigitalOcean DNS extension for npmctl."""
2
+
3
+ from npmctl_digitalocean.client import DigitalOceanClient
4
+ from npmctl_digitalocean.config import DigitalOceanConfig
5
+ from npmctl_digitalocean.provider import DigitalOceanDnsProvider
6
+
7
+ __all__ = ["DigitalOceanClient", "DigitalOceanConfig", "DigitalOceanDnsProvider"]
@@ -0,0 +1,97 @@
1
+ """Small DigitalOcean DNS API client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import requests
8
+
9
+ from npmctl_digitalocean.config import DigitalOceanConfig
10
+ from npmctl_digitalocean.errors import DigitalOceanError
11
+ from npmctl_digitalocean.models import DigitalOceanRecord
12
+
13
+
14
+ class DigitalOceanClient:
15
+ """HTTP client for DigitalOcean domain records."""
16
+
17
+ def __init__(self, config: DigitalOceanConfig, *, timeout_s: float = 15.0) -> None:
18
+ self.config = config
19
+ self.timeout_s = timeout_s
20
+ self.session = requests.Session()
21
+
22
+ def zones(self) -> tuple[str, ...]:
23
+ data = self._request("GET", "/v2/domains")
24
+ return tuple(sorted(_zone(str(item.get("name", ""))) for item in data.get("domains", []) if item.get("name")))
25
+
26
+ def records(self, zone: str) -> tuple[DigitalOceanRecord, ...]:
27
+ data = self._request("GET", f"/v2/domains/{_zone(zone)}/records")
28
+ return tuple(DigitalOceanRecord.from_mapping(item) for item in data.get("domain_records", []))
29
+
30
+ def create_record(
31
+ self,
32
+ zone: str,
33
+ *,
34
+ type: str,
35
+ name: str,
36
+ value: str,
37
+ ttl: int | None = None,
38
+ priority: int | None = None,
39
+ ) -> DigitalOceanRecord:
40
+ data = self._request(
41
+ "POST", f"/v2/domains/{_zone(zone)}/records", json=_record_payload(type, name, value, ttl, priority)
42
+ )
43
+ return DigitalOceanRecord.from_mapping(data.get("domain_record", {}))
44
+
45
+ def update_record(
46
+ self,
47
+ zone: str,
48
+ record_id: int,
49
+ *,
50
+ type: str,
51
+ name: str,
52
+ value: str,
53
+ ttl: int | None = None,
54
+ priority: int | None = None,
55
+ ) -> DigitalOceanRecord:
56
+ data = self._request(
57
+ "PUT",
58
+ f"/v2/domains/{_zone(zone)}/records/{record_id}",
59
+ json=_record_payload(type, name, value, ttl, priority),
60
+ )
61
+ return DigitalOceanRecord.from_mapping(data.get("domain_record", {}))
62
+
63
+ def delete_record(self, zone: str, record_id: int) -> None:
64
+ self._request("DELETE", f"/v2/domains/{_zone(zone)}/records/{record_id}")
65
+
66
+ def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
67
+ response = self.session.request(
68
+ method,
69
+ f"{self.config.api_base_url}{path}",
70
+ headers={"Authorization": f"Bearer {self.config.token}"},
71
+ timeout=self.timeout_s,
72
+ **kwargs,
73
+ )
74
+ if response.status_code < 200 or response.status_code >= 300:
75
+ raise DigitalOceanError(f"DigitalOcean API failed: HTTP {response.status_code}")
76
+ if response.status_code == 204:
77
+ return {}
78
+ try:
79
+ data = response.json()
80
+ except ValueError as exc:
81
+ raise DigitalOceanError("DigitalOcean API returned invalid JSON") from exc
82
+ if isinstance(data, dict) and data.get("id") == "unauthorized":
83
+ raise DigitalOceanError(str(data.get("message", "DigitalOcean API request failed")))
84
+ return data
85
+
86
+
87
+ def _record_payload(type: str, name: str, value: str, ttl: int | None, priority: int | None) -> dict[str, object]:
88
+ payload: dict[str, object] = {"type": type.upper(), "name": name, "data": value}
89
+ if ttl is not None:
90
+ payload["ttl"] = ttl
91
+ if priority is not None:
92
+ payload["priority"] = priority
93
+ return payload
94
+
95
+
96
+ def _zone(zone: str) -> str:
97
+ return zone.strip().lower().rstrip(".")
@@ -0,0 +1,26 @@
1
+ """Configuration loading for the DigitalOcean DNS provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from typing import Mapping
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class DigitalOceanConfig:
12
+ """DigitalOcean API configuration."""
13
+
14
+ token: str
15
+ api_base_url: str = "https://api.digitalocean.com"
16
+
17
+ @classmethod
18
+ def from_env(cls, env: Mapping[str, str] | None = None) -> DigitalOceanConfig:
19
+ values = os.environ if env is None else env
20
+ token = values.get("DIGITALOCEAN_TOKEN")
21
+ if not token:
22
+ raise ValueError("missing DigitalOcean config: token")
23
+ return cls(token=token, api_base_url=values.get("DIGITALOCEAN_API_BASE_URL", "https://api.digitalocean.com"))
24
+
25
+ def redacted(self) -> dict[str, str | bool]:
26
+ return {"token": bool(self.token), "api_base_url": self.api_base_url}
@@ -0,0 +1,7 @@
1
+ """DigitalOcean provider errors."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class DigitalOceanError(RuntimeError):
7
+ """Raised when the DigitalOcean API returns an error."""
@@ -0,0 +1,45 @@
1
+ """DigitalOcean DNS response models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Mapping
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class DigitalOceanRecord:
11
+ """One DigitalOcean domain record."""
12
+
13
+ record_id: int | None
14
+ name: str
15
+ type: str
16
+ value: str
17
+ ttl: int | None = None
18
+ priority: int | None = None
19
+
20
+ @classmethod
21
+ def from_mapping(cls, raw: Mapping[str, Any]) -> DigitalOceanRecord:
22
+ return cls(
23
+ record_id=_optional_int(raw.get("id")),
24
+ name=str(raw.get("name", "")).lower().rstrip("."),
25
+ type=str(raw.get("type", "")).upper(),
26
+ value=str(raw.get("data", "")),
27
+ ttl=_optional_int(raw.get("ttl")),
28
+ priority=_optional_int(raw.get("priority")),
29
+ )
30
+
31
+ def to_dict(self) -> dict[str, str | int | None]:
32
+ return {
33
+ "id": self.record_id,
34
+ "name": self.name,
35
+ "type": self.type,
36
+ "value": self.value,
37
+ "ttl": self.ttl,
38
+ "priority": self.priority,
39
+ }
40
+
41
+
42
+ def _optional_int(value: Any) -> int | None:
43
+ if value in (None, ""):
44
+ return None
45
+ return int(value)
@@ -0,0 +1,27 @@
1
+ """npmctl DNS provider implementation for DigitalOcean."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from npmctl_digitalocean.client import DigitalOceanClient
6
+ from npmctl_digitalocean.config import DigitalOceanConfig
7
+
8
+
9
+ class DigitalOceanDnsProvider:
10
+ """DNS provider backed by the DigitalOcean Domain Records API."""
11
+
12
+ name = "digitalocean"
13
+
14
+ def __init__(self, client: DigitalOceanClient | None = None) -> None:
15
+ self._client = client
16
+
17
+ @property
18
+ def client(self) -> DigitalOceanClient:
19
+ if self._client is None:
20
+ self._client = DigitalOceanClient(DigitalOceanConfig.from_env())
21
+ return self._client
22
+
23
+ def zones(self) -> tuple[str, ...]:
24
+ return self.client.zones()
25
+
26
+ def records(self, zone: str) -> tuple[dict[str, object], ...]:
27
+ return tuple(record.to_dict() for record in self.client.records(zone))