Skip to content

Commit 32a933b

Browse files
committed
Merge pull request #62 from todoa2c/issue-61-python33-support
Issue 61 python33 support
2 parents 47acc7d + eee3925 commit 32a933b

File tree

9 files changed

+116
-73
lines changed

9 files changed

+116
-73
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
language: python
22
python:
33
- "2.7"
4+
- "3.3"
45
install:
5-
- pip install mock iso8601 backports.ssl-match-hostname --use-mirrors
6+
- pip install six mock iso8601 backports.ssl-match-hostname --use-mirrors
67
- python setup.py install
78
script:
89
- python -m unittest discover -s tests

recurly/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import logging
2-
from urllib import urlencode
3-
from urlparse import urljoin
2+
from six.moves.urllib.parse import urljoin
43
from xml.etree import ElementTree
54

65
import recurly.js as js

recurly/errors.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import httplib
1+
from six.moves import http_client as httplib
22
from xml.etree import ElementTree
3+
import six
34

45

56
class ResponseError(Exception):
@@ -49,16 +50,16 @@ def error(self):
4950
return el.text
5051

5152
def __str__(self):
52-
return unicode(self).encode('utf8')
53+
return six.text_type(self).encode('utf8')
5354

5455
def __unicode__(self):
5556
symbol = self.symbol
5657
if symbol is None:
5758
return self.error
5859
details = self.details
5960
if details is not None:
60-
return u'%s: %s %s' % (symbol, self.message, details)
61-
return u'%s: %s' % (symbol, self.message)
61+
return six.u('%s: %s %s') % (symbol, self.message, details)
62+
return six.u('%s: %s') % (symbol, self.message)
6263

6364

6465
class ClientError(ResponseError):
@@ -86,10 +87,10 @@ def __init__(self, response_xml):
8687
self.response_text = response_xml
8788

8889
def __str__(self):
89-
return unicode(self).encode('utf-8')
90+
return six.text_type(self).encode('utf-8')
9091

9192
def __unicode__(self):
92-
return unicode(self.response_text)
93+
return six.text_type(self.response_text)
9394

9495

9596
class PaymentRequiredError(ClientError):
@@ -158,7 +159,7 @@ def __str__(self):
158159
return self.message.encode('utf8')
159160

160161
def __unicode__(self):
161-
return u'%s: %s %s' % (self.symbol, self.field, self.message)
162+
return six.u('%s: %s %s') % (self.symbol, self.field, self.message)
162163

163164
@property
164165
def errors(self):
@@ -186,7 +187,7 @@ def errors(self):
186187
return suberrors
187188

188189
def __unicode__(self):
189-
return u'; '.join(unicode(error) for error in self.errors.itervalues())
190+
return six.u('; ').join(six.text_type(error) for error in self.errors.itervalues())
190191

191192

192193
class ServerError(ResponseError):
@@ -232,7 +233,7 @@ def __init__(self, status, response_xml):
232233
self.status = status
233234

234235
def __unicode__(self):
235-
return unicode(self.status)
236+
return six.text_type(self.status)
236237

237238

238239
error_classes = {

recurly/js.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
import os
55
import re
66
import time
7-
import urllib
8-
from urlparse import urlsplit, urljoin
7+
import six
8+
from six.moves.urllib.parse import urljoin, quote_plus
99

1010
import recurly
1111

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

4040

@@ -53,4 +53,4 @@ def to_query(object, key=None):
5353
elif object_type in (list, tuple):
5454
return '&'.join([to_query(o, '%s[]' % key) for o in object])
5555
else:
56-
return '%s=%s' % (urllib.quote_plus(str(key)), urllib.quote_plus(str(object)))
56+
return '%s=%s' % (quote_plus(str(key)), quote_plus(str(object)))

recurly/link_header.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env python
22
# source: https://gist.github.com/1103172
3+
from __future__ import print_function
34

45
"""
56
HTTP Link Header Parsing
@@ -47,7 +48,7 @@ def _unquotestring(instr):
4748

4849

4950
def _splitstring(instr, item, split):
50-
if not instr:
51+
if not instr:
5152
return []
5253
return [h.strip() for h in re.findall(r'%s(?=%s|\s*$)' % (item, split), instr)]
5354

@@ -72,7 +73,7 @@ def parse_link_value(instr):
7273
7374
"""
7475
out = {}
75-
if not instr:
76+
if not instr:
7677
return out
7778
for link in [h.strip() for h in link_splitter.findall(instr)]:
7879
url, params = link.split(">", 1)
@@ -91,4 +92,4 @@ def parse_link_value(instr):
9192
if __name__ == '__main__':
9293
import sys
9394
if len(sys.argv) > 1:
94-
print parse_link_value(sys.argv[1])
95+
print(parse_link_value(sys.argv[1]))

recurly/resource.py

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
import base64
22
from datetime import datetime
3-
import httplib
43
import logging
54
import socket
65
import ssl
76
import sys
8-
from urllib import urlencode
9-
from urlparse import urlsplit, urljoin
107
from xml.etree import ElementTree
118

129
import iso8601
13-
import backports.ssl_match_hostname
10+
import six
1411

1512
import recurly
1613
import recurly.errors
1714
from recurly.link_header import parse_link_value
15+
from six.moves import http_client
16+
from six.moves.urllib.parse import urlencode, urljoin, urlsplit
17+
18+
19+
if six.PY3:
20+
from ssl import match_hostname
21+
else:
22+
from backports.ssl_match_hostname import match_hostname
1823

1924

2025
class Money(object):
@@ -46,7 +51,7 @@ def add_to_element(self, elem):
4651
for currency, amount in self.currencies.items():
4752
currency_el = ElementTree.Element(currency)
4853
currency_el.attrib['type'] = 'integer'
49-
currency_el.text = unicode(amount)
54+
currency_el.text = six.text_type(amount)
5055
elem.append(currency_el)
5156

5257
def __getitem__(self, name):
@@ -158,7 +163,7 @@ def page_for_value(cls, resp, value):
158163
page = cls(value)
159164
page.record_size = resp.getheader('X-Records')
160165
links = parse_link_value(resp.getheader('Link'))
161-
for url, data in links.iteritems():
166+
for url, data in six.iteritems(links):
162167
if data.get('rel') == 'start':
163168
page.start_url = url
164169
if data.get('rel') == 'next':
@@ -167,9 +172,9 @@ def page_for_value(cls, resp, value):
167172
return page
168173

169174

170-
class _ValidatedHTTPSConnection(httplib.HTTPSConnection):
175+
class _ValidatedHTTPSConnection(http_client.HTTPSConnection):
171176

172-
"""An `httplib.HTTPSConnection` that validates the SSL connection by
177+
"""An `http_client.HTTPSConnection` that validates the SSL connection by
173178
requiring certificate validation and checking the connection's intended
174179
hostname again the validated certificate's possible hosts."""
175180

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

192197
# Let the CertificateError for failure be raised to the caller.
193-
backports.ssl_match_hostname.match_hostname(ssl_sock.getpeercert(), self.host)
198+
match_hostname(ssl_sock.getpeercert(), self.host)
194199

195200
self.sock = ssl_sock
196201

@@ -230,13 +235,13 @@ def __init__(self, **kwargs):
230235
except AttributeError:
231236
self.currency = recurly.DEFAULT_CURRENCY
232237

233-
for key, value in kwargs.iteritems():
238+
for key, value in six.iteritems(kwargs):
234239
setattr(self, key, value)
235240

236241
@classmethod
237242
def http_request(cls, url, method='GET', body=None, headers=None):
238243
"""Make an HTTP request with the given method to the given URL,
239-
returning the resulting `httplib.HTTPResponse` instance.
244+
returning the resulting `http_client.HTTPResponse` instance.
240245
241246
If the `body` argument is a `Resource` instance, it is serialized
242247
to XML by calling its `to_element()` method before submitting it.
@@ -250,9 +255,9 @@ def http_request(cls, url, method='GET', body=None, headers=None):
250255
"""
251256
urlparts = urlsplit(url)
252257
if urlparts.scheme != 'https':
253-
connection = httplib.HTTPConnection(urlparts.netloc)
258+
connection = http_client.HTTPConnection(urlparts.netloc)
254259
elif recurly.CA_CERTS_FILE is None:
255-
connection = httplib.HTTPSConnection(urlparts.netloc)
260+
connection = http_client.HTTPSConnection(urlparts.netloc)
256261
else:
257262
connection = _ValidatedHTTPSConnection(urlparts.netloc)
258263

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

268273
log = logging.getLogger('recurly.http.request')
269274
if log.isEnabledFor(logging.DEBUG):
270275
log.debug("%s %s HTTP/1.1", method, url)
271-
for header, value in headers.iteritems():
276+
for header, value in six.iteritems(headers):
272277
if header == 'Authorization':
273278
value = '<redacted>'
274279
log.debug("%s: %s", header, value)
@@ -290,8 +295,11 @@ def http_request(cls, url, method='GET', body=None, headers=None):
290295
log = logging.getLogger('recurly.http.response')
291296
if log.isEnabledFor(logging.DEBUG):
292297
log.debug("HTTP/1.1 %d %s", resp.status, resp.reason)
293-
for header in resp.msg.headers:
294-
log.debug(header.rstrip('\n'))
298+
if six.PY2:
299+
for header in resp.msg.headers:
300+
log.debug(header.rstrip('\n'))
301+
else:
302+
log.debug(resp.msg._headers)
295303
log.debug('')
296304

297305
return resp
@@ -306,7 +314,7 @@ def as_log_output(self):
306314
"""
307315
elem = self.to_element()
308316
for attrname in self.sensitive_attributes:
309-
for sensitive_el in elem.getiterator(attrname):
317+
for sensitive_el in elem.iter(attrname):
310318
sensitive_el.text = 'XXXXXXXXXXXXXXXX'
311319
return ElementTree.tostring(elem, encoding='UTF-8')
312320

@@ -341,7 +349,7 @@ def get(cls, uuid):
341349
@classmethod
342350
def element_for_url(cls, url):
343351
"""Return the resource at the given URL, as a
344-
(`httplib.HTTPResponse`, `xml.etree.ElementTree.Element`) tuple
352+
(`http_client.HTTPResponse`, `xml.etree.ElementTree.Element`) tuple
345353
resulting from a ``GET`` request to that URL."""
346354
response = cls.http_request(url)
347355
if response.status != 200:
@@ -462,7 +470,7 @@ def element_for_value(cls, attrname, value):
462470
elif isinstance(value, Money):
463471
value.add_to_element(el)
464472
else:
465-
el.text = unicode(value)
473+
el.text = six.text_type(value)
466474

467475
return el
468476

@@ -641,7 +649,7 @@ def delete(self):
641649
@classmethod
642650
def raise_http_error(cls, response):
643651
"""Raise a `ResponseError` of the appropriate subclass in
644-
reaction to the given `httplib.HTTPResponse`."""
652+
reaction to the given `http_client.HTTPResponse`."""
645653
response_xml = response.read()
646654
logging.getLogger('recurly.http.response').debug(response_xml)
647655
exc_class = recurly.errors.error_class_for_http_status(response.status)
@@ -661,7 +669,7 @@ def to_element(self):
661669
continue
662670

663671
if attrname in self.xml_attribute_attributes:
664-
elem.attrib[attrname] = unicode(value)
672+
elem.attrib[attrname] = six.text_type(value)
665673
else:
666674
sub_elem = self.element_for_value(attrname, value)
667675
elem.append(sub_elem)

setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,13 @@
3232
'Intended Audience :: Developers',
3333
'License :: OSI Approved :: MIT License',
3434
'Programming Language :: Python',
35+
'Programming Language :: Python :: 3',
3536
'Topic :: Internet :: WWW/HTTP',
3637
],
3738
packages=['recurly'],
38-
install_requires=['iso8601', 'backports.ssl_match_hostname'] + more_install_requires,
39+
install_requires=['iso8601', 'backports.ssl_match_hostname', 'six'] + more_install_requires,
3940
tests_require=['mock',
40-
'unittest2'],
41+
'six'],
4142
test_suite='unittest2.collector',
4243
zip_safe=True,
4344
)

0 commit comments

Comments
 (0)