From 5e2c553aa802060933b10f090b780ea7b207e5e7 Mon Sep 17 00:00:00 2001 From: Stefan Kjartansson Date: Mon, 11 Aug 2014 09:26:32 +0000 Subject: [PATCH] python 3 support --- laterpay/__init__.py | 20 ++++++++-------- laterpay/compat.py | 38 ++++++++++++++++++++++++++++++ laterpay/signing.py | 55 ++++++++++++++++++++++++------------------- tests/test_client.py | 10 ++++---- tests/test_signing.py | 4 ++-- 5 files changed, 86 insertions(+), 41 deletions(-) create mode 100644 laterpay/compat.py diff --git a/laterpay/__init__.py b/laterpay/__init__.py index 88af1f3..c97df41 100644 --- a/laterpay/__init__.py +++ b/laterpay/__init__.py @@ -9,10 +9,10 @@ import re import string import time -import urllib -import urllib2 from . import signing +from . import compat + import warnings @@ -184,22 +184,22 @@ def get_controls_balance_url(self, forcelang=None): return url def _get_dialog_api_url(self, url): - return '%s/dialog-api?url=%s' % (self.web_root, urllib.quote_plus(url)) + return '%s/dialog-api?url=%s' % (self.web_root, compat.quote_plus(url)) def get_login_dialog_url(self, next_url, use_jsevents=False): - url = '%s/account/dialog/login?next=%s%s%s' % (self.web_root, urllib.quote_plus(next_url), + url = '%s/account/dialog/login?next=%s%s%s' % (self.web_root, compat.quote_plus(next_url), "&jsevents=1" if use_jsevents else "", "&cp=%s" % self.cp_key) return self._get_dialog_api_url(url) def get_signup_dialog_url(self, next_url, use_jsevents=False): - url = '%s/account/dialog/signup?next=%s%s%s' % (self.web_root, urllib.quote_plus(next_url), + url = '%s/account/dialog/signup?next=%s%s%s' % (self.web_root, compat.quote_plus(next_url), "&jsevents=1" if use_jsevents else "", "&cp=%s" % self.cp_key) return self._get_dialog_api_url(url) def get_logout_dialog_url(self, next_url, use_jsevents=False): - url = '%s/account/dialog/logout?next=%s%s%s' % (self.web_root, urllib.quote_plus(next_url), + url = '%s/account/dialog/logout?next=%s%s%s' % (self.web_root, compat.quote_plus(next_url), "&jsevents=1" if use_jsevents else "", "&cp=%s" % self.cp_key) return self._get_dialog_api_url(url) @@ -314,16 +314,16 @@ def _make_request(self, url, params, method='GET'): } if method == 'POST': - req = urllib2.Request(url, data=params, headers=headers) + req = compat.Request(url, data=params, headers=headers) else: url = "%s?%s" % (url, params) - req = urllib2.Request(url, headers=headers) + req = compat.Request(url, headers=headers) _log.debug("Making request to %s", url) try: - response = urllib2.urlopen(req).read() - except urllib2.URLError, e: + response = compat.urlopen(req).read() + except compat.URLError as e: _log.debug("Request failed with reason: %s", e.reason) resp = {'status': 'connection_error'} except: diff --git a/laterpay/compat.py b/laterpay/compat.py new file mode 100644 index 0000000..b9fbdba --- /dev/null +++ b/laterpay/compat.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function + +import sys + +py3k = sys.version_info[:2] >= (3, 0) + + +if py3k: + string_types = str, + + def cmp(a, b): + return (a > b) - (a < b) + + def b(s): + return s.encode("latin-1") + + from urllib.parse import quote, quote_plus, urlencode + from urllib.parse import urlparse, parse_qs + + from urllib.request import Request, urlopen + from urllib.error import URLError + +else: + string_types = basestring, + + cmp = cmp + + def b(s): + return s + + from urllib import quote, quote_plus, urlencode + + from urlparse import urlparse, parse_qs + + from urllib2 import Request, urlopen, URLError + diff --git a/laterpay/signing.py b/laterpay/signing.py index 760835b..a2f38df 100644 --- a/laterpay/signing.py +++ b/laterpay/signing.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function + +import functools import hashlib import hmac import time -import urllib -import urlparse + +from . import compat + ALLOWED_METHODS = ('GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD') @@ -18,32 +22,34 @@ def time_independent_HMAC_compare(a, b): def create_HMAC(HMAC_secret, *parts): - hash = hmac.new(HMAC_secret, digestmod=hashlib.sha224) + hash = hmac.new(compat.b(HMAC_secret), digestmod=hashlib.sha224) for part in parts: - hash.update(part) + hash.update(compat.b(part)) return hash.hexdigest() def sort_params(param_dict): def cmp_params(param1, param2): - result = cmp(param1[0], param2[0]) + result = compat.cmp(param1[0], param2[0]) if result == 0: - result = cmp(param1[1], param2[1]) + result = compat.cmp(param1[1], param2[1]) return result param_list = [] - for name, value_list in param_dict.iteritems(): + for name, value_list in param_dict.items(): if isinstance(value_list, (list, tuple)): for value in value_list: - if not isinstance(value, basestring): + if not isinstance(value, compat.string_types): value = str(value) param_list.append((name, value)) else: - if not isinstance(value_list, basestring): + if not isinstance(value_list, compat.string_types): value_list = str(value_list) param_list.append((name, value_list)) + if compat.py3k: + return sorted(param_list, key=functools.cmp_to_key(cmp_params)) return sorted(param_list, cmp_params) @@ -71,7 +77,7 @@ def normalise_param_structure(params): return out # otherwise this is a dictionary, so either it is { a => b } or { a => (b,c) } - for key, value in params.iteritems(): + for key, value in params.items(): if not isinstance(value, (list, tuple)): out[key] = [value] else: @@ -87,7 +93,7 @@ def create_base_message(params, url, method='POST'): data = {} - url = urllib.quote(_encode_if_unicode(url), safe='') + url = compat.quote(_encode_if_unicode(url), safe='') if method not in ALLOWED_METHODS: raise ValueError('method should be one of: {}'.format(ALLOWED_METHODS)) @@ -95,25 +101,25 @@ def create_base_message(params, url, method='POST'): params = normalise_param_structure(params) for key, values in params.items(): - key = urllib.quote(_encode_if_unicode(key), safe='') + key = compat.quote(_encode_if_unicode(key), safe='') if not isinstance(values, (list, tuple)): values = [values] values_str = [] - # If any non basestring objects, ``str()`` them. + # If any non compat.string_types objects, ``str()`` them. for v in values: - if not isinstance(v, basestring): + if not isinstance(v, compat.string_types): v = str(v) values_str.append(v) - data[key] = [urllib.quote(_encode_if_unicode(v), safe='') for v in values_str] + data[key] = [compat.quote(_encode_if_unicode(v), safe='') for v in values_str] sorted_params = sort_params(data) param_str = '&'.join('{}={}'.format(k, v) for k, v in sorted_params) - param_str = urllib.quote(param_str, safe='') + param_str = compat.quote(param_str, safe='') return msg.format(method=method, url=url, params=param_str) @@ -123,7 +129,7 @@ def sign(secret, params, url, method='POST'): Create signature for given `params`, `url` and HTTP method. How params are canonicalized: - - `urllib.quote` every key and value that will be signed + - `compat.quote` every key and value that will be signed - sort the param list - `'&'join` the params @@ -189,7 +195,7 @@ def sign_and_encode(secret, params, url, method="GET"): value = _encode_if_unicode(v) sorted_data.append((k, value)) - encoded = urllib.urlencode(sorted_data) + encoded = compat.urlencode(sorted_data) hmac = sign(secret, params, url=url, method=method) return "%s&hmac=%s" % (encoded, hmac) @@ -216,12 +222,12 @@ def sign_get_url(secret, url, signature_paramname="hmac"): :type signature_paramname: str :returns: ``str`` -- the URL, including the signature as an URL parameter """ - parsed = urlparse.urlparse(url) + parsed = compat.urlparse(url) if parsed.query != "": - # use urlparse.parse_qsl, because .parse_qs seems to create problems - # with urllib.urlencode() - qs = urlparse.parse_qsl(parsed.query, keep_blank_values=True) + # use compat.parse_qsl, because .parse_qs seems to create problems + # with compat.urlencode() + qs = compat.parse_qsl(parsed.query, keep_blank_values=True) ### create string to sign # .sort() will sort in alphabetical order @@ -232,7 +238,7 @@ def sign_get_url(secret, url, signature_paramname="hmac"): qs.append((signature_paramname, hmac)) return parsed.scheme + "://" + parsed.netloc + parsed.path + \ - parsed.params + "?" + urllib.urlencode(qs) + parsed.fragment + parsed.params + "?" + compat.urlencode(qs) + parsed.fragment return None @@ -246,6 +252,7 @@ def _encode_if_unicode(value, encoding='utf-8'): This utility is needed because some web frameworks can provide request arguments as ``str`` instances. """ - if isinstance(value, unicode): + + if not compat.py3k and isinstance(value, unicode): value = value.encode(encoding) return value diff --git a/tests/test_client.py b/tests/test_client.py index f9a0924..d1cbe5a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -3,7 +3,6 @@ from __future__ import absolute_import, print_function import sys -import urlparse import uuid if sys.version_info[:2] < (2, 7): @@ -11,6 +10,7 @@ else: import unittest +from laterpay.compat import urlparse, parse_qs from laterpay import ( APIException, InvalidItemDefinition, @@ -38,10 +38,10 @@ def setUp(self): 'some-secret') def get_qs_dict(self, url): - o = urlparse.urlparse(url) - d = urlparse.parse_qs(o.query) - o = urlparse.urlparse(d['url'][0]) - d = urlparse.parse_qs(o.query) + o = urlparse(url) + d = parse_qs(o.query) + o = urlparse(d['url'][0]) + d = parse_qs(o.query) return d def assertQueryString(self, url, key, value=None): diff --git a/tests/test_signing.py b/tests/test_signing.py index d1fa146..24897d4 100644 --- a/tests/test_signing.py +++ b/tests/test_signing.py @@ -3,13 +3,13 @@ from __future__ import absolute_import, print_function import sys -import urlparse if sys.version_info[:2] < (2, 7): import unittest2 as unittest else: import unittest +from laterpay import compat from laterpay import signing @@ -179,7 +179,7 @@ def test_signing_with_item(self): # changing the price in the url false_string = 'article_id=154&cp=laternews&jsevents=1&pricing=EUR150&purchase_date=1398861228815&title=VIDEO%3A+Rwanda%27s+genocide%2C+20+years+on&tref=4ebbf443-a12e-4ce9-89e4-999ba93ba1dc&ts=1398861228&url=http%3A%2F%2Flocal.laterpaytest.net%3A8003%2Fmmss%2F154&vat=EU19&hmac=4d41f1adcb7c6bf6cf9c5eb15b179fdbec667d53f2749e2845c87315' - false_params = urlparse.parse_qs(false_string) + false_params = compat.parse_qs(false_string) self.assertFalse(signing.verify(signature, secret, false_params, base_url, method))