digitalocean-python-sdk 0.1.0__py3-none-any.whl
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.
- digitalocean_python_sdk-0.1.0.dist-info/METADATA +356 -0
- digitalocean_python_sdk-0.1.0.dist-info/RECORD +18 -0
- digitalocean_python_sdk-0.1.0.dist-info/WHEEL +4 -0
- digitalocean_python_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- digitalocean_sdk/__init__.py +42 -0
- digitalocean_sdk/_base.py +246 -0
- digitalocean_sdk/_version.py +1 -0
- digitalocean_sdk/client.py +33 -0
- digitalocean_sdk/config.py +67 -0
- digitalocean_sdk/errors.py +131 -0
- digitalocean_sdk/messages.py +63 -0
- digitalocean_sdk/messages.yaml +28 -0
- digitalocean_sdk/models.py +157 -0
- digitalocean_sdk/py.typed +0 -0
- digitalocean_sdk/resources/__init__.py +4 -0
- digitalocean_sdk/resources/_paging.py +35 -0
- digitalocean_sdk/resources/dns.py +226 -0
- digitalocean_sdk/resources/domains.py +76 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ipaddress
|
|
4
|
+
from typing import Any, AsyncIterator, Iterable, Iterator, Optional, Union
|
|
5
|
+
|
|
6
|
+
from .._base import Call, Flow
|
|
7
|
+
from ..messages import text
|
|
8
|
+
from ..models import HOST_TYPES, DNSRecord, RecordType, RecordTypeLike, hostname, record_type
|
|
9
|
+
from ._paging import collect, has_next, items_of, page_call
|
|
10
|
+
from .domains import domain_name
|
|
11
|
+
|
|
12
|
+
RecordValues = Union[str, DNSRecord, Iterable[Union[str, DNSRecord]]]
|
|
13
|
+
RecordRef = Union[int, str, DNSRecord]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def relative_name(name: str, domain: str) -> str:
|
|
17
|
+
value = name.strip().lower()
|
|
18
|
+
if not value:
|
|
19
|
+
raise ValueError(text("value.empty_name"))
|
|
20
|
+
if value in ("@", domain, f"{domain}."):
|
|
21
|
+
return "@"
|
|
22
|
+
bare = value.rstrip(".")
|
|
23
|
+
if bare.endswith(f".{domain}"):
|
|
24
|
+
return bare[: -len(domain) - 1]
|
|
25
|
+
if value.endswith("."):
|
|
26
|
+
raise ValueError(text("value.foreign_name", name=name, domain=domain))
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def fqdn(name: str, domain: str) -> str:
|
|
31
|
+
relative = relative_name(name, domain)
|
|
32
|
+
return domain if relative == "@" else f"{relative}.{domain}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def ip_record_type(ip: str) -> RecordType:
|
|
36
|
+
return RecordType.A if ipaddress.ip_address(ip).version == 4 else RecordType.AAAA
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _record(body: Any) -> DNSRecord:
|
|
40
|
+
return DNSRecord.from_api((body or {}).get("domain_record") or {})
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _record_id(ref: RecordRef) -> int:
|
|
44
|
+
if isinstance(ref, DNSRecord):
|
|
45
|
+
if ref.id is None:
|
|
46
|
+
raise ValueError(text("value.needs_id", name=ref.name))
|
|
47
|
+
return ref.id
|
|
48
|
+
return int(ref)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _same_data(kind: str, left: str, right: str) -> bool:
|
|
52
|
+
if kind in HOST_TYPES:
|
|
53
|
+
return left.rstrip(".").lower() == right.rstrip(".").lower()
|
|
54
|
+
return left == right
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class _DNSBase:
|
|
58
|
+
def __init__(self, client: Any) -> None:
|
|
59
|
+
self._client = client
|
|
60
|
+
|
|
61
|
+
def _path(self, endpoint: str, **params: Any) -> str:
|
|
62
|
+
return self._client.settings.path(endpoint, **params)
|
|
63
|
+
|
|
64
|
+
def _filters(self, domain: str, type: Optional[RecordTypeLike], name: Optional[str]) -> dict[str, Any]:
|
|
65
|
+
return {"type": record_type(type) if type is not None else None,
|
|
66
|
+
"name": fqdn(name, domain) if name else None}
|
|
67
|
+
|
|
68
|
+
def _list_flow(self, domain: str, type: Optional[RecordTypeLike] = None, name: Optional[str] = None) -> Flow[Any]:
|
|
69
|
+
zone = domain_name(domain)
|
|
70
|
+
return collect(self._path("records", domain=zone), "domain_records", DNSRecord.from_api,
|
|
71
|
+
self._client.settings.page_size, self._filters(zone, type, name))
|
|
72
|
+
|
|
73
|
+
def _create_call(self, zone: str, record: DNSRecord) -> Call:
|
|
74
|
+
body = record.to_api(self._client.settings.default_ttl)
|
|
75
|
+
body["name"] = relative_name(record.name, zone)
|
|
76
|
+
return Call("POST", self._path("records", domain=zone), json=body, parse=_record)
|
|
77
|
+
|
|
78
|
+
def _delete_call(self, zone: str, record_id: int, missing_ok: bool = False) -> Call:
|
|
79
|
+
allow = frozenset({404}) if missing_ok else frozenset()
|
|
80
|
+
path = self._path("record", domain=zone, record_id=record_id)
|
|
81
|
+
return Call("DELETE", path, parse=lambda _: None, allow_status=allow)
|
|
82
|
+
|
|
83
|
+
def list(self, domain: str, type: Optional[RecordTypeLike] = None, name: Optional[str] = None) -> Any:
|
|
84
|
+
return self._client._drive(self._list_flow(domain, type, name))
|
|
85
|
+
|
|
86
|
+
def get(self, domain: str, record_id: int) -> Any:
|
|
87
|
+
path = self._path("record", domain=domain_name(domain), record_id=int(record_id))
|
|
88
|
+
return self._client._execute(Call("GET", path, parse=_record))
|
|
89
|
+
|
|
90
|
+
def exists(self, domain: str, type: RecordTypeLike, name: str) -> Any:
|
|
91
|
+
def flow() -> Flow[bool]:
|
|
92
|
+
return bool((yield from self._list_flow(domain, type, name)))
|
|
93
|
+
|
|
94
|
+
return self._client._drive(flow())
|
|
95
|
+
|
|
96
|
+
def create(self, domain: str, record: DNSRecord) -> Any:
|
|
97
|
+
return self._client._execute(self._create_call(domain_name(domain), record))
|
|
98
|
+
|
|
99
|
+
def add(self, domain: str, *records: DNSRecord) -> Any:
|
|
100
|
+
if not records:
|
|
101
|
+
raise ValueError(text("value.add_needs_records"))
|
|
102
|
+
zone = domain_name(domain)
|
|
103
|
+
|
|
104
|
+
def flow() -> Flow[list[DNSRecord]]:
|
|
105
|
+
created = []
|
|
106
|
+
for record in records:
|
|
107
|
+
created.append((yield self._create_call(zone, record)))
|
|
108
|
+
return created
|
|
109
|
+
|
|
110
|
+
return self._client._drive(flow())
|
|
111
|
+
|
|
112
|
+
def update(self, domain: str, record: RecordRef, **fields: Any) -> Any:
|
|
113
|
+
zone = domain_name(domain)
|
|
114
|
+
path = self._path("record", domain=zone, record_id=_record_id(record))
|
|
115
|
+
if not fields:
|
|
116
|
+
if not isinstance(record, DNSRecord):
|
|
117
|
+
raise ValueError(text("value.update_needs_fields"))
|
|
118
|
+
body = record.to_api(self._client.settings.default_ttl)
|
|
119
|
+
body["name"] = relative_name(record.name, zone)
|
|
120
|
+
return self._client._execute(Call("PUT", path, json=body, parse=_record))
|
|
121
|
+
if "name" in fields and fields["name"] is not None:
|
|
122
|
+
fields["name"] = relative_name(fields["name"], zone)
|
|
123
|
+
if "type" in fields and fields["type"] is not None:
|
|
124
|
+
fields["type"] = record_type(fields["type"])
|
|
125
|
+
return self._client._execute(Call("PATCH", path, json=fields, parse=_record))
|
|
126
|
+
|
|
127
|
+
def delete(self, domain: str, record: RecordRef, *, missing_ok: bool = False) -> Any:
|
|
128
|
+
return self._client._execute(self._delete_call(domain_name(domain), _record_id(record), missing_ok))
|
|
129
|
+
|
|
130
|
+
def delete_all(self, domain: str, type: RecordTypeLike, name: str) -> Any:
|
|
131
|
+
zone = domain_name(domain)
|
|
132
|
+
|
|
133
|
+
def flow() -> Flow[int]:
|
|
134
|
+
found = yield from self._list_flow(zone, type, name)
|
|
135
|
+
for record in found:
|
|
136
|
+
yield self._delete_call(zone, record.id, missing_ok=True)
|
|
137
|
+
return len(found)
|
|
138
|
+
|
|
139
|
+
return self._client._drive(flow())
|
|
140
|
+
|
|
141
|
+
def replace(
|
|
142
|
+
self, domain: str, type: RecordTypeLike, name: str, values: RecordValues, *, ttl: Optional[int] = None
|
|
143
|
+
) -> Any:
|
|
144
|
+
zone = domain_name(domain)
|
|
145
|
+
kind = record_type(type)
|
|
146
|
+
label = relative_name(name, zone)
|
|
147
|
+
if isinstance(values, (str, DNSRecord)):
|
|
148
|
+
values = [values]
|
|
149
|
+
wanted = [v if isinstance(v, DNSRecord) else DNSRecord(kind, label, str(v), ttl) for v in values]
|
|
150
|
+
if not wanted:
|
|
151
|
+
raise ValueError(text("value.no_values"))
|
|
152
|
+
for record in wanted:
|
|
153
|
+
record.type, record.name = kind, label
|
|
154
|
+
if record.ttl is None:
|
|
155
|
+
record.ttl = ttl
|
|
156
|
+
if kind in HOST_TYPES:
|
|
157
|
+
record.data = hostname(record.data)
|
|
158
|
+
|
|
159
|
+
def flow() -> Flow[list[DNSRecord]]:
|
|
160
|
+
existing = yield from self._list_flow(zone, kind, label)
|
|
161
|
+
result: list[DNSRecord] = []
|
|
162
|
+
spare = list(existing)
|
|
163
|
+
for record in wanted:
|
|
164
|
+
match = next((r for r in spare if _same_data(kind, r.data, record.data)), None)
|
|
165
|
+
if match is None:
|
|
166
|
+
result.append((yield self._create_call(zone, record)))
|
|
167
|
+
continue
|
|
168
|
+
spare.remove(match)
|
|
169
|
+
changes = {key: getattr(record, key) for key in ("ttl", "priority", "port", "weight", "flags", "tag")
|
|
170
|
+
if getattr(record, key) is not None and getattr(record, key) != getattr(match, key)}
|
|
171
|
+
if changes:
|
|
172
|
+
path = self._path("record", domain=zone, record_id=match.id)
|
|
173
|
+
match = yield Call("PATCH", path, json=changes, parse=_record)
|
|
174
|
+
result.append(match)
|
|
175
|
+
for record in spare:
|
|
176
|
+
yield self._delete_call(zone, record.id, missing_ok=True)
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
return self._client._drive(flow())
|
|
180
|
+
|
|
181
|
+
def point(self, domain: str, name: str, ip: str, *, ttl: Optional[int] = None) -> Any:
|
|
182
|
+
address = str(ipaddress.ip_address(ip))
|
|
183
|
+
return self.replace(domain, ip_record_type(address), name, [address], ttl=ttl)
|
|
184
|
+
|
|
185
|
+
def unpoint(self, domain: str, name: str, ip_version: int = 4) -> Any:
|
|
186
|
+
return self.delete_all(domain, RecordType.A if ip_version == 4 else RecordType.AAAA, name)
|
|
187
|
+
|
|
188
|
+
def alias(self, domain: str, name: str, target: str, *, ttl: Optional[int] = None) -> Any:
|
|
189
|
+
return self.replace(domain, RecordType.CNAME, name, [target], ttl=ttl)
|
|
190
|
+
|
|
191
|
+
def set_txt(self, domain: str, name: str, values: Union[str, Iterable[str]], *, ttl: Optional[int] = None) -> Any:
|
|
192
|
+
return self.replace(domain, RecordType.TXT, name, values, ttl=ttl)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class DNS(_DNSBase):
|
|
197
|
+
def iter(
|
|
198
|
+
self, domain: str, type: Optional[RecordTypeLike] = None, name: Optional[str] = None
|
|
199
|
+
) -> Iterator[DNSRecord]:
|
|
200
|
+
zone = domain_name(domain)
|
|
201
|
+
path, params = self._path("records", domain=zone), self._filters(zone, type, name)
|
|
202
|
+
page = 1
|
|
203
|
+
while True:
|
|
204
|
+
body = self._client._execute(page_call(path, page, self._client.settings.page_size, params))
|
|
205
|
+
batch = items_of(body, "domain_records", DNSRecord.from_api)
|
|
206
|
+
yield from batch
|
|
207
|
+
if not batch or not has_next(body):
|
|
208
|
+
return
|
|
209
|
+
page += 1
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class AsyncDNS(_DNSBase):
|
|
213
|
+
async def iter(
|
|
214
|
+
self, domain: str, type: Optional[RecordTypeLike] = None, name: Optional[str] = None
|
|
215
|
+
) -> AsyncIterator[DNSRecord]:
|
|
216
|
+
zone = domain_name(domain)
|
|
217
|
+
path, params = self._path("records", domain=zone), self._filters(zone, type, name)
|
|
218
|
+
page = 1
|
|
219
|
+
while True:
|
|
220
|
+
body = await self._client._execute(page_call(path, page, self._client.settings.page_size, params))
|
|
221
|
+
batch = items_of(body, "domain_records", DNSRecord.from_api)
|
|
222
|
+
for item in batch:
|
|
223
|
+
yield item
|
|
224
|
+
if not batch or not has_next(body):
|
|
225
|
+
return
|
|
226
|
+
page += 1
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, AsyncIterator, Iterator, Optional
|
|
4
|
+
|
|
5
|
+
from .._base import Call
|
|
6
|
+
from ..messages import text
|
|
7
|
+
from ..models import Domain
|
|
8
|
+
from ._paging import collect, has_next, items_of, page_call
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def domain_name(value: str) -> str:
|
|
12
|
+
name = value.strip().lower().rstrip(".")
|
|
13
|
+
if not name:
|
|
14
|
+
raise ValueError(text("value.empty_domain"))
|
|
15
|
+
return name
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _domain(body: Any) -> Domain:
|
|
19
|
+
return Domain.from_api((body or {}).get("domain") or {})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _DomainsBase:
|
|
23
|
+
def __init__(self, client: Any) -> None:
|
|
24
|
+
self._client = client
|
|
25
|
+
|
|
26
|
+
def _path(self, endpoint: str, **params: Any) -> str:
|
|
27
|
+
return self._client.settings.path(endpoint, **params)
|
|
28
|
+
|
|
29
|
+
def list(self) -> Any:
|
|
30
|
+
flow = collect(self._path("domains"), "domains", Domain.from_api, self._client.settings.page_size)
|
|
31
|
+
return self._client._drive(flow)
|
|
32
|
+
|
|
33
|
+
def get(self, name: str) -> Any:
|
|
34
|
+
return self._client._execute(Call("GET", self._path("domain", domain=domain_name(name)), parse=_domain))
|
|
35
|
+
|
|
36
|
+
def exists(self, name: str) -> Any:
|
|
37
|
+
path = self._path("domain", domain=domain_name(name))
|
|
38
|
+
return self._client._execute(Call("GET", path, parse=lambda body: body is not None,
|
|
39
|
+
allow_status=frozenset({404})))
|
|
40
|
+
|
|
41
|
+
def create(self, name: str, ip_address: Optional[str] = None) -> Any:
|
|
42
|
+
body: dict[str, Any] = {"name": domain_name(name)}
|
|
43
|
+
if ip_address:
|
|
44
|
+
body["ip_address"] = ip_address
|
|
45
|
+
return self._client._execute(Call("POST", self._path("domains"), json=body, parse=_domain))
|
|
46
|
+
|
|
47
|
+
def delete(self, name: str, *, missing_ok: bool = False) -> Any:
|
|
48
|
+
allow = frozenset({404}) if missing_ok else frozenset()
|
|
49
|
+
path = self._path("domain", domain=domain_name(name))
|
|
50
|
+
return self._client._execute(Call("DELETE", path, parse=lambda _: None, allow_status=allow))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Domains(_DomainsBase):
|
|
54
|
+
def iter(self) -> Iterator[Domain]:
|
|
55
|
+
page = 1
|
|
56
|
+
while True:
|
|
57
|
+
body = self._client._execute(page_call(self._path("domains"), page, self._client.settings.page_size))
|
|
58
|
+
batch = items_of(body, "domains", Domain.from_api)
|
|
59
|
+
yield from batch
|
|
60
|
+
if not batch or not has_next(body):
|
|
61
|
+
return
|
|
62
|
+
page += 1
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AsyncDomains(_DomainsBase):
|
|
66
|
+
async def iter(self) -> AsyncIterator[Domain]:
|
|
67
|
+
page = 1
|
|
68
|
+
while True:
|
|
69
|
+
call = page_call(self._path("domains"), page, self._client.settings.page_size)
|
|
70
|
+
body = await self._client._execute(call)
|
|
71
|
+
batch = items_of(body, "domains", Domain.from_api)
|
|
72
|
+
for item in batch:
|
|
73
|
+
yield item
|
|
74
|
+
if not batch or not has_next(body):
|
|
75
|
+
return
|
|
76
|
+
page += 1
|