diff --git a/README.md b/README.md index 01c7905..1c92ce8 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,24 @@ nc.dns.set("example.com", **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. +### Nameserver Management + +```python +# Check current nameservers +ns = nc.dns.get_nameservers("example.com") +print(ns.nameservers) # ['dns1.registrar-servers.com', 'dns2.registrar-servers.com'] +print(ns.is_default) # True + +# Switch to custom nameservers (e.g., Cloudflare, Route 53) +nc.dns.set_custom_nameservers("example.com", [ + "ns1.cloudflare.com", + "ns2.cloudflare.com", +]) + +# Reset back to Namecheap BasicDNS +nc.dns.set_default_nameservers("example.com") +``` + ### Domain Management ```python @@ -339,7 +357,7 @@ The following Namecheap API features are planned for future releases: - **SSL API** - Certificate management - **Domain Transfer API** - Transfer domains between registrars -- **Domain NS API** - Custom nameserver management +- **Domain NS API** - Glue record management (child nameservers) - **Users API** - Account management and balance checking - **Whois API** - WHOIS information lookups - **Email Forwarding** - Email forwarding configuration @@ -357,3 +375,7 @@ MIT License - see [LICENSE](LICENSE) file for details. ## 🤝 Contributing Contributions are welcome! Please feel free to submit a Pull Request. See the [Development Guide](docs/dev/README.md) for setup instructions and guidelines. + +### Contributors + +- [@cosmin](https://github.com/cosmin) — Nameserver management diff --git a/pending.md b/pending.md index 6b1893b..5d585f0 100644 --- a/pending.md +++ b/pending.md @@ -32,11 +32,11 @@ Based on the previous Namecheap Python SDK implementation, here's what's still p - `updateStatus()` - Approve/reject transfer - `getList()` - List pending transfers -### 4. **Domains NS API** (`namecheap.domains.ns.*`) -- `create()` - Create nameserver -- `delete()` - Delete nameserver -- `getInfo()` - Get nameserver details -- `update()` - Update nameserver IP +### 4. **Domains NS API** (`namecheap.domains.ns.*`) — Glue Records +- `create()` - Register a child nameserver (e.g., ns1.yourdomain.com → 1.2.3.4) +- `delete()` - Delete a child nameserver +- `getInfo()` - Get child nameserver details +- `update()` - Update child nameserver IP ### 5. **Whois API** (`namecheap.whois.*`) - `getWhoisInfo()` - Get WHOIS information @@ -60,6 +60,9 @@ Based on the previous Namecheap Python SDK implementation, here's what's still p - ✅ `set()` - Set DNS records (with builder pattern!) - ✅ `add()` - Add single record - ✅ `delete()` - Delete records +- ✅ `set_custom_nameservers()` - Switch to custom nameservers (e.g., Route 53) +- ✅ `set_default_nameservers()` - Reset to Namecheap BasicDNS +- ✅ `get_nameservers()` - Get current nameserver configuration ### Enhanced Features - ✅ Smart IP detection and validation diff --git a/pyproject.toml b/pyproject.toml index a8480b0..1c399be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "namecheap-python" -version = "1.0.6" +version = "1.1.0" description = "A friendly Python SDK for Namecheap API" authors = [{name = "Adrian Galilea Delgado", email = "adriangalilea@gmail.com"}] readme = "README.md" diff --git a/src/namecheap/__init__.py b/src/namecheap/__init__.py index 6ab05eb..a867269 100644 --- a/src/namecheap/__init__.py +++ b/src/namecheap/__init__.py @@ -12,9 +12,9 @@ from .client import Namecheap from .errors import ConfigurationError, NamecheapError, ValidationError -from .models import Contact, DNSRecord, Domain, DomainCheck +from .models import Contact, DNSRecord, Domain, DomainCheck, Nameservers -__version__ = "1.0.5" +__version__ = "1.1.0" __all__ = [ "ConfigurationError", "Contact", @@ -23,5 +23,6 @@ "DomainCheck", "Namecheap", "NamecheapError", + "Nameservers", "ValidationError", ] diff --git a/src/namecheap/_api/dns.py b/src/namecheap/_api/dns.py index 62f299a..25bcdfb 100644 --- a/src/namecheap/_api/dns.py +++ b/src/namecheap/_api/dns.py @@ -6,7 +6,7 @@ import tldextract -from namecheap.models import DNSRecord +from namecheap.models import DNSRecord, Nameservers from .base import BaseAPI @@ -381,3 +381,116 @@ def builder() -> DNSRecordBuilder: >>> nc.dns.set("example.com", builder) """ return DNSRecordBuilder() + + def set_custom_nameservers(self, domain: str, nameservers: list[str]) -> bool: + """ + Set custom nameservers for a domain. + + This switches the domain from Namecheap's default DNS to custom + nameservers (e.g., Route 53, Cloudflare, etc.). + + Args: + domain: Domain name + nameservers: List of nameserver hostnames (e.g., ["ns1.example.com", "ns2.example.com"]) + + Returns: + True if successful + + Examples: + >>> nc.dns.set_custom_nameservers("example.com", [ + ... "ns-123.awsdns-45.com", + ... "ns-456.awsdns-67.net", + ... "ns-789.awsdns-89.org", + ... "ns-012.awsdns-12.co.uk", + ... ]) + """ + assert nameservers, "At least one nameserver is required" + assert len(nameservers) <= 5, "Maximum of 5 nameservers allowed" + + ext = tldextract.extract(domain) + if not ext.domain or not ext.suffix: + raise ValueError(f"Invalid domain name: {domain}") + + result: Any = self._request( + "namecheap.domains.dns.setCustom", + { + "SLD": ext.domain, + "TLD": ext.suffix, + "Nameservers": ",".join(nameservers), + }, + path="DomainDNSSetCustomResult", + ) + + return bool(result and result.get("@Updated") == "true") + + def set_default_nameservers(self, domain: str) -> bool: + """ + Reset domain to use Namecheap's default nameservers. + + This switches the domain back to Namecheap BasicDNS from custom nameservers. + + Args: + domain: Domain name + + Returns: + True if successful + + Examples: + >>> nc.dns.set_default_nameservers("example.com") + """ + ext = tldextract.extract(domain) + if not ext.domain or not ext.suffix: + raise ValueError(f"Invalid domain name: {domain}") + + result: Any = self._request( + "namecheap.domains.dns.setDefault", + { + "SLD": ext.domain, + "TLD": ext.suffix, + }, + path="DomainDNSSetDefaultResult", + ) + + return bool(result and result.get("@Updated") == "true") + + def get_nameservers(self, domain: str) -> Nameservers: + """ + Get current nameservers for a domain. + + Args: + domain: Domain name + + Returns: + Nameservers with is_default flag and nameserver hostnames + + Examples: + >>> ns = nc.dns.get_nameservers("example.com") + >>> ns.is_default + True + >>> ns.nameservers + ['dns1.registrar-servers.com', 'dns2.registrar-servers.com'] + """ + ext = tldextract.extract(domain) + if not ext.domain or not ext.suffix: + raise ValueError(f"Invalid domain name: {domain}") + + result: Any = self._request( + "namecheap.domains.dns.getList", + { + "SLD": ext.domain, + "TLD": ext.suffix, + }, + path="DomainDNSGetListResult", + ) + + assert result, f"API returned empty result for {domain} nameserver query" + + is_default = result.get("@IsUsingOurDNS", "false").lower() == "true" + + ns_data = result.get("Nameserver", []) + assert isinstance(ns_data, str | list), ( + f"Unexpected Nameserver type: {type(ns_data)}" + ) + nameservers = [ns_data] if isinstance(ns_data, str) else ns_data + + return Nameservers(is_default=is_default, nameservers=nameservers) diff --git a/src/namecheap/models.py b/src/namecheap/models.py index 804ce41..d1f2168 100644 --- a/src/namecheap/models.py +++ b/src/namecheap/models.py @@ -251,6 +251,13 @@ def parse_datetime(cls, v: Any) -> datetime: raise ValueError(f"Cannot parse datetime from {v}") +class Nameservers(BaseModel): + """Current nameserver configuration for a domain.""" + + is_default: bool = Field(description="True when using Namecheap's own DNS") + nameservers: list[str] = Field(description="Nameserver hostnames") + + class Contact(BaseModel): """Contact information for domain registration.""" diff --git a/src/namecheap_cli/__main__.py b/src/namecheap_cli/__main__.py index c9c8165..e40a472 100644 --- a/src/namecheap_cli/__main__.py +++ b/src/namecheap_cli/__main__.py @@ -700,6 +700,131 @@ def dns_delete( sys.exit(1) +@dns_group.command("nameservers") +@click.argument("domain") +@pass_config +def dns_nameservers(config: Config, domain: str) -> None: + """Show current nameserver configuration for a domain.""" + nc = config.init_client() + + try: + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + transient=True, + ) as progress: + progress.add_task(f"Getting nameserver info for {domain}...", total=None) + ns = nc.dns.get_nameservers(domain) + + if config.output_format == "table": + console.print(f"\n[bold cyan]Nameservers for {domain}[/bold cyan]\n") + + if ns.is_default: + console.print("[green]Using Namecheap BasicDNS[/green]") + else: + console.print("[yellow]Using custom nameservers:[/yellow]") + for nameserver in ns.nameservers: + console.print(f" • {nameserver}") + else: + output_formatter(ns.model_dump(), config.output_format) + + except NamecheapError as e: + console.print(f"[red]❌ Error: {e}[/red]") + sys.exit(1) + + +@dns_group.command("set-nameservers") +@click.argument("domain") +@click.argument("nameservers", nargs=-1, required=True) +@click.option("--yes", "-y", is_flag=True, help="Skip confirmation") +@pass_config +def dns_set_nameservers( + config: Config, domain: str, nameservers: tuple[str, ...], yes: bool +) -> None: + """Set custom nameservers for a domain. + + This switches the domain from Namecheap's default DNS to custom nameservers. + + Example: + namecheap-cli dns set-nameservers example.com ns1.route53.com ns2.route53.com + """ + nc = config.init_client() + + try: + if not yes and not config.quiet: + console.print( + f"\n[yellow]Setting custom nameservers for {domain}:[/yellow]" + ) + for ns in nameservers: + console.print(f" • {ns}") + console.print() + + if not Confirm.ask("Continue?", default=True): + console.print("[yellow]Cancelled[/yellow]") + return + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + transient=True, + ) as progress: + progress.add_task(f"Setting nameservers for {domain}...", total=None) + success = nc.dns.set_custom_nameservers(domain, list(nameservers)) + + if success: + console.print(f"[green]✅ Custom nameservers set for {domain}[/green]") + if not config.quiet: + console.print( + "\n[dim]Note: DNS propagation may take up to 48 hours.[/dim]" + ) + else: + console.print("[red]❌ Failed to set nameservers[/red]") + sys.exit(1) + + except NamecheapError as e: + console.print(f"[red]❌ Error: {e}[/red]") + sys.exit(1) + + +@dns_group.command("reset-nameservers") +@click.argument("domain") +@click.option("--yes", "-y", is_flag=True, help="Skip confirmation") +@pass_config +def dns_reset_nameservers(config: Config, domain: str, yes: bool) -> None: + """Reset domain to use Namecheap's default nameservers. + + This switches the domain back to Namecheap BasicDNS from custom nameservers. + """ + nc = config.init_client() + + try: + if not yes and not config.quiet: + console.print( + f"\n[yellow]This will reset {domain} to Namecheap's default DNS.[/yellow]" + ) + if not Confirm.ask("Continue?", default=True): + console.print("[yellow]Cancelled[/yellow]") + return + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + transient=True, + ) as progress: + progress.add_task(f"Resetting nameservers for {domain}...", total=None) + success = nc.dns.set_default_nameservers(domain) + + if success: + console.print(f"[green]✅ {domain} is now using Namecheap BasicDNS[/green]") + else: + console.print("[red]❌ Failed to reset nameservers[/red]") + sys.exit(1) + + except NamecheapError as e: + console.print(f"[red]❌ Error: {e}[/red]") + sys.exit(1) + + @dns_group.command("export") @click.argument("domain") @click.option( diff --git a/uv.lock b/uv.lock index 0a84313..893dde2 100644 --- a/uv.lock +++ b/uv.lock @@ -200,7 +200,7 @@ wheels = [ [[package]] name = "namecheap-python" -version = "1.0.5" +version = "1.1.0" source = { editable = "." } dependencies = [ { name = "httpx" },