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.
@@ -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