namecheap-python 0.1.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 +49 -0
- namecheap/client.py +887 -0
- namecheap/exceptions.py +39 -0
- namecheap/utils.py +323 -0
- namecheap_python-0.1.0.dist-info/LICENSE +21 -0
- namecheap_python-0.1.0.dist-info/METADATA +346 -0
- namecheap_python-0.1.0.dist-info/RECORD +9 -0
- namecheap_python-0.1.0.dist-info/WHEEL +4 -0
- namecheap_python-0.1.0.dist-info/entry_points.txt +5 -0
namecheap/client.py
ADDED
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Namecheap API Python client
|
|
3
|
+
|
|
4
|
+
A Python wrapper for interacting with the Namecheap API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
import xml.etree.ElementTree as ET
|
|
9
|
+
from typing import Dict, List, Any, Optional, Union, Tuple
|
|
10
|
+
|
|
11
|
+
from .exceptions import NamecheapException
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NamecheapClient:
|
|
15
|
+
"""
|
|
16
|
+
Client for interacting with the Namecheap API
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
SANDBOX_API_URL = "https://api.sandbox.namecheap.com/xml.response"
|
|
20
|
+
PRODUCTION_API_URL = "https://api.namecheap.com/xml.response"
|
|
21
|
+
|
|
22
|
+
# API rate limits
|
|
23
|
+
RATE_LIMIT_MINUTE = 20
|
|
24
|
+
RATE_LIMIT_HOUR = 700
|
|
25
|
+
RATE_LIMIT_DAY = 8000
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
api_user: Optional[str] = None,
|
|
30
|
+
api_key: Optional[str] = None,
|
|
31
|
+
username: Optional[str] = None,
|
|
32
|
+
client_ip: Optional[str] = None,
|
|
33
|
+
sandbox: Optional[bool] = None,
|
|
34
|
+
debug: bool = False,
|
|
35
|
+
load_env: bool = True
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Initialize the Namecheap API client
|
|
39
|
+
|
|
40
|
+
If credentials are not provided directly, they will be loaded from environment variables
|
|
41
|
+
when load_env=True (default):
|
|
42
|
+
- NAMECHEAP_API_USER: Your Namecheap API username
|
|
43
|
+
- NAMECHEAP_API_KEY: Your API key
|
|
44
|
+
- NAMECHEAP_USERNAME: Your Namecheap username (typically the same as API_USER)
|
|
45
|
+
- NAMECHEAP_CLIENT_IP: Your whitelisted IP address
|
|
46
|
+
- NAMECHEAP_USE_SANDBOX: "True" for sandbox environment, "False" for production
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
api_user: Your Namecheap username for API access
|
|
50
|
+
api_key: Your API key generated from Namecheap account
|
|
51
|
+
username: Your Namecheap username (typically the same as api_user)
|
|
52
|
+
client_ip: The whitelisted IP address making the request
|
|
53
|
+
sandbox: Whether to use the sandbox environment (default: read from env or True)
|
|
54
|
+
debug: Whether to enable debug logging (default: False)
|
|
55
|
+
load_env: Whether to load credentials from environment variables (default: True)
|
|
56
|
+
If True, environment values are used as fallbacks for any parameters not provided
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ValueError: If required credentials are missing after attempting to load from environment
|
|
60
|
+
"""
|
|
61
|
+
# Try to load environment variables if load_env is True
|
|
62
|
+
if load_env:
|
|
63
|
+
try:
|
|
64
|
+
# Attempt to import dotenv for enhanced functionality
|
|
65
|
+
from dotenv import load_dotenv, find_dotenv
|
|
66
|
+
dotenv_path = find_dotenv(usecwd=True)
|
|
67
|
+
if dotenv_path:
|
|
68
|
+
load_dotenv(dotenv_path)
|
|
69
|
+
except ImportError:
|
|
70
|
+
# dotenv package not installed, just use os.environ
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
import os
|
|
74
|
+
|
|
75
|
+
# Use provided values or fall back to environment variables
|
|
76
|
+
self.api_user = api_user or os.environ.get("NAMECHEAP_API_USER")
|
|
77
|
+
self.api_key = api_key or os.environ.get("NAMECHEAP_API_KEY")
|
|
78
|
+
self.username = username or os.environ.get("NAMECHEAP_USERNAME")
|
|
79
|
+
self.client_ip = client_ip or os.environ.get("NAMECHEAP_CLIENT_IP")
|
|
80
|
+
|
|
81
|
+
# Handle sandbox setting
|
|
82
|
+
if sandbox is None:
|
|
83
|
+
sandbox_value = os.environ.get("NAMECHEAP_USE_SANDBOX", "True")
|
|
84
|
+
sandbox = sandbox_value.lower() in ("true", "yes", "1")
|
|
85
|
+
else:
|
|
86
|
+
# Use provided values directly
|
|
87
|
+
self.api_user = api_user
|
|
88
|
+
self.api_key = api_key
|
|
89
|
+
self.username = username
|
|
90
|
+
self.client_ip = client_ip
|
|
91
|
+
|
|
92
|
+
# Default to sandbox mode if not specified
|
|
93
|
+
if sandbox is None:
|
|
94
|
+
sandbox = True
|
|
95
|
+
|
|
96
|
+
# Validate required credentials
|
|
97
|
+
missing_vars = []
|
|
98
|
+
if not self.api_user:
|
|
99
|
+
missing_vars.append("api_user (NAMECHEAP_API_USER)")
|
|
100
|
+
if not self.api_key:
|
|
101
|
+
missing_vars.append("api_key (NAMECHEAP_API_KEY)")
|
|
102
|
+
if not self.username:
|
|
103
|
+
missing_vars.append("username (NAMECHEAP_USERNAME)")
|
|
104
|
+
if not self.client_ip:
|
|
105
|
+
missing_vars.append("client_ip (NAMECHEAP_CLIENT_IP)")
|
|
106
|
+
|
|
107
|
+
if missing_vars:
|
|
108
|
+
error_message = (
|
|
109
|
+
f"Missing required Namecheap API credentials: {', '.join(missing_vars)}.\n\n"
|
|
110
|
+
"Please either:\n"
|
|
111
|
+
"1. Create a .env file in your project directory with these variables, or\n"
|
|
112
|
+
"2. Set them as environment variables in your shell, or\n"
|
|
113
|
+
"3. Pass them explicitly when creating the NamecheapClient instance\n\n"
|
|
114
|
+
"Example .env file:\n"
|
|
115
|
+
"NAMECHEAP_API_USER=your_username\n"
|
|
116
|
+
"NAMECHEAP_API_KEY=your_api_key\n"
|
|
117
|
+
"NAMECHEAP_USERNAME=your_username\n"
|
|
118
|
+
"NAMECHEAP_CLIENT_IP=your_whitelisted_ip\n"
|
|
119
|
+
"NAMECHEAP_USE_SANDBOX=True"
|
|
120
|
+
)
|
|
121
|
+
raise ValueError(error_message)
|
|
122
|
+
|
|
123
|
+
# Set URL based on sandbox setting
|
|
124
|
+
self.base_url = self.SANDBOX_API_URL if sandbox else self.PRODUCTION_API_URL
|
|
125
|
+
self.debug = debug
|
|
126
|
+
|
|
127
|
+
def _get_base_params(self) -> Dict[str, str]:
|
|
128
|
+
"""
|
|
129
|
+
Get the base parameters required for all API requests
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Dict containing the base authentication parameters
|
|
133
|
+
"""
|
|
134
|
+
return {
|
|
135
|
+
"ApiUser": self.api_user,
|
|
136
|
+
"ApiKey": self.api_key,
|
|
137
|
+
"UserName": self.username,
|
|
138
|
+
"ClientIp": self.client_ip,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
def _make_request(
|
|
142
|
+
self,
|
|
143
|
+
command: str,
|
|
144
|
+
params: Optional[Dict[str, Any]] = None
|
|
145
|
+
) -> Dict[str, Any]:
|
|
146
|
+
"""
|
|
147
|
+
Make a request to the Namecheap API
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
command: The API command to execute (e.g., "namecheap.domains.check")
|
|
151
|
+
params: Additional parameters for the API request
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Parsed response from the API
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
NamecheapException: If the API returns an error
|
|
158
|
+
requests.RequestException: If there's an issue with the HTTP request
|
|
159
|
+
"""
|
|
160
|
+
request_params = self._get_base_params()
|
|
161
|
+
request_params["Command"] = command
|
|
162
|
+
|
|
163
|
+
if params:
|
|
164
|
+
request_params.update(params)
|
|
165
|
+
|
|
166
|
+
if self.debug:
|
|
167
|
+
print(f"Making request to {self.base_url}")
|
|
168
|
+
print(f"Parameters: {request_params}")
|
|
169
|
+
|
|
170
|
+
response = requests.get(self.base_url, params=request_params)
|
|
171
|
+
|
|
172
|
+
if self.debug:
|
|
173
|
+
print(f"Response status code: {response.status_code}")
|
|
174
|
+
print(f"Response content: {response.text[:1000]}...")
|
|
175
|
+
|
|
176
|
+
response.raise_for_status()
|
|
177
|
+
|
|
178
|
+
return self._parse_response(response.text)
|
|
179
|
+
|
|
180
|
+
def _parse_response(self, xml_response: str) -> Dict[str, Any]:
|
|
181
|
+
"""
|
|
182
|
+
Parse the XML response from the API into a Python dictionary
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
xml_response: The XML response from the API
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Parsed response as a dictionary
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
NamecheapException: If the API returns an error
|
|
192
|
+
"""
|
|
193
|
+
# Direct error handling for common error messages - handles the malformed XML case
|
|
194
|
+
if "API Key is invalid or API access has not been enabled" in xml_response:
|
|
195
|
+
raise NamecheapException("1011102", "API Key is invalid or API access has not been enabled - Please verify your API key and ensure API access is enabled at https://ap.www.namecheap.com/settings/tools/apiaccess/")
|
|
196
|
+
elif "IP is not in the whitelist" in xml_response:
|
|
197
|
+
raise NamecheapException("1011147", "IP is not in the whitelist - Please whitelist your IP address in your Namecheap API settings")
|
|
198
|
+
|
|
199
|
+
# Fix common XML syntax errors
|
|
200
|
+
xml_response = xml_response.replace("</e>", "</Error>")
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
root = ET.fromstring(xml_response)
|
|
204
|
+
except ET.ParseError as e:
|
|
205
|
+
# Last resort error handling
|
|
206
|
+
raise NamecheapException("XML_PARSE_ERROR", f"Failed to parse XML response: {str(e)}")
|
|
207
|
+
|
|
208
|
+
# Check if there was an error
|
|
209
|
+
status = root.attrib.get('Status')
|
|
210
|
+
if status == 'ERROR':
|
|
211
|
+
errors = root.findall('.//Errors/Error')
|
|
212
|
+
|
|
213
|
+
if errors and len(errors) > 0:
|
|
214
|
+
# Get the first error details
|
|
215
|
+
error = errors[0]
|
|
216
|
+
error_text = error.text or "Unknown error"
|
|
217
|
+
error_number = error.attrib.get('Number', '0')
|
|
218
|
+
|
|
219
|
+
# Create descriptive error message based on common error codes
|
|
220
|
+
if error_number == '1011102':
|
|
221
|
+
error_text = f"{error_text} - Please verify your API key and ensure API access is enabled at https://ap.www.namecheap.com/settings/tools/apiaccess/"
|
|
222
|
+
elif error_number == '1011147':
|
|
223
|
+
error_text = f"{error_text} - Please whitelist your IP address in your Namecheap API settings"
|
|
224
|
+
elif error_number == '1010900':
|
|
225
|
+
error_text = f"{error_text} - Please check your username is correct"
|
|
226
|
+
|
|
227
|
+
raise NamecheapException(error_number, error_text)
|
|
228
|
+
else:
|
|
229
|
+
raise NamecheapException("UNKNOWN_ERROR", "Unknown error occurred but no error details provided")
|
|
230
|
+
|
|
231
|
+
# Handle namespaces in the XML
|
|
232
|
+
namespaces = {'ns': 'http://api.namecheap.com/xml.response'}
|
|
233
|
+
|
|
234
|
+
# Special handling for domains.check command
|
|
235
|
+
requested_command = root.find('.//ns:RequestedCommand', namespaces)
|
|
236
|
+
if requested_command is not None and requested_command.text == 'namecheap.domains.check':
|
|
237
|
+
domain_results = []
|
|
238
|
+
for domain_elem in root.findall('.//ns:DomainCheckResult', namespaces):
|
|
239
|
+
# Always convert price to a number
|
|
240
|
+
price_str = domain_elem.get('PremiumRegistrationPrice', '0')
|
|
241
|
+
price = float(price_str) if price_str else 0.0
|
|
242
|
+
|
|
243
|
+
domain_info = {
|
|
244
|
+
'Domain': domain_elem.get('Domain'),
|
|
245
|
+
'Available': domain_elem.get('Available') == 'true',
|
|
246
|
+
'IsPremiumName': domain_elem.get('IsPremiumName') == 'true',
|
|
247
|
+
'PremiumRegistrationPrice': price
|
|
248
|
+
}
|
|
249
|
+
domain_results.append(domain_info)
|
|
250
|
+
return {'DomainCheckResult': domain_results}
|
|
251
|
+
|
|
252
|
+
# Standard parsing for other commands
|
|
253
|
+
command_response = root.find('.//ns:CommandResponse', namespaces)
|
|
254
|
+
if command_response is None:
|
|
255
|
+
return {}
|
|
256
|
+
|
|
257
|
+
return self._element_to_dict(command_response)
|
|
258
|
+
|
|
259
|
+
def _element_to_dict(self, element: ET.Element) -> Dict[str, Any]:
|
|
260
|
+
"""
|
|
261
|
+
Convert an XML element to a Python dictionary
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
element: The XML element to convert
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Dictionary representation of the XML element
|
|
268
|
+
"""
|
|
269
|
+
result = {}
|
|
270
|
+
|
|
271
|
+
# Add element attributes
|
|
272
|
+
for key, value in element.attrib.items():
|
|
273
|
+
# Convert some common boolean-like values
|
|
274
|
+
if value.lower() in ('true', 'yes', 'enabled'):
|
|
275
|
+
result[key] = True
|
|
276
|
+
elif value.lower() in ('false', 'no', 'disabled'):
|
|
277
|
+
result[key] = False
|
|
278
|
+
else:
|
|
279
|
+
result[key] = value
|
|
280
|
+
|
|
281
|
+
# Process child elements
|
|
282
|
+
for child in element:
|
|
283
|
+
child_data = self._element_to_dict(child)
|
|
284
|
+
|
|
285
|
+
# Remove namespace from tag if present
|
|
286
|
+
tag = child.tag
|
|
287
|
+
if '}' in tag:
|
|
288
|
+
tag = tag.split('}', 1)[1] # Remove namespace part
|
|
289
|
+
|
|
290
|
+
# Handle multiple elements with the same tag
|
|
291
|
+
if tag in result:
|
|
292
|
+
if isinstance(result[tag], list):
|
|
293
|
+
result[tag].append(child_data)
|
|
294
|
+
else:
|
|
295
|
+
result[tag] = [result[tag], child_data]
|
|
296
|
+
else:
|
|
297
|
+
result[tag] = child_data
|
|
298
|
+
|
|
299
|
+
# If the element has text and no children, just return the text
|
|
300
|
+
if element.text and element.text.strip() and len(result) == 0:
|
|
301
|
+
text = element.text.strip()
|
|
302
|
+
# Try to convert to appropriate types
|
|
303
|
+
if text.isdigit():
|
|
304
|
+
return int(text)
|
|
305
|
+
elif text.lower() in ('true', 'yes', 'enabled'):
|
|
306
|
+
return True
|
|
307
|
+
elif text.lower() in ('false', 'no', 'disabled'):
|
|
308
|
+
return False
|
|
309
|
+
return text
|
|
310
|
+
|
|
311
|
+
return result
|
|
312
|
+
|
|
313
|
+
def _split_domain_name(self, domain_name: str) -> Tuple[str, str]:
|
|
314
|
+
"""
|
|
315
|
+
Split a domain name into its SLD and TLD parts
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
domain_name: Full domain name (e.g., "example.com")
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Tuple containing (SLD, TLD) parts (e.g., ("example", "com"))
|
|
322
|
+
"""
|
|
323
|
+
parts = domain_name.split('.')
|
|
324
|
+
sld = parts[0]
|
|
325
|
+
tld = '.'.join(parts[1:])
|
|
326
|
+
return sld, tld
|
|
327
|
+
|
|
328
|
+
# Domain API Methods
|
|
329
|
+
|
|
330
|
+
def domains_check(self, domains: List[str]) -> Dict[str, Any]:
|
|
331
|
+
"""
|
|
332
|
+
Check if domains are available for registration
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
domains: List of domains to check availability (max 50)
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Dictionary with availability information for each domain.
|
|
339
|
+
The result is a dictionary with a "DomainCheckResult" key that contains
|
|
340
|
+
a list of dictionaries, each with domain information including:
|
|
341
|
+
- Domain: domain name
|
|
342
|
+
- Available: whether the domain is available (boolean)
|
|
343
|
+
- IsPremiumName: whether the domain is a premium name (boolean)
|
|
344
|
+
- PremiumRegistrationPrice: price for premium domains
|
|
345
|
+
|
|
346
|
+
Raises:
|
|
347
|
+
ValueError: If more than 50 domains are provided
|
|
348
|
+
NamecheapException: If the API returns an error
|
|
349
|
+
"""
|
|
350
|
+
if len(domains) > 50:
|
|
351
|
+
raise ValueError("Maximum of 50 domains can be checked in a single API call")
|
|
352
|
+
|
|
353
|
+
# Format the domain list according to API requirements
|
|
354
|
+
domain_list = ','.join(domains)
|
|
355
|
+
|
|
356
|
+
params = {
|
|
357
|
+
"DomainList": domain_list
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
# Make the API request
|
|
361
|
+
result = self._make_request("namecheap.domains.check", params)
|
|
362
|
+
|
|
363
|
+
# Extract domain check results from the response and normalize format
|
|
364
|
+
normalized_results = []
|
|
365
|
+
|
|
366
|
+
# Handle direct access to DomainCheckResult
|
|
367
|
+
if "DomainCheckResult" in result:
|
|
368
|
+
domain_results = result["DomainCheckResult"]
|
|
369
|
+
if not isinstance(domain_results, list):
|
|
370
|
+
domain_results = [domain_results]
|
|
371
|
+
normalized_results = domain_results
|
|
372
|
+
# Handle nested structure
|
|
373
|
+
elif "CommandResponse" in result and isinstance(result["CommandResponse"], dict):
|
|
374
|
+
command_resp = result["CommandResponse"]
|
|
375
|
+
if "DomainCheckResult" in command_resp:
|
|
376
|
+
domain_results = command_resp["DomainCheckResult"]
|
|
377
|
+
if not isinstance(domain_results, list):
|
|
378
|
+
domain_results = [domain_results]
|
|
379
|
+
normalized_results = domain_results
|
|
380
|
+
|
|
381
|
+
# Normalize result values to standard format
|
|
382
|
+
for domain in normalized_results:
|
|
383
|
+
# Convert string boolean values to Python booleans
|
|
384
|
+
if "Available" in domain and isinstance(domain["Available"], str):
|
|
385
|
+
domain["Available"] = domain["Available"].lower() == "true"
|
|
386
|
+
if "IsPremiumName" in domain and isinstance(domain["IsPremiumName"], str):
|
|
387
|
+
domain["IsPremiumName"] = domain["IsPremiumName"].lower() == "true"
|
|
388
|
+
|
|
389
|
+
# Handle attribute-style values from XML
|
|
390
|
+
if "@Domain" in domain and "Domain" not in domain:
|
|
391
|
+
domain["Domain"] = domain.pop("@Domain")
|
|
392
|
+
if "@Available" in domain and "Available" not in domain:
|
|
393
|
+
avail_val = domain.pop("@Available")
|
|
394
|
+
domain["Available"] = avail_val.lower() == "true"
|
|
395
|
+
if "@IsPremiumName" in domain and "IsPremiumName" not in domain:
|
|
396
|
+
premium_val = domain.pop("@IsPremiumName")
|
|
397
|
+
domain["IsPremiumName"] = premium_val.lower() == "true"
|
|
398
|
+
if "@PremiumRegistrationPrice" in domain and "PremiumRegistrationPrice" not in domain:
|
|
399
|
+
price_str = domain.pop("@PremiumRegistrationPrice")
|
|
400
|
+
domain["PremiumRegistrationPrice"] = float(price_str) if price_str else 0.0
|
|
401
|
+
|
|
402
|
+
# Return a standardized result structure
|
|
403
|
+
return {"DomainCheckResult": normalized_results}
|
|
404
|
+
|
|
405
|
+
def domains_get_list(
|
|
406
|
+
self,
|
|
407
|
+
page: int = 1,
|
|
408
|
+
page_size: int = 20,
|
|
409
|
+
sort_by: str = "NAME",
|
|
410
|
+
list_type: str = "ALL",
|
|
411
|
+
search_term: Optional[str] = None
|
|
412
|
+
) -> Dict[str, Any]:
|
|
413
|
+
"""
|
|
414
|
+
Get a list of domains in the user's account
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
page: Page number to return (default: 1)
|
|
418
|
+
page_size: Number of domains to return per page (default: 20, max: 100)
|
|
419
|
+
sort_by: Column to sort by (NAME, NAME_DESC, EXPIREDATE, EXPIREDATE_DESC, CREATEDATE, CREATEDATE_DESC)
|
|
420
|
+
list_type: Type of domains to list (ALL, EXPIRING, EXPIRED)
|
|
421
|
+
search_term: Keyword to look for in the domain list
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Dictionary with domain list information
|
|
425
|
+
|
|
426
|
+
Raises:
|
|
427
|
+
ValueError: If page_size is greater than 100
|
|
428
|
+
NamecheapException: If the API returns an error
|
|
429
|
+
"""
|
|
430
|
+
if page_size > 100:
|
|
431
|
+
raise ValueError("Maximum page size is 100")
|
|
432
|
+
|
|
433
|
+
valid_sort_options = ['NAME', 'NAME_DESC', 'EXPIREDATE', 'EXPIREDATE_DESC', 'CREATEDATE', 'CREATEDATE_DESC']
|
|
434
|
+
if sort_by not in valid_sort_options:
|
|
435
|
+
raise ValueError(f"sort_by must be one of {valid_sort_options}")
|
|
436
|
+
|
|
437
|
+
valid_list_types = ['ALL', 'EXPIRING', 'EXPIRED']
|
|
438
|
+
if list_type not in valid_list_types:
|
|
439
|
+
raise ValueError(f"list_type must be one of {valid_list_types}")
|
|
440
|
+
|
|
441
|
+
params = {
|
|
442
|
+
"Page": page,
|
|
443
|
+
"PageSize": page_size,
|
|
444
|
+
"SortBy": sort_by,
|
|
445
|
+
"ListType": list_type
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if search_term:
|
|
449
|
+
params["SearchTerm"] = search_term
|
|
450
|
+
|
|
451
|
+
return self._make_request("namecheap.domains.getList", params)
|
|
452
|
+
|
|
453
|
+
def domains_get_contacts(self, domain_name: str) -> Dict[str, Any]:
|
|
454
|
+
"""
|
|
455
|
+
Get contact information for a domain
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
domain_name: The domain name to get contact information for
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Dictionary with contact information for the domain
|
|
462
|
+
|
|
463
|
+
Raises:
|
|
464
|
+
NamecheapException: If the API returns an error
|
|
465
|
+
"""
|
|
466
|
+
sld, tld = self._split_domain_name(domain_name)
|
|
467
|
+
|
|
468
|
+
params = {
|
|
469
|
+
"DomainName": sld,
|
|
470
|
+
"TLD": tld
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return self._make_request("namecheap.domains.getContacts", params)
|
|
474
|
+
|
|
475
|
+
def domains_create(
|
|
476
|
+
self,
|
|
477
|
+
domain_name: str,
|
|
478
|
+
years: int = 1,
|
|
479
|
+
registrant_info: Optional[Dict[str, str]] = None,
|
|
480
|
+
tech_info: Optional[Dict[str, str]] = None,
|
|
481
|
+
admin_info: Optional[Dict[str, str]] = None,
|
|
482
|
+
aux_info: Optional[Dict[str, str]] = None,
|
|
483
|
+
nameservers: Optional[List[str]] = None,
|
|
484
|
+
add_free_whois_guard: bool = True,
|
|
485
|
+
wg_enabled: bool = True,
|
|
486
|
+
**kwargs
|
|
487
|
+
) -> Dict[str, Any]:
|
|
488
|
+
"""
|
|
489
|
+
Register a new domain
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
domain_name: The domain name to register
|
|
493
|
+
years: Number of years to register the domain for (default: 1)
|
|
494
|
+
registrant_info: Registrant contact information (required)
|
|
495
|
+
tech_info: Technical contact information (if not provided, registrant_info is used)
|
|
496
|
+
admin_info: Administrative contact information (if not provided, registrant_info is used)
|
|
497
|
+
aux_info: Billing/Auxiliary contact information (if not provided, registrant_info is used)
|
|
498
|
+
nameservers: List of nameservers to use (comma-separated)
|
|
499
|
+
add_free_whois_guard: Whether to add free WhoisGuard privacy protection (default: True)
|
|
500
|
+
wg_enabled: Whether to enable WhoisGuard (default: True)
|
|
501
|
+
**kwargs: Additional parameters to pass to the API
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
Dictionary with domain registration information
|
|
505
|
+
|
|
506
|
+
Raises:
|
|
507
|
+
ValueError: If registrant_info is not provided
|
|
508
|
+
NamecheapException: If the API returns an error
|
|
509
|
+
"""
|
|
510
|
+
if not registrant_info:
|
|
511
|
+
raise ValueError("registrant_info is required for domain registration")
|
|
512
|
+
|
|
513
|
+
sld, tld = self._split_domain_name(domain_name)
|
|
514
|
+
|
|
515
|
+
params = {
|
|
516
|
+
"DomainName": sld,
|
|
517
|
+
"TLD": tld,
|
|
518
|
+
"Years": years,
|
|
519
|
+
"AddFreeWhoisGuard": "yes" if add_free_whois_guard else "no",
|
|
520
|
+
"WGEnabled": "yes" if wg_enabled else "no"
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
# Add nameservers if provided
|
|
524
|
+
if nameservers:
|
|
525
|
+
params["Nameservers"] = ",".join(nameservers)
|
|
526
|
+
|
|
527
|
+
# Add contact information
|
|
528
|
+
contacts = {
|
|
529
|
+
"Registrant": registrant_info,
|
|
530
|
+
"Tech": tech_info if tech_info else registrant_info,
|
|
531
|
+
"Admin": admin_info if admin_info else registrant_info,
|
|
532
|
+
"AuxBilling": aux_info if aux_info else registrant_info
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
for contact_type, info in contacts.items():
|
|
536
|
+
if info:
|
|
537
|
+
for key, value in info.items():
|
|
538
|
+
params[f"{contact_type}{key}"] = value
|
|
539
|
+
|
|
540
|
+
# Add any additional parameters
|
|
541
|
+
params.update(kwargs)
|
|
542
|
+
|
|
543
|
+
return self._make_request("namecheap.domains.create", params)
|
|
544
|
+
|
|
545
|
+
def domains_get_tld_list(self) -> Dict[str, Any]:
|
|
546
|
+
"""
|
|
547
|
+
Get a list of available TLDs
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
Dictionary with TLD information
|
|
551
|
+
|
|
552
|
+
Raises:
|
|
553
|
+
NamecheapException: If the API returns an error
|
|
554
|
+
"""
|
|
555
|
+
return self._make_request("namecheap.domains.getTldList")
|
|
556
|
+
|
|
557
|
+
def domains_renew(
|
|
558
|
+
self,
|
|
559
|
+
domain_name: str,
|
|
560
|
+
years: int = 1,
|
|
561
|
+
promotion_code: Optional[str] = None
|
|
562
|
+
) -> Dict[str, Any]:
|
|
563
|
+
"""
|
|
564
|
+
Renew a domain
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
domain_name: The domain name to renew
|
|
568
|
+
years: Number of years to renew the domain for (default: 1)
|
|
569
|
+
promotion_code: Promotional (coupon) code for the domain renewal
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
Dictionary with domain renewal information
|
|
573
|
+
|
|
574
|
+
Raises:
|
|
575
|
+
NamecheapException: If the API returns an error
|
|
576
|
+
"""
|
|
577
|
+
sld, tld = self._split_domain_name(domain_name)
|
|
578
|
+
|
|
579
|
+
params = {
|
|
580
|
+
"DomainName": sld,
|
|
581
|
+
"TLD": tld,
|
|
582
|
+
"Years": years
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if promotion_code:
|
|
586
|
+
params["PromotionCode"] = promotion_code
|
|
587
|
+
|
|
588
|
+
return self._make_request("namecheap.domains.renew", params)
|
|
589
|
+
|
|
590
|
+
def domains_get_info(self, domain_name: str) -> Dict[str, Any]:
|
|
591
|
+
"""
|
|
592
|
+
Get information about a domain
|
|
593
|
+
|
|
594
|
+
Args:
|
|
595
|
+
domain_name: The domain name to get information for
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
Dictionary with domain information
|
|
599
|
+
|
|
600
|
+
Raises:
|
|
601
|
+
NamecheapException: If the API returns an error
|
|
602
|
+
"""
|
|
603
|
+
sld, tld = self._split_domain_name(domain_name)
|
|
604
|
+
|
|
605
|
+
params = {
|
|
606
|
+
"DomainName": sld,
|
|
607
|
+
"TLD": tld,
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return self._make_request("namecheap.domains.getInfo", params)
|
|
611
|
+
|
|
612
|
+
def domains_dns_set_custom(
|
|
613
|
+
self,
|
|
614
|
+
domain_name: str,
|
|
615
|
+
nameservers: List[str]
|
|
616
|
+
) -> Dict[str, Any]:
|
|
617
|
+
"""
|
|
618
|
+
Set custom nameservers for a domain
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
domain_name: The domain name to set nameservers for
|
|
622
|
+
nameservers: List of nameservers to use (max 12)
|
|
623
|
+
|
|
624
|
+
Returns:
|
|
625
|
+
Dictionary with status information
|
|
626
|
+
|
|
627
|
+
Raises:
|
|
628
|
+
ValueError: If more than 12 nameservers are provided
|
|
629
|
+
NamecheapException: If the API returns an error
|
|
630
|
+
"""
|
|
631
|
+
if len(nameservers) > 12:
|
|
632
|
+
raise ValueError("Maximum of 12 nameservers can be set")
|
|
633
|
+
|
|
634
|
+
sld, tld = self._split_domain_name(domain_name)
|
|
635
|
+
|
|
636
|
+
params = {
|
|
637
|
+
"DomainName": sld,
|
|
638
|
+
"TLD": tld,
|
|
639
|
+
"Nameservers": ",".join(nameservers)
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return self._make_request("namecheap.domains.dns.setCustom", params)
|
|
643
|
+
|
|
644
|
+
def domains_dns_set_default(self, domain_name: str) -> Dict[str, Any]:
|
|
645
|
+
"""
|
|
646
|
+
Set default nameservers for a domain
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
domain_name: The domain name to set nameservers for
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
Dictionary with status information
|
|
653
|
+
|
|
654
|
+
Raises:
|
|
655
|
+
NamecheapException: If the API returns an error
|
|
656
|
+
"""
|
|
657
|
+
sld, tld = self._split_domain_name(domain_name)
|
|
658
|
+
|
|
659
|
+
params = {
|
|
660
|
+
"DomainName": sld,
|
|
661
|
+
"TLD": tld,
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return self._make_request("namecheap.domains.dns.setDefault", params)
|
|
665
|
+
|
|
666
|
+
def domains_dns_get_hosts(self, domain_name: str) -> Dict[str, Any]:
|
|
667
|
+
"""
|
|
668
|
+
Get DNS host records for a domain
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
domain_name: The domain name to get host records for
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
Dictionary with host record information in a standardized format:
|
|
675
|
+
{
|
|
676
|
+
"DomainDNSGetHostsResult": {
|
|
677
|
+
"Domain": "example.com",
|
|
678
|
+
"IsUsingOurDNS": True,
|
|
679
|
+
"EmailType": "NONE",
|
|
680
|
+
"host": [
|
|
681
|
+
{
|
|
682
|
+
"Name": "@",
|
|
683
|
+
"Type": "A",
|
|
684
|
+
"Address": "192.0.2.1",
|
|
685
|
+
"MXPref": "10",
|
|
686
|
+
"TTL": "1800",
|
|
687
|
+
"HostId": "12345",
|
|
688
|
+
"IsActive": True
|
|
689
|
+
},
|
|
690
|
+
...
|
|
691
|
+
]
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
Raises:
|
|
696
|
+
NamecheapException: If the API returns an error
|
|
697
|
+
"""
|
|
698
|
+
sld, tld = self._split_domain_name(domain_name)
|
|
699
|
+
|
|
700
|
+
params = {
|
|
701
|
+
"SLD": sld,
|
|
702
|
+
"TLD": tld,
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
result = self._make_request("namecheap.domains.dns.getHosts", params)
|
|
706
|
+
|
|
707
|
+
# Sample successful response:
|
|
708
|
+
# {
|
|
709
|
+
# "Type": "namecheap.domains.dns.getHosts",
|
|
710
|
+
# "DomainDNSGetHostsResult": {
|
|
711
|
+
# "Domain": "example.com",
|
|
712
|
+
# "EmailType": "NONE",
|
|
713
|
+
# "IsUsingOurDNS": true,
|
|
714
|
+
# "host": [
|
|
715
|
+
# {
|
|
716
|
+
# "HostId": "123456",
|
|
717
|
+
# "Name": "@",
|
|
718
|
+
# "Type": "A",
|
|
719
|
+
# "Address": "192.0.2.1",
|
|
720
|
+
# "MXPref": "10",
|
|
721
|
+
# "TTL": "1800",
|
|
722
|
+
# "IsActive": true
|
|
723
|
+
# },
|
|
724
|
+
# {
|
|
725
|
+
# "HostId": "123457",
|
|
726
|
+
# "Name": "www",
|
|
727
|
+
# "Type": "CNAME",
|
|
728
|
+
# "Address": "example.com.",
|
|
729
|
+
# "MXPref": "10",
|
|
730
|
+
# "TTL": "1800",
|
|
731
|
+
# "IsActive": true
|
|
732
|
+
# }
|
|
733
|
+
# ]
|
|
734
|
+
# }
|
|
735
|
+
# }
|
|
736
|
+
|
|
737
|
+
# Normalize the response to ensure host is always a list
|
|
738
|
+
if "DomainDNSGetHostsResult" in result:
|
|
739
|
+
hosts_result = result["DomainDNSGetHostsResult"]
|
|
740
|
+
|
|
741
|
+
if "host" in hosts_result:
|
|
742
|
+
host_records = hosts_result["host"]
|
|
743
|
+
# Convert single host record to a list for consistency
|
|
744
|
+
if not isinstance(host_records, list):
|
|
745
|
+
hosts_result["host"] = [host_records]
|
|
746
|
+
else:
|
|
747
|
+
# No host records found, initialize with empty list
|
|
748
|
+
hosts_result["host"] = []
|
|
749
|
+
|
|
750
|
+
return result
|
|
751
|
+
|
|
752
|
+
def domains_dns_set_hosts(
|
|
753
|
+
self,
|
|
754
|
+
domain_name: str,
|
|
755
|
+
hosts: List[Dict[str, Any]]
|
|
756
|
+
) -> Dict[str, Any]:
|
|
757
|
+
"""
|
|
758
|
+
Set DNS host records for a domain
|
|
759
|
+
|
|
760
|
+
Args:
|
|
761
|
+
domain_name: The domain name to set host records for
|
|
762
|
+
hosts: List of host record dictionaries with keys:
|
|
763
|
+
- HostName: Name of the host record (e.g., "@", "www")
|
|
764
|
+
- RecordType: Type of record (A, AAAA, CNAME, MX, TXT, URL, URL301, FRAME)
|
|
765
|
+
- Address: Value of the record
|
|
766
|
+
- MXPref: MX preference (required for MX records)
|
|
767
|
+
- TTL: Time to live in seconds (min 60, max 86400, default 1800)
|
|
768
|
+
|
|
769
|
+
Returns:
|
|
770
|
+
Dictionary with status information in a standardized format:
|
|
771
|
+
{
|
|
772
|
+
"DomainDNSSetHostsResult": {
|
|
773
|
+
"Domain": "example.com",
|
|
774
|
+
"IsSuccess": True
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
Raises:
|
|
779
|
+
ValueError: If any required host record fields are missing
|
|
780
|
+
NamecheapException: If the API returns an error
|
|
781
|
+
"""
|
|
782
|
+
sld, tld = self._split_domain_name(domain_name)
|
|
783
|
+
|
|
784
|
+
params = {
|
|
785
|
+
"SLD": sld,
|
|
786
|
+
"TLD": tld,
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
valid_record_types = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'URL', 'URL301', 'FRAME']
|
|
790
|
+
|
|
791
|
+
# Handle the case when hosts parameter is empty
|
|
792
|
+
if not hosts:
|
|
793
|
+
if self.debug:
|
|
794
|
+
print("Warning: No host records provided for DNS update")
|
|
795
|
+
|
|
796
|
+
# Normalize host record field names
|
|
797
|
+
normalized_hosts = []
|
|
798
|
+
for host in hosts:
|
|
799
|
+
# Convert between the API format (HostName) and a more user-friendly format (Name)
|
|
800
|
+
normalized_host = {}
|
|
801
|
+
|
|
802
|
+
# Handle Name/HostName field
|
|
803
|
+
if 'Name' in host:
|
|
804
|
+
normalized_host['HostName'] = host['Name']
|
|
805
|
+
elif 'HostName' in host:
|
|
806
|
+
normalized_host['HostName'] = host['HostName']
|
|
807
|
+
else:
|
|
808
|
+
raise ValueError(f"Host record is missing required field 'Name' or 'HostName'")
|
|
809
|
+
|
|
810
|
+
# Handle Type/RecordType field
|
|
811
|
+
if 'Type' in host:
|
|
812
|
+
normalized_host['RecordType'] = host['Type']
|
|
813
|
+
elif 'RecordType' in host:
|
|
814
|
+
normalized_host['RecordType'] = host['RecordType']
|
|
815
|
+
else:
|
|
816
|
+
raise ValueError(f"Host record is missing required field 'Type' or 'RecordType'")
|
|
817
|
+
|
|
818
|
+
# Handle Address/Value field
|
|
819
|
+
if 'Value' in host:
|
|
820
|
+
normalized_host['Address'] = host['Value']
|
|
821
|
+
elif 'Address' in host:
|
|
822
|
+
normalized_host['Address'] = host['Address']
|
|
823
|
+
else:
|
|
824
|
+
raise ValueError(f"Host record is missing required field 'Value' or 'Address'")
|
|
825
|
+
|
|
826
|
+
# Handle MXPref/Priority field
|
|
827
|
+
if normalized_host['RecordType'] == 'MX':
|
|
828
|
+
if 'Priority' in host:
|
|
829
|
+
normalized_host['MXPref'] = host['Priority']
|
|
830
|
+
elif 'MXPref' in host:
|
|
831
|
+
normalized_host['MXPref'] = host['MXPref']
|
|
832
|
+
else:
|
|
833
|
+
# Use default value for MX priority
|
|
834
|
+
normalized_host['MXPref'] = '10'
|
|
835
|
+
|
|
836
|
+
# Handle TTL field
|
|
837
|
+
if 'TTL' in host:
|
|
838
|
+
ttl = host['TTL']
|
|
839
|
+
# Convert to string if it's an integer
|
|
840
|
+
if isinstance(ttl, int):
|
|
841
|
+
ttl = str(ttl)
|
|
842
|
+
normalized_host['TTL'] = ttl
|
|
843
|
+
else:
|
|
844
|
+
# Use default TTL
|
|
845
|
+
normalized_host['TTL'] = '1800'
|
|
846
|
+
|
|
847
|
+
normalized_hosts.append(normalized_host)
|
|
848
|
+
|
|
849
|
+
# Add host records to API parameters
|
|
850
|
+
for i, host in enumerate(normalized_hosts):
|
|
851
|
+
record_type = host['RecordType']
|
|
852
|
+
if record_type not in valid_record_types:
|
|
853
|
+
raise ValueError(f"Invalid record type '{record_type}'. Must be one of {valid_record_types}")
|
|
854
|
+
|
|
855
|
+
# If TTL is provided, validate it
|
|
856
|
+
if 'TTL' in host:
|
|
857
|
+
try:
|
|
858
|
+
ttl = int(host['TTL'])
|
|
859
|
+
if ttl < 60 or ttl > 86400:
|
|
860
|
+
raise ValueError("TTL must be between 60 and 86400 seconds")
|
|
861
|
+
except ValueError:
|
|
862
|
+
raise ValueError(f"Invalid TTL value: {host['TTL']}. Must be an integer between 60 and 86400.")
|
|
863
|
+
|
|
864
|
+
params[f"HostName{i+1}"] = host['HostName']
|
|
865
|
+
params[f"RecordType{i+1}"] = host['RecordType']
|
|
866
|
+
params[f"Address{i+1}"] = host['Address']
|
|
867
|
+
|
|
868
|
+
if 'MXPref' in host:
|
|
869
|
+
params[f"MXPref{i+1}"] = host['MXPref']
|
|
870
|
+
if 'TTL' in host:
|
|
871
|
+
params[f"TTL{i+1}"] = host['TTL']
|
|
872
|
+
|
|
873
|
+
if self.debug:
|
|
874
|
+
print(f"Setting {len(normalized_hosts)} host records for {domain_name}")
|
|
875
|
+
|
|
876
|
+
result = self._make_request("namecheap.domains.dns.setHosts", params)
|
|
877
|
+
|
|
878
|
+
# Sample successful response:
|
|
879
|
+
# {
|
|
880
|
+
# "Type": "namecheap.domains.dns.setHosts",
|
|
881
|
+
# "DomainDNSSetHostsResult": {
|
|
882
|
+
# "Domain": "example.com",
|
|
883
|
+
# "IsSuccess": true
|
|
884
|
+
# }
|
|
885
|
+
# }
|
|
886
|
+
|
|
887
|
+
return result
|