diff --git a/.gitignore b/.gitignore index c18dd39..45bfe99 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.pyc -Embedly.egg-info/ +*.egg-info/ +.tox/ build/ dist/ .virtualenv diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..8891f22 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: python +python: + - 2.6 + - 2.7 + - 3.2 + - 3.3 +script: python setup.py test diff --git a/README.rst b/README.rst index c948954..f514ce2 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ embedly-python ============== -Python Library for interacting with Embedly's API. To get started sign up for -a key at `embed.ly/signup `_. +Python library for interacting with Embedly's API. To get started sign up for +a key at `embed.ly/signup `_. Install ------- @@ -9,57 +9,75 @@ Install with `Pip `_ (recommended):: pip install embedly -Or easy_install +Or easy_install:: - sudo easy_install Embedly + easy_install Embedly Or setuptools:: git clone git://github.com/embedly/embedly-python.git - sudo python setup.py + python setup.py +Setup requires Setuptools 0.7+ or Distribute 0.6.2+ in order to take advantage +of the ``2to3`` option. Setup will still run on earlier versions but you'll +see a warning and ``2to3`` won't happen. Read more in the Setuptools +`docs `_ Getting Started --------------- This library is meant to be a dead simple way to interact with the Embedly API. -There are only 2 main objects, the ``Embedly`` client and the ``Url`` model. -Here is a simple example and then we will go into the objects:: +There are only 2 main objects, the ``Embedly`` client and the ``Url`` response +model. Here is a simple example and then we will go into the objects:: >>> from embedly import Embedly >>> client = Embedly(:key) - >>> obj = client.oembed('http://instagr.am/p/BL7ti/') - >>> obj.type - u'photo' + >>> obj = client.oembed('http://instagram.com/p/BL7ti/') >>> obj['type'] u'photo' - >>> obj.url - u'http://distillery.s3.amazonaws.com/media/2011/01/24/cdd759a319184cb79793506607ff5746_7.jpg' + >>> obj['url'] + u'http://images.ak.instagram.com/media/2011/01/24/cdd759a319184cb79793506607ff5746_7.jpg' - >>> obj = client.oembed('http://instagr.am/p/error') - >>> obj.error + >>> obj = client.oembed('http://instagram.com/error/error/') + >>> obj['error'] True Embedly Client """""""""""""" -The Embedly client is a object that takes in a key and an optional User Agent -then handles all the interactions and HTTP requests to Embedly. To initialize -the object pass in your key you got from signing up for Embedly and an optional -User Agent. +The Embedly client is a object that takes in a key and optional User Agent +and timeout parameters then handles all the interactions and HTTP requests +to Embedly. To initialize the object, you'll need the key that you got when +you signed up for Embedly. +:: >>> from embedly import Embedly - >>> client = Embedly('key', 'Mozilla/5.0 (compatible; example-org;)') + >>> client = Embedly('key') + >>> client2 = Embedly('key', 'Mozilla/5.0 (compatible; example-org;)') + >>> client3 = Embedly('key', 'Mozilla/5.0 (compatible; example-org;)', 30) + >>> client4 = Embedly('key', timeout=10, user_agent='Mozilla/5.0 (compatible; example-org;)') The client object now has a bunch of different methods that you can use. ``oembed`` Corresponds to the `oEmbed endpoint - `_. Passes back a simple object that - allows you to retrieve a title, thumbnail, description and the embed html:: + `_. Passes back an object + that allows you to retrieve a title, thumbnail, description and the embed + html:: >>> client.oembed('http://vimeo.com/18150336') +``extract`` + Corresponds to the `Extract endpoint + `_. Passes back an + object that allows you to retrieve a title, description, content, html and a + list of images.:: + + >>> client.extract('http://vimeo.com/18150336') + + ``preview`` + **Preview is no longer available to new users and has been replaced by extract.** + Corresponds to the `Preview endpoint `_. Passes back a simple object that allows you to retrieve a title, description, content, html and a list of @@ -69,6 +87,8 @@ The client object now has a bunch of different methods that you can use. ``objectify`` + **Objectify is no longer available to new users and has been replaced by extract.** + Corresponds to the `Objectify endpoint `_. Passes back a simple object that allows you to retrieve pretty much everything that Embedly knows about a @@ -77,14 +97,14 @@ The client object now has a bunch of different methods that you can use. >>> client.objectify('http://vimeo.com/18150336') -The above functions all take the same arguements, a URL or a list of URLs and +The above functions all take the same arguments, a URL or a list of URLs and keyword arguments that correspond to Embedly's `query arguments `_. Here is an example:: >>> client.oembed(['http://vimeo.com/18150336', 'http://www.youtube.com/watch?v=hD7ydlyhvKs'], maxwidth=500, words=20) -There are some supporting functions that allow you to limit urls before sending +There are some supporting functions that allow you to limit URLs before sending them to Embedly. Embedly can return metadata for any URL, these just allow a developer to only pass a subset of Embedly `providers `_. Note that URL shorteners like bit.ly or t.co are @@ -108,53 +128,65 @@ not supported through these regexes. Url Object """""""""" -The ``Url`` Object is just a smart dictionary that acts more like an object. -For example when you run ``oembed`` you get back a Url Object: +The ``Url`` object is basically a response dictionary returned from +one of the Embedly API endpoints. +:: - >>> obj = client.oembed('http://vimeo.com/18150336', words=10) + >>> response = client.oembed('http://vimeo.com/18150336', words=10) -Depending on the method you are using, the object has a different set of +Depending on the method you are using, the response will have different attributes. We will go through a few, but you should read the `documentation -`_ to get the full list of data that is passed back.:: +`_ to get the full list of data that is passed back. +:: - # Url Object can be accessed like a dictionary - >>> obj['type'] + >>> response['type'] u'video' - # Data can also be accessed like attributes - >> obj.type - u'video' - # Invalid attributes are returned as None - >>> obj.notanattribute + >>> response['title'] + u'Wingsuit Basejumping - The Need 4 Speed: The Art of Flight' + >>> response['provider_name'] + u'Vimeo' + >>> response['width'] + 1280 + +As you can see the ``Url`` object works like a dictionary, but it's slightly +enhanced. It will always have ``method`` and ``original_url`` attributes, +which represent the Embedly request type and the URL requested. +:: - # The url object always has an ``orginal_url`` attrbiute. - >>> obj.original_url - u'http://vimeo.com/18150336' - # The method used to retrive the URL is also on the obj - >>> obj.method - u'oembed' + >>> response.method + 'oembed' + >>> response.original_url + 'http://vimeo.com/18150336' -For the Preview and Objectify endpoints the sub objects can also be accessed in + # useful because the response data itself may not have a URL + # (or it could have a redirected link, querystring params, etc) + >>> response['url'] + ... + KeyError: 'url' + +For the Preview and Objectify endpoints the sub-objects can also be accessed in the same manner. +:: >>> obj = client.preview('http://vimeo.com/18150336', words=10) - >>> obj.object.type + >>> obj['object']['type'] u'video' - >>> obj.images[0].url + >>> obj['images'][0]['url'] u'http://b.vimeocdn.com/ts/117/311/117311910_1280.jpg' Error Handling -------------- -If there was an error processing the request, The ``Url`` object will contain +If there was an error processing the request, the ``Url`` object will contain an error. For example if we use an invalid key, we will get a 401 response back :: >>> client = Embedly('notakey') - >>> obj = client.preview('http://vimeo.com/18150336', words=10) - >>> obj.error + >>> obj = client.preview('http://vimeo.com/18150336') + >>> obj['error'] True - >>> obj.error_code + >>> obj['error_code'] 401 Copyright --------- -Copyright (c) 2011 Embed.ly, Inc. See LICENSE for details. \ No newline at end of file +Copyright (c) 2013 Embed.ly, Inc. See LICENSE for details. diff --git a/embedly/__init__.py b/embedly/__init__.py index e10df67..11f965c 100644 --- a/embedly/__init__.py +++ b/embedly/__init__.py @@ -1,3 +1,4 @@ -from client import Embedly +from __future__ import absolute_import +from .client import Embedly -__version__ = '0.4.3' \ No newline at end of file +__version__ = '0.5.0.post0' diff --git a/embedly/client.py b/embedly/client.py index a12c08f..3aff738 100644 --- a/embedly/client.py +++ b/embedly/client.py @@ -4,39 +4,43 @@ The embedly object that interacts with the service """ +from __future__ import absolute_import, unicode_literals import re -import urllib import httplib2 +import json +from urllib import quote, urlencode -try: - import json -except ImportError: - import simplejson as json +from .models import Url -from models import Url -USER_AGENT = 'Mozilla/5.0 (compatible; embedly-python/0.3;)' +def get_user_agent(): + from . import __version__ + return 'Mozilla/5.0 (compatible; embedly-python/%s;)' % __version__ + class Embedly(object): """ Client """ - def __init__(self, key=None, user_agent=USER_AGENT): + def __init__(self, key=None, user_agent=None, timeout=60): """ Initialize the Embedly client - :param user_agent: User Agent passed to Embedly - :type user_agent: str :param key: Embedly Pro key :type key: str + :param user_agent: User Agent passed to Embedly + :type user_agent: str + :param timeout: timeout for HTTP connection attempts + :type timeout: int :returns: None """ - self.user_agent = user_agent self.key = key - self.services = [] + self.user_agent = user_agent or get_user_agent() + self.timeout = timeout + self.services = [] self._regex = None def get_services(self): @@ -45,22 +49,24 @@ def get_services(self): the list of supported providers and their regexes """ - if self.services: return self.services + if self.services: + return self.services - url = 'http://api.embed.ly/1/services/python' + url = 'https://api.embed.ly/1/services/python' - http = httplib2.Http() - headers = {'User-Agent' : self.user_agent} + http = httplib2.Http(timeout=self.timeout) + headers = {'User-Agent': self.user_agent, + 'Connection': 'close'} resp, content = http.request(url, headers=headers) if resp['status'] == '200': - resp_data = json.loads(content) + resp_data = json.loads(content.decode('utf-8')) self.services = resp_data - #build the regex that we can use later. + # build the regex that we can use later _regex = [] - for each in self.get_services(): - _regex.append('|'.join(each.get('regex',[]))) + for each in self.services: + _regex.append('|'.join(each.get('regex', []))) self._regex = re.compile('|'.join(_regex)) @@ -91,48 +97,49 @@ def _get(self, version, method, url_or_urls, **kwargs): raise ValueError('%s requires a url or a list of urls given: %s' % (method.title(), url_or_urls)) - #A flag we can use instead of calling isinstance all the time. + # a flag we can use instead of calling isinstance() all the time multi = isinstance(url_or_urls, list) - # Throw an error early for too many URLs + # throw an error early for too many URLs if multi and len(url_or_urls) > 20: - raise ValueError('Embedly accepts only 20 urls at a time. Url ' \ - 'Count:%s' % len(url_or_urls)) + raise ValueError('Embedly accepts only 20 urls at a time. Url ' + 'Count:%s' % len(url_or_urls)) query = '' key = kwargs.get('key', self.key) - #make sure that a key was set on the client or passed in. + # make sure that a key was set on the client or passed in if not key: - raise ValueError('Requires a key. None given: %s' % (key)) + raise ValueError('Requires a key. None given: %s' % key) kwargs['key'] = key - query += urllib.urlencode(kwargs) + query += urlencode(kwargs) if multi: - query += '&urls=%s&' % ','.join([urllib.quote(url) for url in url_or_urls]) + query += '&urls=%s&' % ','.join([quote(url) for url in url_or_urls]) else: - query += '&url=%s' % urllib.quote(url_or_urls) + query += '&url=%s' % quote(url_or_urls) - url = 'http://api.embed.ly/%s/%s?%s' % (version, method, query) + url = 'https://api.embed.ly/%s/%s?%s' % (version, method, query) - http = httplib2.Http() + http = httplib2.Http(timeout=self.timeout) - headers = {'User-Agent' : self.user_agent} + headers = {'User-Agent': self.user_agent, + 'Connection': 'close'} resp, content = http.request(url, headers=headers) if resp['status'] == '200': - data = json.loads(content) + data = json.loads(content.decode('utf-8')) if kwargs.get('raw', False): data['raw'] = content else: - data = {'type' : 'error', - 'error' : True, - 'error_code' : int(resp['status'])} + data = {'type': 'error', + 'error': True, + 'error_code': int(resp['status'])} if multi: return map(lambda url, data: Url(data, method, url), @@ -157,3 +164,9 @@ def objectify(self, url_or_urls, **kwargs): oembed """ return self._get(2, 'objectify', url_or_urls, **kwargs) + + def extract(self, url_or_urls, **kwargs): + """ + oembed + """ + return self._get(1, 'extract', url_or_urls, **kwargs) diff --git a/embedly/models.py b/embedly/models.py index 327f1db..e3f7ad8 100644 --- a/embedly/models.py +++ b/embedly/models.py @@ -1,77 +1,19 @@ -""" -Models +from __future__ import absolute_import, unicode_literals +from .py3_utils import python_2_unicode_compatible, IterableUserDict -Creates a sudo model class that makes it easy to access attributes -""" -class AttrDict(object): - """ - UserDict is a pain in the ass. Let's just make our own. - """ - def __init__(self, data=None): - if data is None: - data = {} - - for key, value in data.items(): - if isinstance(value, dict): - data[key] = AttrDict(value) - elif isinstance(value, list): - values = [] - for v in value: - if isinstance(v, dict): - values.append(AttrDict(v)) - else: - values.append(v) - data[key] = values - - self.data = data - - def __getattr__(self, name): - if name in ['data', 'method']: - return object.__getattr__(self, name) - try: - return self.data[name] - except KeyError, e: - return None - raise AttributeError( - "%s instance has no attribute '%s'" % (self.__class__.__name__, - name)) - - def __setattr__(self, name, value): - if name in ['data', 'method']: - object.__setattr__(self, name, value) - else: - self.data[name] = value - - def __getitem__(self, name): return self.data[name] - def __setitem__(self, name, value): self.data[name] = value - def __delitem__(self, name): del self.data[name] - def __len__(self): return len(self.data) - def get(self, name): return self.data.get(name) - def keys(self): return self.data.keys() - def values(self): return self.data.values() - def items(self): return self.data.items() - @property - def dict(self): - return self.data - -class Url(AttrDict): +@python_2_unicode_compatible +class Url(IterableUserDict, object): + """ + A dictionary with two additional attributes for the method and url. + UserDict provides a dictionary interface along with the regular + dictionary accessible via the `data` attribute. - def __init__(self, data=None, method=None, original_url=None): - if data is None: - data = {} - super(Url, self).__init__(data) + """ + def __init__(self, data=None, method=None, original_url=None, **kwargs): + super(Url, self).__init__(data, **kwargs) self.method = method or 'url' self.original_url = original_url def __str__(self): - return self.__unicode__().encode("utf-8") - - def __unicode__(self): - r = u'<%s ' % self.method.title() - - if self.original_url: - r += self.original_url - - r += ' >' - return r + return '<%s %s>' % (self.method.title(), self.original_url or "") diff --git a/embedly/py3_utils.py b/embedly/py3_utils.py new file mode 100644 index 0000000..089ce8e --- /dev/null +++ b/embedly/py3_utils.py @@ -0,0 +1,28 @@ +import sys + +# 2to3 doesn't handle the UserDict relocation +# put the import logic here for cleaner usage +try: + from collections import UserDict as IterableUserDict +except ImportError: # Python 2 + from UserDict import IterableUserDict + + +def python_2_unicode_compatible(klass): + """ + A decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + From django.utils.encoding.py in 1.4.2+, minus the dependency on Six. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if sys.version_info[0] == 2: + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass diff --git a/embedly/tests.py b/embedly/tests.py index f6bd93a..90ac6d9 100644 --- a/embedly/tests.py +++ b/embedly/tests.py @@ -1,149 +1,258 @@ +from __future__ import unicode_literals +import re +import sys +import json -import os -import unittest +try: # pragma: no cover + import unittest2 as unittest # Python 2.6 # pragma: no cover +except ImportError: # pragma: no cover + import unittest # pragma: no cover + +try: # pragma: no cover + from unittest import mock # pragma: no cover +except ImportError: # Python < 3.3 # pragma: no cover + import mock # pragma: no cover from embedly.client import Embedly from embedly.models import Url -class EmbedlyTestCase(unittest.TestCase): - - def __init__(self, *args, **kwargs): - self.key = 'internal' # os.environ['EMBEDLY_API_KEY'] - - if not self.key: - raise ValueError('Set envirnomental varible EMBEDLY_API_KEY '+\ - 'before running these tests like so: $ export '+\ - 'EMBEDLY_API_KEY=key') - - super(EmbedlyTestCase, self).__init__(*args, **kwargs) +class UrlTestCase(unittest.TestCase): def test_model(self): data = { - u'provider_url': u'http://www.google.com/', - u'safe': True, - u'description': u'Google', - u'original_url': u'http://google.com/', - u'url': u'http://www.google.com/', - u'type': u'html', - u'object': {}, - u'provider_display': u'www.google.com', - u'author_name': None, - u'favicon_url': u'http://www.google.com/favicon.ico', - u'place': {}, - u'author_url': None, - u'images': [ - {u'url': u'http://www.google.com/intl/en_ALL/images/srpr/logo1w.png', - u'width': 275, - u'height': 95}], - u'title': u'Google', - u'provider_name': u'Google', - u'cache_age': 86400, - u'embeds': [] + 'provider_url': 'http://www.google.com/', + 'safe': True, + 'description': 'Google', + 'url': 'http://www.google.com/', + 'type': 'html', + 'object': {}, + 'provider_display': 'www.google.com', + 'author_name': None, + 'favicon_url': 'http://www.google.com/favicon.ico', + 'place': {}, + 'author_url': None, + 'images': [ + {'url': 'http://www.google.com/intl/en_ALL/images/srpr/logo1w.png', + 'width': 275, + 'height': 95}], + 'title': 'Google', + 'provider_name': 'Google', + 'cache_age': 86400, + 'embeds': [] } + obj = Url(data, 'preview', 'http://original.url.com/') + + self.assertEqual(len(obj), 16) + self.assertEqual(len(obj.values()), 16) + self.assertEqual(len(obj.keys()), 16) + self.assertEqual(len(obj.items()), 16) + + # check for expected data + self.assertTrue('type' in obj.keys()) + self.assertTrue('html' in obj.values()) + self.assertEqual(obj['type'], 'html') + self.assertEqual(obj.get('type'), 'html') + self.assertEqual(obj.data['type'], 'html') + self.assertEqual(obj.data.get('type'), 'html') + + # our special attrs shouldn't be in the data dict + self.assertFalse('method' in obj.keys()) + with self.assertRaises(KeyError): + obj['method'] + + # attrs and data dict values should be separate + self.assertEqual(obj.original_url, 'http://original.url.com/') + + obj.new_attr = 'attr value' + obj['new_key'] = 'dict value' + self.assertEqual(obj.new_attr, 'attr value') + self.assertEqual(obj['new_key'], 'dict value') + + def test_model_data_can_serialize(self): + obj = Url({'hash': {'key': 'value'}, + 'none': None, + 'empty': '', + 'float': 1.234, + 'int': 1, + 'string': 'string', + 'array': [0, -1]}) + unserialzed = json.loads(json.dumps(obj.data)) + self.assertDictEqual(obj.data, unserialzed) + + def test_str_representation(self): + unistr = 'I\xf1t\xebrn\xe2ti\xf4n\xe0liz\xe6tion' + url = "http://test.com" + obj = Url(method=unistr, original_url=url) + + if sys.version_info[0] == 2: + self.assertTrue(unistr.encode('utf-8') in str(obj)) + self.assertTrue(url.encode('utf-8') in str(obj)) + else: + self.assertTrue(unistr in str(obj)) + self.assertTrue(url in str(obj)) - obj = Url(data, 'preview', 'http://google.com/') - - self.assert_(len(obj) is 17) - self.assert_(len(obj.values()) is 17) - self.assert_(len(obj.keys()) is 17) - self.assert_(len(obj.items()) is 17) - - self.assert_('type' in obj.keys()) - self.assert_('html' in obj.values()) - - #Get the object - self.assert_(obj.type == 'html') - self.assert_(obj['type'] == 'html') - self.assert_(obj.get('type') == 'html') - - #nope - self.assert_(obj.nothing is None) - obj.nothing = 'something' - self.assert_(obj.nothing == 'something') +class EmbedlyTestCase(unittest.TestCase): + def setUp(self): + self.key = 'internal' - obj['nothing'] = 'maybe' - self.assert_(obj['nothing'] == 'maybe') + def test_requires_api_key(self): + with self.assertRaises(ValueError): + Embedly()._get(1, "test", "http://fake") - del obj['nothing'] - self.assert_(obj.nothing is None) + def test_requires_url(self): + with self.assertRaises(ValueError): + Embedly(self.key)._get(1, "test", None) - #Deep Get attrs - self.assert_(obj.images[0].width is 275) - self.assert_(obj.images[0].nothing is None) - self.assert_(obj.object.type is None) + def test_exception_on_too_many_urls(self): + urls = ['http://embed.ly'] * 21 + with self.assertRaises(ValueError): + Embedly(self.key)._get(1, "test", urls) def test_provider(self): http = Embedly(self.key) obj = http.oembed('http://www.scribd.com/doc/13994900/Easter') - self.assert_(obj.provider_url == 'http://www.scribd.com/') + + self.assertEqual(obj['provider_url'], 'http://www.scribd.com/') obj = http.oembed('http://www.scribd.com/doc/28452730/Easter-Cards') - self.assert_(obj.provider_url == 'http://www.scribd.com/') + self.assertEqual(obj['provider_url'], 'http://www.scribd.com/') obj = http.oembed('http://www.youtube.com/watch?v=Zk7dDekYej0') - self.assert_(obj.provider_url == 'http://www.youtube.com/') + self.assertEqual(obj['provider_url'], 'http://www.youtube.com/') obj = http.oembed('http://yfrog.com/h22eu4j') - self.assert_(obj.provider_url == 'http://yfrog.com') + self.assertEqual(obj['provider_url'], 'http://yfrog.com') def test_providers(self): http = Embedly(self.key) - objs = http.oembed(['http://www.scribd.com/doc/13994900/Easter', - 'http://www.scribd.com/doc/28452730/Easter-Cards']) - self.assert_(objs[0].provider_url == 'http://www.scribd.com/') - self.assert_(objs[1].provider_url == 'http://www.scribd.com/') + objs = list(http.oembed(['http://www.scribd.com/doc/13994900/Easter', + 'http://www.scribd.com/doc/28452730/Easter-Cards'])) - objs = http.oembed(['http://www.youtube.com/watch?v=Zk7dDekYej0', - 'http://yfrog.com/h22eu4']) - self.assert_(objs[0].provider_url == 'http://www.youtube.com/') - self.assert_(objs[1].provider_url == 'http://yfrog.com') + self.assertEqual(objs[0]['provider_url'], 'http://www.scribd.com/') + self.assertEqual(objs[1]['provider_url'], 'http://www.scribd.com/') + + objs = list(http.oembed(['http://www.youtube.com/watch?v=Zk7dDekYej0', + 'http://yfrog.com/h22eu4'])) + self.assertEqual(objs[0]['provider_url'], 'http://www.youtube.com/') + self.assertEqual(objs[1]['provider_url'], 'http://yfrog.com') def test_error(self): http = Embedly(self.key) obj = http.oembed('http://www.embedly.com/this/is/a/bad/url') - self.assert_(obj.error is True, obj.dict) + self.assertTrue(obj['error']) obj = http.oembed('http://blog.embed.ly/lsbsdlfldsf/asdfkljlas/klajsdlfkasdf') - self.assert_(obj.error is True, obj.dict) + self.assertTrue(obj['error']) obj = http.oembed('http://twitpic/nothing/to/see/here') - self.assert_(obj.error is True, obj.dict) + self.assertTrue(obj['error']) def test_multi_errors(self): http = Embedly(self.key) - objs = http.oembed(['http://www.embedly.com/this/is/a/bad/url', - 'http://blog.embed.ly/alsd/slsdlf/asdlfj']) - self.assert_(objs[0].type == 'error', objs[0].dict) - self.assert_(objs[1].type == 'error', objs[1].dict) + objs = list(http.oembed(['http://www.embedly.com/this/is/a/bad/url', + 'http://blog.embed.ly/alsd/slsdlf/asdlfj'])) - objs = http.oembed(['http://blog.embed.ly/lsbsdlfldsf/asdf/kl', - 'http://twitpic.com/nothing/to/see/here']) - self.assert_(objs[0].type == 'error',objs[0].dict) - self.assert_(objs[1].type == 'error',objs[1].dict) + self.assertEqual(objs[0]['type'], 'error') + self.assertEqual(objs[1]['type'], 'error') - objs = http.oembed(['http://blog.embed.ly/lsbsdlfldsf/asdf/kl', - 'http://yfrog.com/h22eu4j']) - self.assert_(objs[0].type == 'error',objs[0].dict) - self.assert_(objs[1].type == 'photo',objs[1].dict) + objs = list(http.oembed(['http://blog.embed.ly/lsbsdlfldsf/asdf/kl', + 'http://twitpic.com/nothing/to/see/here'])) + self.assertEqual(objs[0]['type'], 'error') + self.assertEqual(objs[1]['type'], 'error') - objs = http.oembed(['http://yfrog.com/h22eu4j', - 'http://www.scribd.com/asdf/asdf/asdfasdf']) - self.assert_(objs[0].type == 'photo',objs[0].dict) - self.assert_(objs[1].type == 'error',objs[1].dict) + objs = list(http.oembed(['http://blog.embed.ly/lsbsdlfldsf/asdf/kl', + 'http://yfrog.com/h22eu4j'])) + self.assertEqual(objs[0]['type'], 'error') + self.assertEqual(objs[1]['type'], 'photo') + objs = list(http.oembed(['http://yfrog.com/h22eu4j', + 'http://www.scribd.com/asdf/asdf/asdfasdf'])) + self.assertEqual(objs[0]['type'], 'photo') + self.assertEqual(objs[1]['type'], 'error') - def test_too_many_urls(self): - http = Embedly(self.key) + def test_raw_content_in_request(self): + client = Embedly(self.key) + response = client.oembed( + 'http://www.scribd.com/doc/13994900/Easter', + raw=True) - urls = ['http://embed.ly'] * 21 - try: - http.oembed(urls) - self.fail('too many urls, should have thrown an error') - except Exception, e: - self.assertTrue(type(e), ValueError) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file + self.assertEqual(response['raw'], response.data['raw']) + + parsed = json.loads(response['raw'].decode('utf-8')) + self.assertEqual(response['type'], parsed['type']) + + def test_regex_url_matches(self): + regex = [ + 'http://.*youtube\\.com/watch.*', + 'http://www\\.vimeo\\.com/.*'] + client = Embedly(self.key) + client._regex = re.compile('|'.join(regex)) + + self.assertTrue( + client.is_supported('http://www.youtube.com/watch?v=Zk7dDekYej0')) + self.assertTrue( + client.is_supported('http://www.vimeo.com/18150336')) + self.assertFalse( + client.is_supported('http://vimeo.com/18150336')) + self.assertFalse( + client.is_supported('http://yfrog.com/h22eu4j')) + + @mock.patch.object(Embedly, 'get_services') + def test_regex_access_triggers_get_services(self, mock_services): + client = Embedly(self.key) + client.regex + + self.assertTrue(mock_services.called) + self.assertIsNone(client._regex) + + def test_services_can_be_manually_configured(self): + client = Embedly(self.key) + client.services = ['nothing', 'like', 'real', 'response', 'data'] + + self.assertTrue('nothing' in client.get_services()) + self.assertEqual(len(client.get_services()), 5) + + @mock.patch('httplib2.Http', autospec=True) + def test_services_remains_empty_on_failed_http(self, MockHttp): + MockHttp.return_value.request.return_value = ({'status': 500}, "") + + client = Embedly(self.key) + client.get_services() + + self.assertFalse(client.services) + self.assertTrue(MockHttp.return_value.request.called) + + def test_get_services_retrieves_data_and_builds_regex(self): + client = Embedly(self.key) + client.get_services() + + self.assertGreater(len(client.services), 0) + self.assertTrue(client.regex.match('http://yfrog.com/h22eu4j')) + + def test_extract(self): + client = Embedly(self.key) + response = client.extract('http://vimeo.com/18150336') + + self.assertEqual(response.method, 'extract') + self.assertEqual(response['provider_name'], 'Vimeo') + + def test_preview(self): + client = Embedly(self.key) + response = client.preview('http://vimeo.com/18150336') + + self.assertEqual(response.method, 'preview') + self.assertEqual(response['provider_name'], 'Vimeo') + + def test_objectify(self): + client = Embedly(self.key) + response = client.objectify('http://vimeo.com/18150336') + + self.assertEqual(response.method, 'objectify') + self.assertEqual(response['provider_name'], 'Vimeo') + + +if __name__ == '__main__': # pragma: no cover + unittest.main() # pragma: no cover diff --git a/setup.py b/setup.py index 8b7912f..e0fef44 100644 --- a/setup.py +++ b/setup.py @@ -3,12 +3,21 @@ import codecs from setuptools import setup -extra = {} - required = ['httplib2'] +tests_require = [] + +if sys.version_info[:2] < (2, 7): + tests_require.append('unittest2') + +if sys.version_info[:2] < (3, 3): + tests_require.append('mock') + -if sys.version_info[:2] < (2,6): - required.append('simplejson') +def get_version(): + with open(os.path.join('embedly', '__init__.py')) as f: + for line in f: + if line.startswith('__version__ ='): + return line.split('=')[1].strip().strip('"\'') if os.path.exists("README.rst"): long_description = codecs.open("README.rst", "r", "utf-8").read() @@ -17,30 +26,33 @@ setup( - name = 'Embedly', - version = '0.4.3', - author = 'Embed.ly, Inc.', - author_email = 'support@embed.ly', - description = 'Python Library for Embedly', + name='Embedly', + version=get_version(), + author='Embed.ly, Inc.', + author_email='support@embed.ly', + description='Python Library for Embedly', long_description=long_description, - license = """ + license=""" Copyright (c) 2011, Embed.ly, Inc. All rights reserved. Released under the 3-clause BSD license. """, - url = "https://github.com/embedly/embedly-python", - packages = ['embedly'], - install_requires = required, - zip_safe = True, - classifiers=( + url="https://github.com/embedly/embedly-python", + packages=['embedly'], + install_requires=required, + tests_require=tests_require, + test_suite="embedly.tests", + zip_safe=True, + use_2to3=True, + classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Natural Language :: English', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', - ), - **extra -) \ No newline at end of file + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + ] +) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..e039cb9 --- /dev/null +++ b/tox.ini @@ -0,0 +1,16 @@ +[tox] +envlist = py26,py27,py32,py33 + +[testenv] +commands=python embedly/tests.py + +[testenv:py26] +deps = + mock + unittest2 + +[testenv:py27] +deps = mock + +[testenv:py32] +deps = mock