namecheap-python 1.3.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 +5 -1
- namecheap/_api/domains.py +36 -1
- namecheap/_api/whoisguard.py +195 -0
- namecheap/client.py +8 -0
- namecheap/models.py +76 -0
- namecheap_cli/__main__.py +287 -0
- {namecheap_python-1.3.0.dist-info → namecheap_python-1.4.0.dist-info}/METADATA +49 -5
- {namecheap_python-1.3.0.dist-info → namecheap_python-1.4.0.dist-info}/RECORD +11 -10
- {namecheap_python-1.3.0.dist-info → namecheap_python-1.4.0.dist-info}/WHEEL +0 -0
- {namecheap_python-1.3.0.dist-info → namecheap_python-1.4.0.dist-info}/entry_points.txt +0 -0
- {namecheap_python-1.3.0.dist-info → namecheap_python-1.4.0.dist-info}/licenses/LICENSE +0 -0
namecheap/__init__.py
CHANGED
|
@@ -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.
|
|
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
|
]
|
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
|
|
|
@@ -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
|
+
}
|
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
|
@@ -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
|
|
namecheap_cli/__main__.py
CHANGED
|
@@ -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."""
|
|
@@ -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
|
|
@@ -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.
|
|
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
|
|
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
|
|
|
@@ -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
8
|
namecheap/_api/dns.py,sha256=Hny5TsVWmmG-3rF6kb8JwboGFt2wKsd4-Z6T8GannBM,17213
|
|
9
|
-
namecheap/_api/domains.py,sha256=
|
|
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
|