Skip to content
This repository has been archived by the owner on Dec 5, 2018. It is now read-only.

Commit

Permalink
Creates image proxy view (closes #104). (#142)
Browse files Browse the repository at this point in the history
* Creates image proxy view (closes #104).

* Passes .env to environment when creating locally.

* Use image proxy URLs for served images.
  • Loading branch information
chuckharmston committed May 26, 2016
1 parent ed50148 commit 3cdd926
Show file tree
Hide file tree
Showing 16 changed files with 228 additions and 24 deletions.
2 changes: 1 addition & 1 deletion conf/web.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env bash

exec uwsgi --http :${PORT:-8000} --wsgi-file /app/recommendation/wsgi.py --master
exec uwsgi --wsgi-disable-file-wrapper --http :${PORT:-8000} --wsgi-file /app/recommendation/wsgi.py --master
2 changes: 2 additions & 0 deletions recommendation/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
TESTING = env.get('RECOMMENDATION_TESTING', None) == 'true'

KEY_PREFIX = env.get('RECOMMENDATION_KEY_PREFIX', 'query_')
SERVER_NAME = env.get('RECOMMENDATION_SERVER_NAME', 'universal-search.dev')

CACHE_TTL = int(env.get('RECOMMENDATION_CACHE_TTL', 7 * 24 * 60 * 60))
MEMCACHED_TTL = int(env.get('RECOMMENDATION_MEMCACHED_TTL', CACHE_TTL))
IMAGEPROXY_TTL = int(env.get('IMAGEPROXY_TTL', CACHE_TTL))

BING_ACCOUNT_KEY = env.get('BING_ACCOUNT_KEY', '')
EMBEDLY_API_KEY = env.get('EMBEDLY_API_KEY', '')
Expand Down
5 changes: 4 additions & 1 deletion recommendation/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from recommendation.mozlog.middleware import request_timer, request_summary
from recommendation.views.debug import debug
from recommendation.views.dummy import dummy
from recommendation.views.images import images
from recommendation.views.main import main
from recommendation.views.static import static
from recommendation.views.status import status
Expand All @@ -24,6 +25,7 @@ def create_app():
# Register views.
app.register_blueprint(main)
app.register_blueprint(debug)
app.register_blueprint(images)
app.register_blueprint(static)
app.register_blueprint(status)

Expand All @@ -45,7 +47,8 @@ def create_app():

app.config.update(
CELERY_BROKER_URL=conf.CELERY_BROKER_URL,
DEBUG=conf.DEBUG
DEBUG=conf.DEBUG,
SERVER_NAME=conf.SERVER_NAME
)
return app

Expand Down
15 changes: 8 additions & 7 deletions recommendation/mozlog/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
'/__heartbeat__',
'/__lbheartbeat__',
'/nginx_status',
'/robots.txt'
'/robots.txt',
'/images'
]


Expand Down Expand Up @@ -42,20 +43,20 @@ def request_summary(response):

log = {}
query = request.args.get('q')
data = response.get_data(as_text=True)
try:
body = json.loads(data)
except json.decoder.JSONDecodeError:
body = {}

log['agent'] = request.headers.get('User-Agent')
log['errno'] = 0 if response.status_code < 400 else response.status_code
log['lang'] = request.headers.get('Accept-Language')
log['method'] = request.method
log['path'] = request.path
log['t'] = (request.finish_time - request.start_time) * 1000 # in ms
log['t'] = (request.finish_time - request.start_time) * 1000 # in ms

if query:
data = response.get_data(as_text=True)
try:
body = json.loads(data)
except json.decoder.JSONDecodeError:
body = {}
query = query.lower()
log['predicates.query_length'] = len(query) > 20
log['predicates.is_protocol'] = (re.match(IS_PROTOCOL, query) is not
Expand Down
5 changes: 4 additions & 1 deletion recommendation/search/classification/embedly.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from recommendation import conf
from recommendation.memorize import memorize
from recommendation.search.classification.base import BaseClassifier
from recommendation.util import image_url


class BaseEmbedlyClassifier(BaseClassifier):
Expand Down Expand Up @@ -54,7 +55,7 @@ def enhance(self):
return {}
return {
'color': self._get_color(api_data),
'url': favicon_url,
'url': image_url(favicon_url, width=32, height=32),
}


Expand Down Expand Up @@ -128,8 +129,10 @@ def enhance(self):
try:
image_data = self._get_image(api_data)
image = {k: image_data.get(k) for k in ['url', 'height', 'width']}
image['url'] = image_url(image['url'])
except (KeyError, IndexError):
image = {}

return {
'image': image,
'title': self._get_title(api_data),
Expand Down
3 changes: 2 additions & 1 deletion recommendation/search/classification/movies.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from recommendation.memorize import memorize
from recommendation.search.classification.base import BaseClassifier
from recommendation.util import image_url


class MovieClassifier(BaseClassifier):
Expand Down Expand Up @@ -93,7 +94,7 @@ def enhance(self):
'title': data.get('Title'),
'year': data.get('Year'),
'plot': data.get('Plot'),
'poster': data.get('Poster'),
'poster': image_url(data.get('Poster')),
'rating': {
'imdb': self._score(data.get('imdbRating'), 10),
'metacritic': self._score(data.get('Metascore'), 100)
Expand Down
10 changes: 7 additions & 3 deletions recommendation/search/classification/tests/test_embedly.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from unittest import TestCase
from unittest.mock import patch
from urllib.parse import parse_qs, urlparse

Expand All @@ -8,6 +7,8 @@
from recommendation.search.classification.embedly import (
BaseEmbedlyClassifier, FaviconClassifier, WikipediaClassifier)
from recommendation.tests.memcached import mock_memcached
from recommendation.tests.util import AppTestCase
from recommendation.util import image_url


MOCK_API_KEY = '0123456789abcdef'
Expand Down Expand Up @@ -80,7 +81,7 @@
}


class TestBaseEmbedlyClassifier(TestCase):
class TestBaseEmbedlyClassifier(AppTestCase):
classifier_class = BaseEmbedlyClassifier

def tearDown(self):
Expand Down Expand Up @@ -149,7 +150,8 @@ def test_enhance(self, mock_api_url):
status=200)
enhanced = self._classifier(MOCK_RESULT_URL).enhance()
eq_(enhanced['color'], MOCK_RESPONSE['favicon_colors'][0]['color'])
eq_(enhanced['url'], MOCK_RESPONSE['favicon_url'])
eq_(enhanced['url'], image_url(
MOCK_RESPONSE['favicon_url'], width=32, height=32))

@patch('recommendation.search.classification.embedly.FaviconClassifier'
'._api_response')
Expand Down Expand Up @@ -223,6 +225,8 @@ def test_enhance(self, mock_api_url):
responses.add(responses.GET, MOCK_API_URL, json=MOCK_RESPONSE,
status=200)
enhanced = self._classifier(MOCK_WIKIPEDIA_URL).enhance()
MOCK_WIKIPEDIA_RESPONSE['image']['url'] = (
image_url(MOCK_WIKIPEDIA_RESPONSE['image']['url']))
eq_(enhanced, MOCK_WIKIPEDIA_RESPONSE)

@patch('recommendation.search.classification.embedly.WikipediaClassifier'
Expand Down
7 changes: 4 additions & 3 deletions recommendation/search/classification/tests/test_movies.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from copy import copy
from unittest import TestCase
from unittest.mock import patch
from urllib.parse import parse_qs, urlparse

Expand All @@ -8,6 +7,8 @@

from recommendation.search.classification.movies import MovieClassifier
from recommendation.tests.memcached import mock_memcached
from recommendation.tests.util import AppTestCase
from recommendation.util import image_url


IMDB_ID = 'tt0116756'
Expand Down Expand Up @@ -54,7 +55,7 @@
}


class TestMovieClassifier(TestCase):
class TestMovieClassifier(AppTestCase):
def setUp(self):
self.classifier = MovieClassifier(RESULT_IMDB, [])

Expand Down Expand Up @@ -125,7 +126,7 @@ def test_enhance(self, mock_api_response):
eq_(enhanced['title'], MOCK_RESPONSE['Title'])
eq_(enhanced['year'], MOCK_RESPONSE['Year'])
eq_(enhanced['plot'], MOCK_RESPONSE['Plot'])
eq_(enhanced['poster'], MOCK_RESPONSE['Poster'])
eq_(enhanced['poster'], image_url(MOCK_RESPONSE['Poster']))
eq_(enhanced['rating']['imdb']['stars'], 1.4)
eq_(enhanced['rating']['imdb']['raw'], 2.8)
eq_(enhanced['rating']['metacritic']['stars'], 1.2)
Expand Down
7 changes: 4 additions & 3 deletions recommendation/search/classification/tests/test_tld.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
from unittest import TestCase
from unittest.mock import patch
from urllib.parse import ParseResult

import responses
from nose.tools import eq_, ok_

from recommendation.search.classification.tld import TLDClassifier
from recommendation.tests.util import AppTestCase
from recommendation.util import image_url


DOMAIN = 'www.mozilla.com'
URL = 'http://%s/' % DOMAIN
LOGO = 'https://logo.clearbit.com/%s' % DOMAIN


class TestTLDClassifier(TestCase):
class TestTLDClassifier(AppTestCase):
def _result(self, url):
return {
'url': url
Expand Down Expand Up @@ -71,4 +72,4 @@ def test_enhance(self, mock_logo_exists):
mock_logo_exists.return_value = False
eq_(self._enhance(URL), None)
mock_logo_exists.return_value = True
eq_(self._enhance(URL), LOGO)
eq_(self._enhance(URL), image_url(LOGO, width=64, height=64))
3 changes: 2 additions & 1 deletion recommendation/search/classification/tld.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import requests

from recommendation.search.classification.base import BaseClassifier
from recommendation.util import image_url


class TLDClassifier(BaseClassifier):
Expand All @@ -24,4 +25,4 @@ def enhance(self):
logo = self._get_logo()
if not self._logo_exists(logo):
return None
return logo
return image_url(logo, width=64, height=64)
4 changes: 2 additions & 2 deletions recommendation/search/tests/test_recommendation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from unittest import TestCase
from unittest.mock import patch

from nose.tools import eq_, ok_
Expand All @@ -16,6 +15,7 @@
from recommendation.search.query.tests.test_yahoo import (
QUERY as YAHOO_QUERY, MOCK_RESPONSE as YAHOO_RESPONSE)
from recommendation.tests.memcached import mock_memcached
from recommendation.tests.util import AppTestCase


QUERY = 'Cubs'
Expand All @@ -25,7 +25,7 @@
SUGGESTIONS = ['a', 'b', 'c']


class TestSearchRecommendation(TestCase):
class TestSearchRecommendation(AppTestCase):
def setUp(self):
self.instance = SearchRecommendation('')

Expand Down
47 changes: 47 additions & 0 deletions recommendation/tests/test_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from urllib.parse import parse_qs, quote, urlparse

from flask import current_app
from nose.tools import eq_

from recommendation.tests.util import AppTestCase
from recommendation.util import image_url


DIMENSION = '64'
IMAGE = 'https://foo.bar/image.jpg'
EMBEDLY_BASE = 'https://i.embed.ly/'
EMBEDLY_IMAGE = '{}?url={}'.format(EMBEDLY_BASE, quote(IMAGE))


class TestImageUrl(AppTestCase):
def _image_url(self, url, **kwargs):
with current_app.app_context():
url = image_url(url, **kwargs)
parsed = urlparse(url) if url else None
qs = parse_qs(parsed.query) if parsed else None
return url, parsed, qs

def test_none(self):
url, parsed, qs = self._image_url(None)
eq_(url, None)

def test_formed(self):
url, parsed, qs = self._image_url(IMAGE, width=DIMENSION,
height=DIMENSION)
eq_(IMAGE, qs['url'][0])
eq_(DIMENSION, qs['width'][0])
eq_(DIMENSION, qs['height'][0])

def test_embedly(self):
url = self._image_url(IMAGE)
embedly_url = self._image_url(EMBEDLY_IMAGE)
eq_(url, embedly_url)

def test_embedly_no_url(self):
url, parsed, qs = self._image_url(EMBEDLY_BASE)
eq_(qs['url'][0], EMBEDLY_BASE)

def test_embedly_empty_url(self):
URL = '{}?url='.format(EMBEDLY_BASE)
url, parsed, qs = self._image_url(URL)
eq_(qs['url'][0], URL)
22 changes: 22 additions & 0 deletions recommendation/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from urllib.parse import parse_qs, urlparse

from flask import current_app, url_for


def image_url(url, **kwargs):
if not url:
return
kwargs['url'] = url
parsed = urlparse(url)

# If the image is already being proxied by Embedly, pull the `url`
# querystring param out and use that instead to prevent double-billing.
if parsed.netloc == 'i.embed.ly':
qs = parse_qs(parsed.query)
try:
kwargs['url'] = qs['url'][0]
except (IndexError, KeyError):
pass

with current_app.app_context():
return url_for('images.proxy', **kwargs)
44 changes: 44 additions & 0 deletions recommendation/views/images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from urllib.parse import urlencode

import requests
from flask import abort, Blueprint, request, Response, stream_with_context

from recommendation import conf

EMBEDLY_RESIZE = 'https://i.embed.ly/1/display/resize'


images = Blueprint('images', __name__)


def make_embedly_url(url, **kwargs):
"""
Passed the URL to an image, returns a string to the Embedly resize URL for
that image. Accepts optional `width` and `height` keyword arguments.
"""
qs = {}
for param in ['width', 'height']:
if param in kwargs:
qs[param] = kwargs[param][0]
qs['animate'] = 'false'
qs['compresspng'] = 'true'
qs['key'] = conf.EMBEDLY_API_KEY
return '{}?{}'.format(EMBEDLY_RESIZE, urlencode(qs))


@images.route('/images')
def proxy():
try:
url = make_embedly_url(**request.args)
except TypeError:
abort(400)
try:
req = requests.get(url, stream=True, timeout=10)
except requests.RequestException:
abort(400)
if req.status_code != 200:
abort(400)
response = Response(stream_with_context(req.iter_content()),
content_type=req.headers['content-type'])
response.headers['Cache-Control'] = 'max-age=%d' % conf.IMAGEPROXY_TTL
return response

0 comments on commit 3cdd926

Please sign in to comment.