Skip to content

Commit e308005

Browse files
Merge pull request #8 from cosmin/feature/custom-nameservers
feat: add nameserver management
2 parents 08937ea + 8d9ee3a commit e308005

File tree

8 files changed

+282
-11
lines changed

8 files changed

+282
-11
lines changed

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,24 @@ nc.dns.set("example.com",
260260

261261
**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.
262262

263+
### Nameserver Management
264+
265+
```python
266+
# Check current nameservers
267+
ns = nc.dns.get_nameservers("example.com")
268+
print(ns.nameservers) # ['dns1.registrar-servers.com', 'dns2.registrar-servers.com']
269+
print(ns.is_default) # True
270+
271+
# Switch to custom nameservers (e.g., Cloudflare, Route 53)
272+
nc.dns.set_custom_nameservers("example.com", [
273+
"ns1.cloudflare.com",
274+
"ns2.cloudflare.com",
275+
])
276+
277+
# Reset back to Namecheap BasicDNS
278+
nc.dns.set_default_nameservers("example.com")
279+
```
280+
263281
### Domain Management
264282

265283
```python
@@ -339,7 +357,7 @@ The following Namecheap API features are planned for future releases:
339357
340358
- **SSL API** - Certificate management
341359
- **Domain Transfer API** - Transfer domains between registrars
342-
- **Domain NS API** - Custom nameserver management
360+
- **Domain NS API** - Glue record management (child nameservers)
343361
- **Users API** - Account management and balance checking
344362
- **Whois API** - WHOIS information lookups
345363
- **Email Forwarding** - Email forwarding configuration
@@ -357,3 +375,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
357375
## 🤝 Contributing
358376
359377
Contributions are welcome! Please feel free to submit a Pull Request. See the [Development Guide](docs/dev/README.md) for setup instructions and guidelines.
378+
379+
### Contributors
380+
381+
- [@cosmin](https://github.com/cosmin) — Nameserver management

pending.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ Based on the previous Namecheap Python SDK implementation, here's what's still p
3232
- `updateStatus()` - Approve/reject transfer
3333
- `getList()` - List pending transfers
3434

35-
### 4. **Domains NS API** (`namecheap.domains.ns.*`)
36-
- `create()` - Create nameserver
37-
- `delete()` - Delete nameserver
38-
- `getInfo()` - Get nameserver details
39-
- `update()` - Update nameserver IP
35+
### 4. **Domains NS API** (`namecheap.domains.ns.*`) — Glue Records
36+
- `create()` - Register a child nameserver (e.g., ns1.yourdomain.com → 1.2.3.4)
37+
- `delete()` - Delete a child nameserver
38+
- `getInfo()` - Get child nameserver details
39+
- `update()` - Update child nameserver IP
4040

4141
### 5. **Whois API** (`namecheap.whois.*`)
4242
- `getWhoisInfo()` - Get WHOIS information
@@ -60,6 +60,9 @@ Based on the previous Namecheap Python SDK implementation, here's what's still p
6060
-`set()` - Set DNS records (with builder pattern!)
6161
-`add()` - Add single record
6262
-`delete()` - Delete records
63+
-`set_custom_nameservers()` - Switch to custom nameservers (e.g., Route 53)
64+
-`set_default_nameservers()` - Reset to Namecheap BasicDNS
65+
-`get_nameservers()` - Get current nameserver configuration
6366

6467
### Enhanced Features
6568
- ✅ Smart IP detection and validation

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "namecheap-python"
3-
version = "1.0.6"
3+
version = "1.1.0"
44
description = "A friendly Python SDK for Namecheap API"
55
authors = [{name = "Adrian Galilea Delgado", email = "adriangalilea@gmail.com"}]
66
readme = "README.md"

src/namecheap/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212

1313
from .client import Namecheap
1414
from .errors import ConfigurationError, NamecheapError, ValidationError
15-
from .models import Contact, DNSRecord, Domain, DomainCheck
15+
from .models import Contact, DNSRecord, Domain, DomainCheck, Nameservers
1616

17-
__version__ = "1.0.5"
17+
__version__ = "1.1.0"
1818
__all__ = [
1919
"ConfigurationError",
2020
"Contact",
@@ -23,5 +23,6 @@
2323
"DomainCheck",
2424
"Namecheap",
2525
"NamecheapError",
26+
"Nameservers",
2627
"ValidationError",
2728
]

src/namecheap/_api/dns.py

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import tldextract
88

9-
from namecheap.models import DNSRecord
9+
from namecheap.models import DNSRecord, Nameservers
1010

1111
from .base import BaseAPI
1212

@@ -381,3 +381,116 @@ def builder() -> DNSRecordBuilder:
381381
>>> nc.dns.set("example.com", builder)
382382
"""
383383
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)

src/namecheap/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,13 @@ def parse_datetime(cls, v: Any) -> datetime:
251251
raise ValueError(f"Cannot parse datetime from {v}")
252252

253253

254+
class Nameservers(BaseModel):
255+
"""Current nameserver configuration for a domain."""
256+
257+
is_default: bool = Field(description="True when using Namecheap's own DNS")
258+
nameservers: list[str] = Field(description="Nameserver hostnames")
259+
260+
254261
class Contact(BaseModel):
255262
"""Contact information for domain registration."""
256263

src/namecheap_cli/__main__.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,131 @@ def dns_delete(
700700
sys.exit(1)
701701

702702

703+
@dns_group.command("nameservers")
704+
@click.argument("domain")
705+
@pass_config
706+
def dns_nameservers(config: Config, domain: str) -> None:
707+
"""Show current nameserver configuration for a domain."""
708+
nc = config.init_client()
709+
710+
try:
711+
with Progress(
712+
SpinnerColumn(),
713+
TextColumn("[progress.description]{task.description}"),
714+
transient=True,
715+
) as progress:
716+
progress.add_task(f"Getting nameserver info for {domain}...", total=None)
717+
ns = nc.dns.get_nameservers(domain)
718+
719+
if config.output_format == "table":
720+
console.print(f"\n[bold cyan]Nameservers for {domain}[/bold cyan]\n")
721+
722+
if ns.is_default:
723+
console.print("[green]Using Namecheap BasicDNS[/green]")
724+
else:
725+
console.print("[yellow]Using custom nameservers:[/yellow]")
726+
for nameserver in ns.nameservers:
727+
console.print(f" • {nameserver}")
728+
else:
729+
output_formatter(ns.model_dump(), config.output_format)
730+
731+
except NamecheapError as e:
732+
console.print(f"[red]❌ Error: {e}[/red]")
733+
sys.exit(1)
734+
735+
736+
@dns_group.command("set-nameservers")
737+
@click.argument("domain")
738+
@click.argument("nameservers", nargs=-1, required=True)
739+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
740+
@pass_config
741+
def dns_set_nameservers(
742+
config: Config, domain: str, nameservers: tuple[str, ...], yes: bool
743+
) -> None:
744+
"""Set custom nameservers for a domain.
745+
746+
This switches the domain from Namecheap's default DNS to custom nameservers.
747+
748+
Example:
749+
namecheap-cli dns set-nameservers example.com ns1.route53.com ns2.route53.com
750+
"""
751+
nc = config.init_client()
752+
753+
try:
754+
if not yes and not config.quiet:
755+
console.print(
756+
f"\n[yellow]Setting custom nameservers for {domain}:[/yellow]"
757+
)
758+
for ns in nameservers:
759+
console.print(f" • {ns}")
760+
console.print()
761+
762+
if not Confirm.ask("Continue?", default=True):
763+
console.print("[yellow]Cancelled[/yellow]")
764+
return
765+
766+
with Progress(
767+
SpinnerColumn(),
768+
TextColumn("[progress.description]{task.description}"),
769+
transient=True,
770+
) as progress:
771+
progress.add_task(f"Setting nameservers for {domain}...", total=None)
772+
success = nc.dns.set_custom_nameservers(domain, list(nameservers))
773+
774+
if success:
775+
console.print(f"[green]✅ Custom nameservers set for {domain}[/green]")
776+
if not config.quiet:
777+
console.print(
778+
"\n[dim]Note: DNS propagation may take up to 48 hours.[/dim]"
779+
)
780+
else:
781+
console.print("[red]❌ Failed to set nameservers[/red]")
782+
sys.exit(1)
783+
784+
except NamecheapError as e:
785+
console.print(f"[red]❌ Error: {e}[/red]")
786+
sys.exit(1)
787+
788+
789+
@dns_group.command("reset-nameservers")
790+
@click.argument("domain")
791+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
792+
@pass_config
793+
def dns_reset_nameservers(config: Config, domain: str, yes: bool) -> None:
794+
"""Reset domain to use Namecheap's default nameservers.
795+
796+
This switches the domain back to Namecheap BasicDNS from custom nameservers.
797+
"""
798+
nc = config.init_client()
799+
800+
try:
801+
if not yes and not config.quiet:
802+
console.print(
803+
f"\n[yellow]This will reset {domain} to Namecheap's default DNS.[/yellow]"
804+
)
805+
if not Confirm.ask("Continue?", default=True):
806+
console.print("[yellow]Cancelled[/yellow]")
807+
return
808+
809+
with Progress(
810+
SpinnerColumn(),
811+
TextColumn("[progress.description]{task.description}"),
812+
transient=True,
813+
) as progress:
814+
progress.add_task(f"Resetting nameservers for {domain}...", total=None)
815+
success = nc.dns.set_default_nameservers(domain)
816+
817+
if success:
818+
console.print(f"[green]✅ {domain} is now using Namecheap BasicDNS[/green]")
819+
else:
820+
console.print("[red]❌ Failed to reset nameservers[/red]")
821+
sys.exit(1)
822+
823+
except NamecheapError as e:
824+
console.print(f"[red]❌ Error: {e}[/red]")
825+
sys.exit(1)
826+
827+
703828
@dns_group.command("export")
704829
@click.argument("domain")
705830
@click.option(

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)