namecheap-python 1.2.0__py3-none-any.whl → 1.4.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.
- namecheap/__init__.py +7 -1
- namecheap/_api/dns.py +38 -0
- namecheap/_api/domains.py +82 -1
- namecheap/_api/whoisguard.py +195 -0
- namecheap/client.py +8 -0
- namecheap/models.py +87 -0
- namecheap_cli/__main__.py +384 -0
- {namecheap_python-1.2.0.dist-info → namecheap_python-1.4.0.dist-info}/METADATA +65 -7
- {namecheap_python-1.2.0.dist-info → namecheap_python-1.4.0.dist-info}/RECORD +12 -11
- {namecheap_python-1.2.0.dist-info → namecheap_python-1.4.0.dist-info}/WHEEL +0 -0
- {namecheap_python-1.2.0.dist-info → namecheap_python-1.4.0.dist-info}/entry_points.txt +0 -0
- {namecheap_python-1.2.0.dist-info → namecheap_python-1.4.0.dist-info}/licenses/LICENSE +0 -0
namecheap/__init__.py
CHANGED
|
@@ -18,12 +18,15 @@ from .models import (
|
|
|
18
18
|
DNSRecord,
|
|
19
19
|
Domain,
|
|
20
20
|
DomainCheck,
|
|
21
|
+
DomainContacts,
|
|
21
22
|
DomainInfo,
|
|
22
23
|
EmailForward,
|
|
23
24
|
Nameservers,
|
|
25
|
+
Tld,
|
|
26
|
+
WhoisguardEntry,
|
|
24
27
|
)
|
|
25
28
|
|
|
26
|
-
__version__ = "1.
|
|
29
|
+
__version__ = "1.4.0"
|
|
27
30
|
__all__ = [
|
|
28
31
|
"AccountBalance",
|
|
29
32
|
"ConfigurationError",
|
|
@@ -31,10 +34,13 @@ __all__ = [
|
|
|
31
34
|
"DNSRecord",
|
|
32
35
|
"Domain",
|
|
33
36
|
"DomainCheck",
|
|
37
|
+
"DomainContacts",
|
|
34
38
|
"DomainInfo",
|
|
35
39
|
"EmailForward",
|
|
36
40
|
"Namecheap",
|
|
37
41
|
"NamecheapError",
|
|
38
42
|
"Nameservers",
|
|
43
|
+
"Tld",
|
|
39
44
|
"ValidationError",
|
|
45
|
+
"WhoisguardEntry",
|
|
40
46
|
]
|
namecheap/_api/dns.py
CHANGED
|
@@ -528,3 +528,41 @@ class DnsAPI(BaseAPI):
|
|
|
528
528
|
EmailForward(mailbox=f.get("@mailbox", ""), forward_to=f.get("#text", ""))
|
|
529
529
|
for f in forwards
|
|
530
530
|
]
|
|
531
|
+
|
|
532
|
+
def set_email_forwarding(
|
|
533
|
+
self, domain: str, rules: list[EmailForward] | list[dict[str, str]]
|
|
534
|
+
) -> bool:
|
|
535
|
+
"""
|
|
536
|
+
Set email forwarding rules for a domain. Replaces all existing rules.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
domain: Domain name
|
|
540
|
+
rules: List of EmailForward or dicts with 'mailbox' and 'forward_to' keys
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
True if successful
|
|
544
|
+
|
|
545
|
+
Examples:
|
|
546
|
+
>>> nc.dns.set_email_forwarding("example.com", [
|
|
547
|
+
... EmailForward(mailbox="info", forward_to="me@gmail.com"),
|
|
548
|
+
... EmailForward(mailbox="support", forward_to="help@gmail.com"),
|
|
549
|
+
... ])
|
|
550
|
+
"""
|
|
551
|
+
assert rules, "At least one forwarding rule is required"
|
|
552
|
+
|
|
553
|
+
params: dict[str, Any] = {"DomainName": domain}
|
|
554
|
+
for i, rule in enumerate(rules, 1):
|
|
555
|
+
if isinstance(rule, EmailForward):
|
|
556
|
+
params[f"MailBox{i}"] = rule.mailbox
|
|
557
|
+
params[f"ForwardTo{i}"] = rule.forward_to
|
|
558
|
+
else:
|
|
559
|
+
params[f"MailBox{i}"] = rule["mailbox"]
|
|
560
|
+
params[f"ForwardTo{i}"] = rule["forward_to"]
|
|
561
|
+
|
|
562
|
+
result: Any = self._request(
|
|
563
|
+
"namecheap.domains.dns.setEmailForwarding",
|
|
564
|
+
params,
|
|
565
|
+
path="DomainDNSSetEmailForwardingResult",
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
return bool(result and result.get("@IsSuccess", "false").lower() == "true")
|
namecheap/_api/domains.py
CHANGED
|
@@ -9,7 +9,14 @@ from typing import Any
|
|
|
9
9
|
import tldextract
|
|
10
10
|
|
|
11
11
|
from namecheap.logging import logger
|
|
12
|
-
from namecheap.models import
|
|
12
|
+
from namecheap.models import (
|
|
13
|
+
Contact,
|
|
14
|
+
Domain,
|
|
15
|
+
DomainCheck,
|
|
16
|
+
DomainContacts,
|
|
17
|
+
DomainInfo,
|
|
18
|
+
Tld,
|
|
19
|
+
)
|
|
13
20
|
|
|
14
21
|
from .base import BaseAPI
|
|
15
22
|
|
|
@@ -156,6 +163,80 @@ class DomainsAPI(BaseAPI):
|
|
|
156
163
|
|
|
157
164
|
return DomainInfo.model_validate(flat)
|
|
158
165
|
|
|
166
|
+
def get_contacts(self, domain: str) -> DomainContacts:
|
|
167
|
+
"""
|
|
168
|
+
Get contact information for a domain.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
domain: Domain name
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
DomainContacts with registrant, tech, admin, and aux_billing contacts
|
|
175
|
+
|
|
176
|
+
Examples:
|
|
177
|
+
>>> contacts = nc.domains.get_contacts("example.com")
|
|
178
|
+
>>> print(contacts.registrant.email)
|
|
179
|
+
"""
|
|
180
|
+
result: Any = self._request(
|
|
181
|
+
"namecheap.domains.getContacts",
|
|
182
|
+
{"DomainName": domain},
|
|
183
|
+
path="DomainContactsResult",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
assert result, f"API returned empty result for {domain} getContacts"
|
|
187
|
+
|
|
188
|
+
def parse_contact(data: dict[str, Any]) -> Contact:
|
|
189
|
+
return Contact.model_validate(
|
|
190
|
+
{
|
|
191
|
+
"FirstName": data.get("FirstName", ""),
|
|
192
|
+
"LastName": data.get("LastName", ""),
|
|
193
|
+
"Organization": data.get("Organization"),
|
|
194
|
+
"Address1": data.get("Address1", ""),
|
|
195
|
+
"Address2": data.get("Address2"),
|
|
196
|
+
"City": data.get("City", ""),
|
|
197
|
+
"StateProvince": data.get("StateProvince", ""),
|
|
198
|
+
"PostalCode": data.get("PostalCode", ""),
|
|
199
|
+
"Country": data.get("Country", ""),
|
|
200
|
+
"Phone": data.get("Phone", ""),
|
|
201
|
+
"EmailAddress": data.get("EmailAddress", ""),
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return DomainContacts(
|
|
206
|
+
registrant=parse_contact(result.get("Registrant", {})),
|
|
207
|
+
tech=parse_contact(result.get("Tech", {})),
|
|
208
|
+
admin=parse_contact(result.get("Admin", {})),
|
|
209
|
+
aux_billing=parse_contact(result.get("AuxBilling", {})),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def get_tld_list(self) -> builtins.list[Tld]:
|
|
213
|
+
"""
|
|
214
|
+
Get list of all TLDs supported by Namecheap.
|
|
215
|
+
|
|
216
|
+
NOTE: Cache this response — it rarely changes and the API docs recommend it.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
List of Tld objects with registration/renewal constraints and capabilities
|
|
220
|
+
|
|
221
|
+
Examples:
|
|
222
|
+
>>> tlds = nc.domains.get_tld_list()
|
|
223
|
+
>>> registerable = [t for t in tlds if t.is_api_registerable]
|
|
224
|
+
>>> print(f"{len(registerable)} TLDs available for API registration")
|
|
225
|
+
"""
|
|
226
|
+
result: Any = self._request(
|
|
227
|
+
"namecheap.domains.getTldList",
|
|
228
|
+
path="Tlds",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
assert result, "API returned empty result for getTldList"
|
|
232
|
+
|
|
233
|
+
tlds = result.get("Tld", [])
|
|
234
|
+
if isinstance(tlds, dict):
|
|
235
|
+
tlds = [tlds]
|
|
236
|
+
assert isinstance(tlds, list), f"Unexpected Tld type: {type(tlds)}"
|
|
237
|
+
|
|
238
|
+
return [Tld.model_validate(t) for t in tlds]
|
|
239
|
+
|
|
159
240
|
def register(
|
|
160
241
|
self,
|
|
161
242
|
domain: str,
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Domain privacy (WhoisGuard) API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
from namecheap.models import WhoisguardEntry
|
|
9
|
+
|
|
10
|
+
from .base import BaseAPI
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WhoisguardAPI(BaseAPI):
|
|
14
|
+
"""Domain privacy (WhoisGuard) management.
|
|
15
|
+
|
|
16
|
+
The Namecheap API uses WhoisGuard IDs internally, but this class
|
|
17
|
+
provides domain-name-based convenience methods that resolve the ID
|
|
18
|
+
automatically via get_list().
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def get_list(
|
|
22
|
+
self,
|
|
23
|
+
*,
|
|
24
|
+
list_type: Literal["ALL", "ALLOTED", "FREE", "DISCARD"] = "ALL",
|
|
25
|
+
page: int = 1,
|
|
26
|
+
page_size: int = 100,
|
|
27
|
+
) -> list[WhoisguardEntry]:
|
|
28
|
+
"""
|
|
29
|
+
Get all WhoisGuard subscriptions.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
list_type: Filter type (ALL, ALLOTED, FREE, DISCARD)
|
|
33
|
+
page: Page number
|
|
34
|
+
page_size: Items per page (2-100)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of WhoisguardEntry subscriptions
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
>>> entries = nc.whoisguard.get_list()
|
|
41
|
+
>>> for e in entries:
|
|
42
|
+
... print(f"{e.domain} (ID={e.id}) status={e.status}")
|
|
43
|
+
"""
|
|
44
|
+
result: Any = self._request(
|
|
45
|
+
"namecheap.whoisguard.getList",
|
|
46
|
+
{
|
|
47
|
+
"ListType": list_type,
|
|
48
|
+
"Page": page,
|
|
49
|
+
"PageSize": min(page_size, 100),
|
|
50
|
+
},
|
|
51
|
+
path="WhoisguardGetListResult",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if not result:
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
entries = result.get("Whoisguard", [])
|
|
58
|
+
if isinstance(entries, dict):
|
|
59
|
+
entries = [entries]
|
|
60
|
+
assert isinstance(entries, list), f"Unexpected Whoisguard type: {type(entries)}"
|
|
61
|
+
|
|
62
|
+
return [WhoisguardEntry.model_validate(e) for e in entries]
|
|
63
|
+
|
|
64
|
+
def _resolve_id(self, domain: str) -> int:
|
|
65
|
+
"""Resolve a domain name to its WhoisGuard ID."""
|
|
66
|
+
entries = self.get_list(list_type="ALLOTED")
|
|
67
|
+
for entry in entries:
|
|
68
|
+
if entry.domain.lower() == domain.lower():
|
|
69
|
+
return entry.id
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"No WhoisGuard subscription found for {domain}. "
|
|
72
|
+
f"Domain must have WhoisGuard allotted to enable/disable it."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def enable(self, domain: str, forwarded_to_email: str) -> bool:
|
|
76
|
+
"""
|
|
77
|
+
Enable domain privacy for a domain.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
domain: Domain name (resolved to WhoisGuard ID automatically)
|
|
81
|
+
forwarded_to_email: Email where privacy-masked emails get forwarded
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if successful
|
|
85
|
+
|
|
86
|
+
Examples:
|
|
87
|
+
>>> nc.whoisguard.enable("example.com", "me@gmail.com")
|
|
88
|
+
"""
|
|
89
|
+
wg_id = self._resolve_id(domain)
|
|
90
|
+
|
|
91
|
+
result: Any = self._request(
|
|
92
|
+
"namecheap.whoisguard.enable",
|
|
93
|
+
{
|
|
94
|
+
"WhoisguardID": wg_id,
|
|
95
|
+
"ForwardedToEmail": forwarded_to_email,
|
|
96
|
+
},
|
|
97
|
+
path="WhoisguardEnableResult",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
assert result, f"API returned empty result for whoisguard.enable on {domain}"
|
|
101
|
+
return result.get("@IsSuccess", "false").lower() == "true"
|
|
102
|
+
|
|
103
|
+
def disable(self, domain: str) -> bool:
|
|
104
|
+
"""
|
|
105
|
+
Disable domain privacy for a domain.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
domain: Domain name (resolved to WhoisGuard ID automatically)
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
True if successful
|
|
112
|
+
|
|
113
|
+
Examples:
|
|
114
|
+
>>> nc.whoisguard.disable("example.com")
|
|
115
|
+
"""
|
|
116
|
+
wg_id = self._resolve_id(domain)
|
|
117
|
+
|
|
118
|
+
result: Any = self._request(
|
|
119
|
+
"namecheap.whoisguard.disable",
|
|
120
|
+
{"WhoisguardID": wg_id},
|
|
121
|
+
path="WhoisguardDisableResult",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
assert result, f"API returned empty result for whoisguard.disable on {domain}"
|
|
125
|
+
return result.get("@IsSuccess", "false").lower() == "true"
|
|
126
|
+
|
|
127
|
+
def renew(self, domain: str, *, years: int = 1) -> dict[str, Any]:
|
|
128
|
+
"""
|
|
129
|
+
Renew domain privacy for a domain.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
domain: Domain name (resolved to WhoisGuard ID automatically)
|
|
133
|
+
years: Number of years to renew (1-9)
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Dict with OrderId, TransactionId, ChargedAmount
|
|
137
|
+
|
|
138
|
+
Examples:
|
|
139
|
+
>>> result = nc.whoisguard.renew("example.com", years=1)
|
|
140
|
+
>>> print(f"Charged: {result['charged_amount']}")
|
|
141
|
+
"""
|
|
142
|
+
assert 1 <= years <= 9, "Years must be between 1 and 9"
|
|
143
|
+
wg_id = self._resolve_id(domain)
|
|
144
|
+
|
|
145
|
+
result: Any = self._request(
|
|
146
|
+
"namecheap.whoisguard.renew",
|
|
147
|
+
{
|
|
148
|
+
"WhoisguardID": wg_id,
|
|
149
|
+
"Years": years,
|
|
150
|
+
},
|
|
151
|
+
path="WhoisguardRenewResult",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
assert result, f"API returned empty result for whoisguard.renew on {domain}"
|
|
155
|
+
return {
|
|
156
|
+
"whoisguard_id": int(result.get("@WhoisguardId", wg_id)),
|
|
157
|
+
"years": int(result.get("@Years", years)),
|
|
158
|
+
"is_renewed": result.get("@Renew", "false").lower() == "true",
|
|
159
|
+
"order_id": int(result.get("@OrderId", 0)),
|
|
160
|
+
"transaction_id": int(result.get("@TransactionId", 0)),
|
|
161
|
+
"charged_amount": Decimal(result.get("@ChargedAmount", "0")),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
def change_email(self, domain: str) -> dict[str, str]:
|
|
165
|
+
"""
|
|
166
|
+
Rotate the privacy forwarding email address for a domain.
|
|
167
|
+
|
|
168
|
+
Namecheap generates a new masked email and retires the old one.
|
|
169
|
+
No input email needed — the API handles the rotation.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
domain: Domain name (resolved to WhoisGuard ID automatically)
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Dict with new_email and old_email
|
|
176
|
+
|
|
177
|
+
Examples:
|
|
178
|
+
>>> result = nc.whoisguard.change_email("example.com")
|
|
179
|
+
>>> print(f"New: {result['new_email']}")
|
|
180
|
+
"""
|
|
181
|
+
wg_id = self._resolve_id(domain)
|
|
182
|
+
|
|
183
|
+
result: Any = self._request(
|
|
184
|
+
"namecheap.whoisguard.changeEmailAddress",
|
|
185
|
+
{"WhoisguardID": wg_id},
|
|
186
|
+
path="WhoisguardChangeEmailAddressResult",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
assert result, (
|
|
190
|
+
f"API returned empty result for whoisguard.changeEmailAddress on {domain}"
|
|
191
|
+
)
|
|
192
|
+
return {
|
|
193
|
+
"new_email": result.get("@WGEmail", ""),
|
|
194
|
+
"old_email": result.get("@WGOldEmail", ""),
|
|
195
|
+
}
|
namecheap/client.py
CHANGED
|
@@ -17,6 +17,7 @@ if TYPE_CHECKING:
|
|
|
17
17
|
from ._api.dns import DnsAPI
|
|
18
18
|
from ._api.domains import DomainsAPI
|
|
19
19
|
from ._api.users import UsersAPI
|
|
20
|
+
from ._api.whoisguard import WhoisguardAPI
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class Namecheap:
|
|
@@ -111,6 +112,13 @@ class Namecheap:
|
|
|
111
112
|
|
|
112
113
|
return DnsAPI(self)
|
|
113
114
|
|
|
115
|
+
@cached_property
|
|
116
|
+
def whoisguard(self) -> WhoisguardAPI:
|
|
117
|
+
"""Domain privacy (WhoisGuard) management."""
|
|
118
|
+
from ._api.whoisguard import WhoisguardAPI
|
|
119
|
+
|
|
120
|
+
return WhoisguardAPI(self)
|
|
121
|
+
|
|
114
122
|
@cached_property
|
|
115
123
|
def users(self) -> UsersAPI:
|
|
116
124
|
"""User account operations."""
|
namecheap/models.py
CHANGED
|
@@ -336,6 +336,93 @@ class Contact(BaseModel):
|
|
|
336
336
|
model_config = ConfigDict(populate_by_name=True)
|
|
337
337
|
|
|
338
338
|
|
|
339
|
+
class DomainContacts(BaseModel):
|
|
340
|
+
"""Contact information for all roles on a domain."""
|
|
341
|
+
|
|
342
|
+
registrant: Contact
|
|
343
|
+
tech: Contact
|
|
344
|
+
admin: Contact
|
|
345
|
+
aux_billing: Contact
|
|
346
|
+
|
|
347
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class Tld(BaseModel):
|
|
351
|
+
"""TLD information from Namecheap's supported TLD list."""
|
|
352
|
+
|
|
353
|
+
name: str = Field(alias="@Name")
|
|
354
|
+
description: str = Field(alias="#text", default="")
|
|
355
|
+
type: str = Field(alias="@Type")
|
|
356
|
+
min_register_years: int = Field(alias="@MinRegisterYears")
|
|
357
|
+
max_register_years: int = Field(alias="@MaxRegisterYears")
|
|
358
|
+
min_renew_years: int = Field(alias="@MinRenewYears")
|
|
359
|
+
max_renew_years: int = Field(alias="@MaxRenewYears")
|
|
360
|
+
min_transfer_years: int = Field(alias="@MinTransferYears")
|
|
361
|
+
max_transfer_years: int = Field(alias="@MaxTransferYears")
|
|
362
|
+
is_api_registerable: bool = Field(alias="@IsApiRegisterable")
|
|
363
|
+
is_api_renewable: bool = Field(alias="@IsApiRenewable")
|
|
364
|
+
is_api_transferable: bool = Field(alias="@IsApiTransferable")
|
|
365
|
+
is_epp_required: bool = Field(alias="@IsEppRequired")
|
|
366
|
+
is_disable_mod_contact: bool = Field(alias="@IsDisableModContact")
|
|
367
|
+
is_disable_wg_allot: bool = Field(alias="@IsDisableWGAllot")
|
|
368
|
+
is_supports_idn: bool = Field(alias="@IsSupportsIDN")
|
|
369
|
+
category: str = Field(alias="@Category", default="")
|
|
370
|
+
sequence_number: int = Field(alias="@SequenceNumber", default=0)
|
|
371
|
+
non_real_time: bool = Field(alias="@NonRealTime", default=False)
|
|
372
|
+
|
|
373
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
374
|
+
|
|
375
|
+
@field_validator(
|
|
376
|
+
"is_api_registerable",
|
|
377
|
+
"is_api_renewable",
|
|
378
|
+
"is_api_transferable",
|
|
379
|
+
"is_epp_required",
|
|
380
|
+
"is_disable_mod_contact",
|
|
381
|
+
"is_disable_wg_allot",
|
|
382
|
+
"is_supports_idn",
|
|
383
|
+
"non_real_time",
|
|
384
|
+
mode="before",
|
|
385
|
+
)
|
|
386
|
+
@classmethod
|
|
387
|
+
def parse_bool(cls, v: Any) -> bool:
|
|
388
|
+
if isinstance(v, bool):
|
|
389
|
+
return v
|
|
390
|
+
if isinstance(v, str):
|
|
391
|
+
return v.lower() == "true"
|
|
392
|
+
return False
|
|
393
|
+
|
|
394
|
+
@field_validator(
|
|
395
|
+
"min_register_years",
|
|
396
|
+
"max_register_years",
|
|
397
|
+
"min_renew_years",
|
|
398
|
+
"max_renew_years",
|
|
399
|
+
"min_transfer_years",
|
|
400
|
+
"max_transfer_years",
|
|
401
|
+
"sequence_number",
|
|
402
|
+
mode="before",
|
|
403
|
+
)
|
|
404
|
+
@classmethod
|
|
405
|
+
def parse_int(cls, v: Any) -> int:
|
|
406
|
+
return int(v) if v else 0
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class WhoisguardEntry(BaseModel):
|
|
410
|
+
"""A WhoisGuard/domain privacy subscription."""
|
|
411
|
+
|
|
412
|
+
id: int = Field(alias="@ID")
|
|
413
|
+
domain: str = Field(alias="@DomainName", default="")
|
|
414
|
+
created: str = Field(alias="@Created", default="")
|
|
415
|
+
expires: str = Field(alias="@Expires", default="")
|
|
416
|
+
status: str = Field(alias="@Status", default="")
|
|
417
|
+
|
|
418
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
419
|
+
|
|
420
|
+
@field_validator("id", mode="before")
|
|
421
|
+
@classmethod
|
|
422
|
+
def parse_id(cls, v: Any) -> int:
|
|
423
|
+
return int(v) if v else 0
|
|
424
|
+
|
|
425
|
+
|
|
339
426
|
class Config(BaseModel):
|
|
340
427
|
"""Client configuration with validation."""
|
|
341
428
|
|
namecheap_cli/__main__.py
CHANGED
|
@@ -919,6 +919,390 @@ def dns_email_forwarding(config: Config, domain: str) -> None:
|
|
|
919
919
|
sys.exit(1)
|
|
920
920
|
|
|
921
921
|
|
|
922
|
+
@dns_group.command("set-email-forwarding")
|
|
923
|
+
@click.argument("domain")
|
|
924
|
+
@click.argument("rules", nargs=-1, required=True)
|
|
925
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
|
|
926
|
+
@pass_config
|
|
927
|
+
def dns_set_email_forwarding(
|
|
928
|
+
config: Config, domain: str, rules: tuple[str, ...], yes: bool
|
|
929
|
+
) -> None:
|
|
930
|
+
"""Set email forwarding rules. Replaces all existing rules.
|
|
931
|
+
|
|
932
|
+
Rules are in mailbox:forward_to format.
|
|
933
|
+
|
|
934
|
+
Example:
|
|
935
|
+
namecheap-cli dns set-email-forwarding example.com info:me@gmail.com support:help@gmail.com
|
|
936
|
+
"""
|
|
937
|
+
nc = config.init_client()
|
|
938
|
+
|
|
939
|
+
parsed = []
|
|
940
|
+
for rule in rules:
|
|
941
|
+
assert ":" in rule, f"Invalid rule format '{rule}', expected mailbox:forward_to"
|
|
942
|
+
mailbox, forward_to = rule.split(":", 1)
|
|
943
|
+
parsed.append({"mailbox": mailbox, "forward_to": forward_to})
|
|
944
|
+
|
|
945
|
+
try:
|
|
946
|
+
if not yes and not config.quiet:
|
|
947
|
+
console.print(f"\n[yellow]Setting email forwarding for {domain}:[/yellow]")
|
|
948
|
+
for p in parsed:
|
|
949
|
+
console.print(f" • {p['mailbox']}@{domain} → {p['forward_to']}")
|
|
950
|
+
console.print()
|
|
951
|
+
|
|
952
|
+
if not Confirm.ask("Continue?", default=True):
|
|
953
|
+
console.print("[yellow]Cancelled[/yellow]")
|
|
954
|
+
return
|
|
955
|
+
|
|
956
|
+
with Progress(
|
|
957
|
+
SpinnerColumn(),
|
|
958
|
+
TextColumn("[progress.description]{task.description}"),
|
|
959
|
+
transient=True,
|
|
960
|
+
) as progress:
|
|
961
|
+
progress.add_task(f"Setting email forwarding for {domain}...", total=None)
|
|
962
|
+
success = nc.dns.set_email_forwarding(domain, parsed)
|
|
963
|
+
|
|
964
|
+
if success:
|
|
965
|
+
console.print("[green]✅ Email forwarding updated successfully![/green]")
|
|
966
|
+
else:
|
|
967
|
+
console.print("[red]❌ Failed to update email forwarding[/red]")
|
|
968
|
+
sys.exit(1)
|
|
969
|
+
|
|
970
|
+
except NamecheapError as e:
|
|
971
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
972
|
+
sys.exit(1)
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
@domain_group.command("contacts")
|
|
976
|
+
@click.argument("domain")
|
|
977
|
+
@pass_config
|
|
978
|
+
def domain_contacts(config: Config, domain: str) -> None:
|
|
979
|
+
"""Show contact information for a domain."""
|
|
980
|
+
nc = config.init_client()
|
|
981
|
+
|
|
982
|
+
try:
|
|
983
|
+
with Progress(
|
|
984
|
+
SpinnerColumn(),
|
|
985
|
+
TextColumn("[progress.description]{task.description}"),
|
|
986
|
+
transient=True,
|
|
987
|
+
) as progress:
|
|
988
|
+
progress.add_task(f"Getting contacts for {domain}...", total=None)
|
|
989
|
+
contacts = nc.domains.get_contacts(domain)
|
|
990
|
+
|
|
991
|
+
if config.output_format == "table":
|
|
992
|
+
for role, contact in [
|
|
993
|
+
("Registrant", contacts.registrant),
|
|
994
|
+
("Tech", contacts.tech),
|
|
995
|
+
("Admin", contacts.admin),
|
|
996
|
+
("Billing", contacts.aux_billing),
|
|
997
|
+
]:
|
|
998
|
+
console.print(f"\n[bold cyan]{role}[/bold cyan]")
|
|
999
|
+
console.print(f" {contact.first_name} {contact.last_name}")
|
|
1000
|
+
if contact.organization:
|
|
1001
|
+
console.print(f" {contact.organization}")
|
|
1002
|
+
console.print(f" {contact.email}")
|
|
1003
|
+
console.print(f" {contact.phone}")
|
|
1004
|
+
console.print(f" {contact.address1}")
|
|
1005
|
+
if contact.address2:
|
|
1006
|
+
console.print(f" {contact.address2}")
|
|
1007
|
+
console.print(
|
|
1008
|
+
f" {contact.city}, {contact.state_province} {contact.postal_code}"
|
|
1009
|
+
)
|
|
1010
|
+
console.print(f" {contact.country}")
|
|
1011
|
+
else:
|
|
1012
|
+
output_formatter(contacts.model_dump(), config.output_format)
|
|
1013
|
+
|
|
1014
|
+
except NamecheapError as e:
|
|
1015
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
1016
|
+
sys.exit(1)
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
@domain_group.command("tlds")
|
|
1020
|
+
@click.option("--registerable", is_flag=True, help="Only show API-registerable TLDs")
|
|
1021
|
+
@click.option(
|
|
1022
|
+
"--type",
|
|
1023
|
+
"-t",
|
|
1024
|
+
"tld_type",
|
|
1025
|
+
type=click.Choice(["GTLD", "CCTLD"], case_sensitive=False),
|
|
1026
|
+
help="Filter by TLD type",
|
|
1027
|
+
)
|
|
1028
|
+
@pass_config
|
|
1029
|
+
def domain_tlds(config: Config, registerable: bool, tld_type: str | None) -> None:
|
|
1030
|
+
"""List all supported TLDs."""
|
|
1031
|
+
nc = config.init_client()
|
|
1032
|
+
|
|
1033
|
+
try:
|
|
1034
|
+
with Progress(
|
|
1035
|
+
SpinnerColumn(),
|
|
1036
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1037
|
+
transient=True,
|
|
1038
|
+
) as progress:
|
|
1039
|
+
progress.add_task("Loading TLD list...", total=None)
|
|
1040
|
+
tlds = nc.domains.get_tld_list()
|
|
1041
|
+
|
|
1042
|
+
if registerable:
|
|
1043
|
+
tlds = [t for t in tlds if t.is_api_registerable]
|
|
1044
|
+
if tld_type:
|
|
1045
|
+
tlds = [t for t in tlds if t.type.upper() == tld_type.upper()]
|
|
1046
|
+
|
|
1047
|
+
tlds.sort(key=lambda t: t.name)
|
|
1048
|
+
|
|
1049
|
+
if config.output_format == "table":
|
|
1050
|
+
table = Table(title=f"Supported TLDs ({len(tlds)} total)")
|
|
1051
|
+
table.add_column("TLD", style="cyan")
|
|
1052
|
+
table.add_column("Type", style="magenta")
|
|
1053
|
+
table.add_column("Description", style="dim")
|
|
1054
|
+
table.add_column("Register", justify="center")
|
|
1055
|
+
table.add_column("Renew", justify="center")
|
|
1056
|
+
table.add_column("Transfer", justify="center")
|
|
1057
|
+
table.add_column("Years", justify="center")
|
|
1058
|
+
|
|
1059
|
+
for t in tlds:
|
|
1060
|
+
table.add_row(
|
|
1061
|
+
f".{t.name}",
|
|
1062
|
+
t.type,
|
|
1063
|
+
t.description[:40] if t.description else "",
|
|
1064
|
+
"✓" if t.is_api_registerable else "✗",
|
|
1065
|
+
"✓" if t.is_api_renewable else "✗",
|
|
1066
|
+
"✓" if t.is_api_transferable else "✗",
|
|
1067
|
+
f"{t.min_register_years}-{t.max_register_years}",
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
console.print(table)
|
|
1071
|
+
else:
|
|
1072
|
+
data = [
|
|
1073
|
+
{
|
|
1074
|
+
"tld": t.name,
|
|
1075
|
+
"type": t.type,
|
|
1076
|
+
"description": t.description,
|
|
1077
|
+
"api_registerable": t.is_api_registerable,
|
|
1078
|
+
"api_renewable": t.is_api_renewable,
|
|
1079
|
+
"api_transferable": t.is_api_transferable,
|
|
1080
|
+
}
|
|
1081
|
+
for t in tlds
|
|
1082
|
+
]
|
|
1083
|
+
output_formatter(data, config.output_format)
|
|
1084
|
+
|
|
1085
|
+
except NamecheapError as e:
|
|
1086
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
1087
|
+
sys.exit(1)
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
@cli.group("privacy")
|
|
1091
|
+
def privacy_group() -> None:
|
|
1092
|
+
"""Domain privacy (WhoisGuard) management."""
|
|
1093
|
+
pass
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
@privacy_group.command("list")
|
|
1097
|
+
@click.option(
|
|
1098
|
+
"--type",
|
|
1099
|
+
"-t",
|
|
1100
|
+
"list_type",
|
|
1101
|
+
type=click.Choice(["ALL", "ALLOTED", "FREE", "DISCARD"], case_sensitive=False),
|
|
1102
|
+
default="ALL",
|
|
1103
|
+
help="Filter by subscription type",
|
|
1104
|
+
)
|
|
1105
|
+
@pass_config
|
|
1106
|
+
def privacy_list(config: Config, list_type: str) -> None:
|
|
1107
|
+
"""List WhoisGuard subscriptions."""
|
|
1108
|
+
nc = config.init_client()
|
|
1109
|
+
|
|
1110
|
+
try:
|
|
1111
|
+
with Progress(
|
|
1112
|
+
SpinnerColumn(),
|
|
1113
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1114
|
+
transient=True,
|
|
1115
|
+
) as progress:
|
|
1116
|
+
progress.add_task("Loading WhoisGuard subscriptions...", total=None)
|
|
1117
|
+
entries = nc.whoisguard.get_list(list_type=list_type.upper())
|
|
1118
|
+
|
|
1119
|
+
if config.output_format == "table":
|
|
1120
|
+
table = Table(title=f"WhoisGuard Subscriptions ({len(entries)} total)")
|
|
1121
|
+
table.add_column("ID", style="dim")
|
|
1122
|
+
table.add_column("Domain", style="cyan")
|
|
1123
|
+
table.add_column("Status", style="green")
|
|
1124
|
+
table.add_column("Created", style="yellow")
|
|
1125
|
+
table.add_column("Expires", style="yellow")
|
|
1126
|
+
|
|
1127
|
+
for e in entries:
|
|
1128
|
+
status_style = (
|
|
1129
|
+
"green" if e.status.lower() in ("enabled", "alloted") else "yellow"
|
|
1130
|
+
)
|
|
1131
|
+
table.add_row(
|
|
1132
|
+
str(e.id),
|
|
1133
|
+
e.domain or "[dim]unassigned[/dim]",
|
|
1134
|
+
f"[{status_style}]{e.status}[/{status_style}]",
|
|
1135
|
+
e.created,
|
|
1136
|
+
e.expires,
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
console.print(table)
|
|
1140
|
+
else:
|
|
1141
|
+
output_formatter([e.model_dump() for e in entries], config.output_format)
|
|
1142
|
+
|
|
1143
|
+
except NamecheapError as e:
|
|
1144
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
1145
|
+
sys.exit(1)
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
@privacy_group.command("enable")
|
|
1149
|
+
@click.argument("domain")
|
|
1150
|
+
@click.argument("email")
|
|
1151
|
+
@pass_config
|
|
1152
|
+
def privacy_enable(config: Config, domain: str, email: str) -> None:
|
|
1153
|
+
"""Enable domain privacy. Requires forwarding email.
|
|
1154
|
+
|
|
1155
|
+
Example:
|
|
1156
|
+
namecheap-cli privacy enable example.com me@gmail.com
|
|
1157
|
+
"""
|
|
1158
|
+
nc = config.init_client()
|
|
1159
|
+
|
|
1160
|
+
try:
|
|
1161
|
+
with Progress(
|
|
1162
|
+
SpinnerColumn(),
|
|
1163
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1164
|
+
transient=True,
|
|
1165
|
+
) as progress:
|
|
1166
|
+
progress.add_task(f"Enabling privacy for {domain}...", total=None)
|
|
1167
|
+
success = nc.whoisguard.enable(domain, email)
|
|
1168
|
+
|
|
1169
|
+
if success:
|
|
1170
|
+
console.print(f"[green]✅ Privacy enabled for {domain}[/green]")
|
|
1171
|
+
console.print(f"[dim]Forwarding email: {email}[/dim]")
|
|
1172
|
+
else:
|
|
1173
|
+
console.print("[red]❌ Failed to enable privacy[/red]")
|
|
1174
|
+
sys.exit(1)
|
|
1175
|
+
|
|
1176
|
+
except ValueError as e:
|
|
1177
|
+
console.print(f"[red]❌ {e}[/red]")
|
|
1178
|
+
sys.exit(1)
|
|
1179
|
+
except NamecheapError as e:
|
|
1180
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
1181
|
+
sys.exit(1)
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
@privacy_group.command("disable")
|
|
1185
|
+
@click.argument("domain")
|
|
1186
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
|
|
1187
|
+
@pass_config
|
|
1188
|
+
def privacy_disable(config: Config, domain: str, yes: bool) -> None:
|
|
1189
|
+
"""Disable domain privacy.
|
|
1190
|
+
|
|
1191
|
+
Example:
|
|
1192
|
+
namecheap-cli privacy disable example.com
|
|
1193
|
+
"""
|
|
1194
|
+
nc = config.init_client()
|
|
1195
|
+
|
|
1196
|
+
try:
|
|
1197
|
+
if not yes and not config.quiet:
|
|
1198
|
+
console.print(
|
|
1199
|
+
f"\n[yellow]This will expose your WHOIS information for {domain}.[/yellow]"
|
|
1200
|
+
)
|
|
1201
|
+
if not Confirm.ask("Continue?", default=False):
|
|
1202
|
+
console.print("[yellow]Cancelled[/yellow]")
|
|
1203
|
+
return
|
|
1204
|
+
|
|
1205
|
+
with Progress(
|
|
1206
|
+
SpinnerColumn(),
|
|
1207
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1208
|
+
transient=True,
|
|
1209
|
+
) as progress:
|
|
1210
|
+
progress.add_task(f"Disabling privacy for {domain}...", total=None)
|
|
1211
|
+
success = nc.whoisguard.disable(domain)
|
|
1212
|
+
|
|
1213
|
+
if success:
|
|
1214
|
+
console.print(f"[green]✅ Privacy disabled for {domain}[/green]")
|
|
1215
|
+
else:
|
|
1216
|
+
console.print("[red]❌ Failed to disable privacy[/red]")
|
|
1217
|
+
sys.exit(1)
|
|
1218
|
+
|
|
1219
|
+
except ValueError as e:
|
|
1220
|
+
console.print(f"[red]❌ {e}[/red]")
|
|
1221
|
+
sys.exit(1)
|
|
1222
|
+
except NamecheapError as e:
|
|
1223
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
1224
|
+
sys.exit(1)
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
@privacy_group.command("renew")
|
|
1228
|
+
@click.argument("domain")
|
|
1229
|
+
@click.option("--years", "-y", type=int, default=1, help="Years to renew (1-9)")
|
|
1230
|
+
@click.option("--yes", is_flag=True, help="Skip confirmation")
|
|
1231
|
+
@pass_config
|
|
1232
|
+
def privacy_renew(config: Config, domain: str, years: int, yes: bool) -> None:
|
|
1233
|
+
"""Renew domain privacy subscription.
|
|
1234
|
+
|
|
1235
|
+
Example:
|
|
1236
|
+
namecheap-cli privacy renew example.com --years 2
|
|
1237
|
+
"""
|
|
1238
|
+
nc = config.init_client()
|
|
1239
|
+
|
|
1240
|
+
try:
|
|
1241
|
+
if not yes and not config.quiet:
|
|
1242
|
+
console.print(
|
|
1243
|
+
f"\n[yellow]Renewing WhoisGuard for {domain} ({years} year{'s' if years > 1 else ''}).[/yellow]"
|
|
1244
|
+
)
|
|
1245
|
+
if not Confirm.ask("Continue?", default=True):
|
|
1246
|
+
console.print("[yellow]Cancelled[/yellow]")
|
|
1247
|
+
return
|
|
1248
|
+
|
|
1249
|
+
with Progress(
|
|
1250
|
+
SpinnerColumn(),
|
|
1251
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1252
|
+
transient=True,
|
|
1253
|
+
) as progress:
|
|
1254
|
+
progress.add_task(f"Renewing privacy for {domain}...", total=None)
|
|
1255
|
+
result = nc.whoisguard.renew(domain, years=years)
|
|
1256
|
+
|
|
1257
|
+
if result["is_renewed"]:
|
|
1258
|
+
console.print(f"[green]✅ Privacy renewed for {domain}[/green]")
|
|
1259
|
+
console.print(f"[dim]Charged: {result['charged_amount']}[/dim]")
|
|
1260
|
+
else:
|
|
1261
|
+
console.print("[red]❌ Failed to renew privacy[/red]")
|
|
1262
|
+
sys.exit(1)
|
|
1263
|
+
|
|
1264
|
+
except ValueError as e:
|
|
1265
|
+
console.print(f"[red]❌ {e}[/red]")
|
|
1266
|
+
sys.exit(1)
|
|
1267
|
+
except NamecheapError as e:
|
|
1268
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
1269
|
+
sys.exit(1)
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
@privacy_group.command("change-email")
|
|
1273
|
+
@click.argument("domain")
|
|
1274
|
+
@pass_config
|
|
1275
|
+
def privacy_change_email(config: Config, domain: str) -> None:
|
|
1276
|
+
"""Rotate the privacy forwarding email address.
|
|
1277
|
+
|
|
1278
|
+
Namecheap generates a new masked email automatically.
|
|
1279
|
+
|
|
1280
|
+
Example:
|
|
1281
|
+
namecheap-cli privacy change-email example.com
|
|
1282
|
+
"""
|
|
1283
|
+
nc = config.init_client()
|
|
1284
|
+
|
|
1285
|
+
try:
|
|
1286
|
+
with Progress(
|
|
1287
|
+
SpinnerColumn(),
|
|
1288
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1289
|
+
transient=True,
|
|
1290
|
+
) as progress:
|
|
1291
|
+
progress.add_task(f"Rotating privacy email for {domain}...", total=None)
|
|
1292
|
+
result = nc.whoisguard.change_email(domain)
|
|
1293
|
+
|
|
1294
|
+
console.print(f"[green]✅ Privacy email rotated for {domain}[/green]")
|
|
1295
|
+
console.print(f" New: {result['new_email']}")
|
|
1296
|
+
console.print(f" Old: {result['old_email']}")
|
|
1297
|
+
|
|
1298
|
+
except ValueError as e:
|
|
1299
|
+
console.print(f"[red]❌ {e}[/red]")
|
|
1300
|
+
sys.exit(1)
|
|
1301
|
+
except NamecheapError as e:
|
|
1302
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
1303
|
+
sys.exit(1)
|
|
1304
|
+
|
|
1305
|
+
|
|
922
1306
|
@cli.group("account")
|
|
923
1307
|
def account_group() -> None:
|
|
924
1308
|
"""Account management commands."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: namecheap-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: A friendly Python SDK for Namecheap API
|
|
5
5
|
Project-URL: Homepage, https://github.com/adriangalilea/namecheap-python
|
|
6
6
|
Project-URL: Repository, https://github.com/adriangalilea/namecheap-python
|
|
@@ -380,9 +380,59 @@ print(bal.funds_required_for_auto_renew) # Decimal('20.16')
|
|
|
380
380
|
### Email Forwarding
|
|
381
381
|
|
|
382
382
|
```python
|
|
383
|
+
# Read
|
|
383
384
|
rules = nc.dns.get_email_forwarding("example.com")
|
|
384
385
|
for r in rules:
|
|
385
386
|
print(f"{r.mailbox} -> {r.forward_to}")
|
|
387
|
+
|
|
388
|
+
# Write (replaces all existing rules)
|
|
389
|
+
nc.dns.set_email_forwarding("example.com", [
|
|
390
|
+
EmailForward(mailbox="info", forward_to="me@gmail.com"),
|
|
391
|
+
EmailForward(mailbox="support", forward_to="help@gmail.com"),
|
|
392
|
+
])
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Domain Contacts
|
|
396
|
+
|
|
397
|
+
```python
|
|
398
|
+
contacts = nc.domains.get_contacts("example.com")
|
|
399
|
+
print(f"{contacts.registrant.first_name} {contacts.registrant.last_name}")
|
|
400
|
+
print(contacts.registrant.email)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### TLD List
|
|
404
|
+
|
|
405
|
+
```python
|
|
406
|
+
tlds = nc.domains.get_tld_list()
|
|
407
|
+
print(f"{len(tlds)} TLDs supported")
|
|
408
|
+
|
|
409
|
+
# Filter to API-registerable TLDs
|
|
410
|
+
registerable = [t for t in tlds if t.is_api_registerable]
|
|
411
|
+
for t in registerable[:5]:
|
|
412
|
+
print(f".{t.name} ({t.type}) — {t.min_register_years}-{t.max_register_years} years")
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Domain Privacy (WhoisGuard)
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
# List all WhoisGuard subscriptions
|
|
419
|
+
entries = nc.whoisguard.get_list()
|
|
420
|
+
for e in entries:
|
|
421
|
+
print(f"{e.domain} (ID={e.id}) status={e.status}")
|
|
422
|
+
|
|
423
|
+
# Enable privacy (resolves WhoisGuard ID from domain name automatically)
|
|
424
|
+
nc.whoisguard.enable("example.com", "me@gmail.com")
|
|
425
|
+
|
|
426
|
+
# Disable privacy
|
|
427
|
+
nc.whoisguard.disable("example.com")
|
|
428
|
+
|
|
429
|
+
# Renew privacy
|
|
430
|
+
result = nc.whoisguard.renew("example.com", years=1)
|
|
431
|
+
print(f"Charged: {result['charged_amount']}")
|
|
432
|
+
|
|
433
|
+
# Rotate the masked forwarding email
|
|
434
|
+
result = nc.whoisguard.change_email("example.com")
|
|
435
|
+
print(f"New: {result['new_email']}")
|
|
386
436
|
```
|
|
387
437
|
|
|
388
438
|
### Domain Management
|
|
@@ -446,6 +496,15 @@ except NamecheapError as e:
|
|
|
446
496
|
|
|
447
497
|
This section documents undocumented or unusual Namecheap API behaviors we've discovered:
|
|
448
498
|
|
|
499
|
+
### No WHOIS lookups or Marketplace data
|
|
500
|
+
|
|
501
|
+
The Namecheap API only operates on domains **in your account**. There is no API for:
|
|
502
|
+
- WHOIS lookups on arbitrary domains
|
|
503
|
+
- Checking if a domain is listed on [Namecheap Marketplace](https://www.namecheap.com/domains/marketplace/)
|
|
504
|
+
- Aftermarket pricing or availability
|
|
505
|
+
|
|
506
|
+
`domains.check()` tells you if a domain is **unregistered**, not if it's for sale by its owner.
|
|
507
|
+
|
|
449
508
|
### TTL "Automatic" = 1799 seconds
|
|
450
509
|
|
|
451
510
|
The Namecheap web interface displays TTL as **"Automatic"** when the value is exactly **1799 seconds**, but shows **"30 min"** when it's **1800 seconds**. This behavior is completely undocumented in their official API documentation.
|
|
@@ -462,16 +521,15 @@ nc.dns.builder().a("www", "192.0.2.1", ttl=1800) # Shows as "30 min"
|
|
|
462
521
|
|
|
463
522
|
| API | Status | Methods |
|
|
464
523
|
|-----|--------|---------|
|
|
465
|
-
| `namecheap.domains.*` | ✅ Done | `check`, `list`, `getInfo`, `register`, `renew`, `setContacts`, `lock`/`unlock` |
|
|
466
|
-
| `namecheap.domains.dns.*` | ✅ Done | `getHosts`, `setHosts` (builder pattern), `add`, `delete`, `export`, `getList`, `setCustom`, `setDefault`, `getEmailForwarding` |
|
|
524
|
+
| `namecheap.domains.*` | ✅ Done | `check`, `list`, `getInfo`, `getContacts`, `getTldList`, `register`, `renew`, `setContacts`, `lock`/`unlock` |
|
|
525
|
+
| `namecheap.domains.dns.*` | ✅ Done | `getHosts`, `setHosts` (builder pattern), `add`, `delete`, `export`, `getList`, `setCustom`, `setDefault`, `getEmailForwarding`, `setEmailForwarding` |
|
|
526
|
+
| `namecheap.whoisguard.*` | ✅ Done | `getList`, `enable`, `disable`, `renew`, `changeEmailAddress` |
|
|
467
527
|
| `namecheap.users.*` | ⚠️ Partial | `getBalances`, `getPricing` (needs debugging). Planned: `changePassword`, `update`, `create`, `login`, `resetPassword` |
|
|
468
|
-
| `namecheap.domains.*` | 🚧 Planned | `getContacts`, `getTldList`, `reactivate` |
|
|
469
|
-
| `namecheap.domains.dns.*` | 🚧 Planned | `setEmailForwarding` |
|
|
470
528
|
| `namecheap.users.address.*` | 🚧 Planned | `create`, `delete`, `getInfo`, `getList`, `setDefault`, `update` |
|
|
471
529
|
| `namecheap.ssl.*` | 🚧 Planned | `create`, `activate`, `renew`, `revoke`, `getList`, `getInfo`, `parseCSR`, `reissue`, and more |
|
|
472
530
|
| `namecheap.domains.transfer.*` | 🚧 Planned | `create`, `getStatus`, `updateStatus`, `getList` |
|
|
473
531
|
| `namecheap.domains.ns.*` | 🚧 Planned | Glue records — `create`, `delete`, `getInfo`, `update` |
|
|
474
|
-
| `namecheap.
|
|
532
|
+
| `namecheap.domains.*` | 🚧 Planned | `reactivate` |
|
|
475
533
|
|
|
476
534
|
## 🛠️ Development
|
|
477
535
|
|
|
@@ -483,7 +541,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
483
541
|
|
|
484
542
|
## 🤝 Contributing
|
|
485
543
|
|
|
486
|
-
Contributions are welcome! Please feel free to submit a Pull Request. See
|
|
544
|
+
Contributions are welcome! Please feel free to submit a Pull Request. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and guidelines.
|
|
487
545
|
|
|
488
546
|
### Contributors
|
|
489
547
|
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
namecheap/__init__.py,sha256=
|
|
2
|
-
namecheap/client.py,sha256=
|
|
1
|
+
namecheap/__init__.py,sha256=d4na6bJ0UVqU2CUBwP5eROrinnD92qpFBNHn8l6IAxo,943
|
|
2
|
+
namecheap/client.py,sha256=c-JxBygXjD62bcTDzD9jv8hzd9K-C4TzzSoQQxT7H6I,6897
|
|
3
3
|
namecheap/errors.py,sha256=5bGbV1e4_jkK8YXZXbLF6GJCVUTKw1CtMl9-mz7ogZg,5010
|
|
4
4
|
namecheap/logging.py,sha256=lMR1fr1dWWz3z2NFEY-vl8b52FmmhH76R2NjyifSdYA,3396
|
|
5
|
-
namecheap/models.py,sha256=
|
|
5
|
+
namecheap/models.py,sha256=Rz6wc-6uQJPI-i1eALorRRjiTs9-XRhqGsEiD0jFVfg,17130
|
|
6
6
|
namecheap/_api/__init__.py,sha256=ymQxKCySphoeoo4s_J0tLziXttLNhOQ8AZbCzFcuAHs,36
|
|
7
7
|
namecheap/_api/base.py,sha256=FoczO1Q860PaFUFv-S3IoIV2xaGVJAlchkWnmTI6dlw,6121
|
|
8
|
-
namecheap/_api/dns.py,sha256=
|
|
9
|
-
namecheap/_api/domains.py,sha256=
|
|
8
|
+
namecheap/_api/dns.py,sha256=Hny5TsVWmmG-3rF6kb8JwboGFt2wKsd4-Z6T8GannBM,17213
|
|
9
|
+
namecheap/_api/domains.py,sha256=fo9JBsKtPlqme_nkEw8KseQ93STnH2Nn744669rn7TA,21030
|
|
10
10
|
namecheap/_api/users.py,sha256=CCXSZJiPkQiLHYRAlYKTBCDG3-JSPdNkNWWww71JXV0,795
|
|
11
|
+
namecheap/_api/whoisguard.py,sha256=R7jBiWlFqxu0EKwtayJEFHZwqub2ELd6pziaiQvxvd8,6207
|
|
11
12
|
namecheap_cli/README.md,sha256=liduIiGr8DHXGTht5swrYnvtAlcdCMQOnSdCD61g4Vw,7337
|
|
12
13
|
namecheap_cli/__init__.py,sha256=nGRHc_CkO4xKhSQdAVG-koEffP8VS0TvbfbZkg7Jg4k,108
|
|
13
|
-
namecheap_cli/__main__.py,sha256=
|
|
14
|
+
namecheap_cli/__main__.py,sha256=V4qNuZfsLYHmjQV9awjG4ZvBusiUf_lkQcFP9I1ulx8,49629
|
|
14
15
|
namecheap_cli/completion.py,sha256=JTEMnceQli7TombjZkHh-IcZKW4RFRI8Yk5VynxPsEA,2777
|
|
15
16
|
namecheap_dns_tui/README.md,sha256=It16ZiZh0haEeaENfF5HX0Ec4dBawdTYiAi-TiG9wi0,1690
|
|
16
17
|
namecheap_dns_tui/__init__.py,sha256=-yL_1Ha41FlQcmjG-raUrZP9CjTJD3d0w2BW2X-twJg,106
|
|
@@ -19,8 +20,8 @@ namecheap_dns_tui/assets/screenshot1.png,sha256=OXO2P80ll5WRzLYgaakcNnzos8svlJoX
|
|
|
19
20
|
namecheap_dns_tui/assets/screenshot2.png,sha256=5VN_qDMNhWEyrOqKw7vxl1h-TgmZQ_V9aph3Xmf_AFg,279194
|
|
20
21
|
namecheap_dns_tui/assets/screenshot3.png,sha256=h39wSKxx1JCkgeAB7Q3_JlBcAtX1vsRFKtWtOwbBVso,220625
|
|
21
22
|
namecheap_dns_tui/assets/screenshot4.png,sha256=J4nCOW16z3vaRiPbcMiiIRgV7q3XFbi_1N1ivD1Pa4Y,238068
|
|
22
|
-
namecheap_python-1.
|
|
23
|
-
namecheap_python-1.
|
|
24
|
-
namecheap_python-1.
|
|
25
|
-
namecheap_python-1.
|
|
26
|
-
namecheap_python-1.
|
|
23
|
+
namecheap_python-1.4.0.dist-info/METADATA,sha256=mJMPACBtcu8-nOEIArjlR52KYdPub1ZdkOTavx5GvN0,20120
|
|
24
|
+
namecheap_python-1.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
25
|
+
namecheap_python-1.4.0.dist-info/entry_points.txt,sha256=AyhiXroLUpM0Vdo_-RvH0S8o4XDPsDlsEl_65vm6DEk,96
|
|
26
|
+
namecheap_python-1.4.0.dist-info/licenses/LICENSE,sha256=pemTblFP6BBje3bBv_yL_sr2iAqB2H0-LdWMvVIR42o,1062
|
|
27
|
+
namecheap_python-1.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|