From fb3249cba550700a7dfb69bc1e2de42a8b62fab2 Mon Sep 17 00:00:00 2001 From: Max Harley Date: Thu, 28 Jan 2021 16:45:26 -0500 Subject: [PATCH 1/2] Fix error adding MX records The field EmailType must be without a number, so I removed the numbering for that field in the _list_of_dictionaries_to_numbered_payload function. --- namecheap.py | 46 +++++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/namecheap.py b/namecheap.py index 05911ca..65c8d0e 100644 --- a/namecheap.py +++ b/namecheap.py @@ -41,7 +41,8 @@ def __init__(self, ApiUser, ApiKey, UserName, ClientIP, self.ClientIP = ClientIP self.endpoint = ENDPOINTS['sandbox' if sandbox else 'production'] self.debug = debug - self.payload_limit = 10 # After hitting this lenght limit script will move payload from POST params to POST data + # After hitting this lenght limit script will move payload from POST params to POST data + self.payload_limit = 10 self.attempts_count = attempts_count self.attempts_delay = attempts_delay @@ -104,12 +105,13 @@ def _payload(self, Command, extra_payload={}): extra_payload = {} return payload, extra_payload - def _fetch_xml(self, payload, extra_payload = None): + def _fetch_xml(self, payload, extra_payload=None): """Make network call and return parsed XML element""" attempts_left = self.attempts_count while attempts_left > 0: if extra_payload: - r = requests.post(self.endpoint, params=payload, data=extra_payload) + r = requests.post( + self.endpoint, params=payload, data=extra_payload) else: r = requests.post(self.endpoint, params=payload) if 200 <= r.status_code <= 299: @@ -148,9 +150,11 @@ class LazyGetListIterator(object): """When listing domain names, only one page is returned initially. The list needs to be paged through to see all. This iterator gets the next page when necessary.""" + def _get_more_results(self): xml = self.api._fetch_xml(self.payload) - xpath = './/{%(ns)s}CommandResponse/{%(ns)s}DomainGetListResult/{%(ns)s}Domain' % {'ns': NAMESPACE} + xpath = './/{%(ns)s}CommandResponse/{%(ns)s}DomainGetListResult/{%(ns)s}Domain' % { + 'ns': NAMESPACE} domains = xml.findall(xpath) for domain in domains: self.results.append(domain.attrib) @@ -211,7 +215,8 @@ def domains_check(self, domains): xpath = './/{%(ns)s}CommandResponse/{%(ns)s}DomainCheckResult' % {'ns': NAMESPACE} results = {} for check_result in xml.findall(xpath): - results[check_result.attrib['Domain']] = check_result.attrib['Available'] == 'true' + results[check_result.attrib['Domain'] + ] = check_result.attrib['Available'] == 'true' return results @classmethod @@ -236,8 +241,10 @@ def _list_of_dictionaries_to_numbered_payload(cls, l): 'cat3' : 'meow' } """ + + disallow_change = ['EmailType'] return dict(sum([ - [(k + str(i + 1), v) for k, v in d.items()] for i, d in enumerate(l) + [(k + str(i + 1), v) if k not in disallow_change else (k, v) for k, v in d.items()] for i, d in enumerate(l) ], [])) @classmethod @@ -291,14 +298,17 @@ def domains_getContacts(self, DomainName): ... } """ - xml = self._call('namecheap.domains.getContacts', {'DomainName': DomainName}) + xml = self._call('namecheap.domains.getContacts', + {'DomainName': DomainName}) xpath = './/{%(ns)s}CommandResponse/{%(ns)s}DomainContactsResult/*' % {'ns': NAMESPACE} results = {} for contact_type in xml.findall(xpath): fields_for_one_contact_type = {} for contact_detail in contact_type.findall('*'): - fields_for_one_contact_type[self._tag_without_namespace(contact_detail)] = contact_detail.text - results[self._tag_without_namespace(contact_type)] = fields_for_one_contact_type + fields_for_one_contact_type[self._tag_without_namespace( + contact_detail)] = contact_detail.text + results[self._tag_without_namespace( + contact_type)] = fields_for_one_contact_type return results # https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx @@ -317,7 +327,8 @@ def domains_dns_setHosts(self, domain, host_records): } ])""" - extra_payload = self._list_of_dictionaries_to_numbered_payload(host_records) + extra_payload = self._list_of_dictionaries_to_numbered_payload( + host_records) sld, tld = domain.split(".") extra_payload.update({ 'SLD': sld, @@ -375,11 +386,13 @@ def domains_dns_addHost(self, domain, host_record): print("Remote: %i" % len(host_records_remote)) host_records_remote.append(host_record) - host_records_remote = [self._elements_names_fix(x) for x in host_records_remote] + host_records_remote = [self._elements_names_fix( + x) for x in host_records_remote] print("To set: %i" % len(host_records_remote)) - extra_payload = self._list_of_dictionaries_to_numbered_payload(host_records_remote) + extra_payload = self._list_of_dictionaries_to_numbered_payload( + host_records_remote) sld, tld = domain.split(".") extra_payload.update({ 'SLD': sld, @@ -416,7 +429,8 @@ def domains_dns_delHost(self, domain, host_record): else: host_records_new.append(r) - host_records_new = [self._elements_names_fix(x) for x in host_records_new] + host_records_new = [self._elements_names_fix( + x) for x in host_records_new] print("To set: %i" % len(host_records_new)) @@ -430,7 +444,8 @@ def domains_dns_delHost(self, domain, host_record): ) return False - extra_payload = self._list_of_dictionaries_to_numbered_payload(host_records_new) + extra_payload = self._list_of_dictionaries_to_numbered_payload( + host_records_new) sld, tld = domain.split(".") extra_payload.update({ 'SLD': sld, @@ -467,5 +482,6 @@ def domains_getList(self, ListType=None, SearchTerm=None, PageSize=None, SortBy= extra_payload['PageSize'] = PageSize if SortBy: extra_payload['SortBy'] = SortBy - payload, extra_payload = self._payload('namecheap.domains.getList', extra_payload) + payload, extra_payload = self._payload( + 'namecheap.domains.getList', extra_payload) return self.LazyGetListIterator(self, payload) From 946feaceed8f984cb7ef6a730468c627b028101c Mon Sep 17 00:00:00 2001 From: Max Harley Date: Wed, 7 Apr 2021 13:42:19 -0400 Subject: [PATCH 2/2] Add json listing option --- namecheap-api-cli | 51 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/namecheap-api-cli b/namecheap-api-cli index 903d66f..cc46606 100755 --- a/namecheap-api-cli +++ b/namecheap-api-cli @@ -1,6 +1,7 @@ #!/usr/bin/env python import argparse +import json from namecheap import Api, ApiError @@ -11,22 +12,35 @@ except: def get_args(): - parser = argparse.ArgumentParser(description="CLI tool to manage NameCheap.com domain records") + parser = argparse.ArgumentParser( + description="CLI tool to manage NameCheap.com domain records") - parser.add_argument("--debug", action="store_true", help="If set, enables debug output") - parser.add_argument("--sandbox", action="store_true", help="If set, forcing usage of Sandbox API, see README.md for details") + parser.add_argument("--debug", action="store_true", + help="If set, enables debug output") + parser.add_argument("--sandbox", action="store_true", + help="If set, forcing usage of Sandbox API, see README.md for details") group = parser.add_mutually_exclusive_group(required=True) - group.add_argument("--add", action="store_true", help="Use to add a record") - group.add_argument("--delete", action="store_true", help="Use to remove a record") - group.add_argument("--list", action="store_true", help="List existing records") - - parser.add_argument("--domain", type=str, default="example.org", help="Domain to manage, default is \"example.org\", don't forget to override") - - parser.add_argument("--type", type=str, default="A", help="Record type, default is \"A\"") - parser.add_argument("--name", type=str, default="test", help="Record name, default is \"test\"") - parser.add_argument("--address", type=str, default="127.0.0.1", help="Address for record to point to, default is \"127.0.0.1\"") - parser.add_argument("--ttl", type=int, default=300, help="Time-To-Live, in seconds, default is 300") + group.add_argument("--add", action="store_true", + help="Use to add a record") + group.add_argument("--delete", action="store_true", + help="Use to remove a record") + group.add_argument("--list", action="store_true", + help="List existing records") + group.add_argument("--json", action="store_true", + help="List existing records and return JSON list") + + parser.add_argument("--domain", type=str, default="example.org", + help="Domain to manage, default is \"example.org\", don't forget to override") + + parser.add_argument("--type", type=str, default="A", + help="Record type, default is \"A\"") + parser.add_argument("--name", type=str, default="test", + help="Record name, default is \"test\"") + parser.add_argument("--address", type=str, default="127.0.0.1", + help="Address for record to point to, default is \"127.0.0.1\"") + parser.add_argument("--ttl", type=int, default=300, + help="Time-To-Live, in seconds, default is 300") args = parser.parse_args() @@ -56,12 +70,13 @@ def record_add(record_type, hostname, address, ttl=300): } api.domains_dns_addHost(domain, record) + args = get_args() domain = args.domain -print("domain: %s" % domain) -api = Api(username, api_key, username, ip_address, sandbox=args.sandbox, debug=args.debug) +api = Api(username, api_key, username, ip_address, + sandbox=args.sandbox, debug=args.debug) if args.add: record_add( @@ -78,4 +93,8 @@ elif args.delete: ) elif args.list: for line in list_records(): - print("\t%s \t%s\t%s -> %s" % (line["Type"], line["TTL"], line["Name"], line["Address"])) + print("\t%s \t%s\t%s -> %s" % + (line["Type"], line["TTL"], line["Name"], line["Address"])) +elif args.json: + records = json.dumps([l for l in list_records()]) + print(records)