Skip to content

Commit 66970c5

Browse files
committed
feat: add getTldList and full WhoisGuard API (domainprivacy)
- domains.get_tld_list() returns all 1197 supported TLDs - whoisguard.get_list/enable/disable/renew/change_email - Domain-name-based interface that resolves WhoisGuard IDs automatically - CLI: domain tlds, privacy list/enable/disable/renew/change-email
1 parent 74fe099 commit 66970c5

File tree

10 files changed

+651
-9
lines changed

10 files changed

+651
-9
lines changed

CONTRIBUTING.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ src/
5454
│ └── _api/ # API implementations
5555
│ ├── base.py # BaseAPI with _request() — all API calls go through here
5656
│ ├── domains.py # namecheap.domains.* endpoints
57-
│ └── dns.py # namecheap.domains.dns.* endpoints + builder
57+
│ ├── dns.py # namecheap.domains.dns.* endpoints + builder
58+
│ ├── users.py # namecheap.users.* endpoints
59+
│ └── whoisguard.py # namecheap.whoisguard.* endpoints (domain privacy)
5860
├── namecheap_cli/ # CLI (click)
5961
│ ├── __main__.py # All commands in one file
6062
│ └── completion.py # Shell completions

README.md

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,41 @@ print(f"{contacts.registrant.first_name} {contacts.registrant.last_name}")
358358
print(contacts.registrant.email)
359359
```
360360
361+
### TLD List
362+
363+
```python
364+
tlds = nc.domains.get_tld_list()
365+
print(f"{len(tlds)} TLDs supported")
366+
367+
# Filter to API-registerable TLDs
368+
registerable = [t for t in tlds if t.is_api_registerable]
369+
for t in registerable[:5]:
370+
print(f".{t.name} ({t.type}) — {t.min_register_years}-{t.max_register_years} years")
371+
```
372+
373+
### Domain Privacy (WhoisGuard)
374+
375+
```python
376+
# List all WhoisGuard subscriptions
377+
entries = nc.whoisguard.get_list()
378+
for e in entries:
379+
print(f"{e.domain} (ID={e.id}) status={e.status}")
380+
381+
# Enable privacy (resolves WhoisGuard ID from domain name automatically)
382+
nc.whoisguard.enable("example.com", "me@gmail.com")
383+
384+
# Disable privacy
385+
nc.whoisguard.disable("example.com")
386+
387+
# Renew privacy
388+
result = nc.whoisguard.renew("example.com", years=1)
389+
print(f"Charged: {result['charged_amount']}")
390+
391+
# Rotate the masked forwarding email
392+
result = nc.whoisguard.change_email("example.com")
393+
print(f"New: {result['new_email']}")
394+
```
395+
361396
### Domain Management
362397
363398
```python
@@ -444,15 +479,15 @@ nc.dns.builder().a("www", "192.0.2.1", ttl=1800) # Shows as "30 min"
444479
445480
| API | Status | Methods |
446481
|-----|--------|---------|
447-
| `namecheap.domains.*` | ✅ Done | `check`, `list`, `getInfo`, `getContacts`, `register`, `renew`, `setContacts`, `lock`/`unlock` |
482+
| `namecheap.domains.*` | ✅ Done | `check`, `list`, `getInfo`, `getContacts`, `getTldList`, `register`, `renew`, `setContacts`, `lock`/`unlock` |
448483
| `namecheap.domains.dns.*` | ✅ Done | `getHosts`, `setHosts` (builder pattern), `add`, `delete`, `export`, `getList`, `setCustom`, `setDefault`, `getEmailForwarding`, `setEmailForwarding` |
484+
| `namecheap.whoisguard.*` | ✅ Done | `getList`, `enable`, `disable`, `renew`, `changeEmailAddress` |
449485
| `namecheap.users.*` | ⚠️ Partial | `getBalances`, `getPricing` (needs debugging). Planned: `changePassword`, `update`, `create`, `login`, `resetPassword` |
450-
| `namecheap.domains.*` | 🚧 Planned | `getTldList`, `reactivate` |
451486
| `namecheap.users.address.*` | 🚧 Planned | `create`, `delete`, `getInfo`, `getList`, `setDefault`, `update` |
452487
| `namecheap.ssl.*` | 🚧 Planned | `create`, `activate`, `renew`, `revoke`, `getList`, `getInfo`, `parseCSR`, `reissue`, and more |
453488
| `namecheap.domains.transfer.*` | 🚧 Planned | `create`, `getStatus`, `updateStatus`, `getList` |
454489
| `namecheap.domains.ns.*` | 🚧 Planned | Glue records — `create`, `delete`, `getInfo`, `update` |
455-
| `namecheap.domainprivacy.*` | 🚧 Planned | `enable`, `disable`, `renew`, `getList`, `changeemailaddress` |
490+
| `namecheap.domains.*` | 🚧 Planned | `reactivate` |
456491
457492
## 🛠️ Development
458493
@@ -464,7 +499,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
464499
465500
## 🤝 Contributing
466501
467-
Contributions are welcome! Please feel free to submit a Pull Request. See the [Development Guide](docs/dev/README.md) for setup instructions and guidelines.
502+
Contributions are welcome! Please feel free to submit a Pull Request. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and guidelines.
468503
469504
### Contributors
470505

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "namecheap-python"
3-
version = "1.3.0"
3+
version = "1.4.0"
44
description = "A friendly Python SDK for Namecheap API"
55
authors = [{name = "Adrian Galilea Delgado", email = "adriangalilea@gmail.com"}]
66
readme = "README.md"

src/namecheap/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
DomainInfo,
2323
EmailForward,
2424
Nameservers,
25+
Tld,
26+
WhoisguardEntry,
2527
)
2628

27-
__version__ = "1.3.0"
29+
__version__ = "1.4.0"
2830
__all__ = [
2931
"AccountBalance",
3032
"ConfigurationError",
@@ -38,5 +40,7 @@
3840
"Namecheap",
3941
"NamecheapError",
4042
"Nameservers",
43+
"Tld",
4144
"ValidationError",
45+
"WhoisguardEntry",
4246
]

src/namecheap/_api/domains.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@
99
import tldextract
1010

1111
from namecheap.logging import logger
12-
from namecheap.models import Contact, Domain, DomainCheck, DomainContacts, DomainInfo
12+
from namecheap.models import (
13+
Contact,
14+
Domain,
15+
DomainCheck,
16+
DomainContacts,
17+
DomainInfo,
18+
Tld,
19+
)
1320

1421
from .base import BaseAPI
1522

@@ -202,6 +209,34 @@ def parse_contact(data: dict[str, Any]) -> Contact:
202209
aux_billing=parse_contact(result.get("AuxBilling", {})),
203210
)
204211

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+
205240
def register(
206241
self,
207242
domain: str,

src/namecheap/_api/whoisguard.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

src/namecheap/client.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from ._api.dns import DnsAPI
1818
from ._api.domains import DomainsAPI
1919
from ._api.users import UsersAPI
20+
from ._api.whoisguard import WhoisguardAPI
2021

2122

2223
class Namecheap:
@@ -111,6 +112,13 @@ def dns(self) -> DnsAPI:
111112

112113
return DnsAPI(self)
113114

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+
114122
@cached_property
115123
def users(self) -> UsersAPI:
116124
"""User account operations."""

0 commit comments

Comments
 (0)