Commit 48a80aa1 authored by richardARPANET's avatar richardARPANET

Merge branch 'facade' into 'master'

Facade

See merge request !7
parents 15ae885d cce78f70
Pipeline #109 failed with stage
in 18 minutes and 56 seconds
test:
script:
before_script:
# Install pyenv
- apt-get update
- apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev
- git clone https://github.com/pyenv/pyenv.git ~/.pyenv
- export PYENV_ROOT="$HOME/.pyenv"
- export PATH="$PYENV_ROOT/bin:$PATH"
- eval "$(pyenv init -)"
# Install tox
- pip install tox
- tox
test:python26:
script:
- pyenv install 2.6.9
- pyenv shell 2.6.9
- tox -e py26-normal
test:python27:
script:
- pyenv install 2.7.14
- pyenv shell 2.7.14
- tox -e py27-normal
test:python34:
script:
- pyenv install 3.4.7
- pyenv shell 3.4.7
- tox -e py34-normal
test:python35:
script:
- pyenv install 3.5.4
- pyenv shell 3.5.4
- tox -e py35-normal
test:python36:
script:
- pyenv install 3.6.4
- pyenv shell 3.6.4
- tox -e py36-normal
This diff is collapsed.
ImdbPie Facade Usage Examples
=============================
Init the Facade
---------------
.. code:: python
from imdbpie import ImdbFacade
imdb = ImdbFacade()
# Or, Specify a custom imdb-pie client instance for the facade to use
from imdbpie import Imdb
client = Imdb(locale='en_US')
imdb = ImdbFacade(client=client)
Exceptions
----------
For each method, if the resource cannot be found they will raise ``LookupError``,
for any other API status codes > 399 the client will raise ``ImdbAPIError``.
Get a title
-----------
.. code:: python
imdb.get_title(imdb_id='tt1023114')
Returns a ``Title`` object with the following attributes:
- certification
- creators
- credits
- directors
- episode
- episodes
- genres
- image
- imdb_id
- plot_outline
- rating
- rating_count
- release_date
- releases
- season
- stars
- title
- type
- writers
- year
Get a Name
----------
.. code:: python
imdb.get_name(imdb_id='nm0000151')
Returns a ``Name`` object with the following attributes:
- bios
- birth_place
- date_of_birth
- filmography
- gender
- image
- imdb_id
- name
Search for a name
-----------------
.. code:: python
imdb.search_for_name('Tom Hanks')
Returns a ``tuple`` containing ``NameSearchResult`` objects with the
following attributes:
- imdb_id
- name
Search for a title
------------------
.. code:: python
imdb.search_for_title('The Dark Knight')
Returns a ``tuple`` containing ``TitleSearchResult`` objects with the
following attributes:
- imdb_id
- title
- type
- year
......@@ -3,10 +3,11 @@
Release History
---------------
5.4.6 (unreleased)
5.5.0 (unreleased)
++++++++++++++++++
- Nothing changed yet.
- Adds ``get_title_auxiliary`` method to client.
- Adds `ImdbFacade` facade for API.
5.4.5 (2018-04-29)
......
This diff is collapsed.
......@@ -5,3 +5,4 @@ boto==2.48.0
python-dateutil==2.6.1
diskcache==2.9.0
setuptools>=39.0.1
attrs==18.1.0
......@@ -7,3 +7,7 @@ create-wheel = yes
[tool:pytest]
addopts = -x -s -v
norecursedirs = .git
[flake8]
exclude = .git,__pycache__,legacy,build,dist,.tox
max-complexity = 13
# -*- coding: utf-8 -*-
from .imdbpie import Imdb # noqa
from .exceptions import ImdbAPIError # noqa
from .facade import ImdbFacade # noqa
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import re
from dateutil.parser import parse
from .imdbpie import Imdb
from .objects import (
Title, TitleEpisodes, Name, TitleName, Image, TitleRelease,
TitleSearchResult, NameSearchResult,
)
REGEX_IMDB_ID = re.compile(r'([a-zA-Z]{2}[0-9]{7})')
class ImdbFacade(object):
def __init__(self, client=None):
self._client = client or Imdb()
def get_title(self, imdb_id):
title_data, title_aux_data = self._get_title_data(imdb_id=imdb_id)
try:
episodes = TitleEpisodes(facade=self, imdb_id=imdb_id)
except LookupError:
episodes = ()
try:
season = title_aux_data['season']
episode = title_aux_data['episode']
except KeyError:
season = None
episode = None
return Title(
season=season, episode=episode, episodes=episodes, **title_data
)
def get_name(self, imdb_id):
name_data = self._client.get_name(imdb_id=imdb_id)
name = name_data['base']['name']
imdb_id = self._parse_id(name_data['base']['id'])
try:
image_data = name_data['base']['image']
image = Image(
url=image_data['url'],
height=image_data['height'],
width=image_data['width'],
)
except KeyError:
image = None
gender = name_data['base']['gender'].lower()
date_of_birth = parse(name_data['base']['birthDate']).date()
birth_place = name_data['base']['birthPlace']
try:
bios = tuple(b['text'] for b in name_data['base']['miniBios'])
except KeyError:
bios = ()
filmography_data = self._client.get_name_filmography(imdb_id)
filmography = tuple(
self._parse_id(f['id']) for f in filmography_data['filmography']
)
return Name(
name=name, imdb_id=imdb_id, date_of_birth=date_of_birth,
gender=gender, birth_place=birth_place, bios=bios, image=image,
filmography=filmography,
)
def search_for_name(self, query):
results = []
for result in self._client.search_for_name(query):
result = NameSearchResult(
imdb_id=result['imdb_id'], name=result['name'],
)
results.append(result)
return tuple(results)
def search_for_title(self, query):
results = []
for result in self._client.search_for_title(query):
if result['year']:
year = int(result['year'])
else:
year = None
result = TitleSearchResult(
imdb_id=result['imdb_id'], title=result['title'],
type=result['type'], year=year,
)
results.append(result)
return tuple(results)
def _get_writers(self, top_crew_data):
return tuple(
TitleName(
name=i['name'],
job=i.get('job'),
category=i.get('category'),
imdb_id=self._parse_id(i['id'])
) for i in top_crew_data['writers']
)
def _get_stars(self, principals_data):
return tuple(
TitleName(
name=i['name'],
job=i.get('job'),
characters=tuple(i.get('characters', ())),
category=i.get('category'),
imdb_id=self._parse_id(i['id'])
) for i in principals_data
)
def _get_creators(self, top_crew_data):
return tuple(
TitleName(
name=i['name'],
job=i.get('job'),
category=i.get('category'),
imdb_id=self._parse_id(i['id'])
) for i in top_crew_data['writers']
if i.get('job') == 'creator'
)
def _get_directors(self, top_crew_data):
return tuple(
TitleName(
name=i['name'],
job=i.get('job'),
category=i.get('category'),
imdb_id=self._parse_id(i['id'])
) for i in top_crew_data['directors']
)
def _get_credits(self, credits_data):
credits = []
for category in credits_data['credits']:
for item in credits_data['credits'][category]:
credits.append(TitleName(
name=item['name'],
category=item.get('category'),
job=item.get('job'),
imdb_id=self._parse_id(item['id'])
))
return tuple(credits)
def _parse_id(self, string):
return REGEX_IMDB_ID.findall(string)[0]
def _get_title_data(self, imdb_id):
base_title_data = self._client.get_title(imdb_id=imdb_id)
top_crew_data = self._client.get_title_top_crew(imdb_id=imdb_id)
title_aux_data = self._client.get_title_auxiliary(imdb_id=imdb_id)
credits_data = self._client.get_title_credits(imdb_id=imdb_id)
title = base_title_data['base']['title']
year = base_title_data['base'].get('year')
try:
rating = float(base_title_data['ratings']['rating'])
except KeyError:
rating = None
type_ = base_title_data['base']['titleType'].lower()
try:
releases_data = self._client.get_title_releases(imdb_id=imdb_id)
except LookupError:
release_date = None
releases = ()
else:
release_date = parse(releases_data['releases'][0]['date']).date()
releases = tuple(
TitleRelease(date=parse(r['date']).date(), region=r['region'])
for r in releases_data['releases']
)
try:
rating_count = base_title_data['ratings']['ratingCount']
except KeyError:
rating_count = 0
try:
plot_outline = base_title_data['plot']['outline']['text']
except KeyError:
plot_outline = None
writers = self._get_writers(top_crew_data)
directors = self._get_directors(top_crew_data)
creators = self._get_creators(top_crew_data)
genres = tuple(g.lower() for g in title_aux_data['genres'])
credits = self._get_credits(credits_data)
try:
certification = title_aux_data['certificate']['certificate']
except TypeError:
certification = None
stars = self._get_stars(title_aux_data['principals'])
try:
image_data = title_aux_data['image']
image = Image(
url=image_data['url'],
height=image_data['height'],
width=image_data['width'],
)
except KeyError:
image = None
return dict(
imdb_id=imdb_id,
title=title,
year=year,
rating=rating,
type=type_,
release_date=release_date,
releases=releases,
plot_outline=plot_outline,
rating_count=rating_count,
writers=writers,
directors=directors,
creators=creators,
genres=genres,
credits=credits,
certification=certification,
image=image,
stars=stars,
), title_aux_data
......@@ -3,15 +3,14 @@ from __future__ import absolute_import, unicode_literals
import re
import json
from datetime import date
import tempfile
import logging
import requests
from six import text_type
from six.moves import http_client as httplib
from six.moves.urllib.parse import (
urlencode, urljoin, quote, unquote, urlparse
)
from six.moves.urllib.parse import urlencode, urljoin, quote, unquote
from .constants import BASE_URI, SEARCH_BASE_URI
from .auth import Auth
......@@ -55,6 +54,7 @@ class Imdb(Auth):
def __init__(self, locale=None, exclude_episodes=False, session=None):
self.locale = locale or 'en_US'
self.region = self.locale.split('_')[-1].upper()
self.exclude_episodes = exclude_episodes
self.session = session or requests.Session()
self._cachedir = tempfile.gettempdir()
......@@ -92,6 +92,36 @@ class Imdb(Auth):
)
return resource
def get_title_auxiliary(self, imdb_id):
logger.info('called get_title_auxiliary %s', imdb_id)
self.validate_imdb_id(imdb_id)
self._redirection_title_check(imdb_id)
path = '/template/imdb-ios-writable/title-auxiliary-v31.jstl/render'
try:
resource = self._get(
url=urljoin(BASE_URI, path),
params={
'inlineBannerAdWeblabOn': 'false',
'minwidth': '320',
'osVersion': '11.3.0',
'region': self.region,
'tconst': imdb_id,
'today': date.today().strftime('%Y-%m-%d'),
}
)
except LookupError:
self._title_not_found()
if (
self.exclude_episodes is True and
resource['titleType'].lower() == 'tvepisode'
):
raise LookupError(
'Title not found. Title was an episode and '
'"exclude_episodes" is set to true'
)
return resource
def _simple_get_method(self, method, path):
"""Return client method generated from ``_SIMPLE_GET_ENDPOINTS``."""
def get(imdb_id):
......@@ -117,16 +147,20 @@ class Imdb(Auth):
else:
response.raise_for_status()
def _suggest_search(self, query):
query_encoded = quote(query)
first_alphanum_char = self._query_first_alpha_num(query)
path = '/suggests/{0}/{1}.json'.format(
first_alphanum_char, query_encoded
)
url = urljoin(SEARCH_BASE_URI, path)
search_results = self._get(url=url, query=query_encoded)
return search_results
def search_for_name(self, name):
logger.info('called search_for_name %s', name)
name = re.sub(r'\W+', '_', name).strip('_')
query = quote(name)
first_alphanum_char = self._query_first_alpha_num(name)
url = (
'{0}/suggests/{1}/{2}.json'.format(SEARCH_BASE_URI,
first_alphanum_char, query)
)
search_results = self._get(url=url, query=query)
search_results = self._suggest_search(name)
results = []
for result in search_results.get('d', ()):
if not result['id'].startswith('nm'):
......@@ -142,15 +176,12 @@ class Imdb(Auth):
def search_for_title(self, title):
logger.info('called search_for_title %s', title)
title = re.sub(r'\W+', '_', title).strip('_')
query = quote(title)
first_alphanum_char = self._query_first_alpha_num(title)
url = (
'{0}/suggests/{1}/{2}.json'.format(SEARCH_BASE_URI,
first_alphanum_char, query)
)
search_results = self._get(url=url, query=query)
search_results = self._suggest_search(title)
results = []
for result in search_results.get('d', ()):
if not result['id'].startswith('tt'):
# ignore non-title results
continue
result_item = {
'title': result['l'],
'year': text_type(result['y']) if result.get('y') else None,
......@@ -261,20 +292,21 @@ class Imdb(Auth):
return False
def _get_resource(self, path):
url = '{0}{1}'.format(BASE_URI, path)
url = urljoin(BASE_URI, path)
return self._get(url=url)['resource']
def _get(self, url, query=None, params=None):
path = urlparse(url).path
if params:
path += '?' + urlencode(params)
headers = {'Accept-Language': self.locale}
headers.update(self.get_auth_headers(path))
if params:
full_url = '{0}?{1}'.format(url, urlencode(params))
else:
full_url = url
headers.update(self.get_auth_headers(full_url))
resp = self.session.get(url, headers=headers, params=params)
if not resp.ok:
if resp.status_code == httplib.NOT_FOUND:
raise LookupError('Resource {0} not found'.format(path))
raise LookupError('Resource {0} not found'.format(url))
else:
msg = '{0} {1}'.format(resp.status_code, resp.text)
raise ImdbAPIError(msg)
......
import attr
@attr.s
class Image(object):
url = attr.ib()
width = attr.ib()
height = attr.ib()
class TitleEpisodes(object):
def __init__(self, facade, imdb_id):
self._facade = facade
episodes = self._facade._client.get_title_episodes(
imdb_id=imdb_id
)
self._episode_imdb_ids = []
for season in episodes['seasons']:
for episode in season['episodes']:
imdb_id = self._facade._parse_id(episode['id'])
self._episode_imdb_ids.append(imdb_id)
self._count = len(self._episode_imdb_ids)
def __len__(self):
return self._count
def __bool__(self):
return self._count > 0
def __getitem__(self, index):
imdb_id = self._episode_imdb_ids[index]
return self._facade.get_title(imdb_id=imdb_id)
@attr.s
class Title(object):
imdb_id = attr.ib()
title = attr.ib()
type = attr.ib()
certification = attr.ib()
year = attr.ib()
genres = attr.ib()
writers = attr.ib()
creators = attr.ib()
credits = attr.ib()
directors = attr.ib()
stars = attr.ib()
image = attr.ib()
episodes = attr.ib()
rating_count = attr.ib(default=0)
releases = attr.ib(default=())
season = attr.ib(default=None)
episode = attr.ib(default=None)
rating = attr.ib(default=None)
plot_outline = attr.ib(default=None)
release_date = attr.ib(default=None)
def __repr__(self):
return 'Title(imdb_id={0}, title={1})'.format(self.imdb_id, self.title)
@attr.s
class TitleSearchResult(object):
imdb_id = attr.ib()
title = attr.ib()
type = attr.ib()
year = attr.ib()
@attr.s
class NameSearchResult(object):
imdb_id = attr.ib()
name = attr.ib()
@attr.s
class TitleRelease(object):
date = attr.ib()
region = attr.ib()
@attr.s
class TitleName(object):
name = attr.ib()
category = attr.ib()
imdb_id = attr.ib()
job = attr.ib(default=None)
characters = attr.ib(default=())
@attr.s
class Name(object):
name = attr.ib()
imdb_id = attr.ib()
image = attr.ib()
birth_place = attr.ib()
gender = attr.ib()
bios = attr.ib()
date_of_birth = attr.ib()
filmography = attr.ib()
from datetime import date
import pytest
import six
from imdbpie import ImdbFacade
from imdbpie.objects import (
Title, Name, TitleName, TitleSearchResult, NameSearchResult
)
@pytest.fixture(scope='module')
def facade():
return ImdbFacade()
class TestGetTitle(object):
def test_tv_show(self, facade):
tv_show_imdb_id = 'tt0096697'
title = facade.get_title(imdb_id=tv_show_imdb_id)
assert isinstance(title, Title)
_check_title(title=title, facade=facade)
assert title.type == 'tvseries'
num_checked = 0
for episode in title.episodes:
assert isinstance(episode, Title)
assert episode.imdb_id
assert isinstance(episode.season, int)
assert isinstance(episode.episode, int)
_check_title(title=episode, facade=facade)
num_checked += 1
if num_checked > 5:
break
# Sequence operations
assert title.episodes[0].season == 1
assert title.episodes[0].episode == 1
assert title.episodes
assert len(title.episodes)
assert title.episodes[-1].imdb_id
assert title.episodes[10].imdb_id
def test_movie(self, facade):
tv_show_imdb_id = 'tt0468569'
title = facade.get_title(imdb_id=tv_show_imdb_id)
assert isinstance(title, Title)
_check_title(title=title, facade=facade)
assert title.type == 'movie'
assert len(title.episodes) == 0
@pytest.mark.parametrize('imdb_id', [
'tt0795176',
'tt7983794',
])
def test_get_title_documentary(self, facade, imdb_id):
title = facade.get_title(imdb_id=imdb_id)
assert isinstance(title, Title)
_check_title(title=title, facade=facade)
assert title.type in ('tvminiseries', 'movie')
num_checked = 0
for episode in title.episodes:
assert episode
assert episode.imdb_id
assert isinstance(episode.season, int)
assert isinstance(episode.episode, int)
_check_title(title=episode, facade=facade)
num_checked += 1
if num_checked > 5:
break
def test_tv_episode(self, facade):
episode_imdb_id = 'tt4847050'
title = facade.get_title(imdb_id=episode_imdb_id)
assert isinstance(title, Title)
assert title.imdb_id == episode_imdb_id
assert len(title.episodes) == 0
assert isinstance(title.season, int)
assert isinstance(title.episode, int)
@pytest.mark.parametrize('imdb_id', [
'nm0000151',
'nm0588033',
'nm0047800',
'nm1799952',
])