cloudfloordns.models.domain

  1# from dataclasses import dataclass, field
  2import datetime
  3import logging
  4from collections import ChainMap
  5from typing import Any, List, Literal, Optional
  6
  7from pydantic import AliasChoices, BaseModel, ConfigDict, Field, field_validator
  8
  9from cloudfloordns.constants import NS1, NS2, NS3, NS4
 10
 11
 12class DomainDescription(BaseModel):
 13    """
 14    Pydantic model
 15    """
 16
 17    status: Optional[str]
 18    status_extended: Optional[str]
 19
 20    class Config:
 21        extra = "allow"
 22
 23
 24class Contact(BaseModel):
 25    """
 26    Pydantic model
 27    """
 28
 29    firstname: Optional[str] = Field(alias="FirstName", default=None)
 30    lastname: Optional[str] = Field(alias="LastName", default=None)
 31    companyname: Optional[str] = Field(alias="Organization", default=None)
 32    streetaddress: Optional[str] = Field(alias="Address", default=None)
 33    city: Optional[str] = Field(alias="City", default=None)
 34    state: Optional[str] = Field(alias="State", default=None)
 35    postalcode: Optional[str] = Field(alias="PostalCode", default=None)
 36    country: Optional[str] = Field(alias="Country", default=None)
 37    phone: Optional[str] = Field(alias="Phone", default=None)
 38    fax: Optional[str] = Field(alias="Fax", default=None)
 39    email: Optional[str] = Field(alias="Email", default=None)
 40
 41    class Config:
 42        populate_by_name = True
 43        extra = "ignore"
 44
 45    def dump_as(self, prefix, by_alias=False):
 46        prefix = prefix.title() if by_alias else prefix.lower()
 47        return {
 48            f"{prefix}{k}": v for k, v in self.model_dump(by_alias=by_alias).items()
 49        }
 50
 51    def as_owner(self, by_alias=False):
 52        return self.dump_as("owner", by_alias=by_alias)
 53
 54    def as_admin(self, by_alias=False):
 55        return self.dump_as("admin", by_alias=by_alias)
 56
 57    def as_tech(self, by_alias=False):
 58        return self.dump_as("tech", by_alias=by_alias)
 59
 60    def as_bill(self, by_alias=False):
 61        return self.dump_as("bill", by_alias=by_alias)
 62
 63
 64class DomainPayload(BaseModel):
 65    """
 66    Pydantic model
 67    """
 68
 69    domainname: str
 70    organisation: Optional[str] = Field(default=None, alias="DomainOrganization")
 71
 72    # Owner informations
 73    ownerfirstname: str = Field(alias="OwnerFirstName")
 74    ownerlastname: str = Field(alias="OwnerLastName")
 75    ownercompanyname: str = Field(alias="OwnerOrganization")
 76    ownerstreetaddress: str = Field(alias="OwnerAddress")
 77    ownercity: str = Field(alias="OwnerCity")
 78    ownerstate: str = Field(alias="OwnerState")
 79    ownerpostalcode: str = Field(alias="OwnerPostalCode")
 80    ownercountry: str = Field(alias="OwnerCountry")
 81    ownerphone: str = Field(alias="OwnerPhone")
 82    ownerfax: str = Field(alias="OwnerFax")
 83    owneremail: str = Field(alias="OwnerEmail")
 84
 85    # Admin informations
 86    adminfirstname: str = Field(alias="AdminFirstName")
 87    adminlastname: str = Field(alias="AdminLastName")
 88    admincompanyname: str = Field(alias="AdminOrganization")
 89    adminstreetaddress: str = Field(alias="AdminAddress")
 90    admincity: str = Field(alias="AdminCity")
 91    adminstate: str = Field(alias="AdminState")
 92    adminpostalcode: str = Field(alias="AdminPostalCode")
 93    admincountry: str = Field(alias="AdminCountry")
 94    adminphone: str = Field(alias="AdminPhone")
 95    adminfax: str = Field(alias="AdminFax")
 96    adminemail: str = Field(alias="AdminEmail")
 97
 98    # Billing Contact informations
 99    billfirstname: str = Field(alias="BillFirstName")
100    billlastname: str = Field(alias="BillLastName")
101    billcompanyname: str = Field(alias="BillOrganization")
102    billstreetaddress: str = Field(alias="BillAddress")
103    billcity: str = Field(alias="BillCity")
104    billstate: str = Field(alias="BillState")
105    billpostalcode: str = Field(alias="BillPostalCode")
106    billcountry: str = Field(alias="BillCountry")
107    billphone: str = Field(alias="BillPhone")
108    billfax: str = Field(alias="BillFax")
109    billemail: str = Field(alias="BillEmail")
110
111    # Technical Contact informations
112    techfirstname: str = Field(alias="TechFirstName")
113    techlastname: str = Field(alias="TechLastName")
114    techcompanyname: str = Field(alias="TechOrganization")
115    techstreetaddress: str = Field(alias="TechAddress")
116    techcity: str = Field(alias="TechCity")
117    techstate: str = Field(alias="TechState")
118    techcountry: str = Field(alias="TechCountry")
119    techpostalcode: str = Field(alias="TechPostalCode")
120    techphone: str = Field(alias="TechPhone")
121    techfax: str = Field(alias="TechFax")
122    techemail: str = Field(alias="TechEmail")
123
124    # Other informations
125    groups_ids: List[str] = Field(default_factory=list)
126    assign_default_groups_nameserver: Literal[0, 1] = 1
127    autorenew: Optional[str] = Field(
128        validation_alias=AliasChoices("auto_renew", "autorenew"), default=None
129    )
130    reg_opt_out: Optional[str] = None
131    nom_type: Optional[str] = None
132    lock: Optional[str] = Field(
133        validation_alias=AliasChoices("lock", "locked"), default=None
134    )
135
136    domain_ns1: Optional[str] = Field(alias="DomainNS1", default=NS1)
137    domain_ns2: Optional[str] = Field(alias="DomainNS2", default=NS2)
138    domain_ns3: Optional[str] = Field(alias="DomainNS3", default=NS3)
139    domain_ns4: Optional[str] = Field(alias="DomainNS4", default=NS4)
140    domain_ns1_ip: Optional[str] = Field(alias="DomainNS1IP", default=None)
141    domain_ns2_ip: Optional[str] = Field(alias="DomainNS2IP", default=None)
142    domain_ns3_ip: Optional[str] = Field(alias="DomainNS3IP", default=None)
143    domain_ns4_ip: Optional[str] = Field(alias="DomainNS4IP", default=None)
144
145    @field_validator("lock", mode="before")
146    @classmethod
147    def ensure_locked_as_string(cls, v: Any):
148        if not isinstance(v, str):
149            return str(v)
150        return v
151
152    def _setcontact(self, prefix, info: Contact):
153        data = info.dump_as(prefix)
154        for k, v in data.items():
155            setattr(self, k, v)
156        # Domain.model_validate(self)
157        return self
158
159    def set_owner(self, info: Contact):
160        return self._setcontact("Owner", info)
161
162    def set_admin(self, info: Contact):
163        return self._setcontact("Admin", info)
164
165    def set_bill(self, info: Contact):
166        return self._setcontact("Bill", info)
167
168    def set_tech(self, info: Contact):
169        return self._setcontact("Tech", info)
170
171    @classmethod
172    def prepare(
173        cls,
174        domainname: str,
175        owner: Contact,
176        admin: Contact,
177        bill: Contact,
178        tech: Contact,
179    ) -> "DomainPayload":
180        contact_data = {
181            k: v
182            for prefix, c in (
183                ("Owner", owner),
184                ("Admin", admin),
185                ("Bill", bill),
186                ("Tech", tech),
187            )
188            for k, v in c.dump_as(prefix).items()
189        }
190        return cls.model_validate({"domainname": domainname, **contact_data})
191
192    def dump_for_update(self):
193        payload = self.model_dump(
194            by_alias=True,
195            exclude=[
196                "domainname",
197                # "domain_ns1",
198                # "domain_ns2",
199            ],
200            exclude_none=True,
201            exclude_unset=True,
202        )
203        payload = {
204            k: v for k, v in payload.items() if not k.lower().startswith("owner")
205        }
206        payload = {k: v for k, v in payload.items() if not k.lower().endswith("fax")}
207        return payload
208
209    # https://docs.pydantic.dev/latest/concepts/config/
210    # https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.validate_assignment
211    model_config = ConfigDict(
212        populate_by_name=True,
213        extra="ignore",
214        validate_assignment=True,
215    )
216
217
218CLOUDFLOORDNS_NAMESERVERS = (
219    "dns1.name-s.net.",
220    "dns2.name-s.net.",
221    "dns0.mtgsy.com.",
222    "dns3.mtgsy.com.",
223    "dns4.mtgsy.com.",
224    "ns1.g02.cfdns.net.",
225    "ns1.g02.cfdns.net.",
226    "ns2.g02.cfdns.biz.",
227    "ns3.g02.cfdns.info.",
228    "ns4.g02.cfdns.co.uk.",
229)
230CLOUDFLOORDNS_NAMESERVERS_DOMAINS = (
231    "name-s.net.",
232    "mtgsy.com.",
233    "cfdns.net.",
234    "cfdns.net.",
235    "cfdns.biz.",
236    "cfdns.info.",
237    "cfdns.co.uk.",
238)
239
240
241def is_cloudlfoordns_ns(ns):
242    # Ensure the nameserver ends with a single dot.
243    ns = ns.strip().rstrip(".").lower() + "."
244    return ns.endswith(CLOUDFLOORDNS_NAMESERVERS_DOMAINS)
245
246
247class Domain(BaseModel):
248    """
249    Pydantic model
250    """
251
252    model_config = ConfigDict(
253        populate_by_name=True,
254        extra="allow",
255        # https://docs.pydantic.dev/latest/concepts/pydantic_settings/#case-sensitivity
256        # case_sensitive = True
257        # https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.validate_assignment
258        validate_assignment=True,
259    )
260
261    domainname: str = Field(validation_alias=AliasChoices("domainname", "domain"))
262
263    id: Optional[str] = None
264    epp: Optional[str] = Field(default=None, alias="EPP")
265    organisation: Optional[str] = None
266
267    # Owner informations
268    ownerfirstname: Optional[str] = Field(default=None, alias="OwnerFirstName")
269    ownerlastname: Optional[str] = Field(default=None, alias="OwnerLastName")
270    ownercompanyname: Optional[str] = Field(default=None, alias="OwnerOrganization")
271    ownerstreetaddress: Optional[str] = Field(default=None, alias="OwnerAddress")
272    ownercity: Optional[str] = Field(default=None, alias="OwnerCity")
273    ownerstate: Optional[str] = Field(default=None, alias="OwnerState")
274    ownerpostalcode: Optional[str] = Field(default=None, alias="OwnerPostalCode")
275    ownercountry: Optional[str] = Field(default=None, alias="OwnerCountry")
276    ownerphone: Optional[str] = Field(default=None, alias="OwnerPhone")
277    ownerfax: Optional[str] = Field(default=None, alias="OwnerFax")
278    owneremail: Optional[str] = Field(default=None, alias="OwnerEmail")
279
280    # Admin informations
281    adminfirstname: Optional[str] = Field(default=None, alias="AdminFirstName")
282    adminlastname: Optional[str] = Field(default=None, alias="AdminLastName")
283    admincompanyname: Optional[str] = Field(default=None, alias="AdminOrganization")
284    adminstreetaddress: Optional[str] = Field(default=None, alias="AdminAddress")
285    admincity: Optional[str] = Field(default=None, alias="AdminCity")
286    adminstate: Optional[str] = Field(default=None, alias="AdminState")
287    adminpostalcode: Optional[str] = Field(default=None, alias="AdminPostalCode")
288    admincountry: Optional[str] = Field(default=None, alias="AdminCountry")
289    adminphone: Optional[str] = Field(default=None, alias="AdminPhone")
290    adminfax: Optional[str] = Field(default=None, alias="AdminFax")
291    adminemail: Optional[str] = Field(default=None, alias="AdminEmail")
292
293    # Billing Contact informations
294    billfirstname: Optional[str] = Field(default=None, alias="BillFirstName")
295    billlastname: Optional[str] = Field(default=None, alias="BillLastName")
296    billcompanyname: Optional[str] = Field(default=None, alias="BillOrganization")
297    billstreetaddress: Optional[str] = Field(default=None, alias="BillAddress")
298    billcity: Optional[str] = Field(default=None, alias="BillCity")
299    # There is a typo in the returned value.
300    billstate: Optional[str] = Field(
301        default=None,
302        alias="BillState",
303        validation_alias=AliasChoices("billState", "BillState"),
304    )
305    billpostalcode: Optional[str] = Field(default=None, alias="BillPostalCode")
306    billcountry: Optional[str] = Field(default=None, alias="BillCountry")
307    billphone: Optional[str] = Field(default=None, alias="BillPhone")
308    billfax: Optional[str] = Field(default=None, alias="BillFax")
309    billemail: Optional[str] = Field(default=None, alias="BillEmail")
310
311    # Technical Contact informations
312    techfirstname: Optional[str] = Field(default=None, alias="TechFirstName")
313    techlastname: Optional[str] = Field(default=None, alias="TechLastName")
314    techcompanyname: Optional[str] = Field(default=None, alias="TechOrganization")
315    techstreetaddress: Optional[str] = Field(default=None, alias="TechAddress")
316    techcity: Optional[str] = Field(default=None, alias="TechCity")
317    techstate: Optional[str] = Field(default=None, alias="TechState")
318    techcountry: Optional[str] = Field(default=None, alias="TechCountry")
319    techpostalcode: Optional[str] = Field(default=None, alias="TechPostalCode")
320    techphone: Optional[str] = Field(default=None, alias="TechPhone")
321    techfax: Optional[str] = Field(default=None, alias="TechFax")
322    techemail: Optional[str] = Field(default=None, alias="TechEmail")
323
324    # Other informations
325    auto_renew: Optional[str] = None
326    reg_opt_out: Optional[str] = None
327    username: Optional[str] = None
328    status: Optional[str] = None
329    use_trustee: Optional[str] = None
330    locked: Optional[str] = None
331    editzone: Optional[str] = None
332    expires: Optional[datetime.date] = None
333    deleteonexpiry: Optional[str] = None
334    companyregno: Optional[str] = None
335    client_delete_prohibited_lock: Optional[str] = None
336    client_update_prohibited_lock: Optional[str] = None
337    client_transfer_prohibited_lock: Optional[str] = None
338    registeredhere: Optional[str] = None
339    nameserver: List[str] = Field(default_factory=list)
340    domain_description: Optional[DomainDescription] = None
341
342    @property
343    def is_externally_managed(self):
344        is_cfdns_ns = [is_cloudlfoordns_ns(ns) for ns in self.nameserver]
345        if all(is_cfdns_ns):
346            return False
347        if not any(is_cfdns_ns):
348            return True
349        # Part of the nameserver are owned by CFDns, the rest is not
350        # => The nameserver configuration is wrong
351        ns_txt = ", ".join(self.nameserver)
352        logging.warning(
353            f"Domain {self.domainname} has inconsistant nameservers: {ns_txt}"
354        )
355        return False
356
357    @field_validator("locked", mode="before")
358    @classmethod
359    def ensure_locked_as_string(cls, v: Any):
360        if not isinstance(v, str):
361            return str(v)
362        return v
363
364    # @model_validator(mode='before')
365    # @classmethod
366    # def check_card_number_omitted(cls, data: Any) -> Any:
367    #     if isinstance(data, dict):
368    #         assert (
369    #             'card_number' not in data
370    #         ), 'card_number should not be included'
371    #     return data
372
373    def _setcontact(self, prefix, info: Contact):
374        data = info.dump_as(prefix)
375        for k, v in data.items():
376            setattr(self, k, v)
377        # Domain.model_validate(self)
378        return self
379
380    def _getcontact(self, prefix: str) -> Contact:
381        data = {k.removeprefix(prefix): v for k, v in self.model_dump().items()}
382        return Contact.model_validate(data)
383
384    def set_owner(self, info: Contact):
385        return self._setcontact("owner", info)
386
387    def set_admin(self, info: Contact):
388        return self._setcontact("admin", info)
389
390    def set_bill(self, info: Contact):
391        return self._setcontact("bill", info)
392
393    def set_tech(self, info: Contact):
394        return self._setcontact("tech", info)
395
396    def update_contact(
397        self,
398        owner: Optional[Contact] = None,
399        admin: Optional[Contact] = None,
400        tech: Optional[Contact] = None,
401        bill: Optional[Contact] = None,
402        timeout=None,
403    ):
404        converted = (
405            d
406            for d in (
407                owner and owner.as_owner(),
408                admin and admin.as_admin(),
409                tech and tech.as_tech(),
410                bill and bill.as_bill(),
411            )
412            if d
413        )
414        data = dict(ChainMap(*converted))
415        for k, v in data.items():
416            setattr(self, k, v)
417        # Domain.model_validate(self)
418        return self
419
420    def register_payload(self, use_default_ns: bool = True) -> DomainPayload:
421        data = self.model_dump(by_alias=True)
422        data.update(
423            {
424                # "groups_ids": ...,
425                "assign_default_groups_nameserver": 1 if use_default_ns else 0,
426            }
427        )
428
429        return DomainPayload.model_validate(data)
430
431    def dump_for_update(self):
432        return self.model_dump(
433            by_alias=True,
434            exclude=[
435                "epp",
436                "status",
437                "use_trustee",
438                "locked",
439                "domain_description",
440            ],
441        )
class DomainDescription(pydantic.main.BaseModel):
13class DomainDescription(BaseModel):
14    """
15    Pydantic model
16    """
17
18    status: Optional[str]
19    status_extended: Optional[str]
20
21    class Config:
22        extra = "allow"

Pydantic model

status: Optional[str]
status_extended: Optional[str]
model_config: ClassVar[pydantic.config.ConfigDict] = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class DomainDescription.Config:
21    class Config:
22        extra = "allow"
extra = 'allow'
class Contact(pydantic.main.BaseModel):
25class Contact(BaseModel):
26    """
27    Pydantic model
28    """
29
30    firstname: Optional[str] = Field(alias="FirstName", default=None)
31    lastname: Optional[str] = Field(alias="LastName", default=None)
32    companyname: Optional[str] = Field(alias="Organization", default=None)
33    streetaddress: Optional[str] = Field(alias="Address", default=None)
34    city: Optional[str] = Field(alias="City", default=None)
35    state: Optional[str] = Field(alias="State", default=None)
36    postalcode: Optional[str] = Field(alias="PostalCode", default=None)
37    country: Optional[str] = Field(alias="Country", default=None)
38    phone: Optional[str] = Field(alias="Phone", default=None)
39    fax: Optional[str] = Field(alias="Fax", default=None)
40    email: Optional[str] = Field(alias="Email", default=None)
41
42    class Config:
43        populate_by_name = True
44        extra = "ignore"
45
46    def dump_as(self, prefix, by_alias=False):
47        prefix = prefix.title() if by_alias else prefix.lower()
48        return {
49            f"{prefix}{k}": v for k, v in self.model_dump(by_alias=by_alias).items()
50        }
51
52    def as_owner(self, by_alias=False):
53        return self.dump_as("owner", by_alias=by_alias)
54
55    def as_admin(self, by_alias=False):
56        return self.dump_as("admin", by_alias=by_alias)
57
58    def as_tech(self, by_alias=False):
59        return self.dump_as("tech", by_alias=by_alias)
60
61    def as_bill(self, by_alias=False):
62        return self.dump_as("bill", by_alias=by_alias)

Pydantic model

firstname: Optional[str]
lastname: Optional[str]
companyname: Optional[str]
streetaddress: Optional[str]
city: Optional[str]
state: Optional[str]
postalcode: Optional[str]
country: Optional[str]
phone: Optional[str]
fax: Optional[str]
email: Optional[str]
def dump_as(self, prefix, by_alias=False):
46    def dump_as(self, prefix, by_alias=False):
47        prefix = prefix.title() if by_alias else prefix.lower()
48        return {
49            f"{prefix}{k}": v for k, v in self.model_dump(by_alias=by_alias).items()
50        }
def as_owner(self, by_alias=False):
52    def as_owner(self, by_alias=False):
53        return self.dump_as("owner", by_alias=by_alias)
def as_admin(self, by_alias=False):
55    def as_admin(self, by_alias=False):
56        return self.dump_as("admin", by_alias=by_alias)
def as_tech(self, by_alias=False):
58    def as_tech(self, by_alias=False):
59        return self.dump_as("tech", by_alias=by_alias)
def as_bill(self, by_alias=False):
61    def as_bill(self, by_alias=False):
62        return self.dump_as("bill", by_alias=by_alias)
model_config: ClassVar[pydantic.config.ConfigDict] = {'extra': 'ignore', 'populate_by_name': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class Contact.Config:
42    class Config:
43        populate_by_name = True
44        extra = "ignore"
populate_by_name = True
extra = 'ignore'
class DomainPayload(pydantic.main.BaseModel):
 65class DomainPayload(BaseModel):
 66    """
 67    Pydantic model
 68    """
 69
 70    domainname: str
 71    organisation: Optional[str] = Field(default=None, alias="DomainOrganization")
 72
 73    # Owner informations
 74    ownerfirstname: str = Field(alias="OwnerFirstName")
 75    ownerlastname: str = Field(alias="OwnerLastName")
 76    ownercompanyname: str = Field(alias="OwnerOrganization")
 77    ownerstreetaddress: str = Field(alias="OwnerAddress")
 78    ownercity: str = Field(alias="OwnerCity")
 79    ownerstate: str = Field(alias="OwnerState")
 80    ownerpostalcode: str = Field(alias="OwnerPostalCode")
 81    ownercountry: str = Field(alias="OwnerCountry")
 82    ownerphone: str = Field(alias="OwnerPhone")
 83    ownerfax: str = Field(alias="OwnerFax")
 84    owneremail: str = Field(alias="OwnerEmail")
 85
 86    # Admin informations
 87    adminfirstname: str = Field(alias="AdminFirstName")
 88    adminlastname: str = Field(alias="AdminLastName")
 89    admincompanyname: str = Field(alias="AdminOrganization")
 90    adminstreetaddress: str = Field(alias="AdminAddress")
 91    admincity: str = Field(alias="AdminCity")
 92    adminstate: str = Field(alias="AdminState")
 93    adminpostalcode: str = Field(alias="AdminPostalCode")
 94    admincountry: str = Field(alias="AdminCountry")
 95    adminphone: str = Field(alias="AdminPhone")
 96    adminfax: str = Field(alias="AdminFax")
 97    adminemail: str = Field(alias="AdminEmail")
 98
 99    # Billing Contact informations
100    billfirstname: str = Field(alias="BillFirstName")
101    billlastname: str = Field(alias="BillLastName")
102    billcompanyname: str = Field(alias="BillOrganization")
103    billstreetaddress: str = Field(alias="BillAddress")
104    billcity: str = Field(alias="BillCity")
105    billstate: str = Field(alias="BillState")
106    billpostalcode: str = Field(alias="BillPostalCode")
107    billcountry: str = Field(alias="BillCountry")
108    billphone: str = Field(alias="BillPhone")
109    billfax: str = Field(alias="BillFax")
110    billemail: str = Field(alias="BillEmail")
111
112    # Technical Contact informations
113    techfirstname: str = Field(alias="TechFirstName")
114    techlastname: str = Field(alias="TechLastName")
115    techcompanyname: str = Field(alias="TechOrganization")
116    techstreetaddress: str = Field(alias="TechAddress")
117    techcity: str = Field(alias="TechCity")
118    techstate: str = Field(alias="TechState")
119    techcountry: str = Field(alias="TechCountry")
120    techpostalcode: str = Field(alias="TechPostalCode")
121    techphone: str = Field(alias="TechPhone")
122    techfax: str = Field(alias="TechFax")
123    techemail: str = Field(alias="TechEmail")
124
125    # Other informations
126    groups_ids: List[str] = Field(default_factory=list)
127    assign_default_groups_nameserver: Literal[0, 1] = 1
128    autorenew: Optional[str] = Field(
129        validation_alias=AliasChoices("auto_renew", "autorenew"), default=None
130    )
131    reg_opt_out: Optional[str] = None
132    nom_type: Optional[str] = None
133    lock: Optional[str] = Field(
134        validation_alias=AliasChoices("lock", "locked"), default=None
135    )
136
137    domain_ns1: Optional[str] = Field(alias="DomainNS1", default=NS1)
138    domain_ns2: Optional[str] = Field(alias="DomainNS2", default=NS2)
139    domain_ns3: Optional[str] = Field(alias="DomainNS3", default=NS3)
140    domain_ns4: Optional[str] = Field(alias="DomainNS4", default=NS4)
141    domain_ns1_ip: Optional[str] = Field(alias="DomainNS1IP", default=None)
142    domain_ns2_ip: Optional[str] = Field(alias="DomainNS2IP", default=None)
143    domain_ns3_ip: Optional[str] = Field(alias="DomainNS3IP", default=None)
144    domain_ns4_ip: Optional[str] = Field(alias="DomainNS4IP", default=None)
145
146    @field_validator("lock", mode="before")
147    @classmethod
148    def ensure_locked_as_string(cls, v: Any):
149        if not isinstance(v, str):
150            return str(v)
151        return v
152
153    def _setcontact(self, prefix, info: Contact):
154        data = info.dump_as(prefix)
155        for k, v in data.items():
156            setattr(self, k, v)
157        # Domain.model_validate(self)
158        return self
159
160    def set_owner(self, info: Contact):
161        return self._setcontact("Owner", info)
162
163    def set_admin(self, info: Contact):
164        return self._setcontact("Admin", info)
165
166    def set_bill(self, info: Contact):
167        return self._setcontact("Bill", info)
168
169    def set_tech(self, info: Contact):
170        return self._setcontact("Tech", info)
171
172    @classmethod
173    def prepare(
174        cls,
175        domainname: str,
176        owner: Contact,
177        admin: Contact,
178        bill: Contact,
179        tech: Contact,
180    ) -> "DomainPayload":
181        contact_data = {
182            k: v
183            for prefix, c in (
184                ("Owner", owner),
185                ("Admin", admin),
186                ("Bill", bill),
187                ("Tech", tech),
188            )
189            for k, v in c.dump_as(prefix).items()
190        }
191        return cls.model_validate({"domainname": domainname, **contact_data})
192
193    def dump_for_update(self):
194        payload = self.model_dump(
195            by_alias=True,
196            exclude=[
197                "domainname",
198                # "domain_ns1",
199                # "domain_ns2",
200            ],
201            exclude_none=True,
202            exclude_unset=True,
203        )
204        payload = {
205            k: v for k, v in payload.items() if not k.lower().startswith("owner")
206        }
207        payload = {k: v for k, v in payload.items() if not k.lower().endswith("fax")}
208        return payload
209
210    # https://docs.pydantic.dev/latest/concepts/config/
211    # https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.validate_assignment
212    model_config = ConfigDict(
213        populate_by_name=True,
214        extra="ignore",
215        validate_assignment=True,
216    )

Pydantic model

domainname: str
organisation: Optional[str]
ownerfirstname: str
ownerlastname: str
ownercompanyname: str
ownerstreetaddress: str
ownercity: str
ownerstate: str
ownerpostalcode: str
ownercountry: str
ownerphone: str
ownerfax: str
owneremail: str
adminfirstname: str
adminlastname: str
admincompanyname: str
adminstreetaddress: str
admincity: str
adminstate: str
adminpostalcode: str
admincountry: str
adminphone: str
adminfax: str
adminemail: str
billfirstname: str
billlastname: str
billcompanyname: str
billstreetaddress: str
billcity: str
billstate: str
billpostalcode: str
billcountry: str
billphone: str
billfax: str
billemail: str
techfirstname: str
techlastname: str
techcompanyname: str
techstreetaddress: str
techcity: str
techstate: str
techcountry: str
techpostalcode: str
techphone: str
techfax: str
techemail: str
groups_ids: List[str]
assign_default_groups_nameserver: Literal[0, 1]
autorenew: Optional[str]
reg_opt_out: Optional[str]
nom_type: Optional[str]
lock: Optional[str]
domain_ns1: Optional[str]
domain_ns2: Optional[str]
domain_ns3: Optional[str]
domain_ns4: Optional[str]
domain_ns1_ip: Optional[str]
domain_ns2_ip: Optional[str]
domain_ns3_ip: Optional[str]
domain_ns4_ip: Optional[str]
@field_validator('lock', mode='before')
@classmethod
def ensure_locked_as_string(cls, v: Any):
146    @field_validator("lock", mode="before")
147    @classmethod
148    def ensure_locked_as_string(cls, v: Any):
149        if not isinstance(v, str):
150            return str(v)
151        return v
def set_owner(self, info: Contact):
160    def set_owner(self, info: Contact):
161        return self._setcontact("Owner", info)
def set_admin(self, info: Contact):
163    def set_admin(self, info: Contact):
164        return self._setcontact("Admin", info)
def set_bill(self, info: Contact):
166    def set_bill(self, info: Contact):
167        return self._setcontact("Bill", info)
def set_tech(self, info: Contact):
169    def set_tech(self, info: Contact):
170        return self._setcontact("Tech", info)
@classmethod
def prepare( cls, domainname: str, owner: Contact, admin: Contact, bill: Contact, tech: Contact) -> DomainPayload:
172    @classmethod
173    def prepare(
174        cls,
175        domainname: str,
176        owner: Contact,
177        admin: Contact,
178        bill: Contact,
179        tech: Contact,
180    ) -> "DomainPayload":
181        contact_data = {
182            k: v
183            for prefix, c in (
184                ("Owner", owner),
185                ("Admin", admin),
186                ("Bill", bill),
187                ("Tech", tech),
188            )
189            for k, v in c.dump_as(prefix).items()
190        }
191        return cls.model_validate({"domainname": domainname, **contact_data})
def dump_for_update(self):
193    def dump_for_update(self):
194        payload = self.model_dump(
195            by_alias=True,
196            exclude=[
197                "domainname",
198                # "domain_ns1",
199                # "domain_ns2",
200            ],
201            exclude_none=True,
202            exclude_unset=True,
203        )
204        payload = {
205            k: v for k, v in payload.items() if not k.lower().startswith("owner")
206        }
207        payload = {k: v for k, v in payload.items() if not k.lower().endswith("fax")}
208        return payload
model_config = {'populate_by_name': True, 'extra': 'ignore', 'validate_assignment': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

CLOUDFLOORDNS_NAMESERVERS = ('dns1.name-s.net.', 'dns2.name-s.net.', 'dns0.mtgsy.com.', 'dns3.mtgsy.com.', 'dns4.mtgsy.com.', 'ns1.g02.cfdns.net.', 'ns1.g02.cfdns.net.', 'ns2.g02.cfdns.biz.', 'ns3.g02.cfdns.info.', 'ns4.g02.cfdns.co.uk.')
CLOUDFLOORDNS_NAMESERVERS_DOMAINS = ('name-s.net.', 'mtgsy.com.', 'cfdns.net.', 'cfdns.net.', 'cfdns.biz.', 'cfdns.info.', 'cfdns.co.uk.')
def is_cloudlfoordns_ns(ns):
242def is_cloudlfoordns_ns(ns):
243    # Ensure the nameserver ends with a single dot.
244    ns = ns.strip().rstrip(".").lower() + "."
245    return ns.endswith(CLOUDFLOORDNS_NAMESERVERS_DOMAINS)
class Domain(pydantic.main.BaseModel):
248class Domain(BaseModel):
249    """
250    Pydantic model
251    """
252
253    model_config = ConfigDict(
254        populate_by_name=True,
255        extra="allow",
256        # https://docs.pydantic.dev/latest/concepts/pydantic_settings/#case-sensitivity
257        # case_sensitive = True
258        # https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.validate_assignment
259        validate_assignment=True,
260    )
261
262    domainname: str = Field(validation_alias=AliasChoices("domainname", "domain"))
263
264    id: Optional[str] = None
265    epp: Optional[str] = Field(default=None, alias="EPP")
266    organisation: Optional[str] = None
267
268    # Owner informations
269    ownerfirstname: Optional[str] = Field(default=None, alias="OwnerFirstName")
270    ownerlastname: Optional[str] = Field(default=None, alias="OwnerLastName")
271    ownercompanyname: Optional[str] = Field(default=None, alias="OwnerOrganization")
272    ownerstreetaddress: Optional[str] = Field(default=None, alias="OwnerAddress")
273    ownercity: Optional[str] = Field(default=None, alias="OwnerCity")
274    ownerstate: Optional[str] = Field(default=None, alias="OwnerState")
275    ownerpostalcode: Optional[str] = Field(default=None, alias="OwnerPostalCode")
276    ownercountry: Optional[str] = Field(default=None, alias="OwnerCountry")
277    ownerphone: Optional[str] = Field(default=None, alias="OwnerPhone")
278    ownerfax: Optional[str] = Field(default=None, alias="OwnerFax")
279    owneremail: Optional[str] = Field(default=None, alias="OwnerEmail")
280
281    # Admin informations
282    adminfirstname: Optional[str] = Field(default=None, alias="AdminFirstName")
283    adminlastname: Optional[str] = Field(default=None, alias="AdminLastName")
284    admincompanyname: Optional[str] = Field(default=None, alias="AdminOrganization")
285    adminstreetaddress: Optional[str] = Field(default=None, alias="AdminAddress")
286    admincity: Optional[str] = Field(default=None, alias="AdminCity")
287    adminstate: Optional[str] = Field(default=None, alias="AdminState")
288    adminpostalcode: Optional[str] = Field(default=None, alias="AdminPostalCode")
289    admincountry: Optional[str] = Field(default=None, alias="AdminCountry")
290    adminphone: Optional[str] = Field(default=None, alias="AdminPhone")
291    adminfax: Optional[str] = Field(default=None, alias="AdminFax")
292    adminemail: Optional[str] = Field(default=None, alias="AdminEmail")
293
294    # Billing Contact informations
295    billfirstname: Optional[str] = Field(default=None, alias="BillFirstName")
296    billlastname: Optional[str] = Field(default=None, alias="BillLastName")
297    billcompanyname: Optional[str] = Field(default=None, alias="BillOrganization")
298    billstreetaddress: Optional[str] = Field(default=None, alias="BillAddress")
299    billcity: Optional[str] = Field(default=None, alias="BillCity")
300    # There is a typo in the returned value.
301    billstate: Optional[str] = Field(
302        default=None,
303        alias="BillState",
304        validation_alias=AliasChoices("billState", "BillState"),
305    )
306    billpostalcode: Optional[str] = Field(default=None, alias="BillPostalCode")
307    billcountry: Optional[str] = Field(default=None, alias="BillCountry")
308    billphone: Optional[str] = Field(default=None, alias="BillPhone")
309    billfax: Optional[str] = Field(default=None, alias="BillFax")
310    billemail: Optional[str] = Field(default=None, alias="BillEmail")
311
312    # Technical Contact informations
313    techfirstname: Optional[str] = Field(default=None, alias="TechFirstName")
314    techlastname: Optional[str] = Field(default=None, alias="TechLastName")
315    techcompanyname: Optional[str] = Field(default=None, alias="TechOrganization")
316    techstreetaddress: Optional[str] = Field(default=None, alias="TechAddress")
317    techcity: Optional[str] = Field(default=None, alias="TechCity")
318    techstate: Optional[str] = Field(default=None, alias="TechState")
319    techcountry: Optional[str] = Field(default=None, alias="TechCountry")
320    techpostalcode: Optional[str] = Field(default=None, alias="TechPostalCode")
321    techphone: Optional[str] = Field(default=None, alias="TechPhone")
322    techfax: Optional[str] = Field(default=None, alias="TechFax")
323    techemail: Optional[str] = Field(default=None, alias="TechEmail")
324
325    # Other informations
326    auto_renew: Optional[str] = None
327    reg_opt_out: Optional[str] = None
328    username: Optional[str] = None
329    status: Optional[str] = None
330    use_trustee: Optional[str] = None
331    locked: Optional[str] = None
332    editzone: Optional[str] = None
333    expires: Optional[datetime.date] = None
334    deleteonexpiry: Optional[str] = None
335    companyregno: Optional[str] = None
336    client_delete_prohibited_lock: Optional[str] = None
337    client_update_prohibited_lock: Optional[str] = None
338    client_transfer_prohibited_lock: Optional[str] = None
339    registeredhere: Optional[str] = None
340    nameserver: List[str] = Field(default_factory=list)
341    domain_description: Optional[DomainDescription] = None
342
343    @property
344    def is_externally_managed(self):
345        is_cfdns_ns = [is_cloudlfoordns_ns(ns) for ns in self.nameserver]
346        if all(is_cfdns_ns):
347            return False
348        if not any(is_cfdns_ns):
349            return True
350        # Part of the nameserver are owned by CFDns, the rest is not
351        # => The nameserver configuration is wrong
352        ns_txt = ", ".join(self.nameserver)
353        logging.warning(
354            f"Domain {self.domainname} has inconsistant nameservers: {ns_txt}"
355        )
356        return False
357
358    @field_validator("locked", mode="before")
359    @classmethod
360    def ensure_locked_as_string(cls, v: Any):
361        if not isinstance(v, str):
362            return str(v)
363        return v
364
365    # @model_validator(mode='before')
366    # @classmethod
367    # def check_card_number_omitted(cls, data: Any) -> Any:
368    #     if isinstance(data, dict):
369    #         assert (
370    #             'card_number' not in data
371    #         ), 'card_number should not be included'
372    #     return data
373
374    def _setcontact(self, prefix, info: Contact):
375        data = info.dump_as(prefix)
376        for k, v in data.items():
377            setattr(self, k, v)
378        # Domain.model_validate(self)
379        return self
380
381    def _getcontact(self, prefix: str) -> Contact:
382        data = {k.removeprefix(prefix): v for k, v in self.model_dump().items()}
383        return Contact.model_validate(data)
384
385    def set_owner(self, info: Contact):
386        return self._setcontact("owner", info)
387
388    def set_admin(self, info: Contact):
389        return self._setcontact("admin", info)
390
391    def set_bill(self, info: Contact):
392        return self._setcontact("bill", info)
393
394    def set_tech(self, info: Contact):
395        return self._setcontact("tech", info)
396
397    def update_contact(
398        self,
399        owner: Optional[Contact] = None,
400        admin: Optional[Contact] = None,
401        tech: Optional[Contact] = None,
402        bill: Optional[Contact] = None,
403        timeout=None,
404    ):
405        converted = (
406            d
407            for d in (
408                owner and owner.as_owner(),
409                admin and admin.as_admin(),
410                tech and tech.as_tech(),
411                bill and bill.as_bill(),
412            )
413            if d
414        )
415        data = dict(ChainMap(*converted))
416        for k, v in data.items():
417            setattr(self, k, v)
418        # Domain.model_validate(self)
419        return self
420
421    def register_payload(self, use_default_ns: bool = True) -> DomainPayload:
422        data = self.model_dump(by_alias=True)
423        data.update(
424            {
425                # "groups_ids": ...,
426                "assign_default_groups_nameserver": 1 if use_default_ns else 0,
427            }
428        )
429
430        return DomainPayload.model_validate(data)
431
432    def dump_for_update(self):
433        return self.model_dump(
434            by_alias=True,
435            exclude=[
436                "epp",
437                "status",
438                "use_trustee",
439                "locked",
440                "domain_description",
441            ],
442        )

Pydantic model

model_config = {'populate_by_name': True, 'extra': 'allow', 'validate_assignment': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

domainname: str
id: Optional[str]
epp: Optional[str]
organisation: Optional[str]
ownerfirstname: Optional[str]
ownerlastname: Optional[str]
ownercompanyname: Optional[str]
ownerstreetaddress: Optional[str]
ownercity: Optional[str]
ownerstate: Optional[str]
ownerpostalcode: Optional[str]
ownercountry: Optional[str]
ownerphone: Optional[str]
ownerfax: Optional[str]
owneremail: Optional[str]
adminfirstname: Optional[str]
adminlastname: Optional[str]
admincompanyname: Optional[str]
adminstreetaddress: Optional[str]
admincity: Optional[str]
adminstate: Optional[str]
adminpostalcode: Optional[str]
admincountry: Optional[str]
adminphone: Optional[str]
adminfax: Optional[str]
adminemail: Optional[str]
billfirstname: Optional[str]
billlastname: Optional[str]
billcompanyname: Optional[str]
billstreetaddress: Optional[str]
billcity: Optional[str]
billstate: Optional[str]
billpostalcode: Optional[str]
billcountry: Optional[str]
billphone: Optional[str]
billfax: Optional[str]
billemail: Optional[str]
techfirstname: Optional[str]
techlastname: Optional[str]
techcompanyname: Optional[str]
techstreetaddress: Optional[str]
techcity: Optional[str]
techstate: Optional[str]
techcountry: Optional[str]
techpostalcode: Optional[str]
techphone: Optional[str]
techfax: Optional[str]
techemail: Optional[str]
auto_renew: Optional[str]
reg_opt_out: Optional[str]
username: Optional[str]
status: Optional[str]
use_trustee: Optional[str]
locked: Optional[str]
editzone: Optional[str]
expires: Optional[datetime.date]
deleteonexpiry: Optional[str]
companyregno: Optional[str]
client_delete_prohibited_lock: Optional[str]
client_update_prohibited_lock: Optional[str]
client_transfer_prohibited_lock: Optional[str]
registeredhere: Optional[str]
nameserver: List[str]
domain_description: Optional[DomainDescription]
is_externally_managed
343    @property
344    def is_externally_managed(self):
345        is_cfdns_ns = [is_cloudlfoordns_ns(ns) for ns in self.nameserver]
346        if all(is_cfdns_ns):
347            return False
348        if not any(is_cfdns_ns):
349            return True
350        # Part of the nameserver are owned by CFDns, the rest is not
351        # => The nameserver configuration is wrong
352        ns_txt = ", ".join(self.nameserver)
353        logging.warning(
354            f"Domain {self.domainname} has inconsistant nameservers: {ns_txt}"
355        )
356        return False
@field_validator('locked', mode='before')
@classmethod
def ensure_locked_as_string(cls, v: Any):
358    @field_validator("locked", mode="before")
359    @classmethod
360    def ensure_locked_as_string(cls, v: Any):
361        if not isinstance(v, str):
362            return str(v)
363        return v
def set_owner(self, info: Contact):
385    def set_owner(self, info: Contact):
386        return self._setcontact("owner", info)
def set_admin(self, info: Contact):
388    def set_admin(self, info: Contact):
389        return self._setcontact("admin", info)
def set_bill(self, info: Contact):
391    def set_bill(self, info: Contact):
392        return self._setcontact("bill", info)
def set_tech(self, info: Contact):
394    def set_tech(self, info: Contact):
395        return self._setcontact("tech", info)
def update_contact( self, owner: Optional[Contact] = None, admin: Optional[Contact] = None, tech: Optional[Contact] = None, bill: Optional[Contact] = None, timeout=None):
397    def update_contact(
398        self,
399        owner: Optional[Contact] = None,
400        admin: Optional[Contact] = None,
401        tech: Optional[Contact] = None,
402        bill: Optional[Contact] = None,
403        timeout=None,
404    ):
405        converted = (
406            d
407            for d in (
408                owner and owner.as_owner(),
409                admin and admin.as_admin(),
410                tech and tech.as_tech(),
411                bill and bill.as_bill(),
412            )
413            if d
414        )
415        data = dict(ChainMap(*converted))
416        for k, v in data.items():
417            setattr(self, k, v)
418        # Domain.model_validate(self)
419        return self
def register_payload( self, use_default_ns: bool = True) -> DomainPayload:
421    def register_payload(self, use_default_ns: bool = True) -> DomainPayload:
422        data = self.model_dump(by_alias=True)
423        data.update(
424            {
425                # "groups_ids": ...,
426                "assign_default_groups_nameserver": 1 if use_default_ns else 0,
427            }
428        )
429
430        return DomainPayload.model_validate(data)
def dump_for_update(self):
432    def dump_for_update(self):
433        return self.model_dump(
434            by_alias=True,
435            exclude=[
436                "epp",
437                "status",
438                "use_trustee",
439                "locked",
440                "domain_description",
441            ],
442        )