hostinger-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.
- hostinger_python_sdk-0.1.0.dist-info/METADATA +366 -0
- hostinger_python_sdk-0.1.0.dist-info/RECORD +17 -0
- hostinger_python_sdk-0.1.0.dist-info/WHEEL +4 -0
- hostinger_python_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- hostinger_sdk/__init__.py +47 -0
- hostinger_sdk/_base.py +246 -0
- hostinger_sdk/_version.py +1 -0
- hostinger_sdk/client.py +20 -0
- hostinger_sdk/config.py +75 -0
- hostinger_sdk/errors.py +151 -0
- hostinger_sdk/messages.py +63 -0
- hostinger_sdk/messages.yaml +28 -0
- hostinger_sdk/models.py +252 -0
- hostinger_sdk/py.typed +0 -0
- hostinger_sdk/resources/__init__.py +4 -0
- hostinger_sdk/resources/dns.py +196 -0
- hostinger_sdk/resources/domains.py +97 -0
hostinger_sdk/models.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ipaddress
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Iterable, Optional, Union
|
|
8
|
+
|
|
9
|
+
from .messages import text
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RecordType(str, Enum):
|
|
13
|
+
A = "A"
|
|
14
|
+
AAAA = "AAAA"
|
|
15
|
+
ALIAS = "ALIAS"
|
|
16
|
+
CAA = "CAA"
|
|
17
|
+
CNAME = "CNAME"
|
|
18
|
+
MX = "MX"
|
|
19
|
+
NS = "NS"
|
|
20
|
+
SOA = "SOA"
|
|
21
|
+
SRV = "SRV"
|
|
22
|
+
TXT = "TXT"
|
|
23
|
+
|
|
24
|
+
def __str__(self) -> str:
|
|
25
|
+
return self.value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
RecordTypeLike = Union[RecordType, str]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def record_type(value: RecordTypeLike) -> str:
|
|
32
|
+
kind = value.value if isinstance(value, RecordType) else str(value).strip().upper()
|
|
33
|
+
if not kind:
|
|
34
|
+
raise ValueError(text("value.empty_type"))
|
|
35
|
+
return kind
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def hostname(value: str) -> str:
|
|
39
|
+
host = value.strip()
|
|
40
|
+
if host == "@" or host.endswith(".") or "." not in host:
|
|
41
|
+
return host
|
|
42
|
+
return f"{host}."
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_datetime(value: Any) -> Optional[datetime]:
|
|
46
|
+
if not value or not isinstance(value, str):
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
50
|
+
except ValueError:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class DNSRecord:
|
|
56
|
+
type: str
|
|
57
|
+
name: str
|
|
58
|
+
data: str
|
|
59
|
+
ttl: Optional[int] = None
|
|
60
|
+
disabled: bool = field(default=False, compare=False)
|
|
61
|
+
|
|
62
|
+
def __post_init__(self) -> None:
|
|
63
|
+
self.type = record_type(self.type)
|
|
64
|
+
self.name = self.name.strip() or "@"
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def a(cls, name: str, ip: str, ttl: Optional[int] = None) -> DNSRecord:
|
|
68
|
+
return cls("A", name, str(ipaddress.IPv4Address(ip)), ttl)
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def aaaa(cls, name: str, ip: str, ttl: Optional[int] = None) -> DNSRecord:
|
|
72
|
+
return cls("AAAA", name, str(ipaddress.IPv6Address(ip)), ttl)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def cname(cls, name: str, target: str, ttl: Optional[int] = None) -> DNSRecord:
|
|
76
|
+
return cls("CNAME", name, hostname(target), ttl)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def alias(cls, name: str, target: str, ttl: Optional[int] = None) -> DNSRecord:
|
|
80
|
+
return cls("ALIAS", name, hostname(target), ttl)
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def txt(cls, name: str, value: str, ttl: Optional[int] = None) -> DNSRecord:
|
|
84
|
+
return cls("TXT", name, value, ttl)
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def mx(cls, name: str, host: str, priority: int = 10, ttl: Optional[int] = None) -> DNSRecord:
|
|
88
|
+
return cls("MX", name, f"{priority} {hostname(host)}", ttl)
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def ns(cls, name: str, host: str, ttl: Optional[int] = None) -> DNSRecord:
|
|
92
|
+
return cls("NS", name, hostname(host), ttl)
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def srv(
|
|
96
|
+
cls, name: str, target: str, port: int, priority: int = 10, weight: int = 5, ttl: Optional[int] = None
|
|
97
|
+
) -> DNSRecord:
|
|
98
|
+
return cls("SRV", name, f"{priority} {weight} {port} {hostname(target)}", ttl)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def caa(cls, name: str, value: str, tag: str = "issue", flags: int = 0, ttl: Optional[int] = None) -> DNSRecord:
|
|
102
|
+
return cls("CAA", name, f'{flags} {tag} "{value}"', ttl)
|
|
103
|
+
|
|
104
|
+
def fqdn(self, domain: str) -> str:
|
|
105
|
+
return domain if self.name == "@" else f"{self.name}.{domain}"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def records_from_zone(rows: Any) -> list[DNSRecord]:
|
|
109
|
+
found = []
|
|
110
|
+
for row in rows or []:
|
|
111
|
+
if not isinstance(row, dict):
|
|
112
|
+
continue
|
|
113
|
+
for item in row.get("records") or []:
|
|
114
|
+
if isinstance(item, dict):
|
|
115
|
+
found.append(DNSRecord(
|
|
116
|
+
type=str(row.get("type", "")),
|
|
117
|
+
name=str(row.get("name", "@")),
|
|
118
|
+
data=str(item.get("content", "")),
|
|
119
|
+
ttl=row.get("ttl"),
|
|
120
|
+
disabled=bool(item.get("is_disabled", False)),
|
|
121
|
+
))
|
|
122
|
+
return found
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def zone_from_records(records: Iterable[DNSRecord], default_ttl: int) -> list[dict[str, Any]]:
|
|
126
|
+
groups: dict[tuple[str, str], dict[str, Any]] = {}
|
|
127
|
+
for record in records:
|
|
128
|
+
key = (record.name, record.type)
|
|
129
|
+
group = groups.setdefault(key, {"name": record.name, "type": record.type, "ttl": None, "records": []})
|
|
130
|
+
if group["ttl"] is None and record.ttl is not None:
|
|
131
|
+
group["ttl"] = record.ttl
|
|
132
|
+
group["records"].append({"content": record.data})
|
|
133
|
+
for group in groups.values():
|
|
134
|
+
if group["ttl"] is None:
|
|
135
|
+
group["ttl"] = default_ttl
|
|
136
|
+
return list(groups.values())
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class Snapshot:
|
|
141
|
+
id: int
|
|
142
|
+
reason: str
|
|
143
|
+
created_at: Optional[datetime] = None
|
|
144
|
+
records: list[DNSRecord] = field(default_factory=list)
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def from_api(cls, data: dict[str, Any]) -> Snapshot:
|
|
148
|
+
return cls(
|
|
149
|
+
id=int(data.get("id", 0)),
|
|
150
|
+
reason=str(data.get("reason", "")),
|
|
151
|
+
created_at=parse_datetime(data.get("created_at")),
|
|
152
|
+
records=records_from_zone(data.get("snapshot")),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class Domain:
|
|
158
|
+
domain: str
|
|
159
|
+
id: Optional[int] = None
|
|
160
|
+
type: str = ""
|
|
161
|
+
status: str = ""
|
|
162
|
+
created_at: Optional[datetime] = None
|
|
163
|
+
expires_at: Optional[datetime] = None
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def from_api(cls, data: dict[str, Any]) -> Domain:
|
|
167
|
+
return cls(
|
|
168
|
+
domain=str(data.get("domain") or ""),
|
|
169
|
+
id=data.get("id"),
|
|
170
|
+
type=str(data.get("type", "")),
|
|
171
|
+
status=str(data.get("status", "")),
|
|
172
|
+
created_at=parse_datetime(data.get("created_at")),
|
|
173
|
+
expires_at=parse_datetime(data.get("expires_at")),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class DomainInfo:
|
|
179
|
+
domain: str
|
|
180
|
+
status: str = ""
|
|
181
|
+
message: Optional[str] = None
|
|
182
|
+
is_locked: bool = False
|
|
183
|
+
is_lockable: bool = False
|
|
184
|
+
is_privacy_protected: bool = False
|
|
185
|
+
is_privacy_protection_allowed: bool = False
|
|
186
|
+
name_servers: list[str] = field(default_factory=list)
|
|
187
|
+
child_name_servers: dict[str, list[str]] = field(default_factory=dict)
|
|
188
|
+
contacts: dict[str, int] = field(default_factory=dict)
|
|
189
|
+
created_at: Optional[datetime] = None
|
|
190
|
+
updated_at: Optional[datetime] = None
|
|
191
|
+
registered_at: Optional[datetime] = None
|
|
192
|
+
expires_at: Optional[datetime] = None
|
|
193
|
+
transfer_lock_expires_at: Optional[datetime] = None
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def from_api(cls, data: dict[str, Any]) -> DomainInfo:
|
|
197
|
+
servers = data.get("name_servers") or {}
|
|
198
|
+
names = [str(servers[key]) for key in sorted(servers) if servers[key]] if isinstance(servers, dict) else []
|
|
199
|
+
children = data.get("child_name_servers")
|
|
200
|
+
return cls(
|
|
201
|
+
domain=str(data.get("domain", "")),
|
|
202
|
+
status=str(data.get("status", "")),
|
|
203
|
+
message=data.get("message"),
|
|
204
|
+
is_locked=bool(data.get("is_locked", False)),
|
|
205
|
+
is_lockable=bool(data.get("is_lockable", False)),
|
|
206
|
+
is_privacy_protected=bool(data.get("is_privacy_protected", False)),
|
|
207
|
+
is_privacy_protection_allowed=bool(data.get("is_privacy_protection_allowed", False)),
|
|
208
|
+
name_servers=names,
|
|
209
|
+
child_name_servers=dict(children) if isinstance(children, dict) else {},
|
|
210
|
+
contacts=dict(data.get("domain_contacts") or {}),
|
|
211
|
+
created_at=parse_datetime(data.get("created_at")),
|
|
212
|
+
updated_at=parse_datetime(data.get("updated_at")),
|
|
213
|
+
registered_at=parse_datetime(data.get("registered_at")),
|
|
214
|
+
expires_at=parse_datetime(data.get("expires_at")),
|
|
215
|
+
transfer_lock_expires_at=parse_datetime(data.get("60_days_lock_expires_at")),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass
|
|
220
|
+
class Availability:
|
|
221
|
+
domain: str
|
|
222
|
+
available: bool
|
|
223
|
+
alternative: bool = False
|
|
224
|
+
restriction: Optional[str] = None
|
|
225
|
+
|
|
226
|
+
@classmethod
|
|
227
|
+
def from_api(cls, data: dict[str, Any]) -> Availability:
|
|
228
|
+
return cls(
|
|
229
|
+
domain=str(data.get("domain") or ""),
|
|
230
|
+
available=bool(data.get("is_available", False)),
|
|
231
|
+
alternative=bool(data.get("is_alternative", False)),
|
|
232
|
+
restriction=data.get("restriction"),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass
|
|
237
|
+
class Forwarding:
|
|
238
|
+
domain: str
|
|
239
|
+
redirect_type: str
|
|
240
|
+
redirect_url: str
|
|
241
|
+
created_at: Optional[datetime] = None
|
|
242
|
+
updated_at: Optional[datetime] = None
|
|
243
|
+
|
|
244
|
+
@classmethod
|
|
245
|
+
def from_api(cls, data: dict[str, Any]) -> Forwarding:
|
|
246
|
+
return cls(
|
|
247
|
+
domain=str(data.get("domain") or ""),
|
|
248
|
+
redirect_type=str(data.get("redirect_type", "")),
|
|
249
|
+
redirect_url=str(data.get("redirect_url", "")),
|
|
250
|
+
created_at=parse_datetime(data.get("created_at")),
|
|
251
|
+
updated_at=parse_datetime(data.get("updated_at")),
|
|
252
|
+
)
|
hostinger_sdk/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ipaddress
|
|
4
|
+
from typing import Any, Iterable, Optional, Sequence, Union
|
|
5
|
+
|
|
6
|
+
from .._base import Call, Flow
|
|
7
|
+
from ..messages import text
|
|
8
|
+
from ..models import (
|
|
9
|
+
DNSRecord,
|
|
10
|
+
RecordType,
|
|
11
|
+
RecordTypeLike,
|
|
12
|
+
Snapshot,
|
|
13
|
+
hostname,
|
|
14
|
+
record_type,
|
|
15
|
+
records_from_zone,
|
|
16
|
+
zone_from_records,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
RecordValues = Union[str, DNSRecord, Iterable[Union[str, DNSRecord]]]
|
|
20
|
+
HOST_TYPES = frozenset({"CNAME", "ALIAS", "NS"})
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def domain_name(value: str) -> str:
|
|
24
|
+
name = value.strip().lower().rstrip(".")
|
|
25
|
+
if not name:
|
|
26
|
+
raise ValueError(text("value.empty_domain"))
|
|
27
|
+
return name
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def relative_name(name: str, domain: str) -> str:
|
|
31
|
+
value = name.strip().lower()
|
|
32
|
+
if not value:
|
|
33
|
+
raise ValueError(text("value.empty_name"))
|
|
34
|
+
if value in ("@", domain, f"{domain}."):
|
|
35
|
+
return "@"
|
|
36
|
+
bare = value.rstrip(".")
|
|
37
|
+
if bare.endswith(f".{domain}"):
|
|
38
|
+
return bare[: -len(domain) - 1]
|
|
39
|
+
if value.endswith("."):
|
|
40
|
+
raise ValueError(text("value.foreign_name", name=name, domain=domain))
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def ip_record_type(ip: str) -> RecordType:
|
|
45
|
+
return RecordType.A if ipaddress.ip_address(ip).version == 4 else RecordType.AAAA
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _done(_: Any) -> None:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DNS:
|
|
53
|
+
def __init__(self, client: Any) -> None:
|
|
54
|
+
self._client = client
|
|
55
|
+
|
|
56
|
+
def _path(self, endpoint: str, **params: Any) -> str:
|
|
57
|
+
return self._client.settings.path(endpoint, **params)
|
|
58
|
+
|
|
59
|
+
def _zone_call(self, zone: str) -> Call:
|
|
60
|
+
return Call("GET", self._path("zone", domain=zone), parse=records_from_zone)
|
|
61
|
+
|
|
62
|
+
def _put_call(self, zone: str, records: Sequence[DNSRecord], overwrite: bool) -> Call:
|
|
63
|
+
body = {"overwrite": overwrite, "zone": zone_from_records(records, self._client.settings.default_ttl)}
|
|
64
|
+
return Call("PUT", self._path("zone", domain=zone), json=body, parse=_done)
|
|
65
|
+
|
|
66
|
+
def _delete_call(self, zone: str, pairs: Iterable[tuple[str, str]]) -> Call:
|
|
67
|
+
filters = [{"name": name, "type": kind} for kind, name in dict.fromkeys(pairs)]
|
|
68
|
+
return Call("DELETE", self._path("zone", domain=zone), json={"filters": filters}, parse=_done)
|
|
69
|
+
|
|
70
|
+
def _prepare(self, zone: str, records: Iterable[DNSRecord]) -> list[DNSRecord]:
|
|
71
|
+
prepared = []
|
|
72
|
+
for record in records:
|
|
73
|
+
copy = DNSRecord(record.type, relative_name(record.name, zone), record.data, record.ttl)
|
|
74
|
+
if copy.type in HOST_TYPES:
|
|
75
|
+
copy.data = hostname(copy.data)
|
|
76
|
+
prepared.append(copy)
|
|
77
|
+
return prepared
|
|
78
|
+
|
|
79
|
+
def list(self, domain: str, type: Optional[RecordTypeLike] = None, name: Optional[str] = None) -> Any:
|
|
80
|
+
zone = domain_name(domain)
|
|
81
|
+
kind = record_type(type) if type is not None else None
|
|
82
|
+
label = relative_name(name, zone) if name else None
|
|
83
|
+
|
|
84
|
+
def parse(body: Any) -> list[DNSRecord]:
|
|
85
|
+
return [r for r in records_from_zone(body)
|
|
86
|
+
if (kind is None or r.type == kind) and (label is None or r.name == label)]
|
|
87
|
+
|
|
88
|
+
return self._client._execute(Call("GET", self._path("zone", domain=zone), parse=parse))
|
|
89
|
+
|
|
90
|
+
def exists(self, domain: str, type: RecordTypeLike, name: str) -> Any:
|
|
91
|
+
zone, kind, label = domain_name(domain), record_type(type), relative_name(name, domain_name(domain))
|
|
92
|
+
path = self._path("zone", domain=zone)
|
|
93
|
+
parse = lambda body: any(r.type == kind and r.name == label for r in records_from_zone(body))
|
|
94
|
+
return self._client._execute(Call("GET", path, parse=parse))
|
|
95
|
+
|
|
96
|
+
def add(self, domain: str, *records: DNSRecord) -> Any:
|
|
97
|
+
if not records:
|
|
98
|
+
raise ValueError(text("value.add_needs_records"))
|
|
99
|
+
zone = domain_name(domain)
|
|
100
|
+
return self._client._execute(self._put_call(zone, self._prepare(zone, records), overwrite=False))
|
|
101
|
+
|
|
102
|
+
def replace(
|
|
103
|
+
self, domain: str, type: RecordTypeLike, name: str, values: RecordValues, *, ttl: Optional[int] = None
|
|
104
|
+
) -> Any:
|
|
105
|
+
zone, kind = domain_name(domain), record_type(type)
|
|
106
|
+
label = relative_name(name, zone)
|
|
107
|
+
if isinstance(values, (str, DNSRecord)):
|
|
108
|
+
values = [values]
|
|
109
|
+
wanted = [v if isinstance(v, DNSRecord) else DNSRecord(kind, label, str(v), ttl) for v in values]
|
|
110
|
+
if not wanted:
|
|
111
|
+
raise ValueError(text("value.no_values"))
|
|
112
|
+
for record in wanted:
|
|
113
|
+
record.type, record.name = kind, label
|
|
114
|
+
if record.ttl is None:
|
|
115
|
+
record.ttl = ttl
|
|
116
|
+
return self._client._execute(self._put_call(zone, self._prepare(zone, wanted), overwrite=True))
|
|
117
|
+
|
|
118
|
+
def replace_many(self, domain: str, records: Sequence[DNSRecord]) -> Any:
|
|
119
|
+
if not records:
|
|
120
|
+
raise ValueError(text("value.add_needs_records"))
|
|
121
|
+
zone = domain_name(domain)
|
|
122
|
+
return self._client._execute(self._put_call(zone, self._prepare(zone, records), overwrite=True))
|
|
123
|
+
|
|
124
|
+
def delete(self, domain: str, type: RecordTypeLike, name: str, data: Optional[str] = None) -> Any:
|
|
125
|
+
zone, kind = domain_name(domain), record_type(type)
|
|
126
|
+
label = relative_name(name, zone)
|
|
127
|
+
if data is None:
|
|
128
|
+
return self._client._execute(self._delete_call(zone, [(kind, label)]))
|
|
129
|
+
|
|
130
|
+
def flow() -> Flow[int]:
|
|
131
|
+
current = yield self._zone_call(zone)
|
|
132
|
+
group = [r for r in current if r.type == kind and r.name == label]
|
|
133
|
+
keep = [r for r in group if r.data != data and r.data.rstrip(".") != data.rstrip(".")]
|
|
134
|
+
if len(keep) == len(group):
|
|
135
|
+
return 0
|
|
136
|
+
if keep:
|
|
137
|
+
yield self._put_call(zone, keep, overwrite=True)
|
|
138
|
+
else:
|
|
139
|
+
yield self._delete_call(zone, [(kind, label)])
|
|
140
|
+
return len(group) - len(keep)
|
|
141
|
+
|
|
142
|
+
return self._client._drive(flow())
|
|
143
|
+
|
|
144
|
+
def delete_many(self, domain: str, pairs: Iterable[tuple[RecordTypeLike, str]]) -> Any:
|
|
145
|
+
zone = domain_name(domain)
|
|
146
|
+
cleaned = [(record_type(kind), relative_name(name, zone)) for kind, name in pairs]
|
|
147
|
+
return self._client._execute(self._delete_call(zone, cleaned))
|
|
148
|
+
|
|
149
|
+
def point(self, domain: str, name: str, ip: str, *, ttl: Optional[int] = None) -> Any:
|
|
150
|
+
address = str(ipaddress.ip_address(ip))
|
|
151
|
+
return self.replace(domain, ip_record_type(address), name, [address], ttl=ttl)
|
|
152
|
+
|
|
153
|
+
def unpoint(self, domain: str, name: str, ip_version: int = 4) -> Any:
|
|
154
|
+
return self.delete(domain, RecordType.A if ip_version == 4 else RecordType.AAAA, name)
|
|
155
|
+
|
|
156
|
+
def alias(self, domain: str, name: str, target: str, *, ttl: Optional[int] = None) -> Any:
|
|
157
|
+
return self.replace(domain, RecordType.CNAME, name, [target], ttl=ttl)
|
|
158
|
+
|
|
159
|
+
def set_txt(self, domain: str, name: str, values: Union[str, Iterable[str]], *, ttl: Optional[int] = None) -> Any:
|
|
160
|
+
return self.replace(domain, RecordType.TXT, name, values, ttl=ttl)
|
|
161
|
+
|
|
162
|
+
def validate(self, domain: str, records: Sequence[DNSRecord], *, overwrite: bool = True) -> Any:
|
|
163
|
+
zone = domain_name(domain)
|
|
164
|
+
body = {"overwrite": overwrite,
|
|
165
|
+
"zone": zone_from_records(self._prepare(zone, records), self._client.settings.default_ttl)}
|
|
166
|
+
return self._client._execute(Call("POST", self._path("zone_validate", domain=zone), json=body, parse=_done))
|
|
167
|
+
|
|
168
|
+
def reset(
|
|
169
|
+
self,
|
|
170
|
+
domain: str,
|
|
171
|
+
*,
|
|
172
|
+
reset_email_records: bool = True,
|
|
173
|
+
keep_types: Optional[Iterable[RecordTypeLike]] = None,
|
|
174
|
+
wait: bool = True,
|
|
175
|
+
) -> Any:
|
|
176
|
+
body: dict[str, Any] = {"sync": wait, "reset_email_records": reset_email_records}
|
|
177
|
+
if keep_types:
|
|
178
|
+
body["whitelisted_record_types"] = [record_type(kind) for kind in keep_types]
|
|
179
|
+
path = self._path("zone_reset", domain=domain_name(domain))
|
|
180
|
+
return self._client._execute(Call("POST", path, json=body, parse=_done))
|
|
181
|
+
|
|
182
|
+
def snapshots(self, domain: str) -> Any:
|
|
183
|
+
parse = lambda body: [Snapshot.from_api(row) for row in body or [] if isinstance(row, dict)]
|
|
184
|
+
return self._client._execute(Call("GET", self._path("snapshots", domain=domain_name(domain)), parse=parse))
|
|
185
|
+
|
|
186
|
+
def snapshot(self, domain: str, snapshot_id: int) -> Any:
|
|
187
|
+
path = self._path("snapshot", domain=domain_name(domain), snapshot_id=int(snapshot_id))
|
|
188
|
+
return self._client._execute(Call("GET", path, parse=lambda body: Snapshot.from_api(body or {})))
|
|
189
|
+
|
|
190
|
+
def restore(self, domain: str, snapshot_id: int) -> Any:
|
|
191
|
+
path = self._path("snapshot_restore", domain=domain_name(domain), snapshot_id=int(snapshot_id))
|
|
192
|
+
return self._client._execute(Call("POST", path, parse=_done))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class AsyncDNS(DNS):
|
|
196
|
+
pass
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Iterable
|
|
4
|
+
|
|
5
|
+
from .._base import Call, Flow
|
|
6
|
+
from ..messages import text
|
|
7
|
+
from ..models import Availability, Domain, DomainInfo, Forwarding
|
|
8
|
+
from .dns import domain_name
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _done(_: Any) -> None:
|
|
12
|
+
return None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _redirect(value: Any) -> str:
|
|
16
|
+
kind = str(value).strip()
|
|
17
|
+
if kind not in ("301", "302"):
|
|
18
|
+
raise ValueError(text("value.redirect_type", value=value))
|
|
19
|
+
return kind
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Domains:
|
|
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 _run(self, call: Call) -> Any:
|
|
30
|
+
return self._client._execute(call)
|
|
31
|
+
|
|
32
|
+
def list(self) -> Any:
|
|
33
|
+
parse = lambda body: [Domain.from_api(row) for row in body or [] if isinstance(row, dict)]
|
|
34
|
+
return self._run(Call("GET", self._path("domains"), parse=parse))
|
|
35
|
+
|
|
36
|
+
def get(self, domain: str) -> Any:
|
|
37
|
+
path = self._path("domain", domain=domain_name(domain))
|
|
38
|
+
return self._run(Call("GET", path, parse=lambda body: DomainInfo.from_api(body or {})))
|
|
39
|
+
|
|
40
|
+
def available(self, name: str, tlds: Iterable[str], *, with_alternatives: bool = False) -> Any:
|
|
41
|
+
endings = [tld.strip().lstrip(".").lower() for tld in tlds if tld.strip()]
|
|
42
|
+
if not endings:
|
|
43
|
+
raise ValueError(text("value.no_tlds"))
|
|
44
|
+
body = {"domain": name.strip().lower(), "tlds": endings, "with_alternatives": with_alternatives}
|
|
45
|
+
parse = lambda rows: [Availability.from_api(row) for row in rows or [] if isinstance(row, dict)]
|
|
46
|
+
return self._run(Call("POST", self._path("availability"), json=body, parse=parse))
|
|
47
|
+
|
|
48
|
+
def set_nameservers(self, domain: str, servers: Iterable[str]) -> Any:
|
|
49
|
+
names = [server.strip().rstrip(".") for server in servers if server.strip()]
|
|
50
|
+
if not 2 <= len(names) <= 4:
|
|
51
|
+
raise ValueError(text("value.nameserver_count", count=len(names)))
|
|
52
|
+
body = {f"ns{index}": value for index, value in enumerate(names, start=1)}
|
|
53
|
+
return self._run(Call("PUT", self._path("nameservers", domain=domain_name(domain)), json=body, parse=_done))
|
|
54
|
+
|
|
55
|
+
def lock(self, domain: str) -> Any:
|
|
56
|
+
return self._run(Call("PUT", self._path("domain_lock", domain=domain_name(domain)), parse=_done))
|
|
57
|
+
|
|
58
|
+
def unlock(self, domain: str) -> Any:
|
|
59
|
+
return self._run(Call("DELETE", self._path("domain_lock", domain=domain_name(domain)), parse=_done))
|
|
60
|
+
|
|
61
|
+
def enable_privacy(self, domain: str) -> Any:
|
|
62
|
+
return self._run(Call("PUT", self._path("privacy", domain=domain_name(domain)), parse=_done))
|
|
63
|
+
|
|
64
|
+
def disable_privacy(self, domain: str) -> Any:
|
|
65
|
+
return self._run(Call("DELETE", self._path("privacy", domain=domain_name(domain)), parse=_done))
|
|
66
|
+
|
|
67
|
+
def forwarding(self, domain: str) -> Any:
|
|
68
|
+
path = self._path("forwarding", domain=domain_name(domain))
|
|
69
|
+
parse = lambda body: Forwarding.from_api(body) if isinstance(body, dict) else None
|
|
70
|
+
return self._run(Call("GET", path, parse=parse, allow_status=frozenset({404})))
|
|
71
|
+
|
|
72
|
+
def forward(self, domain: str, url: str, *, redirect_type: Any = 301) -> Any:
|
|
73
|
+
name, kind = domain_name(domain), _redirect(redirect_type)
|
|
74
|
+
|
|
75
|
+
def flow() -> Flow[Forwarding]:
|
|
76
|
+
path = self._path("forwarding", domain=name)
|
|
77
|
+
current = yield Call("GET", path, allow_status=frozenset({404}))
|
|
78
|
+
body = {"redirect_type": kind, "redirect_url": url}
|
|
79
|
+
if isinstance(current, dict) and current.get("redirect_url"):
|
|
80
|
+
result = yield Call("PUT", path, json=body)
|
|
81
|
+
else:
|
|
82
|
+
result = yield Call("POST", self._path("forwardings"), json={"domain": name, **body})
|
|
83
|
+
return Forwarding.from_api(result if isinstance(result, dict) else {"domain": name, **body})
|
|
84
|
+
|
|
85
|
+
return self._client._drive(flow())
|
|
86
|
+
|
|
87
|
+
def stop_forwarding(self, domain: str, *, missing_ok: bool = False) -> Any:
|
|
88
|
+
allow = frozenset({404}) if missing_ok else frozenset()
|
|
89
|
+
path = self._path("forwarding", domain=domain_name(domain))
|
|
90
|
+
return self._run(Call("DELETE", path, parse=_done, allow_status=allow))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class AsyncDomains(Domains):
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|