namecheap-python 1.1.0__py3-none-any.whl → 1.3.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 +16 -2
- namecheap/_api/dns.py +73 -1
- namecheap/_api/domains.py +93 -1
- namecheap/_api/users.py +32 -0
- namecheap/client.py +8 -0
- namecheap/models.py +71 -0
- namecheap_cli/__main__.py +183 -50
- {namecheap_python-1.1.0.dist-info → namecheap_python-1.3.0.dist-info}/METADATA +96 -13
- {namecheap_python-1.1.0.dist-info → namecheap_python-1.3.0.dist-info}/RECORD +12 -11
- {namecheap_python-1.1.0.dist-info → namecheap_python-1.3.0.dist-info}/WHEEL +0 -0
- {namecheap_python-1.1.0.dist-info → namecheap_python-1.3.0.dist-info}/entry_points.txt +0 -0
- {namecheap_python-1.1.0.dist-info → namecheap_python-1.3.0.dist-info}/licenses/LICENSE +0 -0
namecheap/__init__.py
CHANGED
|
@@ -12,15 +12,29 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
from .client import Namecheap
|
|
14
14
|
from .errors import ConfigurationError, NamecheapError, ValidationError
|
|
15
|
-
from .models import
|
|
15
|
+
from .models import (
|
|
16
|
+
AccountBalance,
|
|
17
|
+
Contact,
|
|
18
|
+
DNSRecord,
|
|
19
|
+
Domain,
|
|
20
|
+
DomainCheck,
|
|
21
|
+
DomainContacts,
|
|
22
|
+
DomainInfo,
|
|
23
|
+
EmailForward,
|
|
24
|
+
Nameservers,
|
|
25
|
+
)
|
|
16
26
|
|
|
17
|
-
__version__ = "1.
|
|
27
|
+
__version__ = "1.3.0"
|
|
18
28
|
__all__ = [
|
|
29
|
+
"AccountBalance",
|
|
19
30
|
"ConfigurationError",
|
|
20
31
|
"Contact",
|
|
21
32
|
"DNSRecord",
|
|
22
33
|
"Domain",
|
|
23
34
|
"DomainCheck",
|
|
35
|
+
"DomainContacts",
|
|
36
|
+
"DomainInfo",
|
|
37
|
+
"EmailForward",
|
|
24
38
|
"Namecheap",
|
|
25
39
|
"NamecheapError",
|
|
26
40
|
"Nameservers",
|
namecheap/_api/dns.py
CHANGED
|
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any, Literal
|
|
|
6
6
|
|
|
7
7
|
import tldextract
|
|
8
8
|
|
|
9
|
-
from namecheap.models import DNSRecord, Nameservers
|
|
9
|
+
from namecheap.models import DNSRecord, EmailForward, Nameservers
|
|
10
10
|
|
|
11
11
|
from .base import BaseAPI
|
|
12
12
|
|
|
@@ -494,3 +494,75 @@ class DnsAPI(BaseAPI):
|
|
|
494
494
|
nameservers = [ns_data] if isinstance(ns_data, str) else ns_data
|
|
495
495
|
|
|
496
496
|
return Nameservers(is_default=is_default, nameservers=nameservers)
|
|
497
|
+
|
|
498
|
+
def get_email_forwarding(self, domain: str) -> list[EmailForward]:
|
|
499
|
+
"""
|
|
500
|
+
Get email forwarding rules for a domain.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
domain: Domain name
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
List of EmailForward rules
|
|
507
|
+
|
|
508
|
+
Examples:
|
|
509
|
+
>>> rules = nc.dns.get_email_forwarding("example.com")
|
|
510
|
+
>>> for r in rules:
|
|
511
|
+
... print(f"{r.mailbox} -> {r.forward_to}")
|
|
512
|
+
"""
|
|
513
|
+
result: Any = self._request(
|
|
514
|
+
"namecheap.domains.dns.getEmailForwarding",
|
|
515
|
+
{"DomainName": domain},
|
|
516
|
+
path="DomainDNSGetEmailForwardingResult",
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
if not result:
|
|
520
|
+
return []
|
|
521
|
+
|
|
522
|
+
forwards = result.get("Forward", [])
|
|
523
|
+
if isinstance(forwards, dict):
|
|
524
|
+
forwards = [forwards]
|
|
525
|
+
assert isinstance(forwards, list), f"Unexpected Forward type: {type(forwards)}"
|
|
526
|
+
|
|
527
|
+
return [
|
|
528
|
+
EmailForward(mailbox=f.get("@mailbox", ""), forward_to=f.get("#text", ""))
|
|
529
|
+
for f in forwards
|
|
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,7 @@ 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
|
|
12
|
+
from namecheap.models import Contact, Domain, DomainCheck, DomainContacts, DomainInfo
|
|
13
13
|
|
|
14
14
|
from .base import BaseAPI
|
|
15
15
|
|
|
@@ -110,6 +110,98 @@ class DomainsAPI(BaseAPI):
|
|
|
110
110
|
return [results]
|
|
111
111
|
return results if isinstance(results, list) else []
|
|
112
112
|
|
|
113
|
+
def get_info(self, domain: str) -> DomainInfo:
|
|
114
|
+
"""
|
|
115
|
+
Get detailed information about a domain.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
domain: Domain name
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
DomainInfo with status, whoisguard, DNS provider, etc.
|
|
122
|
+
|
|
123
|
+
Examples:
|
|
124
|
+
>>> info = nc.domains.get_info("example.com")
|
|
125
|
+
>>> print(f"{info.domain} status={info.status} whoisguard={info.whoisguard_enabled}")
|
|
126
|
+
"""
|
|
127
|
+
result: Any = self._request(
|
|
128
|
+
"namecheap.domains.getInfo",
|
|
129
|
+
{"DomainName": domain},
|
|
130
|
+
path="DomainGetInfoResult",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
assert result, f"API returned empty result for {domain} getInfo"
|
|
134
|
+
|
|
135
|
+
# Extract nested fields into flat structure
|
|
136
|
+
domain_details = result.get("DomainDetails", {})
|
|
137
|
+
whoisguard = result.get("Whoisguard", {})
|
|
138
|
+
dns_details = result.get("DnsDetails", {})
|
|
139
|
+
|
|
140
|
+
flat = {
|
|
141
|
+
"@ID": result.get("@ID"),
|
|
142
|
+
"@DomainName": result.get("@DomainName"),
|
|
143
|
+
"@OwnerName": result.get("@OwnerName"),
|
|
144
|
+
"@IsOwner": result.get("@IsOwner"),
|
|
145
|
+
"@IsPremium": result.get("@IsPremium", "false"),
|
|
146
|
+
"@Status": result.get("@Status"),
|
|
147
|
+
"created": domain_details.get("CreatedDate"),
|
|
148
|
+
"expires": domain_details.get("ExpiredDate"),
|
|
149
|
+
"whoisguard_enabled": whoisguard.get("@Enabled", "false").lower() == "true"
|
|
150
|
+
if isinstance(whoisguard, dict)
|
|
151
|
+
else False,
|
|
152
|
+
"dns_provider": dns_details.get("@ProviderType")
|
|
153
|
+
if isinstance(dns_details, dict)
|
|
154
|
+
else None,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return DomainInfo.model_validate(flat)
|
|
158
|
+
|
|
159
|
+
def get_contacts(self, domain: str) -> DomainContacts:
|
|
160
|
+
"""
|
|
161
|
+
Get contact information for a domain.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
domain: Domain name
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
DomainContacts with registrant, tech, admin, and aux_billing contacts
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
>>> contacts = nc.domains.get_contacts("example.com")
|
|
171
|
+
>>> print(contacts.registrant.email)
|
|
172
|
+
"""
|
|
173
|
+
result: Any = self._request(
|
|
174
|
+
"namecheap.domains.getContacts",
|
|
175
|
+
{"DomainName": domain},
|
|
176
|
+
path="DomainContactsResult",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
assert result, f"API returned empty result for {domain} getContacts"
|
|
180
|
+
|
|
181
|
+
def parse_contact(data: dict[str, Any]) -> Contact:
|
|
182
|
+
return Contact.model_validate(
|
|
183
|
+
{
|
|
184
|
+
"FirstName": data.get("FirstName", ""),
|
|
185
|
+
"LastName": data.get("LastName", ""),
|
|
186
|
+
"Organization": data.get("Organization"),
|
|
187
|
+
"Address1": data.get("Address1", ""),
|
|
188
|
+
"Address2": data.get("Address2"),
|
|
189
|
+
"City": data.get("City", ""),
|
|
190
|
+
"StateProvince": data.get("StateProvince", ""),
|
|
191
|
+
"PostalCode": data.get("PostalCode", ""),
|
|
192
|
+
"Country": data.get("Country", ""),
|
|
193
|
+
"Phone": data.get("Phone", ""),
|
|
194
|
+
"EmailAddress": data.get("EmailAddress", ""),
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
return DomainContacts(
|
|
199
|
+
registrant=parse_contact(result.get("Registrant", {})),
|
|
200
|
+
tech=parse_contact(result.get("Tech", {})),
|
|
201
|
+
admin=parse_contact(result.get("Admin", {})),
|
|
202
|
+
aux_billing=parse_contact(result.get("AuxBilling", {})),
|
|
203
|
+
)
|
|
204
|
+
|
|
113
205
|
def register(
|
|
114
206
|
self,
|
|
115
207
|
domain: str,
|
namecheap/_api/users.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Users API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from namecheap.models import AccountBalance
|
|
8
|
+
|
|
9
|
+
from .base import BaseAPI
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UsersAPI(BaseAPI):
|
|
13
|
+
"""User account operations."""
|
|
14
|
+
|
|
15
|
+
def get_balances(self) -> AccountBalance:
|
|
16
|
+
"""
|
|
17
|
+
Get account balance information.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
AccountBalance with available balance, earned amount, etc.
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
>>> bal = nc.users.get_balances()
|
|
24
|
+
>>> print(f"{bal.available_balance} {bal.currency}")
|
|
25
|
+
"""
|
|
26
|
+
result: Any = self._request(
|
|
27
|
+
"namecheap.users.getBalances",
|
|
28
|
+
path="UserGetBalancesResult",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
assert result, "API returned empty result for getBalances"
|
|
32
|
+
return AccountBalance.model_validate(result)
|
namecheap/client.py
CHANGED
|
@@ -16,6 +16,7 @@ if TYPE_CHECKING:
|
|
|
16
16
|
|
|
17
17
|
from ._api.dns import DnsAPI
|
|
18
18
|
from ._api.domains import DomainsAPI
|
|
19
|
+
from ._api.users import UsersAPI
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class Namecheap:
|
|
@@ -110,6 +111,13 @@ class Namecheap:
|
|
|
110
111
|
|
|
111
112
|
return DnsAPI(self)
|
|
112
113
|
|
|
114
|
+
@cached_property
|
|
115
|
+
def users(self) -> UsersAPI:
|
|
116
|
+
"""User account operations."""
|
|
117
|
+
from ._api.users import UsersAPI
|
|
118
|
+
|
|
119
|
+
return UsersAPI(self)
|
|
120
|
+
|
|
113
121
|
def __enter__(self) -> Self:
|
|
114
122
|
"""Enter context manager."""
|
|
115
123
|
return self
|
namecheap/models.py
CHANGED
|
@@ -251,6 +251,66 @@ class Domain(XMLModel):
|
|
|
251
251
|
raise ValueError(f"Cannot parse datetime from {v}")
|
|
252
252
|
|
|
253
253
|
|
|
254
|
+
class AccountBalance(BaseModel):
|
|
255
|
+
"""Account balance information."""
|
|
256
|
+
|
|
257
|
+
currency: str = Field(alias="@Currency")
|
|
258
|
+
available_balance: Decimal = Field(alias="@AvailableBalance")
|
|
259
|
+
account_balance: Decimal = Field(alias="@AccountBalance")
|
|
260
|
+
earned_amount: Decimal = Field(alias="@EarnedAmount")
|
|
261
|
+
withdrawable_amount: Decimal = Field(alias="@WithdrawableAmount")
|
|
262
|
+
funds_required_for_auto_renew: Decimal = Field(alias="@FundsRequiredForAutoRenew")
|
|
263
|
+
|
|
264
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
265
|
+
|
|
266
|
+
@field_validator(
|
|
267
|
+
"available_balance",
|
|
268
|
+
"account_balance",
|
|
269
|
+
"earned_amount",
|
|
270
|
+
"withdrawable_amount",
|
|
271
|
+
"funds_required_for_auto_renew",
|
|
272
|
+
mode="before",
|
|
273
|
+
)
|
|
274
|
+
@classmethod
|
|
275
|
+
def parse_decimal(cls, v: Any) -> Decimal:
|
|
276
|
+
return Decimal(str(v)) if v is not None else Decimal("0")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class DomainInfo(BaseModel):
|
|
280
|
+
"""Detailed domain information from getInfo."""
|
|
281
|
+
|
|
282
|
+
id: int = Field(alias="@ID")
|
|
283
|
+
domain: str = Field(alias="@DomainName")
|
|
284
|
+
owner: str = Field(alias="@OwnerName")
|
|
285
|
+
is_owner: bool = Field(alias="@IsOwner")
|
|
286
|
+
is_premium: bool = Field(alias="@IsPremium", default=False)
|
|
287
|
+
status: str = Field(alias="@Status")
|
|
288
|
+
created: str | None = Field(default=None)
|
|
289
|
+
expires: str | None = Field(default=None)
|
|
290
|
+
whoisguard_enabled: bool = Field(default=False)
|
|
291
|
+
dns_provider: str | None = Field(default=None)
|
|
292
|
+
|
|
293
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
294
|
+
|
|
295
|
+
@field_validator("is_owner", "is_premium", mode="before")
|
|
296
|
+
@classmethod
|
|
297
|
+
def parse_bool(cls, v: Any) -> bool:
|
|
298
|
+
if isinstance(v, bool):
|
|
299
|
+
return v
|
|
300
|
+
if isinstance(v, str):
|
|
301
|
+
return v.lower() == "true"
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class EmailForward(BaseModel):
|
|
306
|
+
"""Email forwarding rule."""
|
|
307
|
+
|
|
308
|
+
mailbox: str = Field(alias="@mailbox")
|
|
309
|
+
forward_to: str
|
|
310
|
+
|
|
311
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
312
|
+
|
|
313
|
+
|
|
254
314
|
class Nameservers(BaseModel):
|
|
255
315
|
"""Current nameserver configuration for a domain."""
|
|
256
316
|
|
|
@@ -276,6 +336,17 @@ class Contact(BaseModel):
|
|
|
276
336
|
model_config = ConfigDict(populate_by_name=True)
|
|
277
337
|
|
|
278
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
|
+
|
|
279
350
|
class Config(BaseModel):
|
|
280
351
|
"""Client configuration with validation."""
|
|
281
352
|
|
namecheap_cli/__main__.py
CHANGED
|
@@ -367,57 +367,31 @@ def domain_info(config: Config, domain: str) -> None:
|
|
|
367
367
|
nc = config.init_client()
|
|
368
368
|
|
|
369
369
|
try:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
370
|
+
with Progress(
|
|
371
|
+
SpinnerColumn(),
|
|
372
|
+
TextColumn("[progress.description]{task.description}"),
|
|
373
|
+
transient=True,
|
|
374
|
+
) as progress:
|
|
375
|
+
progress.add_task(f"Getting info for {domain}...", total=None)
|
|
376
|
+
info = nc.domains.get_info(domain)
|
|
376
377
|
|
|
377
378
|
if config.output_format == "table":
|
|
378
379
|
console.print(f"\n[bold cyan]Domain Information: {domain}[/bold cyan]\n")
|
|
379
|
-
console.print(
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
f"[bold]
|
|
385
|
-
)
|
|
386
|
-
console.print(
|
|
387
|
-
f"[bold]Expires:[/bold] {domain_obj.expires.strftime('%Y-%m-%d')}"
|
|
388
|
-
)
|
|
389
|
-
console.print(
|
|
390
|
-
f"[bold]Auto-Renew:[/bold] {'✓ Enabled' if domain_obj.auto_renew else '✗ Disabled'}"
|
|
391
|
-
)
|
|
392
|
-
console.print(
|
|
393
|
-
f"[bold]Locked:[/bold] {'🔒 Yes' if domain_obj.is_locked else '🔓 No'}"
|
|
394
|
-
)
|
|
380
|
+
console.print(f"[bold]Status:[/bold] {info.status}")
|
|
381
|
+
console.print(f"[bold]Owner:[/bold] {info.owner}")
|
|
382
|
+
if info.created:
|
|
383
|
+
console.print(f"[bold]Created:[/bold] {info.created}")
|
|
384
|
+
if info.expires:
|
|
385
|
+
console.print(f"[bold]Expires:[/bold] {info.expires}")
|
|
386
|
+
console.print(f"[bold]Premium:[/bold] {'Yes' if info.is_premium else 'No'}")
|
|
395
387
|
console.print(
|
|
396
388
|
f"[bold]WHOIS Guard:[/bold] "
|
|
397
|
-
f"{'✓ Enabled' if
|
|
389
|
+
f"{'✓ Enabled' if info.whoisguard_enabled else '✗ Disabled'}"
|
|
398
390
|
)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
days_left = (domain_obj.expires - datetime.now()).days
|
|
402
|
-
if days_left < 30:
|
|
403
|
-
console.print(
|
|
404
|
-
f"\n⚠️ [yellow]Domain expires in {days_left} days![/yellow]"
|
|
405
|
-
)
|
|
406
|
-
elif days_left < 60:
|
|
407
|
-
console.print(f"\n📅 Domain expires in {days_left} days")
|
|
408
|
-
|
|
391
|
+
if info.dns_provider:
|
|
392
|
+
console.print(f"[bold]DNS Provider:[/bold] {info.dns_provider}")
|
|
409
393
|
else:
|
|
410
|
-
|
|
411
|
-
"domain": domain_obj.name,
|
|
412
|
-
"status": "active" if not domain_obj.is_expired else "expired",
|
|
413
|
-
"created": domain_obj.created.isoformat(),
|
|
414
|
-
"expires": domain_obj.expires.isoformat(),
|
|
415
|
-
"auto_renew": domain_obj.auto_renew,
|
|
416
|
-
"locked": domain_obj.is_locked,
|
|
417
|
-
"whois_guard": domain_obj.whois_guard,
|
|
418
|
-
"days_until_expiration": (domain_obj.expires - datetime.now()).days,
|
|
419
|
-
}
|
|
420
|
-
output_formatter(data, config.output_format)
|
|
394
|
+
output_formatter(info.model_dump(), config.output_format)
|
|
421
395
|
|
|
422
396
|
except NamecheapError as e:
|
|
423
397
|
console.print(f"[red]❌ Error: {e}[/red]")
|
|
@@ -906,6 +880,142 @@ def dns_export(config: Config, domain: str, format: str, output) -> None:
|
|
|
906
880
|
sys.exit(1)
|
|
907
881
|
|
|
908
882
|
|
|
883
|
+
@dns_group.command("email-forwarding")
|
|
884
|
+
@click.argument("domain")
|
|
885
|
+
@pass_config
|
|
886
|
+
def dns_email_forwarding(config: Config, domain: str) -> None:
|
|
887
|
+
"""Show email forwarding rules for a domain."""
|
|
888
|
+
nc = config.init_client()
|
|
889
|
+
|
|
890
|
+
try:
|
|
891
|
+
with Progress(
|
|
892
|
+
SpinnerColumn(),
|
|
893
|
+
TextColumn("[progress.description]{task.description}"),
|
|
894
|
+
transient=True,
|
|
895
|
+
) as progress:
|
|
896
|
+
progress.add_task(f"Getting email forwarding for {domain}...", total=None)
|
|
897
|
+
rules = nc.dns.get_email_forwarding(domain)
|
|
898
|
+
|
|
899
|
+
if config.output_format == "table":
|
|
900
|
+
if not rules:
|
|
901
|
+
console.print(
|
|
902
|
+
f"\n[yellow]No email forwarding rules for {domain}[/yellow]"
|
|
903
|
+
)
|
|
904
|
+
return
|
|
905
|
+
|
|
906
|
+
table = Table(title=f"Email Forwarding for {domain}")
|
|
907
|
+
table.add_column("Mailbox", style="cyan")
|
|
908
|
+
table.add_column("Forwards To", style="green")
|
|
909
|
+
|
|
910
|
+
for rule in rules:
|
|
911
|
+
table.add_row(f"{rule.mailbox}@{domain}", rule.forward_to)
|
|
912
|
+
|
|
913
|
+
console.print(table)
|
|
914
|
+
else:
|
|
915
|
+
output_formatter([r.model_dump() for r in rules], config.output_format)
|
|
916
|
+
|
|
917
|
+
except NamecheapError as e:
|
|
918
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
919
|
+
sys.exit(1)
|
|
920
|
+
|
|
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
|
+
|
|
909
1019
|
@cli.group("account")
|
|
910
1020
|
def account_group() -> None:
|
|
911
1021
|
"""Account management commands."""
|
|
@@ -916,14 +1026,37 @@ def account_group() -> None:
|
|
|
916
1026
|
@pass_config
|
|
917
1027
|
def account_balance(config: Config) -> None:
|
|
918
1028
|
"""Check account balance."""
|
|
919
|
-
config.init_client()
|
|
1029
|
+
nc = config.init_client()
|
|
920
1030
|
|
|
921
1031
|
try:
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
"[
|
|
925
|
-
|
|
926
|
-
|
|
1032
|
+
with Progress(
|
|
1033
|
+
SpinnerColumn(),
|
|
1034
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1035
|
+
transient=True,
|
|
1036
|
+
) as progress:
|
|
1037
|
+
progress.add_task("Getting account balance...", total=None)
|
|
1038
|
+
bal = nc.users.get_balances()
|
|
1039
|
+
|
|
1040
|
+
if config.output_format == "table":
|
|
1041
|
+
table = Table(title="Account Balance")
|
|
1042
|
+
table.add_column("Field", style="cyan")
|
|
1043
|
+
table.add_column("Amount", style="green", justify="right")
|
|
1044
|
+
|
|
1045
|
+
table.add_row(
|
|
1046
|
+
"Available Balance", f"{bal.available_balance} {bal.currency}"
|
|
1047
|
+
)
|
|
1048
|
+
table.add_row("Account Balance", f"{bal.account_balance} {bal.currency}")
|
|
1049
|
+
table.add_row("Earned Amount", f"{bal.earned_amount} {bal.currency}")
|
|
1050
|
+
table.add_row("Withdrawable", f"{bal.withdrawable_amount} {bal.currency}")
|
|
1051
|
+
if bal.funds_required_for_auto_renew > 0:
|
|
1052
|
+
table.add_row(
|
|
1053
|
+
"Auto-Renew Required",
|
|
1054
|
+
f"{bal.funds_required_for_auto_renew} {bal.currency}",
|
|
1055
|
+
)
|
|
1056
|
+
|
|
1057
|
+
console.print(table)
|
|
1058
|
+
else:
|
|
1059
|
+
output_formatter(bal.model_dump(mode="json"), config.output_format)
|
|
927
1060
|
|
|
928
1061
|
except NamecheapError as e:
|
|
929
1062
|
console.print(f"[red]❌ Error: {e}[/red]")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: namecheap-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.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
|
|
@@ -42,6 +42,12 @@ Description-Content-Type: text/markdown
|
|
|
42
42
|
|
|
43
43
|
# Namecheap Python SDK
|
|
44
44
|
|
|
45
|
+
[](https://pypi.org/project/namecheap-python/)
|
|
46
|
+
[](https://pepy.tech/project/namecheap-python)
|
|
47
|
+
[](https://pepy.tech/project/namecheap-python)
|
|
48
|
+
[](https://pypi.org/project/namecheap-python/)
|
|
49
|
+
[](https://opensource.org/licenses/MIT)
|
|
50
|
+
|
|
45
51
|
A modern, friendly Python SDK for the Namecheap API with comprehensive CLI and TUI tools.
|
|
46
52
|
|
|
47
53
|
## 🚀 Features
|
|
@@ -206,6 +212,38 @@ Adding CNAME record to tdo.garden...
|
|
|
206
212
|
```
|
|
207
213
|
|
|
208
214
|
|
|
215
|
+
Check account balance:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
❯ namecheap-cli account balance
|
|
219
|
+
Account Balance
|
|
220
|
+
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
|
|
221
|
+
┃ Field ┃ Amount ┃
|
|
222
|
+
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
|
|
223
|
+
│ Available Balance │ 0.00 USD │
|
|
224
|
+
│ Account Balance │ 0.00 USD │
|
|
225
|
+
│ Earned Amount │ 0.00 USD │
|
|
226
|
+
│ Withdrawable │ 0.00 USD │
|
|
227
|
+
│ Auto-Renew Required │ 20.16 USD │
|
|
228
|
+
└─────────────────────┴───────────┘
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Get detailed domain info:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
❯ namecheap-cli domain info self.fm
|
|
235
|
+
|
|
236
|
+
Domain Information: self.fm
|
|
237
|
+
|
|
238
|
+
Status: Ok
|
|
239
|
+
Owner: adriangalilea
|
|
240
|
+
Created: 07/15/2023
|
|
241
|
+
Expires: 07/15/2026
|
|
242
|
+
Premium: No
|
|
243
|
+
WHOIS Guard: ✓ Enabled
|
|
244
|
+
DNS Provider: CUSTOM
|
|
245
|
+
```
|
|
246
|
+
|
|
209
247
|
You can also export DNS records:
|
|
210
248
|
|
|
211
249
|
```bash
|
|
@@ -320,6 +358,48 @@ nc.dns.set_custom_nameservers("example.com", [
|
|
|
320
358
|
nc.dns.set_default_nameservers("example.com")
|
|
321
359
|
```
|
|
322
360
|
|
|
361
|
+
### Domain Info
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
info = nc.domains.get_info("example.com")
|
|
365
|
+
print(info.status) # 'Ok'
|
|
366
|
+
print(info.whoisguard_enabled) # True
|
|
367
|
+
print(info.dns_provider) # 'CUSTOM'
|
|
368
|
+
print(info.created) # '07/15/2023'
|
|
369
|
+
print(info.expires) # '07/15/2026'
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Account Balance
|
|
373
|
+
|
|
374
|
+
```python
|
|
375
|
+
bal = nc.users.get_balances()
|
|
376
|
+
print(f"{bal.available_balance} {bal.currency}") # '4932.96 USD'
|
|
377
|
+
print(bal.funds_required_for_auto_renew) # Decimal('20.16')
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Email Forwarding
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
# Read
|
|
384
|
+
rules = nc.dns.get_email_forwarding("example.com")
|
|
385
|
+
for r in rules:
|
|
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
|
+
|
|
323
403
|
### Domain Management
|
|
324
404
|
|
|
325
405
|
```python
|
|
@@ -393,22 +473,23 @@ nc.dns.builder().a("www", "192.0.2.1", ttl=1799) # Shows as "Automatic"
|
|
|
393
473
|
nc.dns.builder().a("www", "192.0.2.1", ttl=1800) # Shows as "30 min"
|
|
394
474
|
```
|
|
395
475
|
|
|
396
|
-
##
|
|
397
|
-
|
|
398
|
-
The following Namecheap API features are planned for future releases:
|
|
399
|
-
|
|
400
|
-
- **SSL API** - Certificate management
|
|
401
|
-
- **Domain Transfer API** - Transfer domains between registrars
|
|
402
|
-
- **Domain NS API** - Glue record management (child nameservers)
|
|
403
|
-
- **Users API** - Account management and balance checking
|
|
404
|
-
- **Whois API** - WHOIS information lookups
|
|
405
|
-
- **Email Forwarding** - Email forwarding configuration
|
|
476
|
+
## 📊 [API Coverage](https://www.namecheap.com/support/api/methods/)
|
|
406
477
|
|
|
407
|
-
|
|
478
|
+
| API | Status | Methods |
|
|
479
|
+
|-----|--------|---------|
|
|
480
|
+
| `namecheap.domains.*` | ✅ Done | `check`, `list`, `getInfo`, `getContacts`, `register`, `renew`, `setContacts`, `lock`/`unlock` |
|
|
481
|
+
| `namecheap.domains.dns.*` | ✅ Done | `getHosts`, `setHosts` (builder pattern), `add`, `delete`, `export`, `getList`, `setCustom`, `setDefault`, `getEmailForwarding`, `setEmailForwarding` |
|
|
482
|
+
| `namecheap.users.*` | ⚠️ Partial | `getBalances`, `getPricing` (needs debugging). Planned: `changePassword`, `update`, `create`, `login`, `resetPassword` |
|
|
483
|
+
| `namecheap.domains.*` | 🚧 Planned | `getTldList`, `reactivate` |
|
|
484
|
+
| `namecheap.users.address.*` | 🚧 Planned | `create`, `delete`, `getInfo`, `getList`, `setDefault`, `update` |
|
|
485
|
+
| `namecheap.ssl.*` | 🚧 Planned | `create`, `activate`, `renew`, `revoke`, `getList`, `getInfo`, `parseCSR`, `reissue`, and more |
|
|
486
|
+
| `namecheap.domains.transfer.*` | 🚧 Planned | `create`, `getStatus`, `updateStatus`, `getList` |
|
|
487
|
+
| `namecheap.domains.ns.*` | 🚧 Planned | Glue records — `create`, `delete`, `getInfo`, `update` |
|
|
488
|
+
| `namecheap.domainprivacy.*` | 🚧 Planned | `enable`, `disable`, `renew`, `getList`, `changeemailaddress` |
|
|
408
489
|
|
|
409
490
|
## 🛠️ Development
|
|
410
491
|
|
|
411
|
-
See [
|
|
492
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and development guidelines.
|
|
412
493
|
|
|
413
494
|
## 📝 License
|
|
414
495
|
|
|
@@ -420,4 +501,6 @@ Contributions are welcome! Please feel free to submit a Pull Request. See the [D
|
|
|
420
501
|
|
|
421
502
|
### Contributors
|
|
422
503
|
|
|
504
|
+
- [@huntertur](https://github.com/huntertur) — Rich dependency fix
|
|
505
|
+
- [@jeffmcadams](https://github.com/jeffmcadams) — Domain serialization round-trip
|
|
423
506
|
- [@cosmin](https://github.com/cosmin) — Nameserver management
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
namecheap/__init__.py,sha256=
|
|
2
|
-
namecheap/client.py,sha256=
|
|
1
|
+
namecheap/__init__.py,sha256=sUX2-G_o6k1POjfJ9oV32Hi08zaPNL4WwfgrLr_2Qak,879
|
|
2
|
+
namecheap/client.py,sha256=KesIaZVa9HpXvlp-hc3nr2x-sqzMPiHEa8NibiPq580,6644
|
|
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=0Fhni7yKd84bBopPURIvhwmOCYMzw8tlZwHWzRRZphY,14494
|
|
6
6
|
namecheap/_api/__init__.py,sha256=ymQxKCySphoeoo4s_J0tLziXttLNhOQ8AZbCzFcuAHs,36
|
|
7
7
|
namecheap/_api/base.py,sha256=FoczO1Q860PaFUFv-S3IoIV2xaGVJAlchkWnmTI6dlw,6121
|
|
8
|
-
namecheap/_api/dns.py,sha256=
|
|
9
|
-
namecheap/_api/domains.py,sha256=
|
|
8
|
+
namecheap/_api/dns.py,sha256=Hny5TsVWmmG-3rF6kb8JwboGFt2wKsd4-Z6T8GannBM,17213
|
|
9
|
+
namecheap/_api/domains.py,sha256=txikTYACM82-3IXg2Gqagh9fRZ8aIZiu05unNpy2Zqs,20039
|
|
10
|
+
namecheap/_api/users.py,sha256=CCXSZJiPkQiLHYRAlYKTBCDG3-JSPdNkNWWww71JXV0,795
|
|
10
11
|
namecheap_cli/README.md,sha256=liduIiGr8DHXGTht5swrYnvtAlcdCMQOnSdCD61g4Vw,7337
|
|
11
12
|
namecheap_cli/__init__.py,sha256=nGRHc_CkO4xKhSQdAVG-koEffP8VS0TvbfbZkg7Jg4k,108
|
|
12
|
-
namecheap_cli/__main__.py,sha256=
|
|
13
|
+
namecheap_cli/__main__.py,sha256=0vHk5aAhEzPKQ-UQOQdYqsolCF13DG3k9_z3pUbZXwk,40020
|
|
13
14
|
namecheap_cli/completion.py,sha256=JTEMnceQli7TombjZkHh-IcZKW4RFRI8Yk5VynxPsEA,2777
|
|
14
15
|
namecheap_dns_tui/README.md,sha256=It16ZiZh0haEeaENfF5HX0Ec4dBawdTYiAi-TiG9wi0,1690
|
|
15
16
|
namecheap_dns_tui/__init__.py,sha256=-yL_1Ha41FlQcmjG-raUrZP9CjTJD3d0w2BW2X-twJg,106
|
|
@@ -18,8 +19,8 @@ namecheap_dns_tui/assets/screenshot1.png,sha256=OXO2P80ll5WRzLYgaakcNnzos8svlJoX
|
|
|
18
19
|
namecheap_dns_tui/assets/screenshot2.png,sha256=5VN_qDMNhWEyrOqKw7vxl1h-TgmZQ_V9aph3Xmf_AFg,279194
|
|
19
20
|
namecheap_dns_tui/assets/screenshot3.png,sha256=h39wSKxx1JCkgeAB7Q3_JlBcAtX1vsRFKtWtOwbBVso,220625
|
|
20
21
|
namecheap_dns_tui/assets/screenshot4.png,sha256=J4nCOW16z3vaRiPbcMiiIRgV7q3XFbi_1N1ivD1Pa4Y,238068
|
|
21
|
-
namecheap_python-1.
|
|
22
|
-
namecheap_python-1.
|
|
23
|
-
namecheap_python-1.
|
|
24
|
-
namecheap_python-1.
|
|
25
|
-
namecheap_python-1.
|
|
22
|
+
namecheap_python-1.3.0.dist-info/METADATA,sha256=phjrSgjok2JzRDjOQsEit1eb9Z7WF7Da5beLiKSEbGA,18802
|
|
23
|
+
namecheap_python-1.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
24
|
+
namecheap_python-1.3.0.dist-info/entry_points.txt,sha256=AyhiXroLUpM0Vdo_-RvH0S8o4XDPsDlsEl_65vm6DEk,96
|
|
25
|
+
namecheap_python-1.3.0.dist-info/licenses/LICENSE,sha256=pemTblFP6BBje3bBv_yL_sr2iAqB2H0-LdWMvVIR42o,1062
|
|
26
|
+
namecheap_python-1.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|