Skip to content

Commit

Permalink
Merge pull request #14 from APSL/master
Browse files Browse the repository at this point in the history
Support for Python3 and Django 1.7
  • Loading branch information
heynemann committed Jul 20, 2015
2 parents e0bec9e + d478082 commit 8001d6c
Show file tree
Hide file tree
Showing 15 changed files with 401 additions and 33 deletions.
10 changes: 9 additions & 1 deletion libthumbor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@

'''libthumbor is the library used to access thumbor's images in python'''

__version__ = "1.1.0"
from pkg_resources import get_distribution, DistributionNotFound

__project__ = 'libthumbor'

try:
__version__ = get_distribution(__project__).version
except DistributionNotFound:
# Returns a local version. For tests.
__version__ = '{}-local'.format(__project__)

from libthumbor.crypto import CryptoURL
10 changes: 8 additions & 2 deletions libthumbor/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import hmac
import hashlib

from six import text_type, b, PY3

try:
from Crypto.Cipher import AES
PYCRYPTOFOUND = True
Expand All @@ -33,7 +35,7 @@ def __init__(self, key):
if not PYCRYPTOFOUND:
raise RuntimeError('pyCrypto could not be found,' +
' please install it before using libthumbor')
if isinstance(key, unicode):
if isinstance(key, text_type):
key = str(key)
self.key = key
self.computed_key = (key * 16)[:16]
Expand All @@ -45,12 +47,16 @@ def generate_old(self, options):
cypher = AES.new(self.computed_key)
encrypted = base64.urlsafe_b64encode(cypher.encrypt(pad(url)))

if PY3:
encrypted = encrypted.decode('ascii')
return "/%s/%s" % (encrypted, options['image_url'])

def generate_new(self, options):
url = plain_image_url(**options)
signature = base64.urlsafe_b64encode(hmac.new(self.key, unicode(url).encode('utf-8'), hashlib.sha1).digest())
signature = base64.urlsafe_b64encode(hmac.new(b(self.key), text_type(url).encode('utf-8'), hashlib.sha1).digest())

if PY3:
signature = signature.decode('ascii')
return '/%s/%s' % (signature, url)

def generate(self, **options):
Expand Down
5 changes: 4 additions & 1 deletion libthumbor/django/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from django.conf.urls.defaults import patterns, url
try:
from django.conf.urls.defaults import patterns, url
except ImportError:
from django.conf.urls import patterns, url

urlpatterns = patterns('libthumbor.django.views',
url("^$", 'generate_url', name="generate_thumbor_url"),
Expand Down
12 changes: 6 additions & 6 deletions libthumbor/django/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,33 +38,33 @@ def generate_url(request):
try:
if 'width' in args:
args['width'] = int(args['width'])
except ValueError, e:
except ValueError as e:
error_message = "The width value '%s' is not an integer." % \
args['width']

try:
if 'height' in args:
args['height'] = int(args['height'])
except ValueError, e:
except ValueError as e:
error_message = "The height value '%s' is not an integer." % \
args['height']

try:
if 'crop_top' in args or 'crop_left' in args or 'crop_right' in args or 'crop_bottom' in args:
args['crop'] = ((int(args['crop_left']), int(args['crop_top'])),
(int(args['crop_right']), int(args['crop_bottom'])))
except KeyError, e:
except KeyError as e:
error_message = "Missing values for cropping. Expected all 'crop_left', 'crop_top', 'crop_right', 'crop_bottom' values."
except ValueError, e:
except ValueError as e:
error_message = "Invalid values for cropping. Expected all 'crop_left', 'crop_top', 'crop_right', 'crop_bottom' to be integers."

if error_message is not None:
logging.warning(error_message)
return HttpResponseBadRequest(error_message)

try:
return HttpResponse(THUMBOR_SERVER + crypto.generate(**args).strip("/"), mimetype="text/plain")
except (ValueError, KeyError), e:
return HttpResponse(THUMBOR_SERVER + crypto.generate(**args).strip("/"), content_type="text/plain")
except (ValueError, KeyError) as e:
error_message = str(e)
logging.warning(error_message)
return HttpResponseBadRequest(error_message)
4 changes: 3 additions & 1 deletion libthumbor/url.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

import hashlib

from six import b

AVAILABLE_HALIGN = ['left', 'center', 'right']
AVAILABLE_VALIGN = ['top', 'middle', 'bottom']

Expand Down Expand Up @@ -45,7 +47,7 @@ def url_for(**options):
'''Returns the url for the specified options'''

url_parts = get_url_parts(**options)
image_hash = hashlib.md5(options['image_url']).hexdigest()
image_hash = hashlib.md5(b(options['image_url'])).hexdigest()
url_parts.append(image_hash)

return "/".join(url_parts)
Expand Down
11 changes: 7 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
'''Module that configures setuptools to package libthumbor'''

from setuptools import setup, find_packages
from libthumbor import __version__
import sys

tests_require = [
'mock',
Expand All @@ -21,12 +21,14 @@
'preggy',
'ipdb',
'coveralls',
'thumbor',
]

if sys.version_info[0] == 2:
tests_require.append('thumbor')

setup(
name = 'libthumbor',
version = __version__,
version = "1.1.0",
description = "libthumbor is the python extension to thumbor",
long_description = """
libthumbor is the python extension to thumbor.
Expand Down Expand Up @@ -59,7 +61,8 @@
},

install_requires=[
"pyCrypto"
"pyCrypto",
"six"
],
)

9 changes: 7 additions & 2 deletions tests/test_cryptourl.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
'''libthumbor cryptography tests'''
from unittest import TestCase

from thumbor.crypto import Cryptor
from six import text_type, PY3

if PY3:
from thumbor_py3.crypto import Cryptor
else:
from thumbor.crypto import Cryptor

from libthumbor.crypto import CryptoURL

Expand Down Expand Up @@ -317,7 +322,7 @@ def setUp(self):

class NewFormatUrlWithUnicodeKey(TestCase, NewFormatUrlTestsMixin):
def setUp(self):
self.crypto = CryptoURL(unicode(KEY))
self.crypto = CryptoURL(text_type(KEY))

class GenerateWithUnsafeTestCase(TestCase):

Expand Down
14 changes: 8 additions & 6 deletions tests/test_generic_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

from libthumbor.crypto import CryptoURL

from six import b

os.environ["DJANGO_SETTINGS_MODULE"] = 'testproj.settings'

try:
Expand Down Expand Up @@ -54,7 +56,7 @@ def test_generate_url_with_params_via_get(self):
response = self.client.get('/gen_url/?' + self.url_query.urlencode())

assert HTTP_OK == response.status_code, "Got %d" % response.status_code
assert response.content == settings.THUMBOR_SERVER + crypto.generate(**image_args).strip("/")
assert response.content == b(settings.THUMBOR_SERVER + crypto.generate(**image_args).strip("/"))

def test_passing_invalid_value_for_width(self):
self.url_query.update({
Expand All @@ -65,7 +67,7 @@ def test_passing_invalid_value_for_width(self):
response = self.client.get('/gen_url/?' + self.url_query.urlencode())

assert HTTP_BAD_REQUEST == response.status_code, "Got %d" % response.status_code
assert "The width value '1.2' is not an integer." == response.content
assert b("The width value '1.2' is not an integer.") == response.content

def test_passing_invalid_value_for_height(self):
self.url_query.update({
Expand All @@ -76,7 +78,7 @@ def test_passing_invalid_value_for_height(self):
response = self.client.get('/gen_url/?' + self.url_query.urlencode())

assert HTTP_BAD_REQUEST == response.status_code, "Got %d" % response.status_code
assert "The height value 's' is not an integer." == response.content
assert b("The height value 's' is not an integer.") == response.content

def test_passing_invalid_aligns(self):
self.url_query.update({
Expand All @@ -97,7 +99,7 @@ def test_passing_only_one_crop_value(self):
response = self.client.get('/gen_url/?' + self.url_query.urlencode())

assert HTTP_BAD_REQUEST == response.status_code, "Got %d" % response.status_code
assert "Missing values for cropping. Expected all 'crop_left', 'crop_top', 'crop_right', 'crop_bottom' values." == response.content
assert b("Missing values for cropping. Expected all 'crop_left', 'crop_top', 'crop_right', 'crop_bottom' values.") == response.content

def test_passing_only_one_crop_with_invalid_value(self):
self.url_query.update({
Expand All @@ -111,7 +113,7 @@ def test_passing_only_one_crop_with_invalid_value(self):
response = self.client.get('/gen_url/?' + self.url_query.urlencode())

assert HTTP_BAD_REQUEST == response.status_code, "Got %d" % response.status_code
assert "Invalid values for cropping. Expected all 'crop_left', 'crop_top', 'crop_right', 'crop_bottom' to be integers." == response.content
assert b("Invalid values for cropping. Expected all 'crop_left', 'crop_top', 'crop_right', 'crop_bottom' to be integers.") == response.content

def test_passing_various_erroneous_values(self):
self.url_query.update({
Expand Down Expand Up @@ -151,4 +153,4 @@ def test_passing_all_params(self):
response = self.client.get('/gen_url/?' + self.url_query.urlencode())

assert HTTP_OK == response.status_code, "Got %d" % response.status_code
assert response.content == settings.THUMBOR_SERVER + crypto.generate(**image_args).strip("/")
assert response.content == b(settings.THUMBOR_SERVER + crypto.generate(**image_args).strip("/"))
22 changes: 17 additions & 5 deletions tests/test_libthumbor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@
import re
import hashlib

from thumbor.crypto import Cryptor, Signer
from thumbor.url import Url
from six import b, PY3

if PY3:
from thumbor_py3.crypto import Cryptor, Signer
from thumbor_py3.url import Url
else:
from thumbor.crypto import Cryptor, Signer
from thumbor.url import Url

from libthumbor import CryptoURL

Expand Down Expand Up @@ -40,6 +46,9 @@ def test_usage():
filters=[],
image=image
)

if PY3:
thumbor_options = thumbor_options.decode('ascii')
thumbor_url = "/%s/%s" % (thumbor_options, image)

crypto = CryptoURL(key=key)
Expand Down Expand Up @@ -76,7 +85,10 @@ def test_usage_new_format():
filters=[]
)
thumbor_url = ('%s/%s' % (thumbor_url, image)).lstrip('/')
thumbor_url = '/%s/%s' % (thumbor_signer.signature(thumbor_url), thumbor_url)
signature = thumbor_signer.signature(thumbor_url)
if PY3:
signature = signature.decode('ascii')
thumbor_url = '/%s/%s' % (signature, thumbor_url)

crypto = CryptoURL(key=key)
url = crypto.generate(
Expand Down Expand Up @@ -112,7 +124,7 @@ def test_thumbor_can_decrypt_lib_thumbor_generated_url():
assert decrypted_url['height'] == 200
assert decrypted_url['width'] == 300
assert decrypted_url['smart']
assert decrypted_url['image_hash'] == hashlib.md5(image).hexdigest()
assert decrypted_url['image_hash'] == hashlib.md5(b(image)).hexdigest()

def test_thumbor_can_decrypt_lib_thumbor_generated_url_new_format():
key = "my-security-key"
Expand All @@ -131,4 +143,4 @@ def test_thumbor_can_decrypt_lib_thumbor_generated_url_new_format():
reg = "/([^/]+)/(.+)"
(signature, url) = re.match(reg, url).groups()

assert thumbor_signer.validate(signature, url)
assert thumbor_signer.validate(b(signature), url)
13 changes: 9 additions & 4 deletions tests/test_url_composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
import sys
from unittest import TestCase

from thumbor.crypto import Cryptor
from six import PY3

if PY3:
from thumbor_py3.crypto import Cryptor
else:
from thumbor.crypto import Cryptor

from libthumbor.url import url_for
from libthumbor.url import unsafe_url
Expand Down Expand Up @@ -49,7 +54,7 @@ def test_url_raises_if_no_url():
'''
try:
url_for()
except ValueError, err:
except ValueError as err:
assert str(err) == 'The image_url argument is mandatory.'
return True
assert False, 'Should not have gotten this far'
Expand Down Expand Up @@ -487,7 +492,7 @@ def test_proper_haligns():
'''test_proper_haligns'''
try:
url_for(halign='wrong', image_url=IMAGE_URL)
except ValueError, err:
except ValueError as err:
assert str(err) == 'Only "left", "center" and "right"' + \
' are valid values for horizontal alignment.'
return True
Expand All @@ -497,7 +502,7 @@ def test_proper_valigns():
'''test_proper_haligns'''
try:
url_for(valign='wrong', image_url=IMAGE_URL)
except ValueError, err:
except ValueError as err:
assert str(err) == 'Only "top", "middle" and "bottom"' + \
' are valid values for vertical alignment.'
return True
Expand Down
11 changes: 11 additions & 0 deletions tests/testproj/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@
DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.{}'.format(DATABASE_ENGINE),
'NAME': DATABASE_NAME,
'USER': '',
'PASSWORD': '',
'HOST': '',
'PORT': '',
}
}

# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
Expand Down
5 changes: 4 additions & 1 deletion tests/testproj/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from django.conf.urls.defaults import *
try:
from django.conf.urls.defaults import *
except ImportError:
from django.conf.urls import patterns, include

urlpatterns = patterns('',
(r"^gen_url/", include('libthumbor.django.urls')),
Expand Down
Empty file added tests/thumbor_py3/__init__.py
Empty file.
Loading

0 comments on commit 8001d6c

Please sign in to comment.