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 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.2.0"
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 Contact, Domain, DomainCheck, 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
 
@@ -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.2.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
@@ -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.domainprivacy.*` | 🚧 Planned | `enable`, `disable`, `renew`, `getList`, `changeemailaddress` |
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 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.
487
545
 
488
546
  ### Contributors
489
547
 
@@ -1,16 +1,17 @@
1
- namecheap/__init__.py,sha256=S-84LTSVWDXJeqvDoUoSsq84250m5J0MXYEPJ6xH3kw,837
2
- namecheap/client.py,sha256=KesIaZVa9HpXvlp-hc3nr2x-sqzMPiHEa8NibiPq580,6644
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=t4xloDJOgKeoERujhMRtTR0-B--ENzQsAn1WyFHyNbE,14261
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=MtOHr0AOUgS_Vv7U8WkLBw7xuU0XWbjAF6ptitteNmk,15827
9
- namecheap/_api/domains.py,sha256=bEPshE2GN6ubd0otTwLdZCpKVT9ErmDRy8fYxS2hkIY,18272
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=ZOmdVQZWkA3SHw7_nGCp_j5RdJCQff4NO6h_nIjgDnY,36425
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.2.0.dist-info/METADATA,sha256=R44Jl31VrMFE8lzs_YxqgGuv7sHvN0rIj9qjgdvKyv0,18433
23
- namecheap_python-1.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
24
- namecheap_python-1.2.0.dist-info/entry_points.txt,sha256=AyhiXroLUpM0Vdo_-RvH0S8o4XDPsDlsEl_65vm6DEk,96
25
- namecheap_python-1.2.0.dist-info/licenses/LICENSE,sha256=pemTblFP6BBje3bBv_yL_sr2iAqB2H0-LdWMvVIR42o,1062
26
- namecheap_python-1.2.0.dist-info/RECORD,,
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,,