Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
13 changes: 8 additions & 5 deletions pending.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
5 changes: 3 additions & 2 deletions src/namecheap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -23,5 +23,6 @@
"DomainCheck",
"Namecheap",
"NamecheapError",
"Nameservers",
"ValidationError",
]
115 changes: 114 additions & 1 deletion src/namecheap/_api/dns.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import tldextract

from namecheap.models import DNSRecord
from namecheap.models import DNSRecord, Nameservers

from .base import BaseAPI

Expand Down Expand Up @@ -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)
7 changes: 7 additions & 0 deletions src/namecheap/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
125 changes: 125 additions & 0 deletions src/namecheap_cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.