Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More params + user_payload for domains_create, add returns to method calls, add type annotations + more #32

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 97 additions & 16 deletions namecheap.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import sys
import time
import requests # pip install requests
from xml.etree.ElementTree import fromstring
from xml.etree.ElementTree import fromstring, Element

inPy3k = sys.version_info[0] == 3

Expand Down Expand Up @@ -29,7 +29,10 @@ def __init__(self, number, text):
self.text = text


# noinspection PyPep8Naming
class Api(object):
ENDPOINTS = ENDPOINTS

# Follows API spec capitalization in variable names for consistency.
def __init__(self, ApiUser, ApiKey, UserName, ClientIP,
sandbox=True, debug=True,
Expand All @@ -50,13 +53,55 @@ def domains_create(
self,
DomainName, FirstName, LastName,
Address1, City, StateProvince, PostalCode, Country, Phone,
EmailAddress, Address2=None, years=1, WhoisGuard=False
EmailAddress, Address2=None, years=1, WhoisGuard=False,
OrganizationName=None, JobTitle=None, PromotionCode=None,
Nameservers=None, **user_payload
):
# type: (str, str, str, str, str, str, str, str, str, str, str, int, bool, str, str, str, str or list, any) -> dict
"""
Registers a domain name with the given contact info.
Example of a working phone number: +81.123123123

For simplicity assumes one person acts as all contact types."""
For simplicity assumes one person acts as all contact types.

If you're registering a domain as a company/organisation, specify the company name in ``OrganizationName``,
and specify the job role of the person named in ``FirstName`` / ``LastName`` in ``JobTitle``
e.g. ``domains_create(FirstName='John', LastName='Doe', OrganizationName='ExampleCorp', JobTitle='Chief Technical Officer')``

**Example Usage**::

>>> api = Api('user', 'key', 'user', '12.34.56.78')
>>> res = api.domains_create(
... DomainName = 'somecooldomain.com',
... FirstName = 'Jack',
... LastName = 'Trotter',
... Address1 = 'Ridiculously Big Mansion, Yellow Brick Road',
... City = 'Tokushima',
... StateProvince = 'Tokushima',
... PostalCode = '771-0144',
... Country = 'Japan',
... Phone = '+81.123123123',
... EmailAddress = 'jack.trotter@example.com'
... )
>>> print(res)
{
'Domain': 'somecooldomain.com', 'Registered': 'true', 'ChargedAmount': '12.1600',
'DomainID': '615026', 'OrderID': '2139371', 'TransactionID': '4139125',
'WhoisguardEnable': 'true', 'FreePositiveSSL': 'false', 'NonRealTimeDomain': 'false'
}

**Automatically set nameservers on purchase**::

>>> # For readability, we're not including the required contact information for these calls
>>> # You can specify Nameservers as either a list [] of nameservers as strings
>>> api.domains_create('anotherdomain.com', Nameservers=['ns1.example.com', 'ns2.example.com', 'ns3.example.com'])
>>> # OR you can specify them as a comma separated string - it's up to your preference
>>> # There is no difference in result whether you specify nameservers as a list or string
>>> api.domains_create('anotherdomain.com', Nameservers='ns1.example.com,ns2.example.com,ns3.example.com')

:raises ApiError: When ``<Error />`` in the result is non-empty.
:return dict DomainCreateResult: The attributes of the result's ``<DomainCreateResult />`` as a dictionary
"""

contact_types = ['Registrant', 'Tech', 'Admin', 'AuxBilling']

Expand All @@ -70,7 +115,13 @@ def domains_create(
'AddFreeWhoisguard': 'yes',
'WGEnabled': 'yes',
})

if PromotionCode:
extra_payload['PromotionCode'] = PromotionCode
if Nameservers:
if isinstance(Nameservers, (list, set, tuple)):
Nameservers = ','.join(Nameservers)
extra_payload['Nameservers'] = Nameservers

for contact_type in contact_types:
extra_payload.update({
'%sFirstName' % contact_type: FirstName,
Expand All @@ -85,11 +136,31 @@ def domains_create(
})
if Address2:
extra_payload['%sAddress2' % contact_type] = Address2

self._call('namecheap.domains.create', extra_payload)

def _payload(self, Command, extra_payload={}):
if OrganizationName:
extra_payload['%sOrganizationName' % contact_type] = OrganizationName
if JobTitle:
extra_payload['%sJobTitle' % contact_type] = JobTitle

# Merge in any user payload key:value's on top of our generated payload dictionary
extra_payload = {**extra_payload, **user_payload}

xml = self._call('namecheap.domains.create', extra_payload)
return self.get_element_dict(xml, 'DomainCreateResult')

@staticmethod
def get_element(element, element_name):
# type: (Element, str) -> Element
return element.find('.//{%(ns)s}%(el)s' % {'ns': NAMESPACE, 'el': element_name})

@staticmethod
def get_element_dict(element, element_name):
# type: (Element, str) -> dict
return dict(Api.get_element(element, element_name).items())

def _payload(self, Command, extra_payload=None):
# type: (str, dict) -> (dict, dict)
"""Make dictionary for passing to requests.post"""
extra_payload = {} if extra_payload is None else extra_payload
payload = {
'ApiUser': self.ApiUser,
'ApiKey': self.ApiKey,
Expand All @@ -104,7 +175,8 @@ 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):
# type: (dict, dict) -> Element
"""Make network call and return parsed XML element"""
attempts_left = self.attempts_count
while attempts_left > 0:
Expand Down Expand Up @@ -138,8 +210,10 @@ def _fetch_xml(self, payload, extra_payload = None):

return xml

def _call(self, Command, extra_payload={}):
def _call(self, Command, extra_payload=None):
# type: (str, dict) -> Element
"""Call an API command"""
extra_payload = {} if extra_payload is None else extra_payload
payload, extra_payload = self._payload(Command, extra_payload)
xml = self._fetch_xml(payload, extra_payload)
return xml
Expand Down Expand Up @@ -178,14 +252,17 @@ def __next__(self):

# https://www.namecheap.com/support/api/methods/domains-dns/set-default.aspx
def domains_dns_setDefault(self, domain):
# type: (str) -> dict
sld, tld = domain.split(".")
self._call("namecheap.domains.dns.setDefault", {
xml = self._call("namecheap.domains.dns.setDefault", {
'SLD': sld,
'TLD': tld
})

return self.get_element_dict(xml, 'DomainDNSSetDefaultResult')

# https://www.namecheap.com/support/api/methods/domains/check.aspx
def domains_check(self, domains):
# type: (str or list) -> dict
"""Checks the availability of domains.

For example
Expand Down Expand Up @@ -303,6 +380,7 @@ def domains_getContacts(self, DomainName):

# https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
def domains_dns_setHosts(self, domain, host_records):
# type: (str, list) -> dict
"""Sets the DNS host records for a domain.

Example:
Expand All @@ -323,10 +401,11 @@ def domains_dns_setHosts(self, domain, host_records):
'SLD': sld,
'TLD': tld
})
self._call("namecheap.domains.dns.setHosts", extra_payload)
return self.get_element_dict(self._call("namecheap.domains.dns.setHosts", extra_payload), 'DomainDNSSetHostsResult')

# https://www.namecheap.com/support/api/methods/domains-dns/set-custom.aspx
def domains_dns_setCustom(self, domain, host_records):
# type: (str, dict) -> dict
"""Sets the domain to use the supplied set of nameservers.

Example:
Expand All @@ -337,10 +416,11 @@ def domains_dns_setCustom(self, domain, host_records):
sld, tld = domain.split(".")
extra_payload['SLD'] = sld
extra_payload['TLD'] = tld
self._call("namecheap.domains.dns.setCustom", extra_payload)
return self.get_element_dict(self._call("namecheap.domains.dns.setCustom", extra_payload), 'DomainDNSSetCustomResult')

# https://www.namecheap.com/support/api/methods/domains-dns/get-hosts.aspx
def domains_dns_getHosts(self, domain):
# type: (str) -> list
"""Retrieves DNS host record settings. Note that the key names are different from those
you use when setting the host records."""
sld, tld = domain.split(".")
Expand Down Expand Up @@ -385,9 +465,10 @@ def domains_dns_addHost(self, domain, host_record):
'SLD': sld,
'TLD': tld
})
self._call("namecheap.domains.dns.setHosts", extra_payload)
return self.get_element_dict(self._call("namecheap.domains.dns.setHosts", extra_payload), 'DomainDNSSetHostsResult')

def domains_dns_delHost(self, domain, host_record):
# type: (str, dict) -> dict or bool
"""This method is absent in original API as well. It executes non-atomic
remove operation over the host record which has the following Type,
Hostname and Address.
Expand Down Expand Up @@ -436,7 +517,7 @@ def domains_dns_delHost(self, domain, host_record):
'SLD': sld,
'TLD': tld
})
self._call("namecheap.domains.dns.setHosts", extra_payload)
return self.get_element_dict(self._call("namecheap.domains.dns.setHosts", extra_payload), 'DomainDNSSetHostsResult')

# https://www.namecheap.com/support/api/methods/domains-dns/get-list.aspx
def domains_getList(self, ListType=None, SearchTerm=None, PageSize=None, SortBy=None):
Expand Down
32 changes: 26 additions & 6 deletions namecheap_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
except:
pass


def is_true(val):
return val in ['true', True, 1, '1', 'yes', 'True', 'TRUE']


def random_domain_name():
import random
from time import gmtime, strftime
Expand All @@ -36,7 +41,7 @@ def test_register_domain():

# Try registering a random domain. Fails if exception raised.
domain_name = random_domain_name()
api.domains_create(
res = api.domains_create(
DomainName=domain_name,
FirstName='Jack',
LastName='Trotter',
Expand All @@ -48,6 +53,11 @@ def test_register_domain():
Phone='+81.123123123',
EmailAddress='jack.trotter@example.com'
)

assert_equal(res['Domain'], domain_name)
ok_(int(res['DomainID']) > 0)
ok_(int(res['TransactionID']) > 0)
ok_(is_true(res['Registered']))
return domain_name


Expand All @@ -69,7 +79,9 @@ def test_domains_dns_setDefault_on_nonexisting_domain():
def test_domains_dns_setDefault_on_existing_domain():
api = Api(username, api_key, username, ip_address, sandbox=True)
domain_name = test_register_domain()
api.domains_dns_setDefault(domain_name)
res = api.domains_dns_setDefault(domain_name)
assert_equal(res['Domain'], domain_name)
ok_(is_true(res['Updated']))


def test_domains_getContacts():
Expand All @@ -82,7 +94,7 @@ def test_domains_getContacts():
def test_domains_dns_setHosts():
api = Api(username, api_key, username, ip_address, sandbox=True)
domain_name = test_register_domain()
api.domains_dns_setHosts(
res = api.domains_dns_setHosts(
domain_name,
[{
'HostName': '@',
Expand All @@ -92,6 +104,8 @@ def test_domains_dns_setHosts():
'TTL': '100'
}]
)
assert_equal(res['Domain'], domain_name)
ok_(is_true(res['IsSuccess']))

#
# I wasn't able to get this to work on any public name servers that I tried
Expand Down Expand Up @@ -244,7 +258,8 @@ def test_domains_dns_bulkAddHosts():
def test_domains_dns_delHost():
api = Api(username, api_key, username, ip_address, sandbox=True)
domain_name = test_register_domain()
api.domains_dns_setHosts(

res = api.domains_dns_setHosts(
domain_name,
[{
'HostName': '@',
Expand All @@ -257,15 +272,20 @@ def test_domains_dns_delHost():
'Address': '1.2.3.4'
}]
)
api.domains_dns_delHost(
assert_equal(res['Domain'], domain_name)
ok_(is_true(res['IsSuccess']))

res = api.domains_dns_delHost(
domain_name,
{
'Name': 'test',
'Type': 'A',
'Address': '1.2.3.4'
}
)

assert_equal(res['Domain'], domain_name)
ok_(is_true(res['IsSuccess']))

hosts = api.domains_dns_getHosts(domain_name)

# these might change
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
setuptools>=39.2.0
requests
nose