Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial commit of experimental route53 module.

  • Loading branch information...
commit 3288ca4d4fb28f2b9836e05a4b3cfe97524b34d5 0 parents
@gtaylor authored
8 .gitignore
@@ -0,0 +1,8 @@
+*.pyc
+*~
+*.tmp
+test_basic.py
+MANIFEST
+build
+dist
+.idea
22 LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2012 Gregory Taylor
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
1  MANIFEST.in
@@ -0,0 +1 @@
+include README.rst
26 README.rst
@@ -0,0 +1,26 @@
+pyRoute53
+=========
+
+:Info: A Python 3 compatible Route53 module.
+:Author: Greg Taylor
+
+
+The ``pyRoute53`` Python package is a simple Python 3 compatible API for
+Amazon's `Route 53`_.
+
+Why not boto?
+-------------
+
+boto_ is a wonderful piece of software, one that I continue to contribute to.
+However, we needed a Python 3 compatible route53 module, and boto has a long
+way to go on this front.
+
+License
+-------
+
+pyRoute53 is licensed under the BSD License.
+
+
+.. _Route 53: http://aws.amazon.com/route53/
+.. _boto: http://docs.pythonboto.org/
+.. _issue tracker: https://github.com/gtaylor/pyroute53/issues
2  requirements.txt
@@ -0,0 +1,2 @@
+requests==0.14.1
+nose
14 route53/__init__.py
@@ -0,0 +1,14 @@
+def connect(aws_access_key_id=None, aws_secret_access_key=None, **kwargs):
+ """
+ :keyword str aws_access_key_id: Your AWS Access Key ID
+ :keyword str aws_secret_access_key: Your AWS Secret Access Key
+
+ :rtype: :py:class:`route53.connection.Route53Connection`
+ :return: A connection to Amazon's Route53
+ """
+ from route53.connection import Route53Connection
+ return Route53Connection(
+ aws_access_key_id,
+ aws_secret_access_key,
+ **kwargs
+ )
77 route53/connection.py
@@ -0,0 +1,77 @@
+from route53.exceptions import RecordDoesNotExistError
+from route53.transport import RequestsTransport
+from route53 import parsers
+
+class Route53Connection(object):
+ """
+ This class serves as the interface to the AWS Route53 API.
+ """
+
+ endpoint = 'https://route53.amazonaws.com/2012-02-29/'
+
+ def __init__(self, aws_access_key_id, aws_secret_access_key):
+ """
+ :param str aws_access_key_id: An account's access key ID.
+ :param str aws_secret_access_key: An account's secret access key.
+ """
+
+ self.aws_access_key_id = aws_access_key_id
+ self.aws_secret_access_key = aws_secret_access_key
+
+ def _send_request(self, path, params, method):
+ return RequestsTransport(self).send_request(path, params, method)
+
+ def _do_autopaginating_api_call(self, path, params, method, parser_func):
+ """
+ Given an API method, the arguments passed to it, and a function to
+ hand parsing off to, loop through the record sets in the API call
+ until all records have been yielded.
+
+ This is mostly done this way to reduce duplication through the various
+ API methods.
+
+ :param str method: The API method on the endpoint.
+ :param dict kwargs: The kwargs from the top-level API method.
+ :param callable parser_func: A callable that is used for parsing the
+ output from the API call.
+ :rtype: generator
+ :returns: Returns a generator that may be returned by the top-level
+ API method.
+ """
+ # Used to determine whether to fail noisily if no results are returned.
+ has_records = {"has_records": False}
+
+ while True:
+ try:
+ root = self._send_request(path, params, method)
+ except RecordDoesNotExistError:
+ if not has_records["has_records"]:
+ # No records seen yet, this really is empty.
+ raise
+ # We've seen some records come through. We must have hit the
+ # end of the result set. Finish up silently.
+ return
+
+ # This is used to track whether this go around the call->parse
+ # loop yielded any records.
+ records_returned_by_this_loop = False
+ for record in parser_func(root, has_records):
+ yield record
+ # We saw a record, mark our tracker accordingly.
+ records_returned_by_this_loop = True
+ # There is a really fun bug in the Petfinder API with
+ # shelter.getpets where an offset is returned with no pets,
+ # causing an infinite loop.
+ if not records_returned_by_this_loop:
+ return
+
+ # This will determine at what offset we start the next query.
+ last_offset = root.find("lastOffset").text
+ kwargs["offset"] = last_offset
+
+ def list_hosted_zones(self):
+
+
+ return parsers.list_hosted_zones_parser(
+ self._send_request('hostedzone', {'maxitems': 2}, 'GET')
+ )
16 route53/exceptions.py
@@ -0,0 +1,16 @@
+
+class Route53Error(Exception):
+ """
+ Base class for all Petfinder API exceptions. Mostly here to allow end
+ users to catch all Petfinder exceptions.
+ """
+
+ pass
+
+
+class RecordDoesNotExistError(Route53Error):
+ """
+ Raised when querying for a record that does not exist.
+ """
+
+ pass
5 route53/hosted_zone.py
@@ -0,0 +1,5 @@
+
+class HostedZone(object):
+
+ def __init__(self):
+ pass
1  route53/parsers/__init__.py
@@ -0,0 +1 @@
+from .list_hosted_zones import list_hosted_zones_parser
23 route53/parsers/list_hosted_zones.py
@@ -0,0 +1,23 @@
+from lxml import etree
+from route53.util import prettyprint_xml
+
+def list_hosted_zones_parser(xml_str):
+ root = etree.fromstring(xml_str)
+ print(prettyprint_xml(root))
+ print(root)
+
+ if not 'ListHostedZonesResponse' in root.tag:
+ # TODO: Error condition.
+ raise Exception("Something bad happened!")
+
+ is_truncated = root.find('./{*}IsTruncated').text == 'true'
+
+ for child in root:
+ print(child.tag)
+
+ if not is_truncated:
+ return
+
+
+
+ return xml_str
116 route53/transport.py
@@ -0,0 +1,116 @@
+import time
+import base64
+import hmac
+import hashlib
+import requests
+
+def formatdate(timeval=None, localtime=False, usegmt=False):
+ """Returns a date string as specified by RFC 2822, e.g.:
+
+ Fri, 09 Nov 2001 01:08:47 -0000
+
+ Optional timeval if given is a floating point time value as accepted by
+ gmtime() and localtime(), otherwise the current time is used.
+
+ Optional localtime is a flag that when True, interprets timeval, and
+ returns a date relative to the local timezone instead of UTC, properly
+ taking daylight savings time into account.
+
+ Optional argument usegmt means that the timezone is written out as
+ an ascii string, not numeric one (so "GMT" instead of "+0000"). This
+ is needed for HTTP, and is only used when localtime==False.
+ """
+
+ # Note: we cannot use strftime() because that honors the locale and RFC
+ # 2822 requires that day and month names be the English abbreviations.
+ if timeval is None:
+ timeval = time.time()
+ if localtime:
+ now = time.localtime(timeval)
+ # Calculate timezone offset, based on whether the local zone has
+ # daylight savings time, and whether DST is in effect.
+ if time.daylight and now[-1]:
+ offset = time.altzone
+ else:
+ offset = time.timezone
+ hours, minutes = divmod(abs(offset), 3600)
+ # Remember offset is in seconds west of UTC, but the timezone is in
+ # minutes east of UTC, so the signs differ.
+ if offset > 0:
+ sign = '-'
+ else:
+ sign = '+'
+ zone = '%s%02d%02d' % (sign, hours, minutes // 60)
+ else:
+ now = time.gmtime(timeval)
+ # Timezone offset is always -0000
+ if usegmt:
+ zone = 'GMT'
+ else:
+ zone = '-0000'
+ return '%s, %02d %s %04d %02d:%02d:%02d %s' % (
+ ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][now[6]],
+ now[2],
+ ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now[1] - 1],
+ now[0], now[3], now[4], now[5],
+ zone)
+
+class BaseTransport(object):
+ def __init__(self, connection):
+ self.connection = connection
+
+ @property
+ def endpoint(self):
+ return self.connection.endpoint
+
+ def _hmac_sign_string(self, string_to_sign):
+ new_hmac = hmac.new(
+ self.connection.aws_secret_access_key.encode('utf-8'),
+ digestmod=hashlib.sha256
+ )
+ new_hmac.update(string_to_sign.encode('utf-8'))
+ digest = new_hmac.digest()
+ return base64.b64encode(digest).decode('utf-8')
+
+ def get_request_headers(self):
+ date_header = time.asctime(time.gmtime())
+ signing_key = self._hmac_sign_string(date_header)
+
+ auth_header = "AWS3-HTTPS AWSAccessKeyId=%s,Algorithm=HmacSHA256,Signature=%s" % (
+ self.connection.aws_access_key_id,
+ signing_key,
+ )
+
+ headers = {
+ 'X-Amzn-Authorization': auth_header,
+ 'x-amz-date': date_header,
+ 'Host': 'route53.amazonaws.com',
+ }
+ return headers
+
+ def send_request(self, path, params, method):
+ headers = self.get_request_headers()
+
+ if method == 'GET':
+ return self._send_get_request(path, params, headers)
+ elif method == 'POST':
+ return self._send_post_request(path, params, headers)
+ else:
+ raise Exception("Invalid request method: %s" % method)
+
+ def _send_get_request(self, path, params, headers):
+ raise NotImplementedError
+
+ def _send_post_request(self, path, params, headers):
+ raise NotImplementedError
+
+class RequestsTransport(BaseTransport):
+
+ def _send_get_request(self, path, params, headers):
+ r = requests.get(self.endpoint + path, params=params, headers=headers)
+ return r.text
+
+ def _send_post_request(self, path, params, headers):
+ r = requests.post(self.endpoint + path, params=params, headers=headers)
+ return r.text
8 route53/util.py
@@ -0,0 +1,8 @@
+"""
+Various utility functions that are useful across the codebase.
+"""
+
+from lxml import etree
+
+def prettyprint_xml(node):
+ return etree.tostring(node, pretty_print=True).decode('utf-8')
34 setup.py
@@ -0,0 +1,34 @@
+from setuptools import setup
+
+DESCRIPTION = "A Django email backend for Amazon Simple Email Service, backed by celery."
+
+LONG_DESCRIPTION = open('README.rst').read()
+
+CLASSIFIERS = [
+ 'Development Status :: 5 - Production/Stable',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ 'Framework :: Django',
+]
+
+setup(
+ name='seacucumber',
+ version='1.5',
+ packages=[
+ 'seacucumber',
+ 'seacucumber.management',
+ 'seacucumber.management.commands',
+ ],
+ author='Gregory Taylor',
+ author_email='gtaylor@duointeractive.com',
+ url='https://github.com/duointeractive/sea-cucumber/',
+ license='MIT',
+ description=DESCRIPTION,
+ long_description=LONG_DESCRIPTION,
+ platforms=['any'],
+ classifiers=CLASSIFIERS,
+ install_requires=['boto>=2.3.0', 'celery'],
+)
0  tests/__init__.py
No changes.
Please sign in to comment.
Something went wrong with that request. Please try again.