npmctl-route53 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,198 @@
1
+ Metadata-Version: 2.4
2
+ Name: npmctl-route53
3
+ Version: 0.3.6
4
+ Summary: AWS Route 53 DNS provider extension for npmctl.
5
+ Keywords: aws,route53,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: boto3>=1.34.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-route53
26
+ Project-URL: Issues, https://github.com/groupsum/npmctl/issues
27
+ Description-Content-Type: text/markdown
28
+
29
+ <h1 align="center">npmctl-route53</h1>
30
+
31
+ <p align="center"><strong>AWS Route 53 DNS provider plugin for npmctl</strong></p>
32
+
33
+ <p align="center">
34
+ Extend <code>npmctl</code> with Route 53-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-route53/"><img src="https://img.shields.io/pypi/v/npmctl-route53.svg" alt="PyPI version"></a>
39
+ <a href="https://pypi.org/project/npmctl-route53/"><img src="https://img.shields.io/pypi/pyversions/npmctl-route53.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-route53/README.md/"><img src="https://hits.sh/github.com/groupsum/npmctl/blob/master/packages/npmctl-route53/README.md.svg?label=npmctl-route53%20package%20hits" alt="npmctl-route53 package hits"></a>
46
+ <a href="https://pepy.tech/projects/npmctl-route53"><img src="https://static.pepy.tech/badge/npmctl-route53" alt="npmctl-route53 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-route53` is the AWS Route 53 DNS provider package for `npmctl`. Install it when you want desired-state DNS records or DNS diagnostics to resolve through Route 53 instead of using only the base `npmctl` package.
54
+
55
+ ## Supported Python Versions
56
+
57
+ `npmctl-route53` supports Python `3.10`, `3.11`, `3.12`, `3.13`, and `3.14`.
58
+
59
+ ## Why npmctl-route53
60
+
61
+ - Adds Route 53 DNS provider discovery to `npmctl`
62
+ - Lets DNS workflows live beside proxy and certificate desired state
63
+ - Keeps AWS DNS dependencies out of the core CLI package
64
+ - Supports operator diagnostics through `npmctl dns doctor`
65
+ - Provides client helpers for Route 53 A and CNAME change-batch workflows
66
+
67
+ ## FAQ
68
+
69
+ ### What is npmctl-route53?
70
+
71
+ **Answer:** `npmctl-route53` is a plugin package that teaches `npmctl` how to talk to AWS Route 53 through `boto3` for DNS record operations and DNS provider diagnostics.
72
+
73
+ ### When do I need npmctl-route53?
74
+
75
+ **Answer:** You need `npmctl-route53` when your `npmctl` workflow includes Route 53 hosted-zone DNS records or when you want `npmctl` to validate Route 53 DNS connectivity and credentials.
76
+
77
+ ### Does npmctl-route53 work without npmctl?
78
+
79
+ **Answer:** No. `npmctl-route53` is an extension package for `npmctl`, not a standalone CLI.
80
+
81
+ ### Can npmctl-route53 set A and CNAME records?
82
+
83
+ **Answer:** Yes. Route 53 supports A and CNAME record sets through `ChangeResourceRecordSets`, and this package exposes helpers for `CREATE`, `UPSERT`, and `DELETE` batches.
84
+
85
+ ### What credentials are required?
86
+
87
+ **Answer:** Route 53 access uses the standard AWS credential chain or `ROUTE53_PROFILE`. Diagnostics require hosted-zone and record-set list permissions; mutation helpers require `route53:ChangeResourceRecordSets`.
88
+
89
+ ## Install
90
+
91
+ Install the base CLI and the Route 53 provider package together:
92
+
93
+ ```bash
94
+ pipx install npmctl
95
+ pipx inject npmctl npmctl-route53
96
+ npmctl plugins list
97
+ ```
98
+
99
+ With `uv`:
100
+
101
+ ```bash
102
+ uv tool install npmctl
103
+ uv tool install npmctl-route53
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-route53
113
+ npmctl plugins list
114
+ ```
115
+
116
+ ## Configure Route 53
117
+
118
+ Use the standard AWS credential chain:
119
+
120
+ ```bash
121
+ export AWS_ACCESS_KEY_ID=...
122
+ export AWS_SECRET_ACCESS_KEY=...
123
+ export AWS_SESSION_TOKEN=...
124
+ ```
125
+
126
+ Or use a named profile:
127
+
128
+ ```bash
129
+ export AWS_PROFILE=production-dns
130
+ ```
131
+
132
+ Optional package-specific override:
133
+
134
+ ```bash
135
+ export ROUTE53_PROFILE=production-dns
136
+ ```
137
+
138
+ ## Verify Plugin Discovery
139
+
140
+ Check that `npmctl` can discover the provider:
141
+
142
+ ```bash
143
+ npmctl plugins list
144
+ npmctl dns doctor --provider route53
145
+ ```
146
+
147
+ ## Minimal DNS Workflow
148
+
149
+ Once the provider is installed and configured, `npmctl` can validate or diagnose Route 53-backed DNS behavior through the base CLI:
150
+
151
+ ```bash
152
+ npmctl dns providers
153
+ npmctl dns zones --provider route53
154
+ npmctl dns records --provider route53 --zone example.com
155
+ ```
156
+
157
+ ## Route 53 API Surface
158
+
159
+ The provider follows the AWS Route 53 API through `boto3`:
160
+
161
+ - `ListHostedZones`: discover hosted zones.
162
+ - `ListResourceRecordSets`: list records in one hosted zone.
163
+ - `ChangeResourceRecordSets` with `CREATE`: create record sets.
164
+ - `ChangeResourceRecordSets` with `UPSERT`: create or update record sets.
165
+ - `ChangeResourceRecordSets` with `DELETE`: delete record sets.
166
+
167
+ Required IAM actions for diagnostics:
168
+
169
+ - `route53:ListHostedZones`
170
+ - `route53:ListResourceRecordSets`
171
+
172
+ Required IAM action for mutation helpers:
173
+
174
+ - `route53:ChangeResourceRecordSets`
175
+
176
+ ## Programmatic Record Operations
177
+
178
+ ```python
179
+ from npmctl_route53 import Route53Client, Route53Config
180
+
181
+ client = Route53Client(Route53Config.from_env())
182
+ client.create_record("example.com", type="A", name="www", value="192.0.2.10", ttl=300)
183
+ client.upsert_record("example.com", type="CNAME", name="app", value="target.example.net", ttl=300)
184
+ client.delete_record("example.com", type="A", name="www", value="192.0.2.10", ttl=300)
185
+ ```
186
+
187
+ ## Safety Notes
188
+
189
+ - Route 53 changes are hosted-zone scoped. Confirm the selected hosted zone before mutation.
190
+ - Prefer IAM policies scoped to the intended hosted zone ARN.
191
+ - `UPSERT` can overwrite live DNS answers. Use create-only workflows when adoption is not explicit.
192
+ - Use npmctl owner metadata for desired DNS records so future apply support can remain owner-scoped.
193
+
194
+ ## More Documentation
195
+
196
+ - Related PyPI package: https://pypi.org/project/npmctl/
197
+ - Repository: https://github.com/groupsum/npmctl
198
+ - DNS provider docs: https://github.com/groupsum/npmctl/tree/master/docs/dns-providers.md
@@ -0,0 +1,170 @@
1
+ <h1 align="center">npmctl-route53</h1>
2
+
3
+ <p align="center"><strong>AWS Route 53 DNS provider plugin for npmctl</strong></p>
4
+
5
+ <p align="center">
6
+ Extend <code>npmctl</code> with Route 53-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-route53/"><img src="https://img.shields.io/pypi/v/npmctl-route53.svg" alt="PyPI version"></a>
11
+ <a href="https://pypi.org/project/npmctl-route53/"><img src="https://img.shields.io/pypi/pyversions/npmctl-route53.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-route53/README.md/"><img src="https://hits.sh/github.com/groupsum/npmctl/blob/master/packages/npmctl-route53/README.md.svg?label=npmctl-route53%20package%20hits" alt="npmctl-route53 package hits"></a>
18
+ <a href="https://pepy.tech/projects/npmctl-route53"><img src="https://static.pepy.tech/badge/npmctl-route53" alt="npmctl-route53 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-route53` is the AWS Route 53 DNS provider package for `npmctl`. Install it when you want desired-state DNS records or DNS diagnostics to resolve through Route 53 instead of using only the base `npmctl` package.
26
+
27
+ ## Supported Python Versions
28
+
29
+ `npmctl-route53` supports Python `3.10`, `3.11`, `3.12`, `3.13`, and `3.14`.
30
+
31
+ ## Why npmctl-route53
32
+
33
+ - Adds Route 53 DNS provider discovery to `npmctl`
34
+ - Lets DNS workflows live beside proxy and certificate desired state
35
+ - Keeps AWS DNS dependencies out of the core CLI package
36
+ - Supports operator diagnostics through `npmctl dns doctor`
37
+ - Provides client helpers for Route 53 A and CNAME change-batch workflows
38
+
39
+ ## FAQ
40
+
41
+ ### What is npmctl-route53?
42
+
43
+ **Answer:** `npmctl-route53` is a plugin package that teaches `npmctl` how to talk to AWS Route 53 through `boto3` for DNS record operations and DNS provider diagnostics.
44
+
45
+ ### When do I need npmctl-route53?
46
+
47
+ **Answer:** You need `npmctl-route53` when your `npmctl` workflow includes Route 53 hosted-zone DNS records or when you want `npmctl` to validate Route 53 DNS connectivity and credentials.
48
+
49
+ ### Does npmctl-route53 work without npmctl?
50
+
51
+ **Answer:** No. `npmctl-route53` is an extension package for `npmctl`, not a standalone CLI.
52
+
53
+ ### Can npmctl-route53 set A and CNAME records?
54
+
55
+ **Answer:** Yes. Route 53 supports A and CNAME record sets through `ChangeResourceRecordSets`, and this package exposes helpers for `CREATE`, `UPSERT`, and `DELETE` batches.
56
+
57
+ ### What credentials are required?
58
+
59
+ **Answer:** Route 53 access uses the standard AWS credential chain or `ROUTE53_PROFILE`. Diagnostics require hosted-zone and record-set list permissions; mutation helpers require `route53:ChangeResourceRecordSets`.
60
+
61
+ ## Install
62
+
63
+ Install the base CLI and the Route 53 provider package together:
64
+
65
+ ```bash
66
+ pipx install npmctl
67
+ pipx inject npmctl npmctl-route53
68
+ npmctl plugins list
69
+ ```
70
+
71
+ With `uv`:
72
+
73
+ ```bash
74
+ uv tool install npmctl
75
+ uv tool install npmctl-route53
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-route53
85
+ npmctl plugins list
86
+ ```
87
+
88
+ ## Configure Route 53
89
+
90
+ Use the standard AWS credential chain:
91
+
92
+ ```bash
93
+ export AWS_ACCESS_KEY_ID=...
94
+ export AWS_SECRET_ACCESS_KEY=...
95
+ export AWS_SESSION_TOKEN=...
96
+ ```
97
+
98
+ Or use a named profile:
99
+
100
+ ```bash
101
+ export AWS_PROFILE=production-dns
102
+ ```
103
+
104
+ Optional package-specific override:
105
+
106
+ ```bash
107
+ export ROUTE53_PROFILE=production-dns
108
+ ```
109
+
110
+ ## Verify Plugin Discovery
111
+
112
+ Check that `npmctl` can discover the provider:
113
+
114
+ ```bash
115
+ npmctl plugins list
116
+ npmctl dns doctor --provider route53
117
+ ```
118
+
119
+ ## Minimal DNS Workflow
120
+
121
+ Once the provider is installed and configured, `npmctl` can validate or diagnose Route 53-backed DNS behavior through the base CLI:
122
+
123
+ ```bash
124
+ npmctl dns providers
125
+ npmctl dns zones --provider route53
126
+ npmctl dns records --provider route53 --zone example.com
127
+ ```
128
+
129
+ ## Route 53 API Surface
130
+
131
+ The provider follows the AWS Route 53 API through `boto3`:
132
+
133
+ - `ListHostedZones`: discover hosted zones.
134
+ - `ListResourceRecordSets`: list records in one hosted zone.
135
+ - `ChangeResourceRecordSets` with `CREATE`: create record sets.
136
+ - `ChangeResourceRecordSets` with `UPSERT`: create or update record sets.
137
+ - `ChangeResourceRecordSets` with `DELETE`: delete record sets.
138
+
139
+ Required IAM actions for diagnostics:
140
+
141
+ - `route53:ListHostedZones`
142
+ - `route53:ListResourceRecordSets`
143
+
144
+ Required IAM action for mutation helpers:
145
+
146
+ - `route53:ChangeResourceRecordSets`
147
+
148
+ ## Programmatic Record Operations
149
+
150
+ ```python
151
+ from npmctl_route53 import Route53Client, Route53Config
152
+
153
+ client = Route53Client(Route53Config.from_env())
154
+ client.create_record("example.com", type="A", name="www", value="192.0.2.10", ttl=300)
155
+ client.upsert_record("example.com", type="CNAME", name="app", value="target.example.net", ttl=300)
156
+ client.delete_record("example.com", type="A", name="www", value="192.0.2.10", ttl=300)
157
+ ```
158
+
159
+ ## Safety Notes
160
+
161
+ - Route 53 changes are hosted-zone scoped. Confirm the selected hosted zone before mutation.
162
+ - Prefer IAM policies scoped to the intended hosted zone ARN.
163
+ - `UPSERT` can overwrite live DNS answers. Use create-only workflows when adoption is not explicit.
164
+ - Use npmctl owner metadata for desired DNS records so future apply support can remain owner-scoped.
165
+
166
+ ## More Documentation
167
+
168
+ - Related PyPI package: https://pypi.org/project/npmctl/
169
+ - Repository: https://github.com/groupsum/npmctl
170
+ - DNS provider docs: https://github.com/groupsum/npmctl/tree/master/docs/dns-providers.md
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "npmctl-route53"
3
+ version = "0.3.6"
4
+ description = "AWS Route 53 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 = ["aws", "route53", "dns", "nginx-proxy-manager", "npmctl"]
24
+ dependencies = [
25
+ "boto3>=1.34.0",
26
+ ]
27
+
28
+ [project.entry-points."npmctl.dns_providers"]
29
+ route53 = "npmctl_route53.provider:Route53DnsProvider"
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-route53"
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
+ """AWS Route 53 DNS extension for npmctl."""
2
+
3
+ from npmctl_route53.client import Route53Client
4
+ from npmctl_route53.config import Route53Config
5
+ from npmctl_route53.provider import Route53DnsProvider
6
+
7
+ __all__ = ["Route53Client", "Route53Config", "Route53DnsProvider"]
@@ -0,0 +1,98 @@
1
+ """Small AWS Route 53 API client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from npmctl_route53.config import Route53Config
6
+ from npmctl_route53.errors import Route53Error
7
+ from npmctl_route53.models import Route53Record, Route53Zone
8
+
9
+
10
+ class Route53Client:
11
+ """Client wrapper for Route 53 hosted zones and record sets."""
12
+
13
+ def __init__(self, config: Route53Config, *, api: object | None = None) -> None:
14
+ self.config = config
15
+ self._api = api
16
+
17
+ @property
18
+ def api(self) -> object:
19
+ if self._api is None:
20
+ self._api = _default_api(self.config)
21
+ return self._api
22
+
23
+ def zones(self) -> tuple[str, ...]:
24
+ return tuple(zone.name for zone in self._zones())
25
+
26
+ def records(self, zone: str) -> tuple[Route53Record, ...]:
27
+ zone_id = self._zone_id(zone)
28
+ data = self.api.list_resource_record_sets(HostedZoneId=zone_id) # type: ignore[attr-defined]
29
+ return tuple(Route53Record.from_mapping(item) for item in data.get("ResourceRecordSets", []))
30
+
31
+ def create_record(self, zone: str, *, type: str, name: str, value: str, ttl: int = 300) -> str | None:
32
+ return self._change_record("CREATE", zone, type=type, name=name, value=value, ttl=ttl)
33
+
34
+ def upsert_record(self, zone: str, *, type: str, name: str, value: str, ttl: int = 300) -> str | None:
35
+ return self._change_record("UPSERT", zone, type=type, name=name, value=value, ttl=ttl)
36
+
37
+ def delete_record(self, zone: str, *, type: str, name: str, value: str, ttl: int = 300) -> str | None:
38
+ return self._change_record("DELETE", zone, type=type, name=name, value=value, ttl=ttl)
39
+
40
+ def _zones(self) -> tuple[Route53Zone, ...]:
41
+ data = self.api.list_hosted_zones() # type: ignore[attr-defined]
42
+ return tuple(Route53Zone.from_mapping(item) for item in data.get("HostedZones", []))
43
+
44
+ def _zone_id(self, zone: str) -> str:
45
+ target = _dns_name(zone)
46
+ for item in self._zones():
47
+ if item.name == target:
48
+ return item.zone_id
49
+ raise Route53Error(f"Route 53 hosted zone not found: {zone}")
50
+
51
+ def _change_record(self, action: str, zone: str, *, type: str, name: str, value: str, ttl: int) -> str | None:
52
+ data = self.api.change_resource_record_sets( # type: ignore[attr-defined]
53
+ HostedZoneId=self._zone_id(zone),
54
+ ChangeBatch={
55
+ "Changes": [
56
+ {
57
+ "Action": action,
58
+ "ResourceRecordSet": {
59
+ "Name": _absolute_name(name, zone),
60
+ "Type": type.upper(),
61
+ "TTL": ttl,
62
+ "ResourceRecords": [{"Value": value}],
63
+ },
64
+ }
65
+ ]
66
+ },
67
+ )
68
+ change = data.get("ChangeInfo", {})
69
+ change_id = change.get("Id")
70
+ return None if change_id is None else str(change_id)
71
+
72
+
73
+ def _default_api(config: Route53Config) -> object:
74
+ try:
75
+ import boto3
76
+ except ImportError as exc: # pragma: no cover - exercised only without package dependency installed.
77
+ raise Route53Error("boto3 is required for live Route 53 access") from exc
78
+ session_kwargs: dict[str, str] = {}
79
+ if config.profile:
80
+ session_kwargs["profile_name"] = config.profile
81
+ if config.region_name:
82
+ session_kwargs["region_name"] = config.region_name
83
+ session = boto3.Session(**session_kwargs)
84
+ return session.client("route53")
85
+
86
+
87
+ def _absolute_name(name: str, zone: str) -> str:
88
+ normalized_name = _dns_name(name)
89
+ normalized_zone = _dns_name(zone)
90
+ if normalized_name in ("", "@"):
91
+ return f"{normalized_zone}."
92
+ if normalized_name.endswith(f".{normalized_zone}"):
93
+ return f"{normalized_name}."
94
+ return f"{normalized_name}.{normalized_zone}."
95
+
96
+
97
+ def _dns_name(value: str) -> str:
98
+ return value.strip().lower().rstrip(".")
@@ -0,0 +1,26 @@
1
+ """Configuration loading for the Route 53 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 Route53Config:
12
+ """Route 53 API configuration."""
13
+
14
+ profile: str | None = None
15
+ region_name: str | None = None
16
+
17
+ @classmethod
18
+ def from_env(cls, env: Mapping[str, str] | None = None) -> Route53Config:
19
+ values = os.environ if env is None else env
20
+ return cls(
21
+ profile=values.get("ROUTE53_PROFILE") or values.get("AWS_PROFILE") or None,
22
+ region_name=values.get("AWS_REGION") or values.get("AWS_DEFAULT_REGION") or None,
23
+ )
24
+
25
+ def redacted(self) -> dict[str, str | bool | None]:
26
+ return {"profile": self.profile, "region_name": self.region_name, "aws_credentials": "standard-chain"}
@@ -0,0 +1,7 @@
1
+ """Route 53 provider errors."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class Route53Error(RuntimeError):
7
+ """Raised when the Route 53 API returns an error."""
@@ -0,0 +1,62 @@
1
+ """Route 53 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 Route53Zone:
11
+ """One Route 53 hosted zone."""
12
+
13
+ zone_id: str
14
+ name: str
15
+
16
+ @classmethod
17
+ def from_mapping(cls, raw: Mapping[str, Any]) -> Route53Zone:
18
+ return cls(zone_id=str(raw.get("Id", "")).split("/")[-1], name=_dns_name(str(raw.get("Name", ""))))
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class Route53Record:
23
+ """One Route 53 resource record set."""
24
+
25
+ name: str
26
+ type: str
27
+ values: tuple[str, ...]
28
+ ttl: int | None = None
29
+
30
+ @classmethod
31
+ def from_mapping(cls, raw: Mapping[str, Any]) -> Route53Record:
32
+ records = raw.get("ResourceRecords", [])
33
+ alias_target = raw.get("AliasTarget")
34
+ values = tuple(str(item.get("Value", "")) for item in records if isinstance(item, Mapping))
35
+ if not values and isinstance(alias_target, Mapping):
36
+ values = (str(alias_target.get("DNSName", "")),)
37
+ return cls(
38
+ name=_dns_name(str(raw.get("Name", ""))),
39
+ type=str(raw.get("Type", "")).upper(),
40
+ values=values,
41
+ ttl=_optional_int(raw.get("TTL")),
42
+ )
43
+
44
+ def to_dict(self) -> dict[str, str | int | tuple[str, ...] | None]:
45
+ return {
46
+ "id": None,
47
+ "name": self.name,
48
+ "type": self.type,
49
+ "value": self.values[0] if self.values else "",
50
+ "values": self.values,
51
+ "ttl": self.ttl,
52
+ }
53
+
54
+
55
+ def _dns_name(value: str) -> str:
56
+ return value.strip().lower().rstrip(".")
57
+
58
+
59
+ def _optional_int(value: Any) -> int | None:
60
+ if value in (None, ""):
61
+ return None
62
+ return int(value)
@@ -0,0 +1,27 @@
1
+ """npmctl DNS provider implementation for Route 53."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from npmctl_route53.client import Route53Client
6
+ from npmctl_route53.config import Route53Config
7
+
8
+
9
+ class Route53DnsProvider:
10
+ """DNS provider backed by AWS Route 53."""
11
+
12
+ name = "route53"
13
+
14
+ def __init__(self, client: Route53Client | None = None) -> None:
15
+ self._client = client
16
+
17
+ @property
18
+ def client(self) -> Route53Client:
19
+ if self._client is None:
20
+ self._client = Route53Client(Route53Config.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))