namecheap-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,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ._base import AsyncClient, Call, SyncClient
6
+ from .models import Balances
7
+ from .resources import DNS, AsyncDNS, AsyncDomains, Domains
8
+
9
+
10
+ class Namecheap(SyncClient):
11
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
12
+ super().__init__(*args, **kwargs)
13
+ self.domains = Domains(self)
14
+ self.dns = DNS(self)
15
+
16
+ def balances(self) -> Balances:
17
+ result: Balances = self._execute(Call(self.settings.command("balances"), parse=Balances.from_xml))
18
+ return result
19
+
20
+
21
+ class AsyncNamecheap(AsyncClient):
22
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
23
+ super().__init__(*args, **kwargs)
24
+ self.domains = AsyncDomains(self)
25
+ self.dns = AsyncDNS(self)
26
+
27
+ async def balances(self) -> Balances:
28
+ result: Balances = await self._execute(Call(self.settings.command("balances"), parse=Balances.from_xml))
29
+ return result
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+ from goodconf import Field, GoodConf, GoodConfConfigDict
6
+ from pydantic import BaseModel, SecretStr
7
+
8
+ from ._version import __version__
9
+ from .errors import AuthenticationError
10
+ from .messages import text
11
+
12
+
13
+ class Commands(BaseModel):
14
+ get_hosts: str = "namecheap.domains.dns.getHosts"
15
+ set_hosts: str = "namecheap.domains.dns.setHosts"
16
+ get_nameservers: str = "namecheap.domains.dns.getList"
17
+ set_custom_nameservers: str = "namecheap.domains.dns.setCustom"
18
+ set_default_nameservers: str = "namecheap.domains.dns.setDefault"
19
+ list_domains: str = "namecheap.domains.getList"
20
+ domain_info: str = "namecheap.domains.getInfo"
21
+ check: str = "namecheap.domains.check"
22
+ tlds: str = "namecheap.domains.getTldList"
23
+ get_lock: str = "namecheap.domains.getRegistrarLock"
24
+ set_lock: str = "namecheap.domains.setRegistrarLock"
25
+ balances: str = "namecheap.users.getBalances"
26
+
27
+
28
+ class Settings(GoodConf):
29
+ model_config = GoodConfConfigDict(
30
+ env_prefix="NAMECHEAP_",
31
+ file_env_var="NAMECHEAP_CONFIG",
32
+ default_files=["namecheap.toml", "namecheap.yaml", "namecheap.yml", "namecheap.json"],
33
+ extra="ignore",
34
+ env_nested_delimiter="__",
35
+ )
36
+
37
+ api_user: str = Field(default="", description=text("settings.api_user"))
38
+ api_key: SecretStr = Field(default=SecretStr(""), description=text("settings.api_key"))
39
+ username: Optional[str] = Field(default=None, description=text("settings.username"))
40
+ client_ip: str = Field(default="", description=text("settings.client_ip"))
41
+ environment: str = Field(default="production", description=text("settings.environment"))
42
+ base_url: Optional[str] = Field(default=None, description=text("settings.base_url"))
43
+ production_url: str = Field(default="https://api.namecheap.com/xml.response",
44
+ description=text("settings.production_url"))
45
+ sandbox_url: str = Field(default="https://api.sandbox.namecheap.com/xml.response",
46
+ description=text("settings.sandbox_url"))
47
+ commands: Commands = Field(default_factory=Commands, description=text("settings.commands"))
48
+ timeout: float = Field(default=30.0, gt=0, description=text("settings.timeout"))
49
+ max_retries: int = Field(default=3, ge=0, description=text("settings.max_retries"))
50
+ max_retry_wait: float = Field(default=60.0, ge=0, description=text("settings.max_retry_wait"))
51
+ backoff_base: float = Field(default=1.0, ge=0, description=text("settings.backoff_base"))
52
+ backoff_cap: float = Field(default=16.0, ge=0, description=text("settings.backoff_cap"))
53
+ page_size: int = Field(default=100, ge=10, le=100, description=text("settings.page_size"))
54
+ default_ttl: int = Field(default=1800, ge=60, le=60000, description=text("settings.default_ttl"))
55
+ user_agent: str = Field(default="namecheap-python-sdk/{version}", description=text("settings.user_agent"))
56
+
57
+ @property
58
+ def environments(self) -> dict[str, str]:
59
+ return {"production": self.production_url, "prod": self.production_url,
60
+ "sandbox": self.sandbox_url, "test": self.sandbox_url}
61
+
62
+ def resolved_base_url(self) -> str:
63
+ if self.base_url:
64
+ return self.base_url
65
+ env = self.environment.strip().lower()
66
+ if env not in self.environments:
67
+ raise ValueError(text("error.unknown_environment", environment=env, choices="production, sandbox"))
68
+ return self.environments[env]
69
+
70
+ def command(self, name: str) -> str:
71
+ return str(getattr(self.commands, name))
72
+
73
+ def resolved_user_agent(self) -> str:
74
+ return self.user_agent.replace("{version}", __version__)
75
+
76
+ def require_credentials(self) -> dict[str, str]:
77
+ user = self.api_user.strip()
78
+ key = self.api_key.get_secret_value().strip()
79
+ ip = self.client_ip.strip()
80
+ if not user or not key or not ip:
81
+ raise AuthenticationError(text("error.missing_credentials"))
82
+ return {"ApiUser": user, "ApiKey": key, "UserName": (self.username or user).strip(), "ClientIp": ip}
83
+
84
+
85
+ Settings.__doc__ = text("settings.description")
86
+
87
+
88
+ def load_settings(config_file: Optional[str] = None, **overrides: Any) -> Settings:
89
+ given = {key: value for key, value in overrides.items() if value is not None}
90
+ return Settings(load=True, config_file=config_file, **given)
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Optional
5
+
6
+ import httpx
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class APIErrorDetail:
11
+ number: str
12
+ message: str
13
+
14
+
15
+ class NamecheapError(Exception):
16
+ def __init__(
17
+ self,
18
+ message: str,
19
+ *,
20
+ status_code: Optional[int] = None,
21
+ number: Optional[str] = None,
22
+ errors: Optional[list[APIErrorDetail]] = None,
23
+ command: Optional[str] = None,
24
+ response: Optional[httpx.Response] = None,
25
+ ) -> None:
26
+ super().__init__(message)
27
+ self.message = message
28
+ self.status_code = status_code
29
+ self.number = number
30
+ self.errors = errors or []
31
+ self.command = command
32
+ self.response = response
33
+
34
+ def __str__(self) -> str:
35
+ parts = []
36
+ if self.status_code and self.status_code != 200:
37
+ parts.append(f"[{self.status_code}]")
38
+ if self.number:
39
+ parts.append(self.number)
40
+ parts.append(self.message)
41
+ if self.command:
42
+ parts.append(f"({self.command})")
43
+ return " ".join(parts)
44
+
45
+
46
+ class APIConnectionError(NamecheapError):
47
+ pass
48
+
49
+
50
+ class APITimeoutError(APIConnectionError):
51
+ pass
52
+
53
+
54
+ class APIError(NamecheapError):
55
+ pass
56
+
57
+
58
+ class ValidationError(APIError):
59
+ pass
60
+
61
+
62
+ class AuthenticationError(APIError):
63
+ pass
64
+
65
+
66
+ class PermissionDeniedError(APIError):
67
+ pass
68
+
69
+
70
+ class NotFoundError(APIError):
71
+ pass
72
+
73
+
74
+ class RateLimitError(APIError):
75
+ def __init__(self, message: str, *, retry_after: Optional[float] = None, **kwargs: Any) -> None:
76
+ super().__init__(message, **kwargs)
77
+ self.retry_after = retry_after
78
+
79
+
80
+ class ServerError(APIError):
81
+ pass
82
+
83
+
84
+ AUTH_NUMBERS = frozenset({
85
+ "1010101", "1010102", "1011102", "1010104", "1010105", "1011105", "1030408", "1050900", "1011150",
86
+ "1017150", "1017105", "1017101", "1017410", "1017411", "1019103", "1016103", "1017103",
87
+ })
88
+ NOT_FOUND_NUMBERS = frozenset({"2019166", "2016166"})
89
+ PERMISSION_NUMBERS = frozenset({"2030166", "4011103"})
90
+ VALIDATION_NUMBERS = frozenset({"3013288", "4013288", "2015278", "3011288"})
91
+ SERVER_NUMBERS = frozenset({"3031510", "3050900", "5050900", "5019169", "5050169", "4022288", "4023330"})
92
+ STATUS_MAP: dict[int, type[APIError]] = {401: AuthenticationError, 403: PermissionDeniedError, 404: NotFoundError}
93
+
94
+
95
+ def error_class(number: str, message: str) -> type[APIError]:
96
+ lowered = message.lower()
97
+ if "too many requests" in lowered or "rate limit" in lowered:
98
+ return RateLimitError
99
+ if number in AUTH_NUMBERS:
100
+ return AuthenticationError
101
+ if number in NOT_FOUND_NUMBERS:
102
+ return NotFoundError
103
+ if number in PERMISSION_NUMBERS:
104
+ return PermissionDeniedError
105
+ if number in VALIDATION_NUMBERS:
106
+ return ValidationError
107
+ if number in SERVER_NUMBERS or number.startswith("5"):
108
+ return ServerError
109
+ if number.startswith("2"):
110
+ return ValidationError
111
+ return APIError
112
+
113
+
114
+ def from_details(details: list[APIErrorDetail], command: str, response: Optional[httpx.Response] = None) -> APIError:
115
+ first = details[0] if details else APIErrorDetail("", "")
116
+ cls = error_class(first.number, first.message)
117
+ message = "; ".join(d.message for d in details if d.message) or first.message
118
+ return cls(message, number=first.number or None, errors=details, command=command,
119
+ status_code=response.status_code if response is not None else None, response=response)
120
+
121
+
122
+ def from_status(response: httpx.Response, command: str) -> APIError:
123
+ status = response.status_code
124
+ message = response.reason_phrase or response.text.strip()[:200]
125
+ if status == 429:
126
+ header = response.headers.get("Retry-After")
127
+ try:
128
+ wait = float(header) if header else None
129
+ except ValueError:
130
+ wait = None
131
+ return RateLimitError(message, retry_after=wait, status_code=status, command=command, response=response)
132
+ cls = ServerError if status >= 500 else STATUS_MAP.get(status, APIError)
133
+ return cls(message, status_code=status, command=command, response=response)
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from importlib import resources
5
+ from pathlib import Path
6
+ from typing import Any, Union
7
+
8
+ import yaml
9
+
10
+ PathLike = Union[str, Path]
11
+
12
+ _PACKAGE_FILE = "messages.yaml"
13
+ _catalog: dict[str, str] = {}
14
+
15
+
16
+ def _flatten(node: Mapping[str, Any], prefix: str = "") -> dict[str, str]:
17
+ flat: dict[str, str] = {}
18
+ for key, value in node.items():
19
+ path = f"{prefix}{key}"
20
+ if isinstance(value, Mapping):
21
+ flat.update(_flatten(value, f"{path}."))
22
+ else:
23
+ flat[path] = str(value)
24
+ return flat
25
+
26
+
27
+ def _parse(raw: str) -> dict[str, str]:
28
+ data = yaml.safe_load(raw) or {}
29
+ if not isinstance(data, Mapping):
30
+ raise ValueError("messages file must contain a mapping")
31
+ return _flatten(data)
32
+
33
+
34
+ def _read_packaged() -> dict[str, str]:
35
+ raw = resources.files(__package__).joinpath(_PACKAGE_FILE).read_text(encoding="utf-8")
36
+ return _parse(raw)
37
+
38
+
39
+ def catalog() -> Mapping[str, str]:
40
+ if not _catalog:
41
+ _catalog.update(_read_packaged())
42
+ return _catalog
43
+
44
+
45
+ def use(source: PathLike | Mapping[str, Any]) -> None:
46
+ if isinstance(source, Mapping):
47
+ overrides = _flatten(source)
48
+ else:
49
+ overrides = _parse(Path(source).read_text(encoding="utf-8"))
50
+ unknown = set(overrides) - set(_read_packaged())
51
+ if unknown:
52
+ raise KeyError(", ".join(sorted(unknown)))
53
+ catalog()
54
+ _catalog.update(overrides)
55
+
56
+
57
+ def reset() -> None:
58
+ _catalog.clear()
59
+
60
+
61
+ def text(key: str, **params: Any) -> str:
62
+ template = catalog()[key]
63
+ return template.format(**params) if params else template
@@ -0,0 +1,39 @@
1
+ settings:
2
+ description: Settings for the Namecheap API client.
3
+ api_user: Account name that owns the API key.
4
+ api_key: API key from Profile, Tools, Namecheap API Access.
5
+ username: Account the commands run on. Defaults to api_user.
6
+ client_ip: Whitelisted IPv4 address of the machine that makes the calls.
7
+ environment: 'Which Namecheap environment to call: production or sandbox.'
8
+ base_url: Full API URL. Overrides the environment when set.
9
+ production_url: API URL used for the production environment.
10
+ sandbox_url: API URL used for the sandbox environment.
11
+ commands: Command names sent for every call.
12
+ timeout: Seconds to wait for a single request.
13
+ max_retries: How many times a failed request is tried again.
14
+ max_retry_wait: Longest wait in seconds the client accepts between retries.
15
+ backoff_base: First backoff delay in seconds, doubled on every retry.
16
+ backoff_cap: Longest backoff delay in seconds.
17
+ page_size: Domains asked for on every page when listing, between 10 and 100.
18
+ default_ttl: TTL in seconds used for new records when none is given.
19
+ user_agent: User-Agent header. {version} is replaced with the package version.
20
+ error:
21
+ request_failed: Request failed
22
+ missing_credentials: Missing credentials. Pass api_user, api_key and client_ip, set NAMECHEAP_API_USER,
23
+ NAMECHEAP_API_KEY and NAMECHEAP_CLIENT_IP, or point NAMECHEAP_CONFIG at a config file.
24
+ unknown_environment: 'Unknown environment {environment!r}. Use one of: {choices}.'
25
+ timeout: 'Request timed out: {error}'
26
+ connection: 'Connection failed: {error}'
27
+ not_saved: Namecheap did not confirm the change for {domain}
28
+ bad_xml: 'Namecheap answered with something that is not valid XML: {error}'
29
+ value:
30
+ empty_domain: domain must not be empty
31
+ no_tld: '{domain!r} has no TLD, pass a full domain like example.com'
32
+ empty_name: record name must not be empty, use '@' for the apex
33
+ empty_type: record type must not be empty
34
+ no_values: at least one record value is required
35
+ add_needs_records: add() needs at least one DNSRecord
36
+ foreign_name: '{name!r} is not inside {domain}'
37
+ ttl_range: TTL must be between 60 and 60000 seconds, got {ttl}
38
+ no_domains: available_many() needs at least one domain
39
+ nameserver_count: a domain needs at least 2 name servers, got {count}
@@ -0,0 +1,336 @@
1
+ from __future__ import annotations
2
+
3
+ import ipaddress
4
+ import xml.etree.ElementTree as ET
5
+ from dataclasses import dataclass, field
6
+ from datetime import date, datetime
7
+ from decimal import Decimal, InvalidOperation
8
+ from enum import Enum
9
+ from typing import Any, Optional, Union
10
+
11
+ from .messages import text
12
+
13
+
14
+ class RecordType(str, Enum):
15
+ A = "A"
16
+ AAAA = "AAAA"
17
+ ALIAS = "ALIAS"
18
+ CAA = "CAA"
19
+ CNAME = "CNAME"
20
+ MX = "MX"
21
+ MXE = "MXE"
22
+ NS = "NS"
23
+ TXT = "TXT"
24
+ URL = "URL"
25
+ URL301 = "URL301"
26
+ FRAME = "FRAME"
27
+
28
+ def __str__(self) -> str:
29
+ return self.value
30
+
31
+
32
+ RecordTypeLike = Union[RecordType, str]
33
+ HOST_TYPES = frozenset({"CNAME", "ALIAS", "NS", "MX"})
34
+
35
+
36
+ def record_type(value: RecordTypeLike) -> str:
37
+ kind = value.value if isinstance(value, RecordType) else str(value).strip().upper()
38
+ if not kind:
39
+ raise ValueError(text("value.empty_type"))
40
+ return kind
41
+
42
+
43
+ def hostname(value: str) -> str:
44
+ host = value.strip()
45
+ if host == "@" or host.endswith(".") or "." not in host:
46
+ return host
47
+ return f"{host}."
48
+
49
+
50
+ def flag(value: Any) -> bool:
51
+ return str(value).strip().lower() == "true"
52
+
53
+
54
+ def number(value: Any) -> Optional[int]:
55
+ try:
56
+ return int(str(value).strip())
57
+ except (TypeError, ValueError):
58
+ return None
59
+
60
+
61
+ def money(value: Any) -> Optional[Decimal]:
62
+ if value in (None, ""):
63
+ return None
64
+ try:
65
+ return Decimal(str(value))
66
+ except InvalidOperation:
67
+ return None
68
+
69
+
70
+ def day(value: Any) -> Optional[date]:
71
+ if not value:
72
+ return None
73
+ try:
74
+ return datetime.strptime(str(value).strip(), "%m/%d/%Y").date()
75
+ except ValueError:
76
+ return None
77
+
78
+
79
+ def child_text(element: Optional[ET.Element], path: str) -> Optional[str]:
80
+ if element is None:
81
+ return None
82
+ found = element.find(path)
83
+ return found.text.strip() if found is not None and found.text else None
84
+
85
+
86
+ @dataclass
87
+ class DNSRecord:
88
+ type: str
89
+ name: str
90
+ data: str
91
+ ttl: Optional[int] = None
92
+ priority: Optional[int] = None
93
+ id: Optional[int] = field(default=None, compare=False)
94
+ active: bool = field(default=True, compare=False)
95
+
96
+ def __post_init__(self) -> None:
97
+ self.type = record_type(self.type)
98
+ self.name = self.name.strip() or "@"
99
+
100
+ @classmethod
101
+ def a(cls, name: str, ip: str, ttl: Optional[int] = None) -> DNSRecord:
102
+ return cls("A", name, str(ipaddress.IPv4Address(ip)), ttl)
103
+
104
+ @classmethod
105
+ def aaaa(cls, name: str, ip: str, ttl: Optional[int] = None) -> DNSRecord:
106
+ return cls("AAAA", name, str(ipaddress.IPv6Address(ip)), ttl)
107
+
108
+ @classmethod
109
+ def cname(cls, name: str, target: str, ttl: Optional[int] = None) -> DNSRecord:
110
+ return cls("CNAME", name, hostname(target), ttl)
111
+
112
+ @classmethod
113
+ def alias(cls, name: str, target: str, ttl: Optional[int] = None) -> DNSRecord:
114
+ return cls("ALIAS", name, hostname(target), ttl)
115
+
116
+ @classmethod
117
+ def txt(cls, name: str, value: str, ttl: Optional[int] = None) -> DNSRecord:
118
+ return cls("TXT", name, value, ttl)
119
+
120
+ @classmethod
121
+ def mx(cls, name: str, host: str, priority: int = 10, ttl: Optional[int] = None) -> DNSRecord:
122
+ return cls("MX", name, hostname(host), ttl, priority=priority)
123
+
124
+ @classmethod
125
+ def ns(cls, name: str, host: str, ttl: Optional[int] = None) -> DNSRecord:
126
+ return cls("NS", name, hostname(host), ttl)
127
+
128
+ @classmethod
129
+ def caa(cls, name: str, value: str, tag: str = "issue", flags: int = 0, ttl: Optional[int] = None) -> DNSRecord:
130
+ return cls("CAA", name, f'{flags} {tag} "{value}"', ttl)
131
+
132
+ @classmethod
133
+ def url(cls, name: str, target: str, *, permanent: bool = True, ttl: Optional[int] = None) -> DNSRecord:
134
+ return cls("URL301" if permanent else "URL", name, target, ttl)
135
+
136
+ @classmethod
137
+ def frame(cls, name: str, target: str, ttl: Optional[int] = None) -> DNSRecord:
138
+ return cls("FRAME", name, target, ttl)
139
+
140
+ @classmethod
141
+ def from_xml(cls, element: ET.Element) -> DNSRecord:
142
+ kind = element.get("Type", "")
143
+ return cls(
144
+ type=kind,
145
+ name=element.get("Name", "@"),
146
+ data=element.get("Address", ""),
147
+ ttl=number(element.get("TTL")),
148
+ priority=number(element.get("MXPref")) if kind.upper() in ("MX", "MXE") else None,
149
+ id=number(element.get("HostId")),
150
+ active=element.get("IsActive", "true").lower() != "false",
151
+ )
152
+
153
+ def fqdn(self, domain: str) -> str:
154
+ return domain if self.name == "@" else f"{self.name}.{domain}"
155
+
156
+
157
+ @dataclass
158
+ class Hosts:
159
+ domain: str
160
+ records: list[DNSRecord] = field(default_factory=list)
161
+ using_our_dns: bool = True
162
+ email_type: Optional[str] = None
163
+
164
+ @classmethod
165
+ def from_xml(cls, element: ET.Element) -> Hosts:
166
+ result = element.find("DomainDNSGetHostsResult")
167
+ node = result if result is not None else element
168
+ email = node.get("EmailType")
169
+ return cls(
170
+ domain=node.get("Domain", ""),
171
+ records=[DNSRecord.from_xml(host) for host in node.iter("host")]
172
+ + [DNSRecord.from_xml(host) for host in node.iter("Host")],
173
+ using_our_dns=flag(node.get("IsUsingOurDNS", "true")),
174
+ email_type=email.upper() if email else None,
175
+ )
176
+
177
+
178
+ @dataclass
179
+ class Domain:
180
+ name: str
181
+ id: Optional[int] = None
182
+ user: str = ""
183
+ created: Optional[date] = None
184
+ expires: Optional[date] = None
185
+ is_expired: bool = False
186
+ is_locked: bool = False
187
+ auto_renew: bool = False
188
+ whois_guard: str = ""
189
+ is_premium: bool = False
190
+ is_our_dns: bool = False
191
+
192
+ @classmethod
193
+ def from_xml(cls, element: ET.Element) -> Domain:
194
+ return cls(
195
+ name=element.get("Name", ""),
196
+ id=number(element.get("ID")),
197
+ user=element.get("User", ""),
198
+ created=day(element.get("Created")),
199
+ expires=day(element.get("Expires")),
200
+ is_expired=flag(element.get("IsExpired")),
201
+ is_locked=flag(element.get("IsLocked")),
202
+ auto_renew=flag(element.get("AutoRenew")),
203
+ whois_guard=element.get("WhoisGuard", ""),
204
+ is_premium=flag(element.get("IsPremium")),
205
+ is_our_dns=flag(element.get("IsOurDNS")),
206
+ )
207
+
208
+
209
+ @dataclass
210
+ class DomainInfo:
211
+ name: str
212
+ id: Optional[int] = None
213
+ status: str = ""
214
+ owner: str = ""
215
+ is_owner: bool = False
216
+ is_premium: bool = False
217
+ created: Optional[date] = None
218
+ expires: Optional[date] = None
219
+ whois_guard: bool = False
220
+ dns_provider: str = ""
221
+
222
+ @classmethod
223
+ def from_xml(cls, element: ET.Element) -> DomainInfo:
224
+ result = element.find("DomainGetInfoResult")
225
+ node = result if result is not None else element
226
+ guard = node.find("Whoisguard")
227
+ dns = node.find("DnsDetails")
228
+ return cls(
229
+ name=node.get("DomainName", ""),
230
+ id=number(node.get("ID")),
231
+ status=node.get("Status", ""),
232
+ owner=node.get("OwnerName", ""),
233
+ is_owner=flag(node.get("IsOwner")),
234
+ is_premium=flag(node.get("IsPremium")),
235
+ created=day(child_text(node, "DomainDetails/CreatedDate")),
236
+ expires=day(child_text(node, "DomainDetails/ExpiredDate")),
237
+ whois_guard=flag(guard.get("Enabled")) if guard is not None else False,
238
+ dns_provider=dns.get("ProviderType", "") if dns is not None else "",
239
+ )
240
+
241
+
242
+ @dataclass
243
+ class Availability:
244
+ domain: str
245
+ available: bool
246
+ premium: bool = False
247
+ registration_price: Optional[Decimal] = None
248
+ renewal_price: Optional[Decimal] = None
249
+ restore_price: Optional[Decimal] = None
250
+ transfer_price: Optional[Decimal] = None
251
+ icann_fee: Optional[Decimal] = None
252
+ eap_fee: Optional[Decimal] = None
253
+ description: str = ""
254
+
255
+ @classmethod
256
+ def from_xml(cls, element: ET.Element) -> Availability:
257
+ premium = flag(element.get("IsPremiumName"))
258
+ return cls(
259
+ domain=element.get("Domain", ""),
260
+ available=flag(element.get("Available")),
261
+ premium=premium,
262
+ registration_price=money(element.get("PremiumRegistrationPrice")) if premium else None,
263
+ renewal_price=money(element.get("PremiumRenewalPrice")) if premium else None,
264
+ restore_price=money(element.get("PremiumRestorePrice")) if premium else None,
265
+ transfer_price=money(element.get("PremiumTransferPrice")) if premium else None,
266
+ icann_fee=money(element.get("IcannFee")),
267
+ eap_fee=money(element.get("EapFee")),
268
+ description=element.get("Description", ""),
269
+ )
270
+
271
+
272
+ @dataclass
273
+ class Tld:
274
+ name: str
275
+ description: str = ""
276
+ type: str = ""
277
+ min_years: Optional[int] = None
278
+ max_years: Optional[int] = None
279
+ api_registerable: bool = False
280
+ api_renewable: bool = False
281
+ api_transferable: bool = False
282
+ supports_idn: bool = False
283
+
284
+ @classmethod
285
+ def from_xml(cls, element: ET.Element) -> Tld:
286
+ return cls(
287
+ name=element.get("Name", ""),
288
+ description=(element.text or "").strip(),
289
+ type=element.get("Type", ""),
290
+ min_years=number(element.get("MinRegisterYears")),
291
+ max_years=number(element.get("MaxRegisterYears")),
292
+ api_registerable=flag(element.get("IsApiRegisterable")),
293
+ api_renewable=flag(element.get("IsApiRenewable")),
294
+ api_transferable=flag(element.get("IsApiTransferable")),
295
+ supports_idn=flag(element.get("IsSupportsIDN")),
296
+ )
297
+
298
+
299
+ @dataclass
300
+ class Nameservers:
301
+ domain: str
302
+ servers: list[str] = field(default_factory=list)
303
+ using_our_dns: bool = True
304
+
305
+ @classmethod
306
+ def from_xml(cls, element: ET.Element) -> Nameservers:
307
+ result = element.find("DomainDNSGetListResult")
308
+ node = result if result is not None else element
309
+ return cls(
310
+ domain=node.get("Domain", ""),
311
+ servers=[(ns.text or "").strip() for ns in node.iter("Nameserver") if ns.text],
312
+ using_our_dns=flag(node.get("IsUsingOurDNS", "true")),
313
+ )
314
+
315
+
316
+ @dataclass
317
+ class Balances:
318
+ currency: str
319
+ available: Optional[Decimal] = None
320
+ account: Optional[Decimal] = None
321
+ earned: Optional[Decimal] = None
322
+ withdrawable: Optional[Decimal] = None
323
+ required_for_auto_renew: Optional[Decimal] = None
324
+
325
+ @classmethod
326
+ def from_xml(cls, element: ET.Element) -> Balances:
327
+ result = element.find("UserGetBalancesResult")
328
+ node = result if result is not None else element
329
+ return cls(
330
+ currency=node.get("Currency", ""),
331
+ available=money(node.get("AvailableBalance")),
332
+ account=money(node.get("AccountBalance")),
333
+ earned=money(node.get("EarnedAmount")),
334
+ withdrawable=money(node.get("WithdrawableAmount")),
335
+ required_for_auto_renew=money(node.get("FundsRequiredForAutoRenew")),
336
+ )
namecheap_sdk/py.typed ADDED
File without changes