Skip to content

Commit

Permalink
Merge pull request #62 from todoa2c/issue-61-python33-support
Browse files Browse the repository at this point in the history
Issue 61 python33 support
  • Loading branch information
aheckmann committed Jan 6, 2014
2 parents 47acc7d + eee3925 commit 32a933b
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 73 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
language: python
python:
- "2.7"
- "3.3"
install:
- pip install mock iso8601 backports.ssl-match-hostname --use-mirrors
- pip install six mock iso8601 backports.ssl-match-hostname --use-mirrors
- python setup.py install
script:
- python -m unittest discover -s tests
3 changes: 1 addition & 2 deletions recurly/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import logging
from urllib import urlencode
from urlparse import urljoin
from six.moves.urllib.parse import urljoin
from xml.etree import ElementTree

import recurly.js as js
Expand Down
19 changes: 10 additions & 9 deletions recurly/errors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import httplib
from six.moves import http_client as httplib
from xml.etree import ElementTree
import six


class ResponseError(Exception):
Expand Down Expand Up @@ -49,16 +50,16 @@ def error(self):
return el.text

def __str__(self):
return unicode(self).encode('utf8')
return six.text_type(self).encode('utf8')

def __unicode__(self):
symbol = self.symbol
if symbol is None:
return self.error
details = self.details
if details is not None:
return u'%s: %s %s' % (symbol, self.message, details)
return u'%s: %s' % (symbol, self.message)
return six.u('%s: %s %s') % (symbol, self.message, details)
return six.u('%s: %s') % (symbol, self.message)


class ClientError(ResponseError):
Expand Down Expand Up @@ -86,10 +87,10 @@ def __init__(self, response_xml):
self.response_text = response_xml

def __str__(self):
return unicode(self).encode('utf-8')
return six.text_type(self).encode('utf-8')

def __unicode__(self):
return unicode(self.response_text)
return six.text_type(self.response_text)


class PaymentRequiredError(ClientError):
Expand Down Expand Up @@ -158,7 +159,7 @@ def __str__(self):
return self.message.encode('utf8')

def __unicode__(self):
return u'%s: %s %s' % (self.symbol, self.field, self.message)
return six.u('%s: %s %s') % (self.symbol, self.field, self.message)

@property
def errors(self):
Expand Down Expand Up @@ -186,7 +187,7 @@ def errors(self):
return suberrors

def __unicode__(self):
return u'; '.join(unicode(error) for error in self.errors.itervalues())
return six.u('; ').join(six.text_type(error) for error in self.errors.itervalues())


class ServerError(ResponseError):
Expand Down Expand Up @@ -232,7 +233,7 @@ def __init__(self, status, response_xml):
self.status = status

def __unicode__(self):
return unicode(self.status)
return six.text_type(self.status)


error_classes = {
Expand Down
10 changes: 5 additions & 5 deletions recurly/js.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import os
import re
import time
import urllib
from urlparse import urlsplit, urljoin
import six
from six.moves.urllib.parse import urljoin, quote_plus

import recurly

Expand All @@ -32,9 +32,9 @@ def sign(*records):
if 'timestamp' not in data:
data['timestamp'] = int(time.time())
if 'nonce' not in data:
data['nonce'] = re.sub('\W+', '', base64.b64encode(os.urandom(32)))
data['nonce'] = re.sub(six.b('\W+'), six.b(''), base64.b64encode(os.urandom(32)))
unsigned = to_query(data)
signed = hmac.new(PRIVATE_KEY, unsigned, hashlib.sha1).hexdigest()
signed = hmac.new(six.b(PRIVATE_KEY), six.b(unsigned), hashlib.sha1).hexdigest()
return '|'.join([signed, unsigned])


Expand All @@ -53,4 +53,4 @@ def to_query(object, key=None):
elif object_type in (list, tuple):
return '&'.join([to_query(o, '%s[]' % key) for o in object])
else:
return '%s=%s' % (urllib.quote_plus(str(key)), urllib.quote_plus(str(object)))
return '%s=%s' % (quote_plus(str(key)), quote_plus(str(object)))
7 changes: 4 additions & 3 deletions recurly/link_header.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python
# source: https://gist.github.com/1103172
from __future__ import print_function

"""
HTTP Link Header Parsing
Expand Down Expand Up @@ -47,7 +48,7 @@ def _unquotestring(instr):


def _splitstring(instr, item, split):
if not instr:
if not instr:
return []
return [h.strip() for h in re.findall(r'%s(?=%s|\s*$)' % (item, split), instr)]

Expand All @@ -72,7 +73,7 @@ def parse_link_value(instr):
"""
out = {}
if not instr:
if not instr:
return out
for link in [h.strip() for h in link_splitter.findall(instr)]:
url, params = link.split(">", 1)
Expand All @@ -91,4 +92,4 @@ def parse_link_value(instr):
if __name__ == '__main__':
import sys
if len(sys.argv) > 1:
print parse_link_value(sys.argv[1])
print(parse_link_value(sys.argv[1]))
52 changes: 30 additions & 22 deletions recurly/resource.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import base64
from datetime import datetime
import httplib
import logging
import socket
import ssl
import sys
from urllib import urlencode
from urlparse import urlsplit, urljoin
from xml.etree import ElementTree

import iso8601
import backports.ssl_match_hostname
import six

import recurly
import recurly.errors
from recurly.link_header import parse_link_value
from six.moves import http_client
from six.moves.urllib.parse import urlencode, urljoin, urlsplit


if six.PY3:
from ssl import match_hostname
else:
from backports.ssl_match_hostname import match_hostname


class Money(object):
Expand Down Expand Up @@ -46,7 +51,7 @@ def add_to_element(self, elem):
for currency, amount in self.currencies.items():
currency_el = ElementTree.Element(currency)
currency_el.attrib['type'] = 'integer'
currency_el.text = unicode(amount)
currency_el.text = six.text_type(amount)
elem.append(currency_el)

def __getitem__(self, name):
Expand Down Expand Up @@ -158,7 +163,7 @@ def page_for_value(cls, resp, value):
page = cls(value)
page.record_size = resp.getheader('X-Records')
links = parse_link_value(resp.getheader('Link'))
for url, data in links.iteritems():
for url, data in six.iteritems(links):
if data.get('rel') == 'start':
page.start_url = url
if data.get('rel') == 'next':
Expand All @@ -167,9 +172,9 @@ def page_for_value(cls, resp, value):
return page


class _ValidatedHTTPSConnection(httplib.HTTPSConnection):
class _ValidatedHTTPSConnection(http_client.HTTPSConnection):

"""An `httplib.HTTPSConnection` that validates the SSL connection by
"""An `http_client.HTTPSConnection` that validates the SSL connection by
requiring certificate validation and checking the connection's intended
hostname again the validated certificate's possible hosts."""

Expand All @@ -190,7 +195,7 @@ def connect(self):
ca_certs=recurly.CA_CERTS_FILE)

# Let the CertificateError for failure be raised to the caller.
backports.ssl_match_hostname.match_hostname(ssl_sock.getpeercert(), self.host)
match_hostname(ssl_sock.getpeercert(), self.host)

self.sock = ssl_sock

Expand Down Expand Up @@ -230,13 +235,13 @@ def __init__(self, **kwargs):
except AttributeError:
self.currency = recurly.DEFAULT_CURRENCY

for key, value in kwargs.iteritems():
for key, value in six.iteritems(kwargs):
setattr(self, key, value)

@classmethod
def http_request(cls, url, method='GET', body=None, headers=None):
"""Make an HTTP request with the given method to the given URL,
returning the resulting `httplib.HTTPResponse` instance.
returning the resulting `http_client.HTTPResponse` instance.
If the `body` argument is a `Resource` instance, it is serialized
to XML by calling its `to_element()` method before submitting it.
Expand All @@ -250,9 +255,9 @@ def http_request(cls, url, method='GET', body=None, headers=None):
"""
urlparts = urlsplit(url)
if urlparts.scheme != 'https':
connection = httplib.HTTPConnection(urlparts.netloc)
connection = http_client.HTTPConnection(urlparts.netloc)
elif recurly.CA_CERTS_FILE is None:
connection = httplib.HTTPSConnection(urlparts.netloc)
connection = http_client.HTTPSConnection(urlparts.netloc)
else:
connection = _ValidatedHTTPSConnection(urlparts.netloc)

Expand All @@ -263,12 +268,12 @@ def http_request(cls, url, method='GET', body=None, headers=None):
})
if recurly.API_KEY is None:
raise recurly.UnauthorizedError('recurly.API_KEY not set')
headers['Authorization'] = 'Basic %s' % base64.b64encode('%s:' % recurly.API_KEY)
headers['Authorization'] = 'Basic %s' % base64.b64encode(six.b('%s:' % recurly.API_KEY)).decode()

log = logging.getLogger('recurly.http.request')
if log.isEnabledFor(logging.DEBUG):
log.debug("%s %s HTTP/1.1", method, url)
for header, value in headers.iteritems():
for header, value in six.iteritems(headers):
if header == 'Authorization':
value = '<redacted>'
log.debug("%s: %s", header, value)
Expand All @@ -290,8 +295,11 @@ def http_request(cls, url, method='GET', body=None, headers=None):
log = logging.getLogger('recurly.http.response')
if log.isEnabledFor(logging.DEBUG):
log.debug("HTTP/1.1 %d %s", resp.status, resp.reason)
for header in resp.msg.headers:
log.debug(header.rstrip('\n'))
if six.PY2:
for header in resp.msg.headers:
log.debug(header.rstrip('\n'))
else:
log.debug(resp.msg._headers)
log.debug('')

return resp
Expand All @@ -306,7 +314,7 @@ def as_log_output(self):
"""
elem = self.to_element()
for attrname in self.sensitive_attributes:
for sensitive_el in elem.getiterator(attrname):
for sensitive_el in elem.iter(attrname):
sensitive_el.text = 'XXXXXXXXXXXXXXXX'
return ElementTree.tostring(elem, encoding='UTF-8')

Expand Down Expand Up @@ -341,7 +349,7 @@ def get(cls, uuid):
@classmethod
def element_for_url(cls, url):
"""Return the resource at the given URL, as a
(`httplib.HTTPResponse`, `xml.etree.ElementTree.Element`) tuple
(`http_client.HTTPResponse`, `xml.etree.ElementTree.Element`) tuple
resulting from a ``GET`` request to that URL."""
response = cls.http_request(url)
if response.status != 200:
Expand Down Expand Up @@ -462,7 +470,7 @@ def element_for_value(cls, attrname, value):
elif isinstance(value, Money):
value.add_to_element(el)
else:
el.text = unicode(value)
el.text = six.text_type(value)

return el

Expand Down Expand Up @@ -641,7 +649,7 @@ def delete(self):
@classmethod
def raise_http_error(cls, response):
"""Raise a `ResponseError` of the appropriate subclass in
reaction to the given `httplib.HTTPResponse`."""
reaction to the given `http_client.HTTPResponse`."""
response_xml = response.read()
logging.getLogger('recurly.http.response').debug(response_xml)
exc_class = recurly.errors.error_class_for_http_status(response.status)
Expand All @@ -661,7 +669,7 @@ def to_element(self):
continue

if attrname in self.xml_attribute_attributes:
elem.attrib[attrname] = unicode(value)
elem.attrib[attrname] = six.text_type(value)
else:
sub_elem = self.element_for_value(attrname, value)
elem.append(sub_elem)
Expand Down
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Topic :: Internet :: WWW/HTTP',
],
packages=['recurly'],
install_requires=['iso8601', 'backports.ssl_match_hostname'] + more_install_requires,
install_requires=['iso8601', 'backports.ssl_match_hostname', 'six'] + more_install_requires,
tests_require=['mock',
'unittest2'],
'six'],
test_suite='unittest2.collector',
zip_safe=True,
)
Loading

0 comments on commit 32a933b

Please sign in to comment.