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/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