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.
@@ -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,4 @@
1
+ from .dns import DNS, AsyncDNS, relative_name
2
+ from .domains import AsyncDomains, Domains
3
+
4
+ __all__ = ["DNS", "AsyncDNS", "Domains", "AsyncDomains", "relative_name"]
@@ -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
+