namecheap-python 1.3.0__tar.gz → 1.4.0__tar.gz

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.
Files changed (38) hide show
  1. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/CONTRIBUTING.md +3 -1
  2. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/PKG-INFO +49 -5
  3. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/README.md +48 -4
  4. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/pyproject.toml +1 -1
  5. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap/__init__.py +5 -1
  6. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap/_api/domains.py +36 -1
  7. namecheap_python-1.4.0/src/namecheap/_api/whoisguard.py +195 -0
  8. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap/client.py +8 -0
  9. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap/models.py +76 -0
  10. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap_cli/__main__.py +287 -0
  11. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/uv.lock +1 -1
  12. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/.env.example +0 -0
  13. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/.github/cliff.toml +0 -0
  14. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/.github/workflows/release.yml +0 -0
  15. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/.gitignore +0 -0
  16. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/.pre-commit-config.yaml +0 -0
  17. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/CLAUDE.md +0 -0
  18. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/CLI.md +0 -0
  19. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/LICENSE +0 -0
  20. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/MANIFEST.in +0 -0
  21. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/examples/README.md +0 -0
  22. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/examples/quickstart.py +0 -0
  23. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap/_api/__init__.py +0 -0
  24. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap/_api/base.py +0 -0
  25. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap/_api/dns.py +0 -0
  26. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap/_api/users.py +0 -0
  27. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap/errors.py +0 -0
  28. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap/logging.py +0 -0
  29. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap_cli/README.md +0 -0
  30. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap_cli/__init__.py +0 -0
  31. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap_cli/completion.py +0 -0
  32. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap_dns_tui/README.md +0 -0
  33. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap_dns_tui/__init__.py +0 -0
  34. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap_dns_tui/__main__.py +0 -0
  35. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap_dns_tui/assets/screenshot1.png +0 -0
  36. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap_dns_tui/assets/screenshot2.png +0 -0
  37. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap_dns_tui/assets/screenshot3.png +0 -0
  38. {namecheap_python-1.3.0 → namecheap_python-1.4.0}/src/namecheap_dns_tui/assets/screenshot4.png +0 -0
@@ -54,7 +54,9 @@ src/
54
54
  │ └── _api/ # API implementations
55
55
  │ ├── base.py # BaseAPI with _request() — all API calls go through here
56
56
  │ ├── 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)
58
60
  ├── namecheap_cli/ # CLI (click)
59
61
  │ ├── __main__.py # All commands in one file
60
62
  │ └── completion.py # Shell completions
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: namecheap-python
3
- Version: 1.3.0
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
@@ -400,6 +400,41 @@ print(f"{contacts.registrant.first_name} {contacts.registrant.last_name}")
400
400
  print(contacts.registrant.email)
401
401
  ```
402
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']}")
436
+ ```
437
+
403
438
  ### Domain Management
404
439
 
405
440
  ```python
@@ -461,6 +496,15 @@ except NamecheapError as e:
461
496
 
462
497
  This section documents undocumented or unusual Namecheap API behaviors we've discovered:
463
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
+
464
508
  ### TTL "Automatic" = 1799 seconds
465
509
 
466
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.
@@ -477,15 +521,15 @@ nc.dns.builder().a("www", "192.0.2.1", ttl=1800) # Shows as "30 min"
477
521
 
478
522
  | API | Status | Methods |
479
523
  |-----|--------|---------|
480
- | `namecheap.domains.*` | ✅ Done | `check`, `list`, `getInfo`, `getContacts`, `register`, `renew`, `setContacts`, `lock`/`unlock` |
524
+ | `namecheap.domains.*` | ✅ Done | `check`, `list`, `getInfo`, `getContacts`, `getTldList`, `register`, `renew`, `setContacts`, `lock`/`unlock` |
481
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` |
482
527
  | `namecheap.users.*` | ⚠️ Partial | `getBalances`, `getPricing` (needs debugging). Planned: `changePassword`, `update`, `create`, `login`, `resetPassword` |
483
- | `namecheap.domains.*` | 🚧 Planned | `getTldList`, `reactivate` |
484
528
  | `namecheap.users.address.*` | 🚧 Planned | `create`, `delete`, `getInfo`, `getList`, `setDefault`, `update` |
485
529
  | `namecheap.ssl.*` | 🚧 Planned | `create`, `activate`, `renew`, `revoke`, `getList`, `getInfo`, `parseCSR`, `reissue`, and more |
486
530
  | `namecheap.domains.transfer.*` | 🚧 Planned | `create`, `getStatus`, `updateStatus`, `getList` |
487
531
  | `namecheap.domains.ns.*` | 🚧 Planned | Glue records — `create`, `delete`, `getInfo`, `update` |
488
- | `namecheap.domainprivacy.*` | 🚧 Planned | `enable`, `disable`, `renew`, `getList`, `changeemailaddress` |
532
+ | `namecheap.domains.*` | 🚧 Planned | `reactivate` |
489
533
 
490
534
  ## 🛠️ Development
491
535
 
@@ -497,7 +541,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
497
541
 
498
542
  ## 🤝 Contributing
499
543
 
500
- Contributions are welcome! Please feel free to submit a Pull Request. See the [Development Guide](docs/dev/README.md) for setup instructions and guidelines.
544
+ Contributions are welcome! Please feel free to submit a Pull Request. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and guidelines.
501
545
 
502
546
  ### Contributors
503
547
 
@@ -358,6 +358,41 @@ print(f"{contacts.registrant.first_name} {contacts.registrant.last_name}")
358
358
  print(contacts.registrant.email)
359
359
  ```
360
360
 
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
+
361
396
  ### Domain Management
362
397
 
363
398
  ```python
@@ -419,6 +454,15 @@ except NamecheapError as e:
419
454
 
420
455
  This section documents undocumented or unusual Namecheap API behaviors we've discovered:
421
456
 
457
+ ### No WHOIS lookups or Marketplace data
458
+
459
+ The Namecheap API only operates on domains **in your account**. There is no API for:
460
+ - WHOIS lookups on arbitrary domains
461
+ - Checking if a domain is listed on [Namecheap Marketplace](https://www.namecheap.com/domains/marketplace/)
462
+ - Aftermarket pricing or availability
463
+
464
+ `domains.check()` tells you if a domain is **unregistered**, not if it's for sale by its owner.
465
+
422
466
  ### TTL "Automatic" = 1799 seconds
423
467
 
424
468
  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.
@@ -435,15 +479,15 @@ nc.dns.builder().a("www", "192.0.2.1", ttl=1800) # Shows as "30 min"
435
479
 
436
480
  | API | Status | Methods |
437
481
  |-----|--------|---------|
438
- | `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` |
439
483
  | `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` |
440
485
  | `namecheap.users.*` | ⚠️ Partial | `getBalances`, `getPricing` (needs debugging). Planned: `changePassword`, `update`, `create`, `login`, `resetPassword` |
441
- | `namecheap.domains.*` | 🚧 Planned | `getTldList`, `reactivate` |
442
486
  | `namecheap.users.address.*` | 🚧 Planned | `create`, `delete`, `getInfo`, `getList`, `setDefault`, `update` |
443
487
  | `namecheap.ssl.*` | 🚧 Planned | `create`, `activate`, `renew`, `revoke`, `getList`, `getInfo`, `parseCSR`, `reissue`, and more |
444
488
  | `namecheap.domains.transfer.*` | 🚧 Planned | `create`, `getStatus`, `updateStatus`, `getList` |
445
489
  | `namecheap.domains.ns.*` | 🚧 Planned | Glue records — `create`, `delete`, `getInfo`, `update` |
446
- | `namecheap.domainprivacy.*` | 🚧 Planned | `enable`, `disable`, `renew`, `getList`, `changeemailaddress` |
490
+ | `namecheap.domains.*` | 🚧 Planned | `reactivate` |
447
491
 
448
492
  ## 🛠️ Development
449
493
 
@@ -455,7 +499,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
455
499
 
456
500
  ## 🤝 Contributing
457
501
 
458
- 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.
459
503
 
460
504
  ### Contributors
461
505
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "namecheap-python"
3
- version = "1.3.0"
3
+ version = "1.4.0"
4
4
  description = "A friendly Python SDK for Namecheap API"
5
5
  authors = [{name = "Adrian Galilea Delgado", email = "adriangalilea@gmail.com"}]
6
6
  readme = "README.md"
@@ -22,9 +22,11 @@ from .models import (
22
22
  DomainInfo,
23
23
  EmailForward,
24
24
  Nameservers,
25
+ Tld,
26
+ WhoisguardEntry,
25
27
  )
26
28
 
27
- __version__ = "1.3.0"
29
+ __version__ = "1.4.0"
28
30
  __all__ = [
29
31
  "AccountBalance",
30
32
  "ConfigurationError",
@@ -38,5 +40,7 @@ __all__ = [
38
40
  "Namecheap",
39
41
  "NamecheapError",
40
42
  "Nameservers",
43
+ "Tld",
41
44
  "ValidationError",
45
+ "WhoisguardEntry",
42
46
  ]
@@ -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 Contact, Domain, DomainCheck, DomainContacts, DomainInfo
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
 
@@ -202,6 +209,34 @@ class DomainsAPI(BaseAPI):
202
209
  aux_billing=parse_contact(result.get("AuxBilling", {})),
203
210
  )
204
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
+
205
240
  def register(
206
241
  self,
207
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
+ }
@@ -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."""
@@ -347,6 +347,82 @@ class DomainContacts(BaseModel):
347
347
  model_config = ConfigDict(populate_by_name=True)
348
348
 
349
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
+
350
426
  class Config(BaseModel):
351
427
  """Client configuration with validation."""
352
428
 
@@ -1016,6 +1016,293 @@ def domain_contacts(config: Config, domain: str) -> None:
1016
1016
  sys.exit(1)
1017
1017
 
1018
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
+
1019
1306
  @cli.group("account")
1020
1307
  def account_group() -> None:
1021
1308
  """Account management commands."""
@@ -200,7 +200,7 @@ wheels = [
200
200
 
201
201
  [[package]]
202
202
  name = "namecheap-python"
203
- version = "1.3.0"
203
+ version = "1.4.0"
204
204
  source = { editable = "." }
205
205
  dependencies = [
206
206
  { name = "httpx" },