Initial commit: Namecheap API library with REST/MCP servers
Features: - Domain management (check, register, renew, contacts) - DNS management (nameservers, records) - Glue records (child nameserver) support - TLD price tracking with KRW conversion - FastAPI REST server with OpenAI schema - MCP server for Claude integration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
549
namecheap.py
Normal file
549
namecheap.py
Normal file
@@ -0,0 +1,549 @@
|
||||
"""
|
||||
Namecheap API Python Wrapper
|
||||
"""
|
||||
|
||||
import requests
|
||||
import xml.etree.ElementTree as ET
|
||||
from dataclasses import dataclass
|
||||
|
||||
NS = {"nc": "http://api.namecheap.com/xml.response"}
|
||||
|
||||
|
||||
@dataclass
|
||||
class NamecheapConfig:
|
||||
api_user: str
|
||||
api_key: str
|
||||
username: str
|
||||
client_ip: str
|
||||
sandbox: bool = False
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
if self.sandbox:
|
||||
return "https://api.sandbox.namecheap.com/xml.response"
|
||||
return "https://api.namecheap.com/xml.response"
|
||||
|
||||
|
||||
@dataclass
|
||||
class RegistrantInfo:
|
||||
first_name: str
|
||||
last_name: str
|
||||
address1: str
|
||||
city: str
|
||||
state_province: str
|
||||
postal_code: str
|
||||
country: str
|
||||
phone: str
|
||||
email: str
|
||||
organization: str = ""
|
||||
address2: str = ""
|
||||
|
||||
|
||||
class NamecheapError(Exception):
|
||||
"""Namecheap API error"""
|
||||
def __init__(self, code: str, message: str):
|
||||
self.code = code
|
||||
self.message = message
|
||||
super().__init__(f"[{code}] {message}")
|
||||
|
||||
|
||||
class NamecheapAPI:
|
||||
def __init__(self, config: NamecheapConfig):
|
||||
self.config = config
|
||||
|
||||
def _base_params(self) -> dict:
|
||||
return {
|
||||
"ApiUser": self.config.api_user,
|
||||
"ApiKey": self.config.api_key,
|
||||
"UserName": self.config.username,
|
||||
"ClientIp": self.config.client_ip,
|
||||
}
|
||||
|
||||
def _request(self, command: str, **kwargs) -> ET.Element:
|
||||
params = self._base_params()
|
||||
params["Command"] = command
|
||||
params.update(kwargs)
|
||||
|
||||
response = requests.get(self.config.base_url, params=params, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
root = ET.fromstring(response.text)
|
||||
status = root.attrib.get("Status")
|
||||
|
||||
if status == "ERROR":
|
||||
errors = root.find("nc:Errors", NS)
|
||||
if errors is not None:
|
||||
error = errors.find("nc:Error", NS)
|
||||
if error is not None:
|
||||
raise NamecheapError(
|
||||
error.attrib.get("Number", "Unknown"),
|
||||
error.text or "Unknown error"
|
||||
)
|
||||
raise NamecheapError("Unknown", "API returned error status")
|
||||
|
||||
return root
|
||||
|
||||
# ===== Domains =====
|
||||
|
||||
def domains_check(self, domains: list[str]) -> dict[str, bool]:
|
||||
"""Check domain availability"""
|
||||
domain_list = ",".join(domains)
|
||||
root = self._request("namecheap.domains.check", DomainList=domain_list)
|
||||
|
||||
result = {}
|
||||
for domain in root.findall(".//nc:DomainCheckResult", NS):
|
||||
name = domain.attrib.get("Domain", "")
|
||||
available = domain.attrib.get("Available", "false").lower() == "true"
|
||||
result[name] = available
|
||||
|
||||
return result
|
||||
|
||||
def domains_get_list(self, page: int = 1, page_size: int = 20) -> list[dict]:
|
||||
"""Get list of domains in account"""
|
||||
root = self._request(
|
||||
"namecheap.domains.getList",
|
||||
Page=str(page),
|
||||
PageSize=str(page_size)
|
||||
)
|
||||
|
||||
domains = []
|
||||
for domain in root.findall(".//nc:Domain", NS):
|
||||
domains.append({
|
||||
"id": domain.attrib.get("ID"),
|
||||
"name": domain.attrib.get("Name"),
|
||||
"user": domain.attrib.get("User"),
|
||||
"created": domain.attrib.get("Created"),
|
||||
"expires": domain.attrib.get("Expires"),
|
||||
"is_expired": domain.attrib.get("IsExpired") == "true",
|
||||
"is_locked": domain.attrib.get("IsLocked") == "true",
|
||||
"auto_renew": domain.attrib.get("AutoRenew") == "true",
|
||||
"whois_guard": domain.attrib.get("WhoisGuard"),
|
||||
})
|
||||
|
||||
return domains
|
||||
|
||||
def domains_get_info(self, domain: str) -> dict:
|
||||
"""Get detailed info about a domain"""
|
||||
root = self._request("namecheap.domains.getInfo", DomainName=domain)
|
||||
|
||||
info_elem = root.find(".//nc:DomainGetInfoResult", NS)
|
||||
if info_elem is None:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"domain": info_elem.attrib.get("DomainName"),
|
||||
"owner": info_elem.attrib.get("OwnerName"),
|
||||
"status": info_elem.attrib.get("Status"),
|
||||
"is_premium": info_elem.attrib.get("IsPremiumName") == "true",
|
||||
}
|
||||
|
||||
def domains_create(
|
||||
self,
|
||||
domain: str,
|
||||
registrant: RegistrantInfo,
|
||||
years: int = 1,
|
||||
add_whois_guard: bool = True,
|
||||
) -> dict:
|
||||
"""
|
||||
Register a new domain.
|
||||
|
||||
Args:
|
||||
domain: Domain name to register (e.g., "example.com")
|
||||
registrant: Registrant contact information
|
||||
years: Number of years to register (1-10)
|
||||
add_whois_guard: Enable WhoisGuard privacy protection
|
||||
|
||||
Returns:
|
||||
dict with domain, registered, charged_amount, etc.
|
||||
"""
|
||||
params = {
|
||||
"DomainName": domain,
|
||||
"Years": str(years),
|
||||
"AddFreeWhoisguard": "yes" if add_whois_guard else "no",
|
||||
"WGEnabled": "yes" if add_whois_guard else "no",
|
||||
}
|
||||
|
||||
# Contact info for all 4 contact types (Registrant, Tech, Admin, AuxBilling)
|
||||
for prefix in ["Registrant", "Tech", "Admin", "AuxBilling"]:
|
||||
params[f"{prefix}FirstName"] = registrant.first_name
|
||||
params[f"{prefix}LastName"] = registrant.last_name
|
||||
params[f"{prefix}Address1"] = registrant.address1
|
||||
params[f"{prefix}City"] = registrant.city
|
||||
params[f"{prefix}StateProvince"] = registrant.state_province
|
||||
params[f"{prefix}PostalCode"] = registrant.postal_code
|
||||
params[f"{prefix}Country"] = registrant.country
|
||||
params[f"{prefix}Phone"] = registrant.phone
|
||||
params[f"{prefix}EmailAddress"] = registrant.email
|
||||
if registrant.organization:
|
||||
params[f"{prefix}OrganizationName"] = registrant.organization
|
||||
if registrant.address2:
|
||||
params[f"{prefix}Address2"] = registrant.address2
|
||||
|
||||
root = self._request("namecheap.domains.create", **params)
|
||||
result = root.find(".//nc:DomainCreateResult", NS)
|
||||
|
||||
if result is None:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"domain": result.attrib.get("Domain"),
|
||||
"registered": result.attrib.get("Registered") == "true",
|
||||
"charged_amount": float(result.attrib.get("ChargedAmount", 0)),
|
||||
"domain_id": result.attrib.get("DomainID"),
|
||||
"order_id": result.attrib.get("OrderID"),
|
||||
"transaction_id": result.attrib.get("TransactionID"),
|
||||
"whois_guard_enabled": result.attrib.get("WhoisguardEnable") == "true",
|
||||
}
|
||||
|
||||
def domains_renew(self, domain: str, years: int = 1) -> dict:
|
||||
"""
|
||||
Renew a domain.
|
||||
|
||||
Args:
|
||||
domain: Domain name to renew (e.g., "example.com")
|
||||
years: Number of years to renew (1-10)
|
||||
|
||||
Returns:
|
||||
dict with domain, renewed, charged_amount, expiration_date, etc.
|
||||
"""
|
||||
root = self._request(
|
||||
"namecheap.domains.renew",
|
||||
DomainName=domain,
|
||||
Years=str(years),
|
||||
)
|
||||
|
||||
result = root.find(".//nc:DomainRenewResult", NS)
|
||||
if result is None:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"domain": result.attrib.get("DomainName"),
|
||||
"domain_id": result.attrib.get("DomainID"),
|
||||
"renewed": result.attrib.get("Renew") == "true",
|
||||
"charged_amount": float(result.attrib.get("ChargedAmount", 0)),
|
||||
"order_id": result.attrib.get("OrderID"),
|
||||
"transaction_id": result.attrib.get("TransactionID"),
|
||||
"expiration_date": result.attrib.get("DomainDetails", {}).get("ExpiredDate") if isinstance(result.attrib.get("DomainDetails"), dict) else None,
|
||||
}
|
||||
|
||||
def _parse_contact(self, contact_elem) -> dict:
|
||||
"""Parse contact element to dict"""
|
||||
if contact_elem is None:
|
||||
return {}
|
||||
|
||||
fields = [
|
||||
"OrganizationName", "FirstName", "LastName", "Address1", "Address2",
|
||||
"City", "StateProvince", "PostalCode", "Country", "Phone", "EmailAddress"
|
||||
]
|
||||
result = {}
|
||||
for field in fields:
|
||||
elem = contact_elem.find(f"nc:{field}", NS)
|
||||
key = field[0].lower() + field[1:] # camelCase to snake_case style
|
||||
result[key] = elem.text if elem is not None and elem.text else ""
|
||||
|
||||
return result
|
||||
|
||||
def domains_get_contacts(self, domain: str) -> dict:
|
||||
"""
|
||||
Get domain contact information.
|
||||
|
||||
Args:
|
||||
domain: Domain name (e.g., "example.com")
|
||||
|
||||
Returns:
|
||||
dict with registrant, tech, admin, aux_billing contacts
|
||||
"""
|
||||
root = self._request("namecheap.domains.getContacts", DomainName=domain)
|
||||
|
||||
result = root.find(".//nc:DomainContactsResult", NS)
|
||||
if result is None:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"domain": result.attrib.get("Domain"),
|
||||
"registrant": self._parse_contact(result.find("nc:Registrant", NS)),
|
||||
"tech": self._parse_contact(result.find("nc:Tech", NS)),
|
||||
"admin": self._parse_contact(result.find("nc:Admin", NS)),
|
||||
"aux_billing": self._parse_contact(result.find("nc:AuxBilling", NS)),
|
||||
}
|
||||
|
||||
def domains_set_contacts(self, domain: str, registrant: RegistrantInfo) -> bool:
|
||||
"""
|
||||
Set domain contact information.
|
||||
|
||||
Args:
|
||||
domain: Domain name (e.g., "example.com")
|
||||
registrant: Contact information (applied to all contact types)
|
||||
|
||||
Returns:
|
||||
bool indicating success
|
||||
"""
|
||||
params = {"DomainName": domain}
|
||||
|
||||
for prefix in ["Registrant", "Tech", "Admin", "AuxBilling"]:
|
||||
params[f"{prefix}FirstName"] = registrant.first_name
|
||||
params[f"{prefix}LastName"] = registrant.last_name
|
||||
params[f"{prefix}Address1"] = registrant.address1
|
||||
params[f"{prefix}City"] = registrant.city
|
||||
params[f"{prefix}StateProvince"] = registrant.state_province
|
||||
params[f"{prefix}PostalCode"] = registrant.postal_code
|
||||
params[f"{prefix}Country"] = registrant.country
|
||||
params[f"{prefix}Phone"] = registrant.phone
|
||||
params[f"{prefix}EmailAddress"] = registrant.email
|
||||
if registrant.organization:
|
||||
params[f"{prefix}OrganizationName"] = registrant.organization
|
||||
if registrant.address2:
|
||||
params[f"{prefix}Address2"] = registrant.address2
|
||||
|
||||
root = self._request("namecheap.domains.setContacts", **params)
|
||||
result = root.find(".//nc:DomainSetContactResult", NS)
|
||||
|
||||
return result is not None and result.attrib.get("IsSuccess") == "true"
|
||||
|
||||
# ===== DNS =====
|
||||
|
||||
def dns_get_list(self, sld: str, tld: str) -> dict:
|
||||
"""Get nameserver information for a domain"""
|
||||
root = self._request("namecheap.domains.dns.getList", SLD=sld, TLD=tld)
|
||||
|
||||
result = root.find(".//nc:DomainDNSGetListResult", NS)
|
||||
if result is None:
|
||||
return {}
|
||||
|
||||
nameservers = []
|
||||
for ns in result.findall("nc:Nameserver", NS):
|
||||
nameservers.append(ns.text)
|
||||
|
||||
return {
|
||||
"domain": result.attrib.get("Domain"),
|
||||
"is_using_our_dns": result.attrib.get("IsUsingOurDNS") == "true",
|
||||
"nameservers": nameservers,
|
||||
}
|
||||
|
||||
def dns_get_hosts(self, sld: str, tld: str) -> list[dict]:
|
||||
"""Get DNS host records"""
|
||||
root = self._request("namecheap.domains.dns.getHosts", SLD=sld, TLD=tld)
|
||||
|
||||
records = []
|
||||
for host in root.findall(".//nc:host", NS):
|
||||
records.append({
|
||||
"host_id": host.attrib.get("HostId"),
|
||||
"name": host.attrib.get("Name"),
|
||||
"type": host.attrib.get("Type"),
|
||||
"address": host.attrib.get("Address"),
|
||||
"mx_pref": host.attrib.get("MXPref"),
|
||||
"ttl": host.attrib.get("TTL"),
|
||||
})
|
||||
|
||||
return records
|
||||
|
||||
def dns_set_hosts(self, sld: str, tld: str, records: list[dict]) -> bool:
|
||||
"""
|
||||
Set DNS host records.
|
||||
|
||||
Each record should have: name, type, address, ttl (optional), mx_pref (optional)
|
||||
"""
|
||||
params = {"SLD": sld, "TLD": tld}
|
||||
|
||||
for i, record in enumerate(records, 1):
|
||||
params[f"HostName{i}"] = record["name"]
|
||||
params[f"RecordType{i}"] = record["type"]
|
||||
params[f"Address{i}"] = record["address"]
|
||||
params[f"TTL{i}"] = record.get("ttl", "1800")
|
||||
if record["type"] == "MX":
|
||||
params[f"MXPref{i}"] = record.get("mx_pref", "10")
|
||||
|
||||
root = self._request("namecheap.domains.dns.setHosts", **params)
|
||||
result = root.find(".//nc:DomainDNSSetHostsResult", NS)
|
||||
|
||||
return result is not None and result.attrib.get("IsSuccess") == "true"
|
||||
|
||||
def dns_set_custom(self, sld: str, tld: str, nameservers: list[str]) -> bool:
|
||||
"""Set custom nameservers"""
|
||||
ns_string = ",".join(nameservers)
|
||||
root = self._request(
|
||||
"namecheap.domains.dns.setCustom",
|
||||
SLD=sld,
|
||||
TLD=tld,
|
||||
Nameservers=ns_string
|
||||
)
|
||||
|
||||
result = root.find(".//nc:DomainDNSSetCustomResult", NS)
|
||||
return result is not None and result.attrib.get("Updated") == "true"
|
||||
|
||||
def dns_set_default(self, sld: str, tld: str) -> bool:
|
||||
"""Set default Namecheap nameservers"""
|
||||
root = self._request("namecheap.domains.dns.setDefault", SLD=sld, TLD=tld)
|
||||
|
||||
result = root.find(".//nc:DomainDNSSetDefaultResult", NS)
|
||||
return result is not None and result.attrib.get("Updated") == "true"
|
||||
|
||||
# ===== Nameservers (Glue Records) =====
|
||||
|
||||
def ns_create(self, sld: str, tld: str, nameserver: str, ip: str) -> dict:
|
||||
"""
|
||||
Create a child nameserver (glue record).
|
||||
|
||||
Args:
|
||||
sld: Second-level domain (e.g., "example" for example.com)
|
||||
tld: Top-level domain (e.g., "com")
|
||||
nameserver: Nameserver hostname (e.g., "ns1.example.com")
|
||||
ip: IP address for the nameserver
|
||||
|
||||
Returns:
|
||||
dict with domain, nameserver, ip, success
|
||||
"""
|
||||
root = self._request(
|
||||
"namecheap.domains.ns.create",
|
||||
SLD=sld,
|
||||
TLD=tld,
|
||||
Nameserver=nameserver,
|
||||
IP=ip,
|
||||
)
|
||||
|
||||
result = root.find(".//nc:DomainNSCreateResult", NS)
|
||||
if result is None:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"domain": result.attrib.get("Domain"),
|
||||
"nameserver": result.attrib.get("Nameserver"),
|
||||
"ip": result.attrib.get("IP"),
|
||||
"success": result.attrib.get("IsSuccess") == "true",
|
||||
}
|
||||
|
||||
def ns_delete(self, sld: str, tld: str, nameserver: str) -> dict:
|
||||
"""
|
||||
Delete a child nameserver (glue record).
|
||||
|
||||
Args:
|
||||
sld: Second-level domain (e.g., "example" for example.com)
|
||||
tld: Top-level domain (e.g., "com")
|
||||
nameserver: Nameserver hostname to delete (e.g., "ns1.example.com")
|
||||
|
||||
Returns:
|
||||
dict with domain, nameserver, success
|
||||
"""
|
||||
root = self._request(
|
||||
"namecheap.domains.ns.delete",
|
||||
SLD=sld,
|
||||
TLD=tld,
|
||||
Nameserver=nameserver,
|
||||
)
|
||||
|
||||
result = root.find(".//nc:DomainNSDeleteResult", NS)
|
||||
if result is None:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"domain": result.attrib.get("Domain"),
|
||||
"nameserver": result.attrib.get("Nameserver"),
|
||||
"success": result.attrib.get("IsSuccess") == "true",
|
||||
}
|
||||
|
||||
def ns_get_info(self, sld: str, tld: str, nameserver: str) -> dict:
|
||||
"""
|
||||
Get info about a child nameserver (glue record).
|
||||
|
||||
Args:
|
||||
sld: Second-level domain (e.g., "example" for example.com)
|
||||
tld: Top-level domain (e.g., "com")
|
||||
nameserver: Nameserver hostname (e.g., "ns1.example.com")
|
||||
|
||||
Returns:
|
||||
dict with domain, nameserver, ip, statuses
|
||||
"""
|
||||
root = self._request(
|
||||
"namecheap.domains.ns.getInfo",
|
||||
SLD=sld,
|
||||
TLD=tld,
|
||||
Nameserver=nameserver,
|
||||
)
|
||||
|
||||
result = root.find(".//nc:DomainNSInfoResult", NS)
|
||||
if result is None:
|
||||
return {}
|
||||
|
||||
statuses = []
|
||||
for status in result.findall("nc:NameserverStatuses/nc:Status", NS):
|
||||
if status.text:
|
||||
statuses.append(status.text)
|
||||
|
||||
return {
|
||||
"domain": result.attrib.get("Domain"),
|
||||
"nameserver": result.attrib.get("Nameserver"),
|
||||
"ip": result.attrib.get("IP"),
|
||||
"statuses": statuses,
|
||||
}
|
||||
|
||||
def ns_update(self, sld: str, tld: str, nameserver: str, old_ip: str, ip: str) -> dict:
|
||||
"""
|
||||
Update a child nameserver (glue record) IP address.
|
||||
|
||||
Args:
|
||||
sld: Second-level domain (e.g., "example" for example.com)
|
||||
tld: Top-level domain (e.g., "com")
|
||||
nameserver: Nameserver hostname (e.g., "ns1.example.com")
|
||||
old_ip: Current IP address
|
||||
ip: New IP address
|
||||
|
||||
Returns:
|
||||
dict with domain, nameserver, ip, success
|
||||
"""
|
||||
root = self._request(
|
||||
"namecheap.domains.ns.update",
|
||||
SLD=sld,
|
||||
TLD=tld,
|
||||
Nameserver=nameserver,
|
||||
OldIP=old_ip,
|
||||
IP=ip,
|
||||
)
|
||||
|
||||
result = root.find(".//nc:DomainNSUpdateResult", NS)
|
||||
if result is None:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"domain": result.attrib.get("Domain"),
|
||||
"nameserver": result.attrib.get("Nameserver"),
|
||||
"ip": result.attrib.get("IP"),
|
||||
"success": result.attrib.get("IsSuccess") == "true",
|
||||
}
|
||||
|
||||
# ===== Account =====
|
||||
|
||||
def users_get_balances(self) -> dict:
|
||||
"""Get account balance"""
|
||||
root = self._request("namecheap.users.getBalances")
|
||||
|
||||
balance = root.find(".//nc:UserGetBalancesResult", NS)
|
||||
if balance is None:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"currency": balance.attrib.get("Currency"),
|
||||
"available_balance": float(balance.attrib.get("AvailableBalance", 0)),
|
||||
"account_balance": float(balance.attrib.get("AccountBalance", 0)),
|
||||
"earned_amount": float(balance.attrib.get("EarnedAmount", 0)),
|
||||
}
|
||||
|
||||
def users_get_pricing(self, product_type: str, product_category: str = "") -> list[dict]:
|
||||
"""Get pricing for products"""
|
||||
params = {"ProductType": product_type}
|
||||
if product_category:
|
||||
params["ProductCategory"] = product_category
|
||||
|
||||
root = self._request("namecheap.users.getPricing", **params)
|
||||
|
||||
products = []
|
||||
for product in root.findall(".//nc:Product", NS):
|
||||
for price in product.findall(".//nc:Price", NS):
|
||||
products.append({
|
||||
"name": product.attrib.get("Name"),
|
||||
"duration": price.attrib.get("Duration"),
|
||||
"duration_type": price.attrib.get("DurationType"),
|
||||
"price": float(price.attrib.get("Price", 0)),
|
||||
"currency": price.attrib.get("Currency"),
|
||||
})
|
||||
|
||||
return products
|
||||
Reference in New Issue
Block a user