namecheap-python 1.0.6__py3-none-any.whl → 1.2.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 +15 -2
- namecheap/_api/dns.py +148 -1
- namecheap/_api/domains.py +47 -1
- namecheap/_api/users.py +32 -0
- namecheap/client.py +8 -0
- namecheap/models.py +67 -0
- namecheap_cli/__main__.py +211 -50
- {namecheap_python-1.0.6.dist-info → namecheap_python-1.2.0.dist-info}/METADATA +105 -14
- {namecheap_python-1.0.6.dist-info → namecheap_python-1.2.0.dist-info}/RECORD +12 -11
- {namecheap_python-1.0.6.dist-info → namecheap_python-1.2.0.dist-info}/WHEEL +1 -1
- {namecheap_python-1.0.6.dist-info → namecheap_python-1.2.0.dist-info}/entry_points.txt +0 -0
- {namecheap_python-1.0.6.dist-info → namecheap_python-1.2.0.dist-info}/licenses/LICENSE +0 -0
namecheap/__init__.py
CHANGED
|
@@ -12,16 +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
|
+
DomainInfo,
|
|
22
|
+
EmailForward,
|
|
23
|
+
Nameservers,
|
|
24
|
+
)
|
|
16
25
|
|
|
17
|
-
__version__ = "1.0
|
|
26
|
+
__version__ = "1.2.0"
|
|
18
27
|
__all__ = [
|
|
28
|
+
"AccountBalance",
|
|
19
29
|
"ConfigurationError",
|
|
20
30
|
"Contact",
|
|
21
31
|
"DNSRecord",
|
|
22
32
|
"Domain",
|
|
23
33
|
"DomainCheck",
|
|
34
|
+
"DomainInfo",
|
|
35
|
+
"EmailForward",
|
|
24
36
|
"Namecheap",
|
|
25
37
|
"NamecheapError",
|
|
38
|
+
"Nameservers",
|
|
26
39
|
"ValidationError",
|
|
27
40
|
]
|
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
|
|
9
|
+
from namecheap.models import DNSRecord, EmailForward, Nameservers
|
|
10
10
|
|
|
11
11
|
from .base import BaseAPI
|
|
12
12
|
|
|
@@ -381,3 +381,150 @@ class DnsAPI(BaseAPI):
|
|
|
381
381
|
>>> nc.dns.set("example.com", builder)
|
|
382
382
|
"""
|
|
383
383
|
return DNSRecordBuilder()
|
|
384
|
+
|
|
385
|
+
def set_custom_nameservers(self, domain: str, nameservers: list[str]) -> bool:
|
|
386
|
+
"""
|
|
387
|
+
Set custom nameservers for a domain.
|
|
388
|
+
|
|
389
|
+
This switches the domain from Namecheap's default DNS to custom
|
|
390
|
+
nameservers (e.g., Route 53, Cloudflare, etc.).
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
domain: Domain name
|
|
394
|
+
nameservers: List of nameserver hostnames (e.g., ["ns1.example.com", "ns2.example.com"])
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
True if successful
|
|
398
|
+
|
|
399
|
+
Examples:
|
|
400
|
+
>>> nc.dns.set_custom_nameservers("example.com", [
|
|
401
|
+
... "ns-123.awsdns-45.com",
|
|
402
|
+
... "ns-456.awsdns-67.net",
|
|
403
|
+
... "ns-789.awsdns-89.org",
|
|
404
|
+
... "ns-012.awsdns-12.co.uk",
|
|
405
|
+
... ])
|
|
406
|
+
"""
|
|
407
|
+
assert nameservers, "At least one nameserver is required"
|
|
408
|
+
assert len(nameservers) <= 5, "Maximum of 5 nameservers allowed"
|
|
409
|
+
|
|
410
|
+
ext = tldextract.extract(domain)
|
|
411
|
+
if not ext.domain or not ext.suffix:
|
|
412
|
+
raise ValueError(f"Invalid domain name: {domain}")
|
|
413
|
+
|
|
414
|
+
result: Any = self._request(
|
|
415
|
+
"namecheap.domains.dns.setCustom",
|
|
416
|
+
{
|
|
417
|
+
"SLD": ext.domain,
|
|
418
|
+
"TLD": ext.suffix,
|
|
419
|
+
"Nameservers": ",".join(nameservers),
|
|
420
|
+
},
|
|
421
|
+
path="DomainDNSSetCustomResult",
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
return bool(result and result.get("@Updated") == "true")
|
|
425
|
+
|
|
426
|
+
def set_default_nameservers(self, domain: str) -> bool:
|
|
427
|
+
"""
|
|
428
|
+
Reset domain to use Namecheap's default nameservers.
|
|
429
|
+
|
|
430
|
+
This switches the domain back to Namecheap BasicDNS from custom nameservers.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
domain: Domain name
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
True if successful
|
|
437
|
+
|
|
438
|
+
Examples:
|
|
439
|
+
>>> nc.dns.set_default_nameservers("example.com")
|
|
440
|
+
"""
|
|
441
|
+
ext = tldextract.extract(domain)
|
|
442
|
+
if not ext.domain or not ext.suffix:
|
|
443
|
+
raise ValueError(f"Invalid domain name: {domain}")
|
|
444
|
+
|
|
445
|
+
result: Any = self._request(
|
|
446
|
+
"namecheap.domains.dns.setDefault",
|
|
447
|
+
{
|
|
448
|
+
"SLD": ext.domain,
|
|
449
|
+
"TLD": ext.suffix,
|
|
450
|
+
},
|
|
451
|
+
path="DomainDNSSetDefaultResult",
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
return bool(result and result.get("@Updated") == "true")
|
|
455
|
+
|
|
456
|
+
def get_nameservers(self, domain: str) -> Nameservers:
|
|
457
|
+
"""
|
|
458
|
+
Get current nameservers for a domain.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
domain: Domain name
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Nameservers with is_default flag and nameserver hostnames
|
|
465
|
+
|
|
466
|
+
Examples:
|
|
467
|
+
>>> ns = nc.dns.get_nameservers("example.com")
|
|
468
|
+
>>> ns.is_default
|
|
469
|
+
True
|
|
470
|
+
>>> ns.nameservers
|
|
471
|
+
['dns1.registrar-servers.com', 'dns2.registrar-servers.com']
|
|
472
|
+
"""
|
|
473
|
+
ext = tldextract.extract(domain)
|
|
474
|
+
if not ext.domain or not ext.suffix:
|
|
475
|
+
raise ValueError(f"Invalid domain name: {domain}")
|
|
476
|
+
|
|
477
|
+
result: Any = self._request(
|
|
478
|
+
"namecheap.domains.dns.getList",
|
|
479
|
+
{
|
|
480
|
+
"SLD": ext.domain,
|
|
481
|
+
"TLD": ext.suffix,
|
|
482
|
+
},
|
|
483
|
+
path="DomainDNSGetListResult",
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
assert result, f"API returned empty result for {domain} nameserver query"
|
|
487
|
+
|
|
488
|
+
is_default = result.get("@IsUsingOurDNS", "false").lower() == "true"
|
|
489
|
+
|
|
490
|
+
ns_data = result.get("Nameserver", [])
|
|
491
|
+
assert isinstance(ns_data, str | list), (
|
|
492
|
+
f"Unexpected Nameserver type: {type(ns_data)}"
|
|
493
|
+
)
|
|
494
|
+
nameservers = [ns_data] if isinstance(ns_data, str) else ns_data
|
|
495
|
+
|
|
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
|
+
]
|
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, DomainInfo
|
|
13
13
|
|
|
14
14
|
from .base import BaseAPI
|
|
15
15
|
|
|
@@ -110,6 +110,52 @@ 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
|
+
|
|
113
159
|
def register(
|
|
114
160
|
self,
|
|
115
161
|
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,73 @@ 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
|
+
|
|
314
|
+
class Nameservers(BaseModel):
|
|
315
|
+
"""Current nameserver configuration for a domain."""
|
|
316
|
+
|
|
317
|
+
is_default: bool = Field(description="True when using Namecheap's own DNS")
|
|
318
|
+
nameservers: list[str] = Field(description="Nameserver hostnames")
|
|
319
|
+
|
|
320
|
+
|
|
254
321
|
class Contact(BaseModel):
|
|
255
322
|
"""Contact information for domain registration."""
|
|
256
323
|
|
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]")
|
|
@@ -700,6 +674,131 @@ def dns_delete(
|
|
|
700
674
|
sys.exit(1)
|
|
701
675
|
|
|
702
676
|
|
|
677
|
+
@dns_group.command("nameservers")
|
|
678
|
+
@click.argument("domain")
|
|
679
|
+
@pass_config
|
|
680
|
+
def dns_nameservers(config: Config, domain: str) -> None:
|
|
681
|
+
"""Show current nameserver configuration for a domain."""
|
|
682
|
+
nc = config.init_client()
|
|
683
|
+
|
|
684
|
+
try:
|
|
685
|
+
with Progress(
|
|
686
|
+
SpinnerColumn(),
|
|
687
|
+
TextColumn("[progress.description]{task.description}"),
|
|
688
|
+
transient=True,
|
|
689
|
+
) as progress:
|
|
690
|
+
progress.add_task(f"Getting nameserver info for {domain}...", total=None)
|
|
691
|
+
ns = nc.dns.get_nameservers(domain)
|
|
692
|
+
|
|
693
|
+
if config.output_format == "table":
|
|
694
|
+
console.print(f"\n[bold cyan]Nameservers for {domain}[/bold cyan]\n")
|
|
695
|
+
|
|
696
|
+
if ns.is_default:
|
|
697
|
+
console.print("[green]Using Namecheap BasicDNS[/green]")
|
|
698
|
+
else:
|
|
699
|
+
console.print("[yellow]Using custom nameservers:[/yellow]")
|
|
700
|
+
for nameserver in ns.nameservers:
|
|
701
|
+
console.print(f" • {nameserver}")
|
|
702
|
+
else:
|
|
703
|
+
output_formatter(ns.model_dump(), config.output_format)
|
|
704
|
+
|
|
705
|
+
except NamecheapError as e:
|
|
706
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
707
|
+
sys.exit(1)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
@dns_group.command("set-nameservers")
|
|
711
|
+
@click.argument("domain")
|
|
712
|
+
@click.argument("nameservers", nargs=-1, required=True)
|
|
713
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
|
|
714
|
+
@pass_config
|
|
715
|
+
def dns_set_nameservers(
|
|
716
|
+
config: Config, domain: str, nameservers: tuple[str, ...], yes: bool
|
|
717
|
+
) -> None:
|
|
718
|
+
"""Set custom nameservers for a domain.
|
|
719
|
+
|
|
720
|
+
This switches the domain from Namecheap's default DNS to custom nameservers.
|
|
721
|
+
|
|
722
|
+
Example:
|
|
723
|
+
namecheap-cli dns set-nameservers example.com ns1.route53.com ns2.route53.com
|
|
724
|
+
"""
|
|
725
|
+
nc = config.init_client()
|
|
726
|
+
|
|
727
|
+
try:
|
|
728
|
+
if not yes and not config.quiet:
|
|
729
|
+
console.print(
|
|
730
|
+
f"\n[yellow]Setting custom nameservers for {domain}:[/yellow]"
|
|
731
|
+
)
|
|
732
|
+
for ns in nameservers:
|
|
733
|
+
console.print(f" • {ns}")
|
|
734
|
+
console.print()
|
|
735
|
+
|
|
736
|
+
if not Confirm.ask("Continue?", default=True):
|
|
737
|
+
console.print("[yellow]Cancelled[/yellow]")
|
|
738
|
+
return
|
|
739
|
+
|
|
740
|
+
with Progress(
|
|
741
|
+
SpinnerColumn(),
|
|
742
|
+
TextColumn("[progress.description]{task.description}"),
|
|
743
|
+
transient=True,
|
|
744
|
+
) as progress:
|
|
745
|
+
progress.add_task(f"Setting nameservers for {domain}...", total=None)
|
|
746
|
+
success = nc.dns.set_custom_nameservers(domain, list(nameservers))
|
|
747
|
+
|
|
748
|
+
if success:
|
|
749
|
+
console.print(f"[green]✅ Custom nameservers set for {domain}[/green]")
|
|
750
|
+
if not config.quiet:
|
|
751
|
+
console.print(
|
|
752
|
+
"\n[dim]Note: DNS propagation may take up to 48 hours.[/dim]"
|
|
753
|
+
)
|
|
754
|
+
else:
|
|
755
|
+
console.print("[red]❌ Failed to set nameservers[/red]")
|
|
756
|
+
sys.exit(1)
|
|
757
|
+
|
|
758
|
+
except NamecheapError as e:
|
|
759
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
760
|
+
sys.exit(1)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
@dns_group.command("reset-nameservers")
|
|
764
|
+
@click.argument("domain")
|
|
765
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
|
|
766
|
+
@pass_config
|
|
767
|
+
def dns_reset_nameservers(config: Config, domain: str, yes: bool) -> None:
|
|
768
|
+
"""Reset domain to use Namecheap's default nameservers.
|
|
769
|
+
|
|
770
|
+
This switches the domain back to Namecheap BasicDNS from custom nameservers.
|
|
771
|
+
"""
|
|
772
|
+
nc = config.init_client()
|
|
773
|
+
|
|
774
|
+
try:
|
|
775
|
+
if not yes and not config.quiet:
|
|
776
|
+
console.print(
|
|
777
|
+
f"\n[yellow]This will reset {domain} to Namecheap's default DNS.[/yellow]"
|
|
778
|
+
)
|
|
779
|
+
if not Confirm.ask("Continue?", default=True):
|
|
780
|
+
console.print("[yellow]Cancelled[/yellow]")
|
|
781
|
+
return
|
|
782
|
+
|
|
783
|
+
with Progress(
|
|
784
|
+
SpinnerColumn(),
|
|
785
|
+
TextColumn("[progress.description]{task.description}"),
|
|
786
|
+
transient=True,
|
|
787
|
+
) as progress:
|
|
788
|
+
progress.add_task(f"Resetting nameservers for {domain}...", total=None)
|
|
789
|
+
success = nc.dns.set_default_nameservers(domain)
|
|
790
|
+
|
|
791
|
+
if success:
|
|
792
|
+
console.print(f"[green]✅ {domain} is now using Namecheap BasicDNS[/green]")
|
|
793
|
+
else:
|
|
794
|
+
console.print("[red]❌ Failed to reset nameservers[/red]")
|
|
795
|
+
sys.exit(1)
|
|
796
|
+
|
|
797
|
+
except NamecheapError as e:
|
|
798
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
799
|
+
sys.exit(1)
|
|
800
|
+
|
|
801
|
+
|
|
703
802
|
@dns_group.command("export")
|
|
704
803
|
@click.argument("domain")
|
|
705
804
|
@click.option(
|
|
@@ -781,6 +880,45 @@ def dns_export(config: Config, domain: str, format: str, output) -> None:
|
|
|
781
880
|
sys.exit(1)
|
|
782
881
|
|
|
783
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
|
+
|
|
784
922
|
@cli.group("account")
|
|
785
923
|
def account_group() -> None:
|
|
786
924
|
"""Account management commands."""
|
|
@@ -791,14 +929,37 @@ def account_group() -> None:
|
|
|
791
929
|
@pass_config
|
|
792
930
|
def account_balance(config: Config) -> None:
|
|
793
931
|
"""Check account balance."""
|
|
794
|
-
config.init_client()
|
|
932
|
+
nc = config.init_client()
|
|
795
933
|
|
|
796
934
|
try:
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
"[
|
|
800
|
-
|
|
801
|
-
|
|
935
|
+
with Progress(
|
|
936
|
+
SpinnerColumn(),
|
|
937
|
+
TextColumn("[progress.description]{task.description}"),
|
|
938
|
+
transient=True,
|
|
939
|
+
) as progress:
|
|
940
|
+
progress.add_task("Getting account balance...", total=None)
|
|
941
|
+
bal = nc.users.get_balances()
|
|
942
|
+
|
|
943
|
+
if config.output_format == "table":
|
|
944
|
+
table = Table(title="Account Balance")
|
|
945
|
+
table.add_column("Field", style="cyan")
|
|
946
|
+
table.add_column("Amount", style="green", justify="right")
|
|
947
|
+
|
|
948
|
+
table.add_row(
|
|
949
|
+
"Available Balance", f"{bal.available_balance} {bal.currency}"
|
|
950
|
+
)
|
|
951
|
+
table.add_row("Account Balance", f"{bal.account_balance} {bal.currency}")
|
|
952
|
+
table.add_row("Earned Amount", f"{bal.earned_amount} {bal.currency}")
|
|
953
|
+
table.add_row("Withdrawable", f"{bal.withdrawable_amount} {bal.currency}")
|
|
954
|
+
if bal.funds_required_for_auto_renew > 0:
|
|
955
|
+
table.add_row(
|
|
956
|
+
"Auto-Renew Required",
|
|
957
|
+
f"{bal.funds_required_for_auto_renew} {bal.currency}",
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
console.print(table)
|
|
961
|
+
else:
|
|
962
|
+
output_formatter(bal.model_dump(mode="json"), config.output_format)
|
|
802
963
|
|
|
803
964
|
except NamecheapError as e:
|
|
804
965
|
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.0
|
|
3
|
+
Version: 1.2.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
|
|
@@ -302,6 +340,51 @@ nc.dns.set("example.com",
|
|
|
302
340
|
|
|
303
341
|
**Note on TTL:** The default TTL is **1799 seconds**, which displays as **"Automatic"** in the Namecheap web interface. This is an undocumented Namecheap API behavior. You can specify custom TTL values (60-86400 seconds) in any DNS method.
|
|
304
342
|
|
|
343
|
+
### Nameserver Management
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
# Check current nameservers
|
|
347
|
+
ns = nc.dns.get_nameservers("example.com")
|
|
348
|
+
print(ns.nameservers) # ['dns1.registrar-servers.com', 'dns2.registrar-servers.com']
|
|
349
|
+
print(ns.is_default) # True
|
|
350
|
+
|
|
351
|
+
# Switch to custom nameservers (e.g., Cloudflare, Route 53)
|
|
352
|
+
nc.dns.set_custom_nameservers("example.com", [
|
|
353
|
+
"ns1.cloudflare.com",
|
|
354
|
+
"ns2.cloudflare.com",
|
|
355
|
+
])
|
|
356
|
+
|
|
357
|
+
# Reset back to Namecheap BasicDNS
|
|
358
|
+
nc.dns.set_default_nameservers("example.com")
|
|
359
|
+
```
|
|
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
|
+
rules = nc.dns.get_email_forwarding("example.com")
|
|
384
|
+
for r in rules:
|
|
385
|
+
print(f"{r.mailbox} -> {r.forward_to}")
|
|
386
|
+
```
|
|
387
|
+
|
|
305
388
|
### Domain Management
|
|
306
389
|
|
|
307
390
|
```python
|
|
@@ -375,22 +458,24 @@ nc.dns.builder().a("www", "192.0.2.1", ttl=1799) # Shows as "Automatic"
|
|
|
375
458
|
nc.dns.builder().a("www", "192.0.2.1", ttl=1800) # Shows as "30 min"
|
|
376
459
|
```
|
|
377
460
|
|
|
378
|
-
##
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
461
|
+
## 📊 [API Coverage](https://www.namecheap.com/support/api/methods/)
|
|
462
|
+
|
|
463
|
+
| API | Status | Methods |
|
|
464
|
+
|-----|--------|---------|
|
|
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` |
|
|
467
|
+
| `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
|
+
| `namecheap.users.address.*` | 🚧 Planned | `create`, `delete`, `getInfo`, `getList`, `setDefault`, `update` |
|
|
471
|
+
| `namecheap.ssl.*` | 🚧 Planned | `create`, `activate`, `renew`, `revoke`, `getList`, `getInfo`, `parseCSR`, `reissue`, and more |
|
|
472
|
+
| `namecheap.domains.transfer.*` | 🚧 Planned | `create`, `getStatus`, `updateStatus`, `getList` |
|
|
473
|
+
| `namecheap.domains.ns.*` | 🚧 Planned | Glue records — `create`, `delete`, `getInfo`, `update` |
|
|
474
|
+
| `namecheap.domainprivacy.*` | 🚧 Planned | `enable`, `disable`, `renew`, `getList`, `changeemailaddress` |
|
|
390
475
|
|
|
391
476
|
## 🛠️ Development
|
|
392
477
|
|
|
393
|
-
See [
|
|
478
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and development guidelines.
|
|
394
479
|
|
|
395
480
|
## 📝 License
|
|
396
481
|
|
|
@@ -399,3 +484,9 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
399
484
|
## 🤝 Contributing
|
|
400
485
|
|
|
401
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.
|
|
487
|
+
|
|
488
|
+
### Contributors
|
|
489
|
+
|
|
490
|
+
- [@huntertur](https://github.com/huntertur) — Rich dependency fix
|
|
491
|
+
- [@jeffmcadams](https://github.com/jeffmcadams) — Domain serialization round-trip
|
|
492
|
+
- [@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=S-84LTSVWDXJeqvDoUoSsq84250m5J0MXYEPJ6xH3kw,837
|
|
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=t4xloDJOgKeoERujhMRtTR0-B--ENzQsAn1WyFHyNbE,14261
|
|
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=MtOHr0AOUgS_Vv7U8WkLBw7xuU0XWbjAF6ptitteNmk,15827
|
|
9
|
+
namecheap/_api/domains.py,sha256=bEPshE2GN6ubd0otTwLdZCpKVT9ErmDRy8fYxS2hkIY,18272
|
|
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=ZOmdVQZWkA3SHw7_nGCp_j5RdJCQff4NO6h_nIjgDnY,36425
|
|
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.0.
|
|
22
|
-
namecheap_python-1.0.
|
|
23
|
-
namecheap_python-1.0.
|
|
24
|
-
namecheap_python-1.0.
|
|
25
|
-
namecheap_python-1.0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|