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 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 Contact, DNSRecord, Domain, DomainCheck
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.5"
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,
@@ -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
- domains = nc.domains.list()
371
- domain_obj = next((d for d in domains if d.name == domain), None)
372
-
373
- if not domain_obj:
374
- console.print(f"[red]❌ Domain {domain} not found in your account[/red]")
375
- sys.exit(1)
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
- f"[bold]Status:[/bold] "
381
- f"{'Active' if not domain_obj.is_expired else '[red]Expired[/red]'}"
382
- )
383
- console.print(
384
- f"[bold]Created:[/bold] {domain_obj.created.strftime('%Y-%m-%d')}"
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 domain_obj.whois_guard else '✗ Disabled'}"
389
+ f"{'✓ Enabled' if info.whoisguard_enabled else '✗ Disabled'}"
398
390
  )
399
-
400
- # Calculate days until expiration
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
- data = {
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
- # This would need to be implemented in the SDK
798
- console.print(
799
- "[yellow]Account balance check not yet implemented in SDK[/yellow]"
800
- )
801
- console.print("This feature requires the users.getBalances API method")
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.6
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
+ [![PyPI version](https://badge.fury.io/py/namecheap-python.svg)](https://pypi.org/project/namecheap-python/)
46
+ [![Downloads](https://pepy.tech/badge/namecheap-python)](https://pepy.tech/project/namecheap-python)
47
+ [![Downloads/month](https://pepy.tech/badge/namecheap-python/month)](https://pepy.tech/project/namecheap-python)
48
+ [![Python](https://img.shields.io/pypi/pyversions/namecheap-python)](https://pypi.org/project/namecheap-python/)
49
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- ## 🚧 Pending Features
379
-
380
- The following Namecheap API features are planned for future releases:
381
-
382
- - **SSL API** - Certificate management
383
- - **Domain Transfer API** - Transfer domains between registrars
384
- - **Domain NS API** - Custom nameserver management
385
- - **Users API** - Account management and balance checking
386
- - **Whois API** - WHOIS information lookups
387
- - **Email Forwarding** - Email forwarding configuration
388
-
389
- See [pending.md](pending.md) for full details.
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 [Development Guide](docs/dev/README.md) for detailed development instructions.
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=TyOenTWmRyClXBbw1i4vlMc2GRujp6h9tZqjCNBY5l8,666
2
- namecheap/client.py,sha256=wnhCA5zeZMGypD1uNHbMYCGoP9paaZjtYZucBUsEY6k,6441
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=D_ztY6J816irY8MMj-dx40iFQgar886MmQS8NeV_J18,12115
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=XJj5E1-4SokWawCit6iZGlvMypxPGjb7COTs4mpNTes,11156
9
- namecheap/_api/domains.py,sha256=h6cWc03iCwPqOJTfbR8pyom_8MUVPoqSIRJRws1i3UY,16603
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=-936gBbkwqbCTLM6c_eQdZRLH6NwtC9vywnmWfRAyzw,30814
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.6.dist-info/METADATA,sha256=fOcbCkiglpU6fan-t0STKQT5sVg8VDhXmsvRV0y8IRs,14802
22
- namecheap_python-1.0.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- namecheap_python-1.0.6.dist-info/entry_points.txt,sha256=AyhiXroLUpM0Vdo_-RvH0S8o4XDPsDlsEl_65vm6DEk,96
24
- namecheap_python-1.0.6.dist-info/licenses/LICENSE,sha256=pemTblFP6BBje3bBv_yL_sr2iAqB2H0-LdWMvVIR42o,1062
25
- namecheap_python-1.0.6.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any