epptools 1.0.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.
- epptools/__init__.py +72 -0
- epptools/_version.py +7 -0
- epptools/builders.py +639 -0
- epptools/client.py +435 -0
- epptools/commands.py +907 -0
- epptools/config.py +80 -0
- epptools/exceptions.py +176 -0
- epptools/frame.py +148 -0
- epptools/namespaces.py +95 -0
- epptools/py.typed +1 -0
- epptools/response.py +894 -0
- epptools/result_code.py +53 -0
- epptools/transport.py +151 -0
- epptools-1.0.0.dist-info/METADATA +489 -0
- epptools-1.0.0.dist-info/RECORD +18 -0
- epptools-1.0.0.dist-info/WHEEL +5 -0
- epptools-1.0.0.dist-info/licenses/LICENSE +21 -0
- epptools-1.0.0.dist-info/top_level.txt +1 -0
epptools/builders.py
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
"""Fluent command builders.
|
|
2
|
+
|
|
3
|
+
A builder is a typed façade over the keyword arguments the command takes. It builds no XML of its
|
|
4
|
+
own: ``send()`` hands the options straight to the ordinary method, so a builder and the equivalent
|
|
5
|
+
call produce the identical frame, and every check that applies to one applies to the other.
|
|
6
|
+
|
|
7
|
+
Python's keyword arguments already give you named parameters and a loud ``TypeError`` on a
|
|
8
|
+
misspelling, so reach for a builder when the command is assembled in pieces — across branches, in
|
|
9
|
+
a loop, or from a form — rather than written out in one place. Both forms are supported::
|
|
10
|
+
|
|
11
|
+
client.domain.create("example3.com.ua", years=1, registrant="C1") # direct
|
|
12
|
+
|
|
13
|
+
(client.domain.create_builder("example3.com.ua") # step by step
|
|
14
|
+
.years(1)
|
|
15
|
+
.registrant("C1")
|
|
16
|
+
.nameserver("ns1.example")
|
|
17
|
+
.send())
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import copy
|
|
23
|
+
import re
|
|
24
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
25
|
+
|
|
26
|
+
from .exceptions import ValidationException
|
|
27
|
+
from .response import Response
|
|
28
|
+
|
|
29
|
+
_FEE_AMOUNT_RE = re.compile(r"^\d{1,10}(\.\d{1,2})?$")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _fee_agreement(amount: str, currency: Optional[str]) -> Any:
|
|
33
|
+
"""The RFC 8748 fee AGREEMENT: the most you consent to pay.
|
|
34
|
+
|
|
35
|
+
Not a price you set — the registry charges its own. If the real price is HIGHER the command is
|
|
36
|
+
refused (2004) and nothing is charged, which is the point: a tariff change or a premium name
|
|
37
|
+
cannot bill you more than you agreed to.
|
|
38
|
+
"""
|
|
39
|
+
value = str(amount).strip()
|
|
40
|
+
if _FEE_AMOUNT_RE.match(value) is None:
|
|
41
|
+
# Checked here rather than on the wire: a malformed agreement draws a bare 2001 that names
|
|
42
|
+
# no field, and it arrives after the command has been attempted.
|
|
43
|
+
raise ValidationException(
|
|
44
|
+
"a fee amount must be a plain decimal like '100.00' (got %r)" % (value,)
|
|
45
|
+
)
|
|
46
|
+
return value if currency is None else {"amount": value, "currency": currency}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _ds_record(key_tag: int, alg: int, digest_type: int, digest: str) -> Dict[str, Any]:
|
|
50
|
+
value = str(digest or "").strip()
|
|
51
|
+
if not value:
|
|
52
|
+
raise ValidationException("a DS record needs a digest")
|
|
53
|
+
return {"key_tag": key_tag, "alg": alg, "digest_type": digest_type, "digest": value}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _key_record(flags: int, protocol: int, alg: int, pub_key: str) -> Dict[str, Any]:
|
|
57
|
+
value = str(pub_key or "").strip()
|
|
58
|
+
if not value:
|
|
59
|
+
raise ValidationException("a key record needs a public key")
|
|
60
|
+
return {"flags": flags, "protocol": protocol, "alg": alg, "pub_key": value}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _postal_info(kind: str, name: str, city: str, country_code: str,
|
|
64
|
+
street: Sequence[str] = (), org: Optional[str] = None,
|
|
65
|
+
state_province: Optional[str] = None,
|
|
66
|
+
postal_code: Optional[str] = None) -> Dict[str, Any]:
|
|
67
|
+
block: Dict[str, Any] = {"type": kind, "name": name, "city": city, "cc": country_code}
|
|
68
|
+
if street:
|
|
69
|
+
block["street"] = [str(line) for line in street]
|
|
70
|
+
if org is not None:
|
|
71
|
+
block["org"] = org
|
|
72
|
+
if state_province is not None:
|
|
73
|
+
block["sp"] = state_province
|
|
74
|
+
if postal_code is not None:
|
|
75
|
+
block["pc"] = postal_code
|
|
76
|
+
return block
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _disclosure(publish: bool, fields: Sequence[str]) -> Dict[str, Any]:
|
|
80
|
+
spec: Dict[str, Any] = {"flag": publish}
|
|
81
|
+
for field in fields:
|
|
82
|
+
name = str(field).strip()
|
|
83
|
+
# name/org/addr exist once per postal form, so the choice is per form. Both are named:
|
|
84
|
+
# withholding only the ASCII form while the local one stays public is a privacy setting
|
|
85
|
+
# that reads as applied and is not.
|
|
86
|
+
if name in ("name", "org", "addr"):
|
|
87
|
+
spec[name] = ["int", "loc"]
|
|
88
|
+
elif name in ("voice", "fax", "email"):
|
|
89
|
+
spec[name] = True
|
|
90
|
+
else:
|
|
91
|
+
raise ValidationException(
|
|
92
|
+
"%r is not a disclosable field. Use: name, org, addr, voice, fax, email." % (name,)
|
|
93
|
+
)
|
|
94
|
+
return spec
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _append(target: Dict[str, Any], key: str, values: Sequence[str]) -> Dict[str, Any]:
|
|
98
|
+
existing: List[str] = list(target.get(key) or [])
|
|
99
|
+
for value in values:
|
|
100
|
+
text = str(value).strip()
|
|
101
|
+
if text:
|
|
102
|
+
existing.append(text)
|
|
103
|
+
target[key] = existing
|
|
104
|
+
return target
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _append_contacts(target: Dict[str, Any], role: str, handles: Sequence[str]) -> Dict[str, Any]:
|
|
108
|
+
name = str(role).strip()
|
|
109
|
+
if not name:
|
|
110
|
+
raise ValidationException("a contact role must not be empty (admin, tech, billing, …)")
|
|
111
|
+
return _append(target, name, handles)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Builder:
|
|
115
|
+
"""Shared behaviour of the fluent builders."""
|
|
116
|
+
|
|
117
|
+
def __init__(self, handler: Any, object_id: str) -> None:
|
|
118
|
+
self._handler = handler
|
|
119
|
+
self._id = object_id
|
|
120
|
+
self._options: Dict[str, Any] = {}
|
|
121
|
+
self._sent = False
|
|
122
|
+
|
|
123
|
+
def to_options(self) -> Dict[str, Any]:
|
|
124
|
+
"""The options as the equivalent direct call would take them.
|
|
125
|
+
|
|
126
|
+
Useful for a dry run, for logging what you are about to do, or for handing the command to a
|
|
127
|
+
queue. Sends nothing and does not spend the builder.
|
|
128
|
+
"""
|
|
129
|
+
# A COPY, and a deep one: the result is a value you can keep, log or queue. Handing back
|
|
130
|
+
# the live dict would make it change under the caller every time another step is added, so
|
|
131
|
+
# what was logged and what was sent could differ.
|
|
132
|
+
return copy.deepcopy(self._options)
|
|
133
|
+
|
|
134
|
+
def _mark_sent(self) -> None:
|
|
135
|
+
# A builder is a command that has not happened yet, so sending it twice would be two
|
|
136
|
+
# registrations and two charges — and the second is never what the caller meant.
|
|
137
|
+
if self._sent:
|
|
138
|
+
raise ValidationException(
|
|
139
|
+
"%s has already been sent. A builder carries one command; build another rather "
|
|
140
|
+
"than re-sending this one." % type(self).__name__
|
|
141
|
+
)
|
|
142
|
+
self._sent = True
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class DomainCreateBuilder(Builder):
|
|
146
|
+
"""Registers a domain, one named step at a time."""
|
|
147
|
+
|
|
148
|
+
def years(self, years: int) -> "DomainCreateBuilder":
|
|
149
|
+
"""Registration period in years. Omit it and the registry applies its own default."""
|
|
150
|
+
self._options["years"] = years
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
def registrant(self, handle: str) -> "DomainCreateBuilder":
|
|
154
|
+
"""The registrant — the holder of the domain. Required by the registry."""
|
|
155
|
+
self._options["registrant"] = handle
|
|
156
|
+
return self
|
|
157
|
+
|
|
158
|
+
def contact(self, role: str, *handles: str) -> "DomainCreateBuilder":
|
|
159
|
+
"""Attach contacts in a role.
|
|
160
|
+
|
|
161
|
+
Every list method ACCUMULATES: pass several at once, or call it again, or both — the effect
|
|
162
|
+
is the same. RFC 5731 puts no limit on handles per role.
|
|
163
|
+
"""
|
|
164
|
+
self._options["contacts"] = _append_contacts(self._options.get("contacts") or {}, role, handles)
|
|
165
|
+
return self
|
|
166
|
+
|
|
167
|
+
def admin_contact(self, *handles: str) -> "DomainCreateBuilder":
|
|
168
|
+
"""The people authorised to make decisions about the domain. Accumulates."""
|
|
169
|
+
return self.contact("admin", *handles)
|
|
170
|
+
|
|
171
|
+
def tech_contact(self, *handles: str) -> "DomainCreateBuilder":
|
|
172
|
+
"""The people to reach about DNS and delegation. Accumulates."""
|
|
173
|
+
return self.contact("tech", *handles)
|
|
174
|
+
|
|
175
|
+
def billing_contact(self, *handles: str) -> "DomainCreateBuilder":
|
|
176
|
+
"""The people to reach about invoices for this domain. Accumulates."""
|
|
177
|
+
return self.contact("billing", *handles)
|
|
178
|
+
|
|
179
|
+
def nameserver(self, host: str) -> "DomainCreateBuilder":
|
|
180
|
+
"""One nameserver to delegate to. Accumulates; suits a loop or a conditional."""
|
|
181
|
+
return self.nameservers(host)
|
|
182
|
+
|
|
183
|
+
def nameservers(self, *hosts: str) -> "DomainCreateBuilder":
|
|
184
|
+
"""The nameservers to delegate to. Accumulates."""
|
|
185
|
+
_append(self._options, "nameservers", hosts)
|
|
186
|
+
return self
|
|
187
|
+
|
|
188
|
+
def nameserver_with_glue(self, host: str, *addresses: str) -> "DomainCreateBuilder":
|
|
189
|
+
"""A nameserver given with its glue addresses, inlined in the command (RFC 5731 hostAttr).
|
|
190
|
+
|
|
191
|
+
Use this where the registry expects the addresses with the name rather than a reference to
|
|
192
|
+
a host object created beforehand; ask yours which model it takes. A command cannot mix the
|
|
193
|
+
two, so use either this or :meth:`nameserver`, not both.
|
|
194
|
+
"""
|
|
195
|
+
name = str(host).strip()
|
|
196
|
+
if not name:
|
|
197
|
+
raise ValidationException("a nameserver name must not be empty")
|
|
198
|
+
# Appended directly rather than through _append(), which is for plain names and would
|
|
199
|
+
# stringify this one into a hostObj reading "{'name': ...}".
|
|
200
|
+
existing = list(self._options.get("nameservers") or [])
|
|
201
|
+
existing.append({"name": name, "addresses": [str(a) for a in addresses]})
|
|
202
|
+
self._options["nameservers"] = existing
|
|
203
|
+
return self
|
|
204
|
+
|
|
205
|
+
def auth_info(self, password: str) -> "DomainCreateBuilder":
|
|
206
|
+
"""The transfer secret.
|
|
207
|
+
|
|
208
|
+
Anyone holding it can move the domain to another registrar, so treat it as a credential:
|
|
209
|
+
never log it, and roll it after handing it to a customer.
|
|
210
|
+
"""
|
|
211
|
+
self._options["auth_info"] = password
|
|
212
|
+
return self
|
|
213
|
+
|
|
214
|
+
def license(self, number: str) -> "DomainCreateBuilder":
|
|
215
|
+
"""A trademark or licence number, for registries that require one to register a name."""
|
|
216
|
+
self._options["license"] = number
|
|
217
|
+
return self
|
|
218
|
+
|
|
219
|
+
def max_fee(self, amount: str, currency: Optional[str] = None) -> "DomainCreateBuilder":
|
|
220
|
+
"""The most you agree to pay for this registration (RFC 8748).
|
|
221
|
+
|
|
222
|
+
Optional: without it the registry's own price is charged; with it, a higher real price
|
|
223
|
+
refuses the command instead of billing you the difference.
|
|
224
|
+
"""
|
|
225
|
+
self._options["fee"] = _fee_agreement(amount, currency)
|
|
226
|
+
return self
|
|
227
|
+
|
|
228
|
+
def ds_record(self, key_tag: int, alg: int, digest_type: int, digest: str) -> "DomainCreateBuilder":
|
|
229
|
+
"""Sign the domain with a DS record (RFC 5910). Call it again for a second key."""
|
|
230
|
+
sec = self._options.get("sec_dns") or {}
|
|
231
|
+
sec["ds_data"] = list(sec.get("ds_data") or []) + [_ds_record(key_tag, alg, digest_type, digest)]
|
|
232
|
+
self._options["sec_dns"] = sec
|
|
233
|
+
return self
|
|
234
|
+
|
|
235
|
+
def ds_record_with_key(self, key_tag: int, alg: int, digest_type: int, digest: str,
|
|
236
|
+
flags: int, protocol: int, key_alg: int,
|
|
237
|
+
pub_key: str) -> "DomainCreateBuilder":
|
|
238
|
+
"""A DS record that carries the DNSKEY it was computed from (RFC 5910).
|
|
239
|
+
|
|
240
|
+
A registry that accepts this can verify the digest against the key for you, catching a
|
|
241
|
+
mistyped digest before it reaches the zone. One that does not accept DNSKEY data refuses the
|
|
242
|
+
command rather than ignoring the extra element, so trying costs you nothing but a 2306.
|
|
243
|
+
"""
|
|
244
|
+
sec = self._options.get("sec_dns") or {}
|
|
245
|
+
record = _ds_record(key_tag, alg, digest_type, digest)
|
|
246
|
+
record["key_data"] = _key_record(flags, protocol, key_alg, pub_key)
|
|
247
|
+
sec["ds_data"] = list(sec.get("ds_data") or []) + [record]
|
|
248
|
+
self._options["sec_dns"] = sec
|
|
249
|
+
return self
|
|
250
|
+
|
|
251
|
+
def key_record(self, flags: int, protocol: int, alg: int, pub_key: str) -> "DomainCreateBuilder":
|
|
252
|
+
"""Sign with a public key instead, where the registry accepts one."""
|
|
253
|
+
sec = self._options.get("sec_dns") or {}
|
|
254
|
+
sec["key_data"] = list(sec.get("key_data") or []) + [_key_record(flags, protocol, alg, pub_key)]
|
|
255
|
+
self._options["sec_dns"] = sec
|
|
256
|
+
return self
|
|
257
|
+
|
|
258
|
+
def max_sig_life(self, seconds: int) -> "DomainCreateBuilder":
|
|
259
|
+
"""Maximum signature lifetime in seconds. Only meaningful alongside a DS or key record."""
|
|
260
|
+
sec = self._options.get("sec_dns") or {}
|
|
261
|
+
sec["max_sig_life"] = seconds
|
|
262
|
+
self._options["sec_dns"] = sec
|
|
263
|
+
return self
|
|
264
|
+
|
|
265
|
+
def send(self) -> Response:
|
|
266
|
+
"""Send the command and return the registry's answer."""
|
|
267
|
+
self._mark_sent()
|
|
268
|
+
return self._handler.create(self._id, **self._options)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class DomainUpdateBuilder(Builder):
|
|
272
|
+
"""Changes a domain, one named step at a time.
|
|
273
|
+
|
|
274
|
+
An EPP update is a DELTA, not a replacement: what you do not mention is left alone. The method
|
|
275
|
+
names say which of the three blocks each change lands in — add, rem or chg — because that
|
|
276
|
+
distinction is the whole semantics of the command.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
def _block(self, name: str) -> Dict[str, Any]:
|
|
280
|
+
block = self._options.get(name) or {}
|
|
281
|
+
self._options[name] = block
|
|
282
|
+
return block
|
|
283
|
+
|
|
284
|
+
def add_nameserver(self, host: str) -> "DomainUpdateBuilder":
|
|
285
|
+
"""Delegate to one more nameserver. Accumulates."""
|
|
286
|
+
return self.add_nameservers(host)
|
|
287
|
+
|
|
288
|
+
def add_nameservers(self, *hosts: str) -> "DomainUpdateBuilder":
|
|
289
|
+
"""Delegate to these nameservers as well as the ones already there. Accumulates."""
|
|
290
|
+
_append(self._block("add"), "ns", hosts)
|
|
291
|
+
return self
|
|
292
|
+
|
|
293
|
+
def rem_nameserver(self, host: str) -> "DomainUpdateBuilder":
|
|
294
|
+
"""Stop delegating to one nameserver. Accumulates."""
|
|
295
|
+
return self.rem_nameservers(host)
|
|
296
|
+
|
|
297
|
+
def rem_nameservers(self, *hosts: str) -> "DomainUpdateBuilder":
|
|
298
|
+
"""Stop delegating to these nameservers. Accumulates."""
|
|
299
|
+
_append(self._block("rem"), "ns", hosts)
|
|
300
|
+
return self
|
|
301
|
+
|
|
302
|
+
def add_contact(self, role: str, *handles: str) -> "DomainUpdateBuilder":
|
|
303
|
+
"""Attach contacts in a role, keeping the ones already there."""
|
|
304
|
+
block = self._block("add")
|
|
305
|
+
block["contacts"] = _append_contacts(block.get("contacts") or {}, role, handles)
|
|
306
|
+
return self
|
|
307
|
+
|
|
308
|
+
def rem_contact(self, role: str, *handles: str) -> "DomainUpdateBuilder":
|
|
309
|
+
"""Detach contacts from a role."""
|
|
310
|
+
block = self._block("rem")
|
|
311
|
+
block["contacts"] = _append_contacts(block.get("contacts") or {}, role, handles)
|
|
312
|
+
return self
|
|
313
|
+
|
|
314
|
+
def add_status(self, *statuses: str) -> "DomainUpdateBuilder":
|
|
315
|
+
"""Set a client-side status, e.g. ``clientHold`` (takes the domain out of DNS)."""
|
|
316
|
+
_append(self._block("add"), "statuses", statuses)
|
|
317
|
+
return self
|
|
318
|
+
|
|
319
|
+
def rem_status(self, *statuses: str) -> "DomainUpdateBuilder":
|
|
320
|
+
"""Clear a client-side status."""
|
|
321
|
+
_append(self._block("rem"), "statuses", statuses)
|
|
322
|
+
return self
|
|
323
|
+
|
|
324
|
+
def change_registrant(self, handle: str) -> "DomainUpdateBuilder":
|
|
325
|
+
"""Hand the domain to a different holder.
|
|
326
|
+
|
|
327
|
+
Many registries treat this as a change of ownership with its own rules, so a refusal is
|
|
328
|
+
usually policy rather than a malformed command.
|
|
329
|
+
"""
|
|
330
|
+
self._block("chg")["registrant"] = handle
|
|
331
|
+
return self
|
|
332
|
+
|
|
333
|
+
def change_auth_info(self, password: str) -> "DomainUpdateBuilder":
|
|
334
|
+
"""Replace the transfer secret."""
|
|
335
|
+
self._block("chg")["auth_info"] = password
|
|
336
|
+
return self
|
|
337
|
+
|
|
338
|
+
def clear_auth_info(self) -> "DomainUpdateBuilder":
|
|
339
|
+
"""REMOVE the transfer secret entirely, so no code will move this domain.
|
|
340
|
+
|
|
341
|
+
This is the answer to a leak, and it is not the same as setting an empty one: an empty
|
|
342
|
+
password is a value the holder can still present, so the domain stays exactly as movable as
|
|
343
|
+
it was. Only this clears it. Set a fresh one with :meth:`change_auth_info` when the customer
|
|
344
|
+
needs it again.
|
|
345
|
+
"""
|
|
346
|
+
self._block("chg")["clear_auth_info"] = True
|
|
347
|
+
return self
|
|
348
|
+
|
|
349
|
+
def restore(self) -> "DomainUpdateBuilder":
|
|
350
|
+
"""Ask to bring a deleted domain back from the redemption period (RFC 3915)."""
|
|
351
|
+
self._options["restore"] = True
|
|
352
|
+
return self
|
|
353
|
+
|
|
354
|
+
def license(self, number: str) -> "DomainUpdateBuilder":
|
|
355
|
+
"""A trademark or licence number, for registries that require one."""
|
|
356
|
+
self._options["license"] = number
|
|
357
|
+
return self
|
|
358
|
+
|
|
359
|
+
def max_fee(self, amount: str, currency: Optional[str] = None) -> "DomainUpdateBuilder":
|
|
360
|
+
"""The most you agree to pay, when the change is a billable one (RFC 8748)."""
|
|
361
|
+
self._options["fee"] = _fee_agreement(amount, currency)
|
|
362
|
+
return self
|
|
363
|
+
|
|
364
|
+
def add_ds_record(self, key_tag: int, alg: int, digest_type: int, digest: str) -> "DomainUpdateBuilder":
|
|
365
|
+
"""Add a DS record to an existing domain."""
|
|
366
|
+
return self._sec_dns("add", "ds_data", _ds_record(key_tag, alg, digest_type, digest))
|
|
367
|
+
|
|
368
|
+
def rem_ds_record(self, key_tag: int, alg: int, digest_type: int, digest: str) -> "DomainUpdateBuilder":
|
|
369
|
+
"""Remove one specific DS record. Everything must match what the registry holds."""
|
|
370
|
+
return self._sec_dns("rem", "ds_data", _ds_record(key_tag, alg, digest_type, digest))
|
|
371
|
+
|
|
372
|
+
def add_key_record(self, flags: int, protocol: int, alg: int, pub_key: str) -> "DomainUpdateBuilder":
|
|
373
|
+
"""Add a public key to an existing domain."""
|
|
374
|
+
return self._sec_dns("add", "key_data", _key_record(flags, protocol, alg, pub_key))
|
|
375
|
+
|
|
376
|
+
def rem_key_record(self, flags: int, protocol: int, alg: int, pub_key: str) -> "DomainUpdateBuilder":
|
|
377
|
+
"""Remove one specific public key."""
|
|
378
|
+
return self._sec_dns("rem", "key_data", _key_record(flags, protocol, alg, pub_key))
|
|
379
|
+
|
|
380
|
+
def remove_all_dnssec(self) -> "DomainUpdateBuilder":
|
|
381
|
+
"""Unsign the domain entirely.
|
|
382
|
+
|
|
383
|
+
Mutually exclusive with removing specific records — the protocol has no way to express
|
|
384
|
+
both, and a frame carrying both is refused. Refused here instead, where the message can say
|
|
385
|
+
so.
|
|
386
|
+
"""
|
|
387
|
+
sec = self._options.get("sec_dns") or {}
|
|
388
|
+
if "rem" in sec:
|
|
389
|
+
raise ValidationException(
|
|
390
|
+
"remove_all_dnssec() cannot be combined with rem_ds_record()/rem_key_record() — "
|
|
391
|
+
"remove everything, or name what to remove, not both"
|
|
392
|
+
)
|
|
393
|
+
sec["rem_all"] = True
|
|
394
|
+
self._options["sec_dns"] = sec
|
|
395
|
+
return self
|
|
396
|
+
|
|
397
|
+
def max_sig_life(self, seconds: int) -> "DomainUpdateBuilder":
|
|
398
|
+
"""Maximum signature lifetime in seconds."""
|
|
399
|
+
sec = self._options.get("sec_dns") or {}
|
|
400
|
+
sec["max_sig_life"] = seconds
|
|
401
|
+
self._options["sec_dns"] = sec
|
|
402
|
+
return self
|
|
403
|
+
|
|
404
|
+
def send(self) -> Response:
|
|
405
|
+
"""Send the command and return the registry's answer."""
|
|
406
|
+
self._mark_sent()
|
|
407
|
+
return self._handler.update(self._id, **self._options)
|
|
408
|
+
|
|
409
|
+
def _sec_dns(self, block: str, kind: str, record: Dict[str, Any]) -> "DomainUpdateBuilder":
|
|
410
|
+
sec = self._options.get("sec_dns") or {}
|
|
411
|
+
if block == "rem" and sec.get("rem_all"):
|
|
412
|
+
raise ValidationException(
|
|
413
|
+
"rem_ds_record()/rem_key_record() cannot be combined with remove_all_dnssec() — "
|
|
414
|
+
"remove everything, or name what to remove, not both"
|
|
415
|
+
)
|
|
416
|
+
spec = sec.get(block) or {}
|
|
417
|
+
spec[kind] = list(spec.get(kind) or []) + [record]
|
|
418
|
+
sec[block] = spec
|
|
419
|
+
self._options["sec_dns"] = sec
|
|
420
|
+
return self
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
class ContactCreateBuilder(Builder):
|
|
424
|
+
"""Creates a contact, one named step at a time.
|
|
425
|
+
|
|
426
|
+
The id and the e-mail are constructor arguments because the registry requires both — a builder
|
|
427
|
+
that lets you forget a mandatory field has moved the error from your editor to the wire.
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
def __init__(self, handler: Any, contact_id: str, email: str) -> None:
|
|
431
|
+
super().__init__(handler, contact_id)
|
|
432
|
+
self._options["email"] = email
|
|
433
|
+
|
|
434
|
+
def international_address(self, name: str, city: str, country_code: str,
|
|
435
|
+
street: Sequence[str] = (), org: Optional[str] = None,
|
|
436
|
+
state_province: Optional[str] = None,
|
|
437
|
+
postal_code: Optional[str] = None) -> "ContactCreateBuilder":
|
|
438
|
+
"""The address in ASCII, which every registry accepts.
|
|
439
|
+
|
|
440
|
+
At least one form is required. Give this one unless you have a reason not to: it is the
|
|
441
|
+
form that survives being printed, e-mailed and read by a system that knows no Cyrillic.
|
|
442
|
+
"""
|
|
443
|
+
return self._address("int", name, city, country_code, street, org, state_province, postal_code)
|
|
444
|
+
|
|
445
|
+
def localized_address(self, name: str, city: str, country_code: str,
|
|
446
|
+
street: Sequence[str] = (), org: Optional[str] = None,
|
|
447
|
+
state_province: Optional[str] = None,
|
|
448
|
+
postal_code: Optional[str] = None) -> "ContactCreateBuilder":
|
|
449
|
+
"""The address in the local script — Cyrillic for a Ukrainian registrant.
|
|
450
|
+
|
|
451
|
+
Optional, and additional: a contact may carry this form as well as the international one.
|
|
452
|
+
"""
|
|
453
|
+
return self._address("loc", name, city, country_code, street, org, state_province, postal_code)
|
|
454
|
+
|
|
455
|
+
def voice(self, number: str) -> "ContactCreateBuilder":
|
|
456
|
+
"""Voice number in the EPP form: ``+CC.NNNNNNNNN``."""
|
|
457
|
+
self._options["voice"] = number
|
|
458
|
+
return self
|
|
459
|
+
|
|
460
|
+
def fax(self, number: str) -> "ContactCreateBuilder":
|
|
461
|
+
"""Fax number, same form as :meth:`voice`."""
|
|
462
|
+
self._options["fax"] = number
|
|
463
|
+
return self
|
|
464
|
+
|
|
465
|
+
def auth_info(self, password: str) -> "ContactCreateBuilder":
|
|
466
|
+
"""The transfer secret for this contact."""
|
|
467
|
+
self._options["auth_info"] = password
|
|
468
|
+
return self
|
|
469
|
+
|
|
470
|
+
def publish(self, *fields: str) -> "ContactCreateBuilder":
|
|
471
|
+
"""Consent to publish these fields: name, org, addr, voice, fax, email.
|
|
472
|
+
|
|
473
|
+
Anything not listed takes the opposite treatment, so publish() and withhold() say the same
|
|
474
|
+
thing two ways — pick the one that matches how you think about it, and do not call both.
|
|
475
|
+
"""
|
|
476
|
+
self._options["disclose"] = _disclosure(True, fields)
|
|
477
|
+
return self
|
|
478
|
+
|
|
479
|
+
def withhold(self, *fields: str) -> "ContactCreateBuilder":
|
|
480
|
+
"""Withhold these fields from publication. See :meth:`publish` for the field names."""
|
|
481
|
+
self._options["disclose"] = _disclosure(False, fields)
|
|
482
|
+
return self
|
|
483
|
+
|
|
484
|
+
def send(self) -> Response:
|
|
485
|
+
"""Send the command and return the registry's answer."""
|
|
486
|
+
self._mark_sent()
|
|
487
|
+
return self._handler.create(self._id, **self._options)
|
|
488
|
+
|
|
489
|
+
def _address(self, kind, name, city, country_code, street, org, state_province, postal_code):
|
|
490
|
+
blocks = list(self._options.get("postal_infos") or [])
|
|
491
|
+
blocks.append(_postal_info(kind, name, city, country_code, street, org, state_province, postal_code))
|
|
492
|
+
self._options["postal_infos"] = blocks
|
|
493
|
+
return self
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class ContactUpdateBuilder(Builder):
|
|
497
|
+
"""Changes a contact, one named step at a time.
|
|
498
|
+
|
|
499
|
+
What you do not mention is left alone. That holds inside an address too: a field you leave out
|
|
500
|
+
keeps its value, and a field passed as an empty string is cleared. The one thing to know is
|
|
501
|
+
that the address block is a sequence with a required city and country, so touching any part of
|
|
502
|
+
it sends the whole block — give city and country_code whenever you change street, sp or pc.
|
|
503
|
+
"""
|
|
504
|
+
|
|
505
|
+
def change_international_address(self, name: Optional[str] = None, city: Optional[str] = None,
|
|
506
|
+
country_code: Optional[str] = None,
|
|
507
|
+
street: Optional[Sequence[str]] = None,
|
|
508
|
+
org: Optional[str] = None,
|
|
509
|
+
state_province: Optional[str] = None,
|
|
510
|
+
postal_code: Optional[str] = None) -> "ContactUpdateBuilder":
|
|
511
|
+
"""Change the ASCII address. Only the fields you pass are sent, and PRESENCE decides:
|
|
512
|
+
|
|
513
|
+
================== ===========================================================
|
|
514
|
+
leave it out/None the field is not sent, and the registry keeps its value
|
|
515
|
+
give a value the field is set to it
|
|
516
|
+
give ``""`` the field is CLEARED — the only way to remove org, state
|
|
517
|
+
province or postal code
|
|
518
|
+
================== ===========================================================
|
|
519
|
+
|
|
520
|
+
The other form (local or international) is untouched. The address block is a sequence with
|
|
521
|
+
a required city and country, so passing any part of it sends the whole block: give
|
|
522
|
+
``city`` and ``country_code`` whenever you touch street, state province or postal code.
|
|
523
|
+
"""
|
|
524
|
+
return self._address("int", name, city, country_code, street, org, state_province, postal_code)
|
|
525
|
+
|
|
526
|
+
def change_localized_address(self, name: Optional[str] = None, city: Optional[str] = None,
|
|
527
|
+
country_code: Optional[str] = None,
|
|
528
|
+
street: Optional[Sequence[str]] = None,
|
|
529
|
+
org: Optional[str] = None,
|
|
530
|
+
state_province: Optional[str] = None,
|
|
531
|
+
postal_code: Optional[str] = None) -> "ContactUpdateBuilder":
|
|
532
|
+
"""Change the local-script address. Same rules as :meth:`change_international_address`."""
|
|
533
|
+
return self._address("loc", name, city, country_code, street, org, state_province, postal_code)
|
|
534
|
+
|
|
535
|
+
def change_voice(self, number: str) -> "ContactUpdateBuilder":
|
|
536
|
+
return self._chg("voice", number)
|
|
537
|
+
|
|
538
|
+
def change_fax(self, number: str) -> "ContactUpdateBuilder":
|
|
539
|
+
return self._chg("fax", number)
|
|
540
|
+
|
|
541
|
+
def change_email(self, email: str) -> "ContactUpdateBuilder":
|
|
542
|
+
return self._chg("email", email)
|
|
543
|
+
|
|
544
|
+
def change_auth_info(self, password: str) -> "ContactUpdateBuilder":
|
|
545
|
+
"""Replace the transfer secret."""
|
|
546
|
+
return self._chg("auth_info", password)
|
|
547
|
+
|
|
548
|
+
# There is deliberately no clear_auth_info() here. RFC 5731 gives a domain a nullable form,
|
|
549
|
+
# <domain:authInfo><domain:null/>, and RFC 5733 defines no equivalent for a contact — so a
|
|
550
|
+
# contact's transfer secret can be REPLACED but not removed. Do not reach for an empty password
|
|
551
|
+
# as a substitute: an empty value is still a value the holder can present.
|
|
552
|
+
def publish(self, *fields: str) -> "ContactUpdateBuilder":
|
|
553
|
+
"""Consent to publish these fields: name, org, addr, voice, fax, email."""
|
|
554
|
+
return self._chg("disclose", _disclosure(True, fields))
|
|
555
|
+
|
|
556
|
+
def withhold(self, *fields: str) -> "ContactUpdateBuilder":
|
|
557
|
+
"""Withhold these fields from publication."""
|
|
558
|
+
return self._chg("disclose", _disclosure(False, fields))
|
|
559
|
+
|
|
560
|
+
def add_status(self, *statuses: str) -> "ContactUpdateBuilder":
|
|
561
|
+
"""Set a client-side status, e.g. ``clientUpdateProhibited``."""
|
|
562
|
+
_append(self._options, "add_statuses", statuses)
|
|
563
|
+
return self
|
|
564
|
+
|
|
565
|
+
def rem_status(self, *statuses: str) -> "ContactUpdateBuilder":
|
|
566
|
+
"""Clear a client-side status."""
|
|
567
|
+
_append(self._options, "rem_statuses", statuses)
|
|
568
|
+
return self
|
|
569
|
+
|
|
570
|
+
def send(self) -> Response:
|
|
571
|
+
"""Send the command and return the registry's answer."""
|
|
572
|
+
self._mark_sent()
|
|
573
|
+
return self._handler.update(self._id, **self._options)
|
|
574
|
+
|
|
575
|
+
def _chg(self, key: str, value: Any) -> "ContactUpdateBuilder":
|
|
576
|
+
chg = self._options.get("chg") or {}
|
|
577
|
+
chg[key] = value
|
|
578
|
+
self._options["chg"] = chg
|
|
579
|
+
return self
|
|
580
|
+
|
|
581
|
+
def _address(self, kind, name, city, country_code, street, org, state_province, postal_code):
|
|
582
|
+
# Only the arguments actually given become keys. The emitter reads presence, so an absent
|
|
583
|
+
# key is not sent and a key holding "" is sent empty, which is what clears a field.
|
|
584
|
+
block: Dict[str, Any] = {"type": kind}
|
|
585
|
+
for key, value in (("name", name), ("org", org), ("city", city),
|
|
586
|
+
("sp", state_province), ("pc", postal_code), ("cc", country_code)):
|
|
587
|
+
if value is not None:
|
|
588
|
+
block[key] = value
|
|
589
|
+
if street is not None:
|
|
590
|
+
block["street"] = [str(line) for line in street]
|
|
591
|
+
|
|
592
|
+
chg = self._options.get("chg") or {}
|
|
593
|
+
blocks = list(chg.get("postal_infos") or [])
|
|
594
|
+
blocks.append(block)
|
|
595
|
+
chg["postal_infos"] = blocks
|
|
596
|
+
self._options["chg"] = chg
|
|
597
|
+
return self
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
class HostUpdateBuilder(Builder):
|
|
601
|
+
"""Changes a nameserver's glue addresses or statuses.
|
|
602
|
+
|
|
603
|
+
There is no rename: this registry reads only the add and remove blocks, so a rename request is
|
|
604
|
+
discarded and the command still answers 1000. Create the new host, repoint the domains that use
|
|
605
|
+
it, then delete the old one.
|
|
606
|
+
"""
|
|
607
|
+
|
|
608
|
+
def add_address(self, ip: str) -> "HostUpdateBuilder":
|
|
609
|
+
"""Add one glue address. Accumulates."""
|
|
610
|
+
return self.add_addresses(ip)
|
|
611
|
+
|
|
612
|
+
def add_addresses(self, *ips: str) -> "HostUpdateBuilder":
|
|
613
|
+
"""Add glue addresses. IPv4 and IPv6 are told apart automatically. Accumulates."""
|
|
614
|
+
_append(self._options, "add_addresses", ips)
|
|
615
|
+
return self
|
|
616
|
+
|
|
617
|
+
def rem_address(self, ip: str) -> "HostUpdateBuilder":
|
|
618
|
+
"""Remove one glue address. Accumulates."""
|
|
619
|
+
return self.rem_addresses(ip)
|
|
620
|
+
|
|
621
|
+
def rem_addresses(self, *ips: str) -> "HostUpdateBuilder":
|
|
622
|
+
"""Remove glue addresses. Accumulates."""
|
|
623
|
+
_append(self._options, "rem_addresses", ips)
|
|
624
|
+
return self
|
|
625
|
+
|
|
626
|
+
def add_status(self, *statuses: str) -> "HostUpdateBuilder":
|
|
627
|
+
"""Set a client-side status."""
|
|
628
|
+
_append(self._options, "add_statuses", statuses)
|
|
629
|
+
return self
|
|
630
|
+
|
|
631
|
+
def rem_status(self, *statuses: str) -> "HostUpdateBuilder":
|
|
632
|
+
"""Clear a client-side status."""
|
|
633
|
+
_append(self._options, "rem_statuses", statuses)
|
|
634
|
+
return self
|
|
635
|
+
|
|
636
|
+
def send(self) -> Response:
|
|
637
|
+
"""Send the command and return the registry's answer."""
|
|
638
|
+
self._mark_sent()
|
|
639
|
+
return self._handler.update(self._id, **self._options)
|