Browse Source

Update TMDB api

pull/2875/head
Ruud 11 years ago
parent
commit
a1c0b000a4
  1. 3
      libs/tmdb3/__init__.py
  2. 43
      libs/tmdb3/cache.py
  3. 30
      libs/tmdb3/cache_engine.py
  4. 55
      libs/tmdb3/cache_file.py
  5. 18
      libs/tmdb3/cache_null.py
  6. 24
      libs/tmdb3/locales.py
  7. 31
      libs/tmdb3/pager.py
  8. 66
      libs/tmdb3/request.py
  9. 657
      libs/tmdb3/tmdb_api.py
  10. 35
      libs/tmdb3/tmdb_auth.py
  11. 90
      libs/tmdb3/tmdb_exceptions.py
  12. 201
      libs/tmdb3/util.py

3
libs/tmdb3/__init__.py

@ -2,7 +2,8 @@
from tmdb_api import Configuration, searchMovie, searchMovieWithYear, \ from tmdb_api import Configuration, searchMovie, searchMovieWithYear, \
searchPerson, searchStudio, searchList, searchCollection, \ searchPerson, searchStudio, searchList, searchCollection, \
Person, Movie, Collection, Genre, List, __version__ searchSeries, Person, Movie, Collection, Genre, List, \
Series, Studio, Network, Episode, Season, __version__
from request import set_key, set_cache from request import set_key, set_cache
from locales import get_locale, set_locale from locales import get_locale, set_locale
from tmdb_auth import get_session, set_session from tmdb_auth import get_session, set_session

43
libs/tmdb3/cache.py

@ -7,20 +7,27 @@
# Purpose: Caching framework to store TMDb API results # Purpose: Caching framework to store TMDb API results
#----------------------- #-----------------------
import time
import os
from tmdb_exceptions import * from tmdb_exceptions import *
from cache_engine import Engines from cache_engine import Engines
import cache_null import cache_null
import cache_file import cache_file
class Cache( object ):
class Cache(object):
""" """
This class implements a persistent cache, backed in a file specified in This class implements a cache framework, allowing selecting of a
the object creation. The file is protected for safe, concurrent access pluggable engine. The framework stores data in a key/value manner,
by multiple instances using flock. along with a lifetime, after which data will be expired and
This cache uses JSON for speed and storage efficiency, so only simple pulled fresh next time it is requested from the cache.
data types are supported.
Data is stored in a simple format {key:(expiretimestamp, data)} This class defines a wrapper to be used with query functions. The
wrapper will automatically cache the inputs and outputs of the
wrapped function, pulling the output from local storage for
subsequent calls with those inputs.
""" """
def __init__(self, engine=None, *args, **kwargs): def __init__(self, engine=None, *args, **kwargs):
self._engine = None self._engine = None
@ -37,7 +44,7 @@ class Cache( object ):
self._age = max(self._age, obj.creation) self._age = max(self._age, obj.creation)
def _expire(self): def _expire(self):
for k,v in self._data.items(): for k, v in self._data.items():
if v.expired: if v.expired:
del self._data[k] del self._data[k]
@ -87,19 +94,22 @@ class Cache( object ):
self.__doc__ = func.__doc__ self.__doc__ = func.__doc__
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
if self.func is None: # decorator is waiting to be given a function if self.func is None:
# decorator is waiting to be given a function
if len(kwargs) or (len(args) != 1): if len(kwargs) or (len(args) != 1):
raise TMDBCacheError('Cache.Cached decorator must be called '+\ raise TMDBCacheError(
'a single callable argument before it '+\ 'Cache.Cached decorator must be called a single ' +
'be used.') 'callable argument before it be used.')
elif args[0] is None: elif args[0] is None:
raise TMDBCacheError('Cache.Cached decorator called before '+\ raise TMDBCacheError(
'being given a function to wrap.') 'Cache.Cached decorator called before being given ' +
'a function to wrap.')
elif not callable(args[0]): elif not callable(args[0]):
raise TMDBCacheError('Cache.Cached must be provided a '+\ raise TMDBCacheError(
'callable object.') 'Cache.Cached must be provided a callable object.')
return self.__class__(self.cache, self.callback, args[0]) return self.__class__(self.cache, self.callback, args[0])
elif self.inst.lifetime == 0: elif self.inst.lifetime == 0:
# lifetime of zero means never cache
return self.func(*args, **kwargs) return self.func(*args, **kwargs)
else: else:
key = self.callback() key = self.callback()
@ -118,4 +128,3 @@ class Cache( object ):
func = self.func.__get__(inst, owner) func = self.func.__get__(inst, owner)
callback = self.callback.__get__(inst, owner) callback = self.callback.__get__(inst, owner)
return self.__class__(self.cache, callback, func, inst) return self.__class__(self.cache, callback, func, inst)

30
libs/tmdb3/cache_engine.py

@ -10,35 +10,46 @@
import time import time
from weakref import ref from weakref import ref
class Engines( object ):
class Engines(object):
"""
Static collector for engines to register against.
"""
def __init__(self): def __init__(self):
self._engines = {} self._engines = {}
def register(self, engine): def register(self, engine):
self._engines[engine.__name__] = engine self._engines[engine.__name__] = engine
self._engines[engine.name] = engine self._engines[engine.name] = engine
def __getitem__(self, key): def __getitem__(self, key):
return self._engines[key] return self._engines[key]
def __contains__(self, key): def __contains__(self, key):
return self._engines.__contains__(key) return self._engines.__contains__(key)
Engines = Engines() Engines = Engines()
class CacheEngineType( type ):
class CacheEngineType(type):
""" """
Cache Engine Metaclass that registers new engines against the cache Cache Engine Metaclass that registers new engines against the cache
for named selection and use. for named selection and use.
""" """
def __init__(mcs, name, bases, attrs): def __init__(cls, name, bases, attrs):
super(CacheEngineType, mcs).__init__(name, bases, attrs) super(CacheEngineType, cls).__init__(name, bases, attrs)
if name != 'CacheEngine': if name != 'CacheEngine':
# skip base class # skip base class
Engines.register(mcs) Engines.register(cls)
class CacheEngine( object ):
__metaclass__ = CacheEngineType
class CacheEngine(object):
__metaclass__ = CacheEngineType
name = 'unspecified' name = 'unspecified'
def __init__(self, parent): def __init__(self, parent):
self.parent = ref(parent) self.parent = ref(parent)
def configure(self): def configure(self):
raise RuntimeError raise RuntimeError
def get(self, date): def get(self, date):
@ -48,7 +59,8 @@ class CacheEngine( object ):
def expire(self, key): def expire(self, key):
raise RuntimeError raise RuntimeError
class CacheObject( object ):
class CacheObject(object):
""" """
Cache object class, containing one stored record. Cache object class, containing one stored record.
""" """
@ -64,7 +76,7 @@ class CacheObject( object ):
@property @property
def expired(self): def expired(self):
return (self.remaining == 0) return self.remaining == 0
@property @property
def remaining(self): def remaining(self):

55
libs/tmdb3/cache_file.py

@ -12,6 +12,7 @@
import struct import struct
import errno import errno
import json import json
import time
import os import os
import io import io
@ -54,11 +55,11 @@ def _donothing(*args, **kwargs):
try: try:
import fcntl import fcntl
class Flock( object ): class Flock(object):
""" """
Context manager to flock file for the duration the object exists. Context manager to flock file for the duration the object
Referenced file will be automatically unflocked as the interpreter exists. Referenced file will be automatically unflocked as the
exits the context. interpreter exits the context.
Supports an optional callback to process the error and optionally Supports an optional callback to process the error and optionally
suppress it. suppress it.
""" """
@ -69,8 +70,10 @@ try:
self.fileobj = fileobj self.fileobj = fileobj
self.operation = operation self.operation = operation
self.callback = callback self.callback = callback
def __enter__(self): def __enter__(self):
fcntl.flock(self.fileobj, self.operation) fcntl.flock(self.fileobj, self.operation)
def __exit__(self, exc_type, exc_value, exc_tb): def __exit__(self, exc_type, exc_value, exc_tb):
suppress = False suppress = False
if callable(self.callback): if callable(self.callback):
@ -101,9 +104,11 @@ except ImportError:
self.fileobj = fileobj self.fileobj = fileobj
self.operation = operation self.operation = operation
self.callback = callback self.callback = callback
def __enter__(self): def __enter__(self):
self.size = os.path.getsize(self.fileobj.name) self.size = os.path.getsize(self.fileobj.name)
msvcrt.locking(self.fileobj.fileno(), self.operation, self.size) msvcrt.locking(self.fileobj.fileno(), self.operation, self.size)
def __exit__(self, exc_type, exc_value, exc_tb): def __exit__(self, exc_type, exc_value, exc_tb):
suppress = False suppress = False
if callable(self.callback): if callable(self.callback):
@ -118,7 +123,7 @@ except ImportError:
if filename.startswith('~'): if filename.startswith('~'):
# check for home directory # check for home directory
return os.path.expanduser(filename) return os.path.expanduser(filename)
elif (ord(filename[0]) in (range(65,91)+range(99,123))) \ elif (ord(filename[0]) in (range(65, 91) + range(99, 123))) \
and (filename[1:3] == ':\\'): and (filename[1:3] == ':\\'):
# check for absolute drive path (e.g. C:\...) # check for absolute drive path (e.g. C:\...)
return filename return filename
@ -126,12 +131,12 @@ except ImportError:
# check for absolute UNC path (e.g. \\server\...) # check for absolute UNC path (e.g. \\server\...)
return filename return filename
# return path with temp directory prepended # return path with temp directory prepended
return os.path.expandvars(os.path.join('%TEMP%',filename)) return os.path.expandvars(os.path.join('%TEMP%', filename))
class FileCacheObject( CacheObject ): class FileCacheObject(CacheObject):
_struct = struct.Struct('dII') # double and two ints _struct = struct.Struct('dII') # double and two ints
# timestamp, lifetime, position # timestamp, lifetime, position
@classmethod @classmethod
def fromFile(cls, fd): def fromFile(cls, fd):
@ -150,7 +155,7 @@ class FileCacheObject( CacheObject ):
@property @property
def size(self): def size(self):
if self._size is None: if self._size is None:
self._buff.seek(0,2) self._buff.seek(0, 2)
size = self._buff.tell() size = self._buff.tell()
if size == 0: if size == 0:
if (self._key is None) or (self._data is None): if (self._key is None) or (self._data is None):
@ -159,8 +164,10 @@ class FileCacheObject( CacheObject ):
self._size = self._buff.tell() self._size = self._buff.tell()
self._size = size self._size = size
return self._size return self._size
@size.setter @size.setter
def size(self, value): self._size = value def size(self, value):
self._size = value
@property @property
def key(self): def key(self):
@ -170,16 +177,20 @@ class FileCacheObject( CacheObject ):
except: except:
pass pass
return self._key return self._key
@key.setter @key.setter
def key(self, value): self._key = value def key(self, value):
self._key = value
@property @property
def data(self): def data(self):
if self._data is None: if self._data is None:
self._key, self._data = json.loads(self._buff.getvalue()) self._key, self._data = json.loads(self._buff.getvalue())
return self._data return self._data
@data.setter @data.setter
def data(self, value): self._data = value def data(self, value):
self._data = value
def load(self, fd): def load(self, fd):
fd.seek(self.position) fd.seek(self.position)
@ -199,7 +210,7 @@ class FileCacheObject( CacheObject ):
class FileEngine( CacheEngine ): class FileEngine( CacheEngine ):
"""Simple file-backed engine.""" """Simple file-backed engine."""
name = 'file' name = 'file'
_struct = struct.Struct('HH') # two shorts for version and count _struct = struct.Struct('HH') # two shorts for version and count
_version = 2 _version = 2
def __init__(self, parent): def __init__(self, parent):
@ -219,7 +230,6 @@ class FileEngine( CacheEngine ):
if self.cachefile is None: if self.cachefile is None:
raise TMDBCacheError("No cache filename given.") raise TMDBCacheError("No cache filename given.")
self.cachefile = parse_filename(self.cachefile) self.cachefile = parse_filename(self.cachefile)
try: try:
@ -246,7 +256,7 @@ class FileEngine( CacheEngine ):
else: else:
# let the unhandled error continue through # let the unhandled error continue through
raise raise
elif e.errno == errno.EACCESS: elif e.errno == errno.EACCES:
# file exists, but we do not have permission to access it # file exists, but we do not have permission to access it
raise TMDBCacheReadError(self.cachefile) raise TMDBCacheReadError(self.cachefile)
else: else:
@ -257,7 +267,7 @@ class FileEngine( CacheEngine ):
self._init_cache() self._init_cache()
self._open('r+b') self._open('r+b')
with Flock(self.cachefd, Flock.LOCK_SH): # lock for shared access with Flock(self.cachefd, Flock.LOCK_SH):
# return any new objects in the cache # return any new objects in the cache
return self._read(date) return self._read(date)
@ -265,7 +275,7 @@ class FileEngine( CacheEngine ):
self._init_cache() self._init_cache()
self._open('r+b') self._open('r+b')
with Flock(self.cachefd, Flock.LOCK_EX): # lock for exclusive access with Flock(self.cachefd, Flock.LOCK_EX):
newobjs = self._read(self.age) newobjs = self._read(self.age)
newobjs.append(FileCacheObject(key, value, lifetime)) newobjs.append(FileCacheObject(key, value, lifetime))
@ -283,7 +293,8 @@ class FileEngine( CacheEngine ):
# already opened in requested mode, nothing to do # already opened in requested mode, nothing to do
self.cachefd.seek(0) self.cachefd.seek(0)
return return
except: pass # catch issue of no cachefile yet opened except:
pass # catch issue of no cachefile yet opened
self.cachefd = io.open(self.cachefile, mode) self.cachefd = io.open(self.cachefile, mode)
def _read(self, date): def _read(self, date):
@ -310,7 +321,7 @@ class FileEngine( CacheEngine ):
return [] return []
# get end of file # get end of file
self.cachefd.seek(0,2) self.cachefd.seek(0, 2)
position = self.cachefd.tell() position = self.cachefd.tell()
newobjs = [] newobjs = []
emptycount = 0 emptycount = 0
@ -348,7 +359,7 @@ class FileEngine( CacheEngine ):
data = data[-1] data = data[-1]
# determine write position of data in cache # determine write position of data in cache
self.cachefd.seek(0,2) self.cachefd.seek(0, 2)
end = self.cachefd.tell() end = self.cachefd.tell()
data.position = end data.position = end
@ -387,5 +398,3 @@ class FileEngine( CacheEngine ):
def expire(self, key): def expire(self, key):
pass pass

18
libs/tmdb3/cache_null.py

@ -9,11 +9,19 @@
from cache_engine import CacheEngine from cache_engine import CacheEngine
class NullEngine( CacheEngine ):
class NullEngine(CacheEngine):
"""Non-caching engine for debugging.""" """Non-caching engine for debugging."""
name = 'null' name = 'null'
def configure(self): pass
def get(self, date): return []
def put(self, key, value, lifetime): return []
def expire(self, key): pass
def configure(self):
pass
def get(self, date):
return []
def put(self, key, value, lifetime):
return []
def expire(self, key):
pass

24
libs/tmdb3/locales.py

@ -11,7 +11,8 @@ import locale
syslocale = None syslocale = None
class LocaleBase( object ):
class LocaleBase(object):
__slots__ = ['__immutable'] __slots__ = ['__immutable']
_stored = {} _stored = {}
fallthrough = False fallthrough = False
@ -24,19 +25,21 @@ class LocaleBase( object ):
def __setattr__(self, key, value): def __setattr__(self, key, value):
if getattr(self, '__immutable', False): if getattr(self, '__immutable', False):
raise NotImplementedError(self.__class__.__name__ + raise NotImplementedError(self.__class__.__name__ +
' does not support modification.') ' does not support modification.')
super(LocaleBase, self).__setattr__(key, value) super(LocaleBase, self).__setattr__(key, value)
def __delattr__(self, key): def __delattr__(self, key):
if getattr(self, '__immutable', False): if getattr(self, '__immutable', False):
raise NotImplementedError(self.__class__.__name__ + raise NotImplementedError(self.__class__.__name__ +
' does not support modification.') ' does not support modification.')
super(LocaleBase, self).__delattr__(key) super(LocaleBase, self).__delattr__(key)
def __lt__(self, other): def __lt__(self, other):
return (id(self) != id(other)) and (str(self) > str(other)) return (id(self) != id(other)) and (str(self) > str(other))
def __gt__(self, other): def __gt__(self, other):
return (id(self) != id(other)) and (str(self) < str(other)) return (id(self) != id(other)) and (str(self) < str(other))
def __eq__(self, other): def __eq__(self, other):
return (id(self) == id(other)) or (str(self) == str(other)) return (id(self) == id(other)) or (str(self) == str(other))
@ -48,9 +51,10 @@ class LocaleBase( object ):
return cls._stored[key.lower()] return cls._stored[key.lower()]
except: except:
raise TMDBLocaleError("'{0}' is not a known valid {1} code."\ raise TMDBLocaleError("'{0}' is not a known valid {1} code."\
.format(key, cls.__name__)) .format(key, cls.__name__))
class Language( LocaleBase ): class Language(LocaleBase):
__slots__ = ['ISO639_1', 'ISO639_2', 'ISO639_2B', 'englishname', __slots__ = ['ISO639_1', 'ISO639_2', 'ISO639_2B', 'englishname',
'nativename'] 'nativename']
_stored = {} _stored = {}
@ -69,12 +73,13 @@ class Language( LocaleBase ):
def __repr__(self): def __repr__(self):
return u"<Language '{0.englishname}' ({0.ISO639_1})>".format(self) return u"<Language '{0.englishname}' ({0.ISO639_1})>".format(self)
class Country( LocaleBase ):
class Country(LocaleBase):
__slots__ = ['alpha2', 'name'] __slots__ = ['alpha2', 'name']
_stored = {} _stored = {}
def __init__(self, alpha2, name): def __init__(self, alpha2, name):
self.alpha2 = alpha2 self.alpha2 = alpha2
self.name = name self.name = name
super(Country, self).__init__(alpha2) super(Country, self).__init__(alpha2)
@ -84,7 +89,8 @@ class Country( LocaleBase ):
def __repr__(self): def __repr__(self):
return u"<Country '{0.name}' ({0.alpha2})>".format(self) return u"<Country '{0.name}' ({0.alpha2})>".format(self)
class Locale( LocaleBase ):
class Locale(LocaleBase):
__slots__ = ['language', 'country', 'encoding'] __slots__ = ['language', 'country', 'encoding']
def __init__(self, language, country, encoding): def __init__(self, language, country, encoding):
@ -120,6 +126,7 @@ class Locale( LocaleBase ):
# just return unmodified and hope for the best # just return unmodified and hope for the best
return dat return dat
def set_locale(language=None, country=None, fallthrough=False): def set_locale(language=None, country=None, fallthrough=False):
global syslocale global syslocale
LocaleBase.fallthrough = fallthrough LocaleBase.fallthrough = fallthrough
@ -142,6 +149,7 @@ def set_locale(language=None, country=None, fallthrough=False):
syslocale = Locale(language, country, sysenc) syslocale = Locale(language, country, sysenc)
def get_locale(language=-1, country=-1): def get_locale(language=-1, country=-1):
"""Output locale using provided attributes, or return system locale.""" """Output locale using provided attributes, or return system locale."""
global syslocale global syslocale

31
libs/tmdb3/pager.py

@ -8,7 +8,8 @@
from collections import Sequence, Iterator from collections import Sequence, Iterator
class PagedIterator( Iterator ):
class PagedIterator(Iterator):
def __init__(self, parent): def __init__(self, parent):
self._parent = parent self._parent = parent
self._index = -1 self._index = -1
@ -23,7 +24,8 @@ class PagedIterator( Iterator ):
raise StopIteration raise StopIteration
return self._parent[self._index] return self._parent[self._index]
class UnpagedData( object ):
class UnpagedData(object):
def copy(self): def copy(self):
return self.__class__() return self.__class__()
@ -33,10 +35,11 @@ class UnpagedData( object ):
def __rmul__(self, other): def __rmul__(self, other):
return (self.copy() for a in range(other)) return (self.copy() for a in range(other))
class PagedList( Sequence ):
class PagedList(Sequence):
""" """
List-like object, with support for automatically grabbing additional List-like object, with support for automatically grabbing
pages from a data source. additional pages from a data source.
""" """
_iter_class = None _iter_class = None
@ -87,17 +90,19 @@ class PagedList( Sequence ):
pagestart += 1 pagestart += 1
def _getpage(self, page): def _getpage(self, page):
raise NotImplementedError("PagedList._getpage() must be provided "+\ raise NotImplementedError("PagedList._getpage() must be provided " +
"by subclass") "by subclass")
class PagedRequest( PagedList ):
class PagedRequest(PagedList):
""" """
Derived PageList that provides a list-like object with automatic paging Derived PageList that provides a list-like object with automatic
intended for use with search requests. paging intended for use with search requests.
""" """
def __init__(self, request, handler=None): def __init__(self, request, handler=None):
self._request = request self._request = request
if handler: self._handler = handler if handler:
self._handler = handler
super(PagedRequest, self).__init__(self._getpage(1), 20) super(PagedRequest, self).__init__(self._getpage(1), 20)
def _getpage(self, page): def _getpage(self, page):
@ -105,5 +110,7 @@ class PagedRequest( PagedList ):
res = req.readJSON() res = req.readJSON()
self._len = res['total_results'] self._len = res['total_results']
for item in res['results']: for item in res['results']:
yield self._handler(item) if item is None:
yield None
else:
yield self._handler(item)

66
libs/tmdb3/request.py

@ -15,6 +15,7 @@ from cache import Cache
from urllib import urlencode from urllib import urlencode
import urllib2 import urllib2
import json import json
import os
DEBUG = False DEBUG = False
cache = Cache(filename='pytmdb3.cache') cache = Cache(filename='pytmdb3.cache')
@ -22,10 +23,11 @@ cache = Cache(filename='pytmdb3.cache')
#DEBUG = True #DEBUG = True
#cache = Cache(engine='null') #cache = Cache(engine='null')
def set_key(key): def set_key(key):
""" """
Specify the API key to use retrieving data from themoviedb.org. This Specify the API key to use retrieving data from themoviedb.org.
key must be set before any calls will function. This key must be set before any calls will function.
""" """
if len(key) != 32: if len(key) != 32:
raise TMDBKeyInvalid("Specified API key must be 128-bit hex") raise TMDBKeyInvalid("Specified API key must be 128-bit hex")
@ -35,42 +37,50 @@ def set_key(key):
raise TMDBKeyInvalid("Specified API key must be 128-bit hex") raise TMDBKeyInvalid("Specified API key must be 128-bit hex")
Request._api_key = key Request._api_key = key
def set_cache(engine=None, *args, **kwargs): def set_cache(engine=None, *args, **kwargs):
"""Specify caching engine and properties.""" """Specify caching engine and properties."""
cache.configure(engine, *args, **kwargs) cache.configure(engine, *args, **kwargs)
class Request( urllib2.Request ):
class Request(urllib2.Request):
_api_key = None _api_key = None
_base_url = "http://api.themoviedb.org/3/" _base_url = "http://api.themoviedb.org/3/"
@property @property
def api_key(self): def api_key(self):
if self._api_key is None: if self._api_key is None:
raise TMDBKeyMissing("API key must be specified before "+\ raise TMDBKeyMissing("API key must be specified before " +
"requests can be made") "requests can be made")
return self._api_key return self._api_key
def __init__(self, url, **kwargs): def __init__(self, url, **kwargs):
"""Return a request object, using specified API path and arguments.""" """
Return a request object, using specified API path and
arguments.
"""
kwargs['api_key'] = self.api_key kwargs['api_key'] = self.api_key
self._url = url.lstrip('/') self._url = url.lstrip('/')
self._kwargs = dict([(kwa,kwv) for kwa,kwv in kwargs.items() self._kwargs = dict([(kwa, kwv) for kwa, kwv in kwargs.items()
if kwv is not None]) if kwv is not None])
locale = get_locale() locale = get_locale()
kwargs = {} kwargs = {}
for k,v in self._kwargs.items(): for k, v in self._kwargs.items():
kwargs[k] = locale.encode(v) kwargs[k] = locale.encode(v)
url = '{0}{1}?{2}'.format(self._base_url, self._url, urlencode(kwargs)) url = '{0}{1}?{2}'\
.format(self._base_url, self._url, urlencode(kwargs))
urllib2.Request.__init__(self, url) urllib2.Request.__init__(self, url)
self.add_header('Accept', 'application/json') self.add_header('Accept', 'application/json')
self.lifetime = 3600 # 1hr self.lifetime = 3600 # 1hr
def new(self, **kwargs): def new(self, **kwargs):
"""Create a new instance of the request, with tweaked arguments.""" """
Create a new instance of the request, with tweaked arguments.
"""
args = dict(self._kwargs) args = dict(self._kwargs)
for k,v in kwargs.items(): for k, v in kwargs.items():
if v is None: if v is None:
if k in args: if k in args:
del args[k] del args[k]
@ -119,35 +129,35 @@ class Request( urllib2.Request ):
# no error from TMDB, just raise existing error # no error from TMDB, just raise existing error
raise e raise e
handle_status(data, url) handle_status(data, url)
#if DEBUG: if DEBUG:
# import pprint import pprint
# pprint.PrettyPrinter().pprint(data) pprint.PrettyPrinter().pprint(data)
return data return data
status_handlers = { status_handlers = {
1: None, 1: None,
2: TMDBRequestInvalid('Invalid service - This service does not exist.'), 2: TMDBRequestInvalid('Invalid service - This service does not exist.'),
3: TMDBRequestError('Authentication Failed - You do not have '+\ 3: TMDBRequestError('Authentication Failed - You do not have ' +
'permissions to access this service.'), 'permissions to access this service.'),
4: TMDBRequestInvalid("Invalid format - This service doesn't exist "+\ 4: TMDBRequestInvalid("Invalid format - This service doesn't exist " +
'in that format.'), 'in that format.'),
5: TMDBRequestInvalid('Invalid parameters - Your request parameters '+\ 5: TMDBRequestInvalid('Invalid parameters - Your request parameters ' +
'are incorrect.'), 'are incorrect.'),
6: TMDBRequestInvalid('Invalid id - The pre-requisite id is invalid '+\ 6: TMDBRequestInvalid('Invalid id - The pre-requisite id is invalid ' +
'or not found.'), 'or not found.'),
7: TMDBKeyInvalid('Invalid API key - You must be granted a valid key.'), 7: TMDBKeyInvalid('Invalid API key - You must be granted a valid key.'),
8: TMDBRequestError('Duplicate entry - The data you tried to submit '+\ 8: TMDBRequestError('Duplicate entry - The data you tried to submit ' +
'already exists.'), 'already exists.'),
9: TMDBOffline('This service is tempirarily offline. Try again later.'), 9: TMDBOffline('This service is tempirarily offline. Try again later.'),
10: TMDBKeyRevoked('Suspended API key - Access to your account has been '+\ 10: TMDBKeyRevoked('Suspended API key - Access to your account has been ' +
'suspended, contact TMDB.'), 'suspended, contact TMDB.'),
11: TMDBError('Internal error - Something went wrong. Contact TMDb.'), 11: TMDBError('Internal error - Something went wrong. Contact TMDb.'),
12: None, 12: None,
13: None, 13: None,
14: TMDBRequestError('Authentication Failed.'), 14: TMDBRequestError('Authentication Failed.'),
15: TMDBError('Failed'), 15: TMDBError('Failed'),
16: TMDBError('Device Denied'), 16: TMDBError('Device Denied'),
17: TMDBError('Session Denied')} 17: TMDBError('Session Denied')}
def handle_status(data, query): def handle_status(data, query):
status = status_handlers[data.get('status_code', 1)] status = status_handlers[data.get('status_code', 1)]

657
libs/tmdb3/tmdb_api.py

@ -13,8 +13,8 @@
# (http://creativecommons.org/licenses/GPL/2.0/) # (http://creativecommons.org/licenses/GPL/2.0/)
#----------------------- #-----------------------
__title__ = "tmdb_api - Simple-to-use Python interface to TMDB's API v3 "+\ __title__ = ("tmdb_api - Simple-to-use Python interface to TMDB's API v3 " +
"(www.themoviedb.org)" "(www.themoviedb.org)")
__author__ = "Raymond Wagner" __author__ = "Raymond Wagner"
__purpose__ = """ __purpose__ = """
This Python library is intended to provide a series of classes and methods This Python library is intended to provide a series of classes and methods
@ -22,7 +22,7 @@ for search and retrieval of text metadata and image URLs from TMDB.
Preliminary API specifications can be found at Preliminary API specifications can be found at
http://help.themoviedb.org/kb/api/about-3""" http://help.themoviedb.org/kb/api/about-3"""
__version__="v0.6.17" __version__ = "v0.7.0"
# 0.1.0 Initial development # 0.1.0 Initial development
# 0.2.0 Add caching mechanism for API queries # 0.2.0 Add caching mechanism for API queries
# 0.2.1 Temporary work around for broken search paging # 0.2.1 Temporary work around for broken search paging
@ -61,6 +61,7 @@ __version__="v0.6.17"
# 0.6.16 Make absent primary images return None (previously u'') # 0.6.16 Make absent primary images return None (previously u'')
# 0.6.17 Add userrating/votes to Image, add overview to Collection, remove # 0.6.17 Add userrating/votes to Image, add overview to Collection, remove
# releasedate sorting from Collection Movies # releasedate sorting from Collection Movies
# 0.7.0 Add support for television series data
from request import set_key, Request from request import set_key, Request
from util import Datapoint, Datalist, Datadict, Element, NameRepr, SearchRepr from util import Datapoint, Datalist, Datadict, Element, NameRepr, SearchRepr
@ -69,10 +70,14 @@ from locales import get_locale, set_locale
from tmdb_auth import get_session, set_session from tmdb_auth import get_session, set_session
from tmdb_exceptions import * from tmdb_exceptions import *
import json
import urllib
import urllib2
import datetime import datetime
DEBUG = False DEBUG = False
def process_date(datestr): def process_date(datestr):
try: try:
return datetime.date(*[int(x) for x in datestr.split('-')]) return datetime.date(*[int(x) for x in datestr.split('-')])
@ -82,34 +87,40 @@ def process_date(datestr):
import traceback import traceback
_,_,tb = sys.exc_info() _,_,tb = sys.exc_info()
f,l,_,_ = traceback.extract_tb(tb)[-1] f,l,_,_ = traceback.extract_tb(tb)[-1]
warnings.warn_explicit(('"{0}" is not a supported date format. ' warnings.warn_explicit(('"{0}" is not a supported date format. ' +
'Please fix upstream data at http://www.themoviedb.org.')\ 'Please fix upstream data at ' +
.format(datestr), Warning, f, l) 'http://www.themoviedb.org.'
).format(datestr), Warning, f, l)
return None return None
class Configuration( Element ):
class Configuration(Element):
images = Datapoint('images') images = Datapoint('images')
def _populate(self): def _populate(self):
return Request('configuration') return Request('configuration')
Configuration = Configuration() Configuration = Configuration()
class Account( NameRepr, Element ):
class Account(NameRepr, Element):
def _populate(self): def _populate(self):
return Request('account', session_id=self._session.sessionid) return Request('account', session_id=self._session.sessionid)
id = Datapoint('id') id = Datapoint('id')
adult = Datapoint('include_adult') adult = Datapoint('include_adult')
country = Datapoint('iso_3166_1') country = Datapoint('iso_3166_1')
language = Datapoint('iso_639_1') language = Datapoint('iso_639_1')
name = Datapoint('name') name = Datapoint('name')
username = Datapoint('username') username = Datapoint('username')
@property @property
def locale(self): def locale(self):
return get_locale(self.language, self.country) return get_locale(self.language, self.country)
def searchMovie(query, locale=None, adult=False, year=None): def searchMovie(query, locale=None, adult=False, year=None):
kwargs = {'query':query, 'include_adult':adult} kwargs = {'query': query, 'include_adult': adult}
if year is not None: if year is not None:
try: try:
kwargs['year'] = year.year kwargs['year'] = year.year
@ -117,6 +128,7 @@ def searchMovie(query, locale=None, adult=False, year=None):
kwargs['year'] = year kwargs['year'] = year
return MovieSearchResult(Request('search/movie', **kwargs), locale=locale) return MovieSearchResult(Request('search/movie', **kwargs), locale=locale)
def searchMovieWithYear(query, locale=None, adult=False): def searchMovieWithYear(query, locale=None, adult=False):
year = None year = None
if (len(query) > 6) and (query[-1] == ')') and (query[-6] == '('): if (len(query) > 6) and (query[-1] == ')') and (query[-6] == '('):
@ -134,70 +146,95 @@ def searchMovieWithYear(query, locale=None, adult=False):
year = None year = None
return searchMovie(query, locale, adult, year) return searchMovie(query, locale, adult, year)
class MovieSearchResult( SearchRepr, PagedRequest ):
class MovieSearchResult(SearchRepr, PagedRequest):
"""Stores a list of search matches.""" """Stores a list of search matches."""
_name = None _name = None
def __init__(self, request, locale=None): def __init__(self, request, locale=None):
if locale is None: if locale is None:
locale = get_locale() locale = get_locale()
super(MovieSearchResult, self).__init__( super(MovieSearchResult, self).__init__(
request.new(language=locale.language), request.new(language=locale.language),
lambda x: Movie(raw=x, locale=locale)) lambda x: Movie(raw=x, locale=locale))
def searchSeries(query, first_air_date_year=None, search_type=None, locale=None):
return SeriesSearchResult(
Request('search/tv', query=query, first_air_date_year=first_air_date_year, search_type=search_type),
locale=locale)
class SeriesSearchResult(SearchRepr, PagedRequest):
"""Stores a list of search matches."""
_name = None
def __init__(self, request, locale=None):
if locale is None:
locale = get_locale()
super(SeriesSearchResult, self).__init__(
request.new(language=locale.language),
lambda x: Series(raw=x, locale=locale))
def searchPerson(query, adult=False): def searchPerson(query, adult=False):
return PeopleSearchResult(Request('search/person', query=query, return PeopleSearchResult(Request('search/person', query=query,
include_adult=adult)) include_adult=adult))
class PeopleSearchResult( SearchRepr, PagedRequest ):
class PeopleSearchResult(SearchRepr, PagedRequest):
"""Stores a list of search matches.""" """Stores a list of search matches."""
_name = None _name = None
def __init__(self, request): def __init__(self, request):
super(PeopleSearchResult, self).__init__(request, super(PeopleSearchResult, self).__init__(
lambda x: Person(raw=x)) request, lambda x: Person(raw=x))
def searchStudio(query): def searchStudio(query):
return StudioSearchResult(Request('search/company', query=query)) return StudioSearchResult(Request('search/company', query=query))
class StudioSearchResult( SearchRepr, PagedRequest ):
class StudioSearchResult(SearchRepr, PagedRequest):
"""Stores a list of search matches.""" """Stores a list of search matches."""
_name = None _name = None
def __init__(self, request): def __init__(self, request):
super(StudioSearchResult, self).__init__(request, super(StudioSearchResult, self).__init__(
lambda x: Studio(raw=x)) request, lambda x: Studio(raw=x))
def searchList(query, adult=False): def searchList(query, adult=False):
ListSearchResult(Request('search/list', query=query, include_adult=adult)) ListSearchResult(Request('search/list', query=query, include_adult=adult))
class ListSearchResult( SearchRepr, PagedRequest ):
class ListSearchResult(SearchRepr, PagedRequest):
"""Stores a list of search matches.""" """Stores a list of search matches."""
_name = None _name = None
def __init__(self, request): def __init__(self, request):
super(ListSearchResult, self).__init__(request, super(ListSearchResult, self).__init__(
lambda x: List(raw=x)) request, lambda x: List(raw=x))
def searchCollection(query, locale=None): def searchCollection(query, locale=None):
return CollectionSearchResult(Request('search/collection', query=query), return CollectionSearchResult(Request('search/collection', query=query),
locale=locale) locale=locale)
class CollectionSearchResult( SearchRepr, PagedRequest ):
class CollectionSearchResult(SearchRepr, PagedRequest):
"""Stores a list of search matches.""" """Stores a list of search matches."""
_name=None _name=None
def __init__(self, request, locale=None): def __init__(self, request, locale=None):
if locale is None: if locale is None:
locale = get_locale() locale = get_locale()
super(CollectionSearchResult, self).__init__( super(CollectionSearchResult, self).__init__(
request.new(language=locale.language), request.new(language=locale.language),
lambda x: Collection(raw=x, locale=locale)) lambda x: Collection(raw=x, locale=locale))
class Image( Element ):
filename = Datapoint('file_path', initarg=1, class Image(Element):
handler=lambda x: x.lstrip('/')) filename = Datapoint('file_path', initarg=1,
aspectratio = Datapoint('aspect_ratio') handler=lambda x: x.lstrip('/'))
height = Datapoint('height') aspectratio = Datapoint('aspect_ratio')
width = Datapoint('width') height = Datapoint('height')
language = Datapoint('iso_639_1') width = Datapoint('width')
userrating = Datapoint('vote_average') language = Datapoint('iso_639_1')
votes = Datapoint('vote_count') userrating = Datapoint('vote_average')
votes = Datapoint('vote_count')
def sizes(self): def sizes(self):
return ['original'] return ['original']
@ -205,19 +242,28 @@ class Image( Element ):
def geturl(self, size='original'): def geturl(self, size='original'):
if size not in self.sizes(): if size not in self.sizes():
raise TMDBImageSizeError raise TMDBImageSizeError
url = Configuration.images['base_url'].rstrip('/') url = Configuration.images['secure_base_url'].rstrip('/')
return url+'/{0}/{1}'.format(size, self.filename) return url+'/{0}/{1}'.format(size, self.filename)
# sort preferring locale's language, but keep remaining ordering consistent # sort preferring locale's language, but keep remaining ordering consistent
def __lt__(self, other): def __lt__(self, other):
if not isinstance(other, Image):
return False
return (self.language == self._locale.language) \ return (self.language == self._locale.language) \
and (self.language != other.language) and (self.language != other.language)
def __gt__(self, other): def __gt__(self, other):
if not isinstance(other, Image):
return True
return (self.language != other.language) \ return (self.language != other.language) \
and (other.language == self._locale.language) and (other.language == self._locale.language)
# direct match for comparison # direct match for comparison
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, Image):
return False
return self.filename == other.filename return self.filename == other.filename
# special handling for boolean to see if exists # special handling for boolean to see if exists
def __nonzero__(self): def __nonzero__(self):
if len(self.filename) == 0: if len(self.filename) == 0:
@ -228,20 +274,28 @@ class Image( Element ):
# BASE62 encoded filename, no need to worry about unicode # BASE62 encoded filename, no need to worry about unicode
return u"<{0.__class__.__name__} '{0.filename}'>".format(self) return u"<{0.__class__.__name__} '{0.filename}'>".format(self)
class Backdrop( Image ):
class Backdrop(Image):
def sizes(self): def sizes(self):
return Configuration.images['backdrop_sizes'] return Configuration.images['backdrop_sizes']
class Poster( Image ):
class Poster(Image):
def sizes(self): def sizes(self):
return Configuration.images['poster_sizes'] return Configuration.images['poster_sizes']
class Profile( Image ):
class Profile(Image):
def sizes(self): def sizes(self):
return Configuration.images['profile_sizes'] return Configuration.images['profile_sizes']
class Logo( Image ):
class Logo(Image):
def sizes(self): def sizes(self):
return Configuration.images['logo_sizes'] return Configuration.images['logo_sizes']
class AlternateTitle( Element ):
class AlternateTitle(Element):
country = Datapoint('iso_3166_1') country = Datapoint('iso_3166_1')
title = Datapoint('title') title = Datapoint('title')
@ -249,28 +303,31 @@ class AlternateTitle( Element ):
def __lt__(self, other): def __lt__(self, other):
return (self.country == self._locale.country) \ return (self.country == self._locale.country) \
and (self.country != other.country) and (self.country != other.country)
def __gt__(self, other): def __gt__(self, other):
return (self.country != other.country) \ return (self.country != other.country) \
and (other.country == self._locale.country) and (other.country == self._locale.country)
def __eq__(self, other): def __eq__(self, other):
return self.country == other.country return self.country == other.country
def __repr__(self): def __repr__(self):
return u"<{0.__class__.__name__} '{0.title}' ({0.country})>"\ return u"<{0.__class__.__name__} '{0.title}' ({0.country})>"\
.format(self).encode('utf-8') .format(self).encode('utf-8')
class Person( Element ):
id = Datapoint('id', initarg=1) class Person(Element):
name = Datapoint('name') id = Datapoint('id', initarg=1)
biography = Datapoint('biography') name = Datapoint('name')
dayofbirth = Datapoint('birthday', default=None, handler=process_date) biography = Datapoint('biography')
dayofdeath = Datapoint('deathday', default=None, handler=process_date) dayofbirth = Datapoint('birthday', default=None, handler=process_date)
homepage = Datapoint('homepage') dayofdeath = Datapoint('deathday', default=None, handler=process_date)
birthplace = Datapoint('place_of_birth') homepage = Datapoint('homepage')
profile = Datapoint('profile_path', handler=Profile, \ birthplace = Datapoint('place_of_birth')
raw=False, default=None) profile = Datapoint('profile_path', handler=Profile,
adult = Datapoint('adult') raw=False, default=None)
aliases = Datalist('also_known_as') adult = Datapoint('adult')
aliases = Datalist('also_known_as')
def __repr__(self): def __repr__(self):
return u"<{0.__class__.__name__} '{0.name}'>"\ return u"<{0.__class__.__name__} '{0.name}'>"\
@ -278,55 +335,63 @@ class Person( Element ):
def _populate(self): def _populate(self):
return Request('person/{0}'.format(self.id)) return Request('person/{0}'.format(self.id))
def _populate_credits(self): def _populate_credits(self):
return Request('person/{0}/credits'.format(self.id), \ return Request('person/{0}/credits'.format(self.id),
language=self._locale.language) language=self._locale.language)
def _populate_images(self): def _populate_images(self):
return Request('person/{0}/images'.format(self.id)) return Request('person/{0}/images'.format(self.id))
roles = Datalist('cast', handler=lambda x: ReverseCast(raw=x), \ roles = Datalist('cast', handler=lambda x: ReverseCast(raw=x),
poller=_populate_credits) poller=_populate_credits)
crew = Datalist('crew', handler=lambda x: ReverseCrew(raw=x), \ crew = Datalist('crew', handler=lambda x: ReverseCrew(raw=x),
poller=_populate_credits) poller=_populate_credits)
profiles = Datalist('profiles', handler=Profile, poller=_populate_images) profiles = Datalist('profiles', handler=Profile, poller=_populate_images)
class Cast( Person ):
character = Datapoint('character') class Cast(Person):
order = Datapoint('order') character = Datapoint('character')
order = Datapoint('order')
def __repr__(self): def __repr__(self):
return u"<{0.__class__.__name__} '{0.name}' as '{0.character}'>"\ return u"<{0.__class__.__name__} '{0.name}' as '{0.character}'>"\
.format(self).encode('utf-8') .format(self).encode('utf-8')
class Crew( Person ):
job = Datapoint('job') class Crew(Person):
department = Datapoint('department') job = Datapoint('job')
department = Datapoint('department')
def __repr__(self): def __repr__(self):
return u"<{0.__class__.__name__} '{0.name}','{0.job}'>"\ return u"<{0.__class__.__name__} '{0.name}','{0.job}'>"\
.format(self).encode('utf-8') .format(self).encode('utf-8')
class Keyword( Element ):
class Keyword(Element):
id = Datapoint('id') id = Datapoint('id')
name = Datapoint('name') name = Datapoint('name')
def __repr__(self): def __repr__(self):
return u"<{0.__class__.__name__} {0.name}>".format(self).encode('utf-8') return u"<{0.__class__.__name__} {0.name}>"\
.format(self).encode('utf-8')
class Release( Element ): class Release(Element):
certification = Datapoint('certification') certification = Datapoint('certification')
country = Datapoint('iso_3166_1') country = Datapoint('iso_3166_1')
releasedate = Datapoint('release_date', handler=process_date) releasedate = Datapoint('release_date', handler=process_date)
def __repr__(self): def __repr__(self):
return u"<{0.__class__.__name__} {0.country}, {0.releasedate}>"\ return u"<{0.__class__.__name__} {0.country}, {0.releasedate}>"\
.format(self).encode('utf-8') .format(self).encode('utf-8')
class Trailer(Element):
name = Datapoint('name')
size = Datapoint('size')
source = Datapoint('source')
class Trailer( Element ):
name = Datapoint('name')
size = Datapoint('size')
source = Datapoint('source')
class YoutubeTrailer( Trailer ): class YoutubeTrailer(Trailer):
def geturl(self): def geturl(self):
return "http://www.youtube.com/watch?v={0}".format(self.source) return "http://www.youtube.com/watch?v={0}".format(self.source)
@ -334,8 +399,9 @@ class YoutubeTrailer( Trailer ):
# modified BASE64 encoding, no need to worry about unicode # modified BASE64 encoding, no need to worry about unicode
return u"<{0.__class__.__name__} '{0.name}'>".format(self) return u"<{0.__class__.__name__} '{0.name}'>".format(self)
class AppleTrailer( Element ):
name = Datapoint('name') class AppleTrailer(Element):
name = Datapoint('name')
sources = Datadict('sources', handler=Trailer, attr='size') sources = Datadict('sources', handler=Trailer, attr='size')
def sizes(self): def sizes(self):
@ -344,84 +410,91 @@ class AppleTrailer( Element ):
def geturl(self, size=None): def geturl(self, size=None):
if size is None: if size is None:
# sort assuming ###p format for now, take largest resolution # sort assuming ###p format for now, take largest resolution
size = str(sorted([int(size[:-1]) for size in self.sources])[-1])+'p' size = str(sorted(
[int(size[:-1]) for size in self.sources]
)[-1]) + 'p'
return self.sources[size].source return self.sources[size].source
def __repr__(self): def __repr__(self):
return u"<{0.__class__.__name__} '{0.name}'>".format(self) return u"<{0.__class__.__name__} '{0.name}'>".format(self)
class Translation( Element ):
name = Datapoint('name') class Translation(Element):
language = Datapoint('iso_639_1') name = Datapoint('name')
englishname = Datapoint('english_name') language = Datapoint('iso_639_1')
englishname = Datapoint('english_name')
def __repr__(self): def __repr__(self):
return u"<{0.__class__.__name__} '{0.name}' ({0.language})>"\ return u"<{0.__class__.__name__} '{0.name}' ({0.language})>"\
.format(self).encode('utf-8') .format(self).encode('utf-8')
class Genre( NameRepr, Element ):
id = Datapoint('id') class Genre(NameRepr, Element):
name = Datapoint('name') id = Datapoint('id')
name = Datapoint('name')
def _populate_movies(self): def _populate_movies(self):
return Request('genre/{0}/movies'.format(self.id), \ return Request('genre/{0}/movies'.format(self.id), \
language=self._locale.language) language=self._locale.language)
@property @property
def movies(self): def movies(self):
if 'movies' not in self._data: if 'movies' not in self._data:
search = MovieSearchResult(self._populate_movies(), \ search = MovieSearchResult(self._populate_movies(), \
locale=self._locale) locale=self._locale)
search._name = "{0.name} Movies".format(self) search._name = "{0.name} Movies".format(self)
self._data['movies'] = search self._data['movies'] = search
return self._data['movies'] return self._data['movies']
@classmethod @classmethod
def getAll(cls, locale=None): def getAll(cls, locale=None):
class GenreList( Element ): class GenreList(Element):
genres = Datalist('genres', handler=Genre) genres = Datalist('genres', handler=Genre)
def _populate(self): def _populate(self):
return Request('genre/list', language=self._locale.language) return Request('genre/list', language=self._locale.language)
return GenreList(locale=locale).genres return GenreList(locale=locale).genres
class Studio( NameRepr, Element ): class Studio(NameRepr, Element):
id = Datapoint('id', initarg=1) id = Datapoint('id', initarg=1)
name = Datapoint('name') name = Datapoint('name')
description = Datapoint('description') description = Datapoint('description')
headquarters = Datapoint('headquarters') headquarters = Datapoint('headquarters')
logo = Datapoint('logo_path', handler=Logo, \ logo = Datapoint('logo_path', handler=Logo, raw=False, default=None)
raw=False, default=None)
# FIXME: manage not-yet-defined handlers in a way that will propogate # FIXME: manage not-yet-defined handlers in a way that will propogate
# locale information properly # locale information properly
parent = Datapoint('parent_company', \ parent = Datapoint('parent_company', handler=lambda x: Studio(raw=x))
handler=lambda x: Studio(raw=x))
def _populate(self): def _populate(self):
return Request('company/{0}'.format(self.id)) return Request('company/{0}'.format(self.id))
def _populate_movies(self): def _populate_movies(self):
return Request('company/{0}/movies'.format(self.id), \ return Request('company/{0}/movies'.format(self.id),
language=self._locale.language) language=self._locale.language)
# FIXME: add a cleaner way of adding types with no additional processing # FIXME: add a cleaner way of adding types with no additional processing
@property @property
def movies(self): def movies(self):
if 'movies' not in self._data: if 'movies' not in self._data:
search = MovieSearchResult(self._populate_movies(), \ search = MovieSearchResult(self._populate_movies(),
locale=self._locale) locale=self._locale)
search._name = "{0.name} Movies".format(self) search._name = "{0.name} Movies".format(self)
self._data['movies'] = search self._data['movies'] = search
return self._data['movies'] return self._data['movies']
class Country( NameRepr, Element ):
code = Datapoint('iso_3166_1')
name = Datapoint('name')
class Language( NameRepr, Element ): class Country(NameRepr, Element):
code = Datapoint('iso_639_1') code = Datapoint('iso_3166_1')
name = Datapoint('name') name = Datapoint('name')
class Language(NameRepr, Element):
code = Datapoint('iso_639_1')
name = Datapoint('name')
class Movie( Element ): class Movie(Element):
@classmethod @classmethod
def latest(cls): def latest(cls):
req = Request('latest/movie') req = Request('latest/movie')
@ -459,7 +532,7 @@ class Movie( Element ):
account = Account(session=session) account = Account(session=session)
res = MovieSearchResult( res = MovieSearchResult(
Request('account/{0}/favorite_movies'.format(account.id), Request('account/{0}/favorite_movies'.format(account.id),
session_id=session.sessionid)) session_id=session.sessionid))
res._name = "Favorites" res._name = "Favorites"
return res return res
@ -470,7 +543,7 @@ class Movie( Element ):
account = Account(session=session) account = Account(session=session)
res = MovieSearchResult( res = MovieSearchResult(
Request('account/{0}/rated_movies'.format(account.id), Request('account/{0}/rated_movies'.format(account.id),
session_id=session.sessionid)) session_id=session.sessionid))
res._name = "Movies You Rated" res._name = "Movies You Rated"
return res return res
@ -481,7 +554,7 @@ class Movie( Element ):
account = Account(session=session) account = Account(session=session)
res = MovieSearchResult( res = MovieSearchResult(
Request('account/{0}/movie_watchlist'.format(account.id), Request('account/{0}/movie_watchlist'.format(account.id),
session_id=session.sessionid)) session_id=session.sessionid))
res._name = "Movies You're Watching" res._name = "Movies You're Watching"
return res return res
@ -500,104 +573,116 @@ class Movie( Element ):
movie._populate() movie._populate()
return movie return movie
id = Datapoint('id', initarg=1) id = Datapoint('id', initarg=1)
title = Datapoint('title') title = Datapoint('title')
originaltitle = Datapoint('original_title') originaltitle = Datapoint('original_title')
tagline = Datapoint('tagline') tagline = Datapoint('tagline')
overview = Datapoint('overview') overview = Datapoint('overview')
runtime = Datapoint('runtime') runtime = Datapoint('runtime')
budget = Datapoint('budget') budget = Datapoint('budget')
revenue = Datapoint('revenue') revenue = Datapoint('revenue')
releasedate = Datapoint('release_date', handler=process_date) releasedate = Datapoint('release_date', handler=process_date)
homepage = Datapoint('homepage') homepage = Datapoint('homepage')
imdb = Datapoint('imdb_id') imdb = Datapoint('imdb_id')
backdrop = Datapoint('backdrop_path', handler=Backdrop, \ backdrop = Datapoint('backdrop_path', handler=Backdrop,
raw=False, default=None) raw=False, default=None)
poster = Datapoint('poster_path', handler=Poster, \ poster = Datapoint('poster_path', handler=Poster,
raw=False, default=None) raw=False, default=None)
popularity = Datapoint('popularity') popularity = Datapoint('popularity')
userrating = Datapoint('vote_average') userrating = Datapoint('vote_average')
votes = Datapoint('vote_count') votes = Datapoint('vote_count')
adult = Datapoint('adult') adult = Datapoint('adult')
collection = Datapoint('belongs_to_collection', handler=lambda x: \ collection = Datapoint('belongs_to_collection', handler=lambda x: \
Collection(raw=x)) Collection(raw=x))
genres = Datalist('genres', handler=Genre) genres = Datalist('genres', handler=Genre)
studios = Datalist('production_companies', handler=Studio) studios = Datalist('production_companies', handler=Studio)
countries = Datalist('production_countries', handler=Country) countries = Datalist('production_countries', handler=Country)
languages = Datalist('spoken_languages', handler=Language) languages = Datalist('spoken_languages', handler=Language)
def _populate(self): def _populate(self):
return Request('movie/{0}'.format(self.id), \ return Request('movie/{0}'.format(self.id), \
language=self._locale.language) language=self._locale.language)
def _populate_titles(self): def _populate_titles(self):
kwargs = {} kwargs = {}
if not self._locale.fallthrough: if not self._locale.fallthrough:
kwargs['country'] = self._locale.country kwargs['country'] = self._locale.country
return Request('movie/{0}/alternative_titles'.format(self.id), **kwargs) return Request('movie/{0}/alternative_titles'.format(self.id),
**kwargs)
def _populate_cast(self): def _populate_cast(self):
return Request('movie/{0}/casts'.format(self.id)) return Request('movie/{0}/casts'.format(self.id))
def _populate_images(self): def _populate_images(self):
kwargs = {} kwargs = {}
if not self._locale.fallthrough: if not self._locale.fallthrough:
kwargs['language'] = self._locale.language kwargs['language'] = self._locale.language
return Request('movie/{0}/images'.format(self.id), **kwargs) return Request('movie/{0}/images'.format(self.id), **kwargs)
def _populate_keywords(self): def _populate_keywords(self):
return Request('movie/{0}/keywords'.format(self.id)) return Request('movie/{0}/keywords'.format(self.id))
def _populate_releases(self): def _populate_releases(self):
return Request('movie/{0}/releases'.format(self.id)) return Request('movie/{0}/releases'.format(self.id))
def _populate_trailers(self): def _populate_trailers(self):
return Request('movie/{0}/trailers'.format(self.id), \ return Request('movie/{0}/trailers'.format(self.id),
language=self._locale.language) language=self._locale.language)
def _populate_translations(self): def _populate_translations(self):
return Request('movie/{0}/translations'.format(self.id)) return Request('movie/{0}/translations'.format(self.id))
alternate_titles = Datalist('titles', handler=AlternateTitle, \ alternate_titles = Datalist('titles', handler=AlternateTitle, \
poller=_populate_titles, sort=True) poller=_populate_titles, sort=True)
cast = Datalist('cast', handler=Cast, \
poller=_populate_cast, sort='order') # FIXME: this data point will need to be changed to 'credits' at some point
crew = Datalist('crew', handler=Crew, poller=_populate_cast) cast = Datalist('cast', handler=Cast,
backdrops = Datalist('backdrops', handler=Backdrop, \ poller=_populate_cast, sort='order')
poller=_populate_images, sort=True)
posters = Datalist('posters', handler=Poster, \ crew = Datalist('crew', handler=Crew, poller=_populate_cast)
poller=_populate_images, sort=True) backdrops = Datalist('backdrops', handler=Backdrop,
keywords = Datalist('keywords', handler=Keyword, \ poller=_populate_images, sort=True)
poller=_populate_keywords) posters = Datalist('posters', handler=Poster,
releases = Datadict('countries', handler=Release, \ poller=_populate_images, sort=True)
poller=_populate_releases, attr='country') keywords = Datalist('keywords', handler=Keyword,
youtube_trailers = Datalist('youtube', handler=YoutubeTrailer, \ poller=_populate_keywords)
poller=_populate_trailers) releases = Datadict('countries', handler=Release,
apple_trailers = Datalist('quicktime', handler=AppleTrailer, \ poller=_populate_releases, attr='country')
poller=_populate_trailers) youtube_trailers = Datalist('youtube', handler=YoutubeTrailer,
translations = Datalist('translations', handler=Translation, \ poller=_populate_trailers)
poller=_populate_translations) apple_trailers = Datalist('quicktime', handler=AppleTrailer,
poller=_populate_trailers)
translations = Datalist('translations', handler=Translation,
poller=_populate_translations)
def setFavorite(self, value): def setFavorite(self, value):
req = Request('account/{0}/favorite'.format(\ req = Request('account/{0}/favorite'.format(
Account(session=self._session).id), Account(session=self._session).id),
session_id=self._session.sessionid) session_id=self._session.sessionid)
req.add_data({'movie_id':self.id, 'favorite':str(bool(value)).lower()}) req.add_data({'movie_id': self.id,
'favorite': str(bool(value)).lower()})
req.lifetime = 0 req.lifetime = 0
req.readJSON() req.readJSON()
def setRating(self, value): def setRating(self, value):
if not (0 <= value <= 10): if not (0 <= value <= 10):
raise TMDBError("Ratings must be between '0' and '10'.") raise TMDBError("Ratings must be between '0' and '10'.")
req = Request('movie/{0}/rating'.format(self.id), \ req = Request('movie/{0}/rating'.format(self.id),
session_id=self._session.sessionid) session_id=self._session.sessionid)
req.lifetime = 0 req.lifetime = 0
req.add_data({'value':value}) req.add_data({'value':value})
req.readJSON() req.readJSON()
def setWatchlist(self, value): def setWatchlist(self, value):
req = Request('account/{0}/movie_watchlist'.format(\ req = Request('account/{0}/movie_watchlist'.format(
Account(session=self._session).id), Account(session=self._session).id),
session_id=self._session.sessionid) session_id=self._session.sessionid)
req.lifetime = 0 req.lifetime = 0
req.add_data({'movie_id':self.id, req.add_data({'movie_id': self.id,
'movie_watchlist':str(bool(value)).lower()}) 'movie_watchlist': str(bool(value)).lower()})
req.readJSON() req.readJSON()
def getSimilar(self): def getSimilar(self):
@ -605,9 +690,9 @@ class Movie( Element ):
@property @property
def similar(self): def similar(self):
res = MovieSearchResult(Request('movie/{0}/similar_movies'\ res = MovieSearchResult(Request(
.format(self.id)), 'movie/{0}/similar_movies'.format(self.id)),
locale=self._locale) locale=self._locale)
res._name = 'Similar to {0}'.format(self._printable_name()) res._name = 'Similar to {0}'.format(self._printable_name())
return res return res
@ -629,61 +714,197 @@ class Movie( Element ):
return s return s
def __repr__(self): def __repr__(self):
return u"<{0} {1}>".format(self.__class__.__name__,\ return u"<{0} {1}>".format(self.__class__.__name__,
self._printable_name()).encode('utf-8') self._printable_name()).encode('utf-8')
class ReverseCast( Movie ): class ReverseCast( Movie ):
character = Datapoint('character') character = Datapoint('character')
def __repr__(self): def __repr__(self):
return u"<{0.__class__.__name__} '{0.character}' on {1}>"\ return (u"<{0.__class__.__name__} '{0.character}' on {1}>"
.format(self, self._printable_name()).encode('utf-8') .format(self, self._printable_name()).encode('utf-8'))
class ReverseCrew( Movie ): class ReverseCrew( Movie ):
department = Datapoint('department') department = Datapoint('department')
job = Datapoint('job') job = Datapoint('job')
def __repr__(self): def __repr__(self):
return u"<{0.__class__.__name__} '{0.job}' for {1}>"\ return (u"<{0.__class__.__name__} '{0.job}' for {1}>"
.format(self, self._printable_name()).encode('utf-8') .format(self, self._printable_name()).encode('utf-8'))
class Collection( NameRepr, Element ):
id = Datapoint('id', initarg=1) class Collection(NameRepr, Element):
name = Datapoint('name') id = Datapoint('id', initarg=1)
name = Datapoint('name')
backdrop = Datapoint('backdrop_path', handler=Backdrop, \ backdrop = Datapoint('backdrop_path', handler=Backdrop, \
raw=False, default=None) raw=False, default=None)
poster = Datapoint('poster_path', handler=Poster, \ poster = Datapoint('poster_path', handler=Poster, raw=False, default=None)
raw=False, default=None) members = Datalist('parts', handler=Movie)
members = Datalist('parts', handler=Movie)
overview = Datapoint('overview') overview = Datapoint('overview')
def _populate(self): def _populate(self):
return Request('collection/{0}'.format(self.id), \ return Request('collection/{0}'.format(self.id),
language=self._locale.language) language=self._locale.language)
def _populate_images(self): def _populate_images(self):
kwargs = {} kwargs = {}
if not self._locale.fallthrough: if not self._locale.fallthrough:
kwargs['language'] = self._locale.language kwargs['language'] = self._locale.language
return Request('collection/{0}/images'.format(self.id), **kwargs) return Request('collection/{0}/images'.format(self.id), **kwargs)
backdrops = Datalist('backdrops', handler=Backdrop, \ backdrops = Datalist('backdrops', handler=Backdrop,
poller=_populate_images, sort=True) poller=_populate_images, sort=True)
posters = Datalist('posters', handler=Poster, \ posters = Datalist('posters', handler=Poster,
poller=_populate_images, sort=True) poller=_populate_images, sort=True)
class List( NameRepr, Element ): class List(NameRepr, Element):
id = Datapoint('id', initarg=1) id = Datapoint('id', initarg=1)
name = Datapoint('name') name = Datapoint('name')
author = Datapoint('created_by') author = Datapoint('created_by')
description = Datapoint('description') description = Datapoint('description')
favorites = Datapoint('favorite_count') favorites = Datapoint('favorite_count')
language = Datapoint('iso_639_1') language = Datapoint('iso_639_1')
count = Datapoint('item_count') count = Datapoint('item_count')
poster = Datapoint('poster_path', handler=Poster, \ poster = Datapoint('poster_path', handler=Poster, raw=False, default=None)
raw=False, default=None) members = Datalist('items', handler=Movie)
members = Datalist('items', handler=Movie)
def _populate(self): def _populate(self):
return Request('list/{0}'.format(self.id)) return Request('list/{0}'.format(self.id))
class Network(NameRepr,Element):
id = Datapoint('id', initarg=1)
name = Datapoint('name')
class Episode(NameRepr, Element):
episode_number = Datapoint('episode_number', initarg=3)
season_number = Datapoint('season_number', initarg=2)
series_id = Datapoint('series_id', initarg=1)
air_date = Datapoint('air_date', handler=process_date)
overview = Datapoint('overview')
name = Datapoint('name')
userrating = Datapoint('vote_average')
votes = Datapoint('vote_count')
id = Datapoint('id')
production_code = Datapoint('production_code')
still = Datapoint('still_path', handler=Backdrop, raw=False, default=None)
def _populate(self):
return Request('tv/{0}/season/{1}/episode/{2}'.format(self.series_id, self.season_number, self.episode_number),
language=self._locale.language)
def _populate_cast(self):
return Request('tv/{0}/season/{1}/episode/{2}/credits'.format(
self.series_id, self.season_number, self.episode_number),
language=self._locale.language)
def _populate_external_ids(self):
return Request('tv/{0}/season/{1}/episode/{2}/external_ids'.format(
self.series_id, self.season_number, self.episode_number))
def _populate_images(self):
kwargs = {}
if not self._locale.fallthrough:
kwargs['language'] = self._locale.language
return Request('tv/{0}/season/{1}/episode/{2}/images'.format(
self.series_id, self.season_number, self.episode_number), **kwargs)
cast = Datalist('cast', handler=Cast,
poller=_populate_cast, sort='order')
guest_stars = Datalist('guest_stars', handler=Cast,
poller=_populate_cast, sort='order')
crew = Datalist('crew', handler=Crew, poller=_populate_cast)
imdb_id = Datapoint('imdb_id', poller=_populate_external_ids)
freebase_id = Datapoint('freebase_id', poller=_populate_external_ids)
freebase_mid = Datapoint('freebase_mid', poller=_populate_external_ids)
tvdb_id = Datapoint('tvdb_id', poller=_populate_external_ids)
tvrage_id = Datapoint('tvrage_id', poller=_populate_external_ids)
stills = Datalist('stills', handler=Backdrop, poller=_populate_images, sort=True)
class Season(NameRepr, Element):
season_number = Datapoint('season_number', initarg=2)
series_id = Datapoint('series_id', initarg=1)
id = Datapoint('id')
air_date = Datapoint('air_date', handler=process_date)
poster = Datapoint('poster_path', handler=Poster, raw=False, default=None)
overview = Datapoint('overview')
name = Datapoint('name')
episodes = Datadict('episodes', attr='episode_number', handler=Episode,
passthrough={'series_id': 'series_id', 'season_number': 'season_number'})
def _populate(self):
return Request('tv/{0}/season/{1}'.format(self.series_id, self.season_number),
language=self._locale.language)
def _populate_images(self):
kwargs = {}
if not self._locale.fallthrough:
kwargs['language'] = self._locale.language
return Request('tv/{0}/season/{1}/images'.format(self.series_id, self.season_number), **kwargs)
def _populate_external_ids(self):
return Request('tv/{0}/season/{1}/external_ids'.format(self.series_id, self.season_number))
posters = Datalist('posters', handler=Poster,
poller=_populate_images, sort=True)
freebase_id = Datapoint('freebase_id', poller=_populate_external_ids)
freebase_mid = Datapoint('freebase_mid', poller=_populate_external_ids)
tvdb_id = Datapoint('tvdb_id', poller=_populate_external_ids)
tvrage_id = Datapoint('tvrage_id', poller=_populate_external_ids)
class Series(NameRepr, Element):
id = Datapoint('id', initarg=1)
backdrop = Datapoint('backdrop_path', handler=Backdrop, raw=False, default=None)
authors = Datalist('created_by', handler=Person)
episode_run_times = Datalist('episode_run_time')
first_air_date = Datapoint('first_air_date', handler=process_date)
last_air_date = Datapoint('last_air_date', handler=process_date)
genres = Datalist('genres', handler=Genre)
homepage = Datapoint('homepage')
in_production = Datapoint('in_production')
languages = Datalist('languages')
origin_countries = Datalist('origin_country')
name = Datapoint('name')
original_name = Datapoint('original_name')
number_of_episodes = Datapoint('number_of_episodes')
number_of_seasons = Datapoint('number_of_seasons')
overview = Datapoint('overview')
popularity = Datapoint('popularity')
status = Datapoint('status')
userrating = Datapoint('vote_average')
votes = Datapoint('vote_count')
poster = Datapoint('poster_path', handler=Poster, raw=False, default=None)
networks = Datalist('networks', handler=Network)
seasons = Datadict('seasons', attr='season_number', handler=Season, passthrough={'id': 'series_id'})
def _populate(self):
return Request('tv/{0}'.format(self.id),
language=self._locale.language)
def _populate_cast(self):
return Request('tv/{0}/credits'.format(self.id))
def _populate_images(self):
kwargs = {}
if not self._locale.fallthrough:
kwargs['language'] = self._locale.language
return Request('tv/{0}/images'.format(self.id), **kwargs)
def _populate_external_ids(self):
return Request('tv/{0}/external_ids'.format(self.id))
cast = Datalist('cast', handler=Cast,
poller=_populate_cast, sort='order')
crew = Datalist('crew', handler=Crew, poller=_populate_cast)
backdrops = Datalist('backdrops', handler=Backdrop,
poller=_populate_images, sort=True)
posters = Datalist('posters', handler=Poster,
poller=_populate_images, sort=True)
imdb_id = Datapoint('imdb_id', poller=_populate_external_ids)
freebase_id = Datapoint('freebase_id', poller=_populate_external_ids)
freebase_mid = Datapoint('freebase_mid', poller=_populate_external_ids)
tvdb_id = Datapoint('tvdb_id', poller=_populate_external_ids)
tvrage_id = Datapoint('tvrage_id', poller=_populate_external_ids)

35
libs/tmdb3/tmdb_auth.py

@ -11,7 +11,7 @@
from datetime import datetime as _pydatetime, \ from datetime import datetime as _pydatetime, \
tzinfo as _pytzinfo tzinfo as _pytzinfo
import re import re
class datetime( _pydatetime ): class datetime(_pydatetime):
"""Customized datetime class with ISO format parsing.""" """Customized datetime class with ISO format parsing."""
_reiso = re.compile('(?P<year>[0-9]{4})' _reiso = re.compile('(?P<year>[0-9]{4})'
'-(?P<month>[0-9]{1,2})' '-(?P<month>[0-9]{1,2})'
@ -27,21 +27,27 @@ class datetime( _pydatetime ):
'(?P<tzmin>[0-9]{2})?' '(?P<tzmin>[0-9]{2})?'
')?') ')?')
class _tzinfo( _pytzinfo): class _tzinfo(_pytzinfo):
def __init__(self, direc='+', hr=0, min=0): def __init__(self, direc='+', hr=0, min=0):
if direc == '-': if direc == '-':
hr = -1*int(hr) hr = -1*int(hr)
self._offset = timedelta(hours=int(hr), minutes=int(min)) self._offset = timedelta(hours=int(hr), minutes=int(min))
def utcoffset(self, dt): return self._offset
def tzname(self, dt): return '' def utcoffset(self, dt):
def dst(self, dt): return timedelta(0) return self._offset
def tzname(self, dt):
return ''
def dst(self, dt):
return timedelta(0)
@classmethod @classmethod
def fromIso(cls, isotime, sep='T'): def fromIso(cls, isotime, sep='T'):
match = cls._reiso.match(isotime) match = cls._reiso.match(isotime)
if match is None: if match is None:
raise TypeError("time data '%s' does not match ISO 8601 format" \ raise TypeError("time data '%s' does not match ISO 8601 format"
% isotime) % isotime)
dt = [int(a) for a in match.groups()[:5]] dt = [int(a) for a in match.groups()[:5]]
if match.group('sec') is not None: if match.group('sec') is not None:
@ -52,9 +58,9 @@ class datetime( _pydatetime ):
if match.group('tz') == 'Z': if match.group('tz') == 'Z':
tz = cls._tzinfo() tz = cls._tzinfo()
elif match.group('tzmin'): elif match.group('tzmin'):
tz = cls._tzinfo(*match.group('tzdirec','tzhour','tzmin')) tz = cls._tzinfo(*match.group('tzdirec', 'tzhour', 'tzmin'))
else: else:
tz = cls._tzinfo(*match.group('tzdirec','tzhour')) tz = cls._tzinfo(*match.group('tzdirec', 'tzhour'))
dt.append(0) dt.append(0)
dt.append(tz) dt.append(tz)
return cls(*dt) return cls(*dt)
@ -64,10 +70,12 @@ from tmdb_exceptions import *
syssession = None syssession = None
def set_session(sessionid): def set_session(sessionid):
global syssession global syssession
syssession = Session(sessionid) syssession = Session(sessionid)
def get_session(sessionid=None): def get_session(sessionid=None):
global syssession global syssession
if sessionid: if sessionid:
@ -77,8 +85,8 @@ def get_session(sessionid=None):
else: else:
return Session.new() return Session.new()
class Session( object ):
class Session(object):
@classmethod @classmethod
def new(cls): def new(cls):
return cls(None) return cls(None)
@ -91,9 +99,9 @@ class Session( object ):
if self._sessionid is None: if self._sessionid is None:
if self._authtoken is None: if self._authtoken is None:
raise TMDBError("No Auth Token to produce Session for") raise TMDBError("No Auth Token to produce Session for")
# TODO: check authtokenexpiration against current time # TODO: check authtoken expiration against current time
req = Request('authentication/session/new', \ req = Request('authentication/session/new',
request_token=self._authtoken) request_token=self._authtoken)
req.lifetime = 0 req.lifetime = 0
dat = req.readJSON() dat = req.readJSON()
if not dat['success']: if not dat['success']:
@ -128,4 +136,3 @@ class Session( object ):
@property @property
def callbackurl(self): def callbackurl(self):
return "http://www.themoviedb.org/authenticate/"+self._authtoken return "http://www.themoviedb.org/authenticate/"+self._authtoken

90
libs/tmdb3/tmdb_exceptions.py

@ -6,23 +6,24 @@
# Author: Raymond Wagner # Author: Raymond Wagner
#----------------------- #-----------------------
class TMDBError( Exception ):
Error = 0 class TMDBError(Exception):
KeyError = 10 Error = 0
KeyMissing = 20 KeyError = 10
KeyInvalid = 30 KeyMissing = 20
KeyRevoked = 40 KeyInvalid = 30
RequestError = 50 KeyRevoked = 40
RequestInvalid = 51 RequestError = 50
PagingIssue = 60 RequestInvalid = 51
CacheError = 70 PagingIssue = 60
CacheReadError = 71 CacheError = 70
CacheWriteError = 72 CacheReadError = 71
CacheDirectoryError = 73 CacheWriteError = 72
ImageSizeError = 80 CacheDirectoryError = 73
HTTPError = 90 ImageSizeError = 80
Offline = 100 HTTPError = 90
LocaleError = 110 Offline = 100
LocaleError = 110
def __init__(self, msg=None, errno=0): def __init__(self, msg=None, errno=0):
self.errno = errno self.errno = errno
@ -30,60 +31,77 @@ class TMDBError( Exception ):
self.errno = getattr(self, 'TMDB'+self.__class__.__name__, errno) self.errno = getattr(self, 'TMDB'+self.__class__.__name__, errno)
self.args = (msg,) self.args = (msg,)
class TMDBKeyError( TMDBError ):
class TMDBKeyError(TMDBError):
pass pass
class TMDBKeyMissing( TMDBKeyError ):
class TMDBKeyMissing(TMDBKeyError):
pass pass
class TMDBKeyInvalid( TMDBKeyError ):
class TMDBKeyInvalid(TMDBKeyError):
pass pass
class TMDBKeyRevoked( TMDBKeyInvalid ):
class TMDBKeyRevoked(TMDBKeyInvalid):
pass pass
class TMDBRequestError( TMDBError ):
class TMDBRequestError(TMDBError):
pass pass
class TMDBRequestInvalid( TMDBRequestError ):
class TMDBRequestInvalid(TMDBRequestError):
pass pass
class TMDBPagingIssue( TMDBRequestError ):
class TMDBPagingIssue(TMDBRequestError):
pass pass
class TMDBCacheError( TMDBRequestError ):
class TMDBCacheError(TMDBRequestError):
pass pass
class TMDBCacheReadError( TMDBCacheError ):
class TMDBCacheReadError(TMDBCacheError):
def __init__(self, filename): def __init__(self, filename):
super(TMDBCacheReadError, self).__init__( super(TMDBCacheReadError, self).__init__(
"User does not have permission to access cache file: {0}.".format(filename)) "User does not have permission to access cache file: {0}."\
.format(filename))
self.filename = filename self.filename = filename
class TMDBCacheWriteError( TMDBCacheError ):
class TMDBCacheWriteError(TMDBCacheError):
def __init__(self, filename): def __init__(self, filename):
super(TMDBCacheWriteError, self).__init__( super(TMDBCacheWriteError, self).__init__(
"User does not have permission to write cache file: {0}.".format(filename)) "User does not have permission to write cache file: {0}."\
.format(filename))
self.filename = filename self.filename = filename
class TMDBCacheDirectoryError( TMDBCacheError ):
class TMDBCacheDirectoryError(TMDBCacheError):
def __init__(self, filename): def __init__(self, filename):
super(TMDBCacheDirectoryError, self).__init__( super(TMDBCacheDirectoryError, self).__init__(
"Directory containing cache file does not exist: {0}.".format(filename)) "Directory containing cache file does not exist: {0}."\
.format(filename))
self.filename = filename self.filename = filename
class TMDBImageSizeError( TMDBError ):
class TMDBImageSizeError(TMDBError ):
pass pass
class TMDBHTTPError( TMDBError ):
class TMDBHTTPError(TMDBError):
def __init__(self, err): def __init__(self, err):
self.httperrno = err.code self.httperrno = err.code
self.response = err.fp.read() self.response = err.fp.read()
super(TMDBHTTPError, self).__init__(str(err)) super(TMDBHTTPError, self).__init__(str(err))
class TMDBOffline( TMDBError ):
pass
class TMDBLocaleError( TMDBError ): class TMDBOffline(TMDBError):
pass pass
class TMDBLocaleError(TMDBError):
pass

201
libs/tmdb3/util.py

@ -10,13 +10,15 @@ from copy import copy
from locales import get_locale from locales import get_locale
from tmdb_auth import get_session from tmdb_auth import get_session
class NameRepr( object ):
class NameRepr(object):
"""Mixin for __repr__ methods using 'name' attribute.""" """Mixin for __repr__ methods using 'name' attribute."""
def __repr__(self): def __repr__(self):
return u"<{0.__class__.__name__} '{0.name}'>"\ return u"<{0.__class__.__name__} '{0.name}'>"\
.format(self).encode('utf-8') .format(self).encode('utf-8')
class SearchRepr( object ): class SearchRepr(object):
""" """
Mixin for __repr__ methods for classes with '_name' and Mixin for __repr__ methods for classes with '_name' and
'_request' attributes. '_request' attributes.
@ -25,10 +27,11 @@ class SearchRepr( object ):
name = self._name if self._name else self._request._kwargs['query'] name = self._name if self._name else self._request._kwargs['query']
return u"<Search Results: {0}>".format(name).encode('utf-8') return u"<Search Results: {0}>".format(name).encode('utf-8')
class Poller( object ):
class Poller(object):
""" """
Wrapper for an optional callable to populate an Element derived class Wrapper for an optional callable to populate an Element derived
with raw data, or data from a Request. class with raw data, or data from a Request.
""" """
def __init__(self, func, lookup, inst=None): def __init__(self, func, lookup, inst=None):
self.func = func self.func = func
@ -60,7 +63,7 @@ class Poller( object ):
if not callable(self.func): if not callable(self.func):
raise RuntimeError('Poller object called without a source function') raise RuntimeError('Poller object called without a source function')
req = self.func() req = self.func()
if (('language' in req._kwargs) or ('country' in req._kwargs)) \ if ('language' in req._kwargs) or ('country' in req._kwargs) \
and self.inst._locale.fallthrough: and self.inst._locale.fallthrough:
# request specifies a locale filter, and fallthrough is enabled # request specifies a locale filter, and fallthrough is enabled
# run a first pass with specified filter # run a first pass with specified filter
@ -79,7 +82,7 @@ class Poller( object ):
def apply(self, data, set_nones=True): def apply(self, data, set_nones=True):
# apply data directly, bypassing callable function # apply data directly, bypassing callable function
unfilled = False unfilled = False
for k,v in self.lookup.items(): for k, v in self.lookup.items():
if (k in data) and \ if (k in data) and \
((data[k] is not None) if callable(self.func) else True): ((data[k] is not None) if callable(self.func) else True):
# argument received data, populate it # argument received data, populate it
@ -100,32 +103,38 @@ class Poller( object ):
unfilled = True unfilled = True
return unfilled return unfilled
class Data( object ):
class Data(object):
""" """
Basic response definition class Basic response definition class
This maps to a single key in a JSON dictionary received from the API This maps to a single key in a JSON dictionary received from the API
""" """
def __init__(self, field, initarg=None, handler=None, poller=None, def __init__(self, field, initarg=None, handler=None, poller=None,
raw=True, default=u'', lang=False): raw=True, default=u'', lang=None, passthrough={}):
""" """
This defines how the dictionary value is to be processed by the poller This defines how the dictionary value is to be processed by the
field -- defines the dictionary key that filters what data this uses poller
initarg -- (optional) specifies that this field must be supplied field -- defines the dictionary key that filters what data
when creating a new instance of the Element class this this uses
definition is mapped to. Takes an integer for the order initarg -- (optional) specifies that this field must be
it should be used in the input arguments supplied when creating a new instance of the Element
handler -- (optional) callable used to process the received value class this definition is mapped to. Takes an integer
before being stored in the Element object. for the order it should be used in the input
poller -- (optional) callable to be used if data is requested and arguments
this value has not yet been defined. the callable should handler -- (optional) callable used to process the received
return a dictionary of data from a JSON query. many value before being stored in the Element object.
definitions may share a single poller, which will be poller -- (optional) callable to be used if data is requested
and the data used to populate all referenced definitions and this value has not yet been defined. the
based off their defined field callable should return a dictionary of data from a
raw -- (optional) if the specified handler is an Element class, JSON query. many definitions may share a single
the data will be passed into it using the 'raw' keyword poller, which will be and the data used to populate
attribute. setting this to false will force the data to all referenced definitions based off their defined
instead be passed in as the first argument field
raw -- (optional) if the specified handler is an Element
class, the data will be passed into it using the
'raw' keyword attribute. setting this to false
will force the data to instead be passed in as the
first argument
""" """
self.field = field self.field = field
self.initarg = initarg self.initarg = initarg
@ -133,6 +142,7 @@ class Data( object ):
self.raw = raw self.raw = raw
self.default = default self.default = default
self.sethandler(handler) self.sethandler(handler)
self.passthrough = passthrough
def __get__(self, inst, owner): def __get__(self, inst, owner):
if inst is None: if inst is None:
@ -151,6 +161,9 @@ class Data( object ):
if isinstance(value, Element): if isinstance(value, Element):
value._locale = inst._locale value._locale = inst._locale
value._session = inst._session value._session = inst._session
for source, dest in self.passthrough:
setattr(value, dest, getattr(inst, source))
inst._data[self.field] = value inst._data[self.field] = value
def sethandler(self, handler): def sethandler(self, handler):
@ -162,37 +175,44 @@ class Data( object ):
else: else:
self.handler = lambda x: handler(x) self.handler = lambda x: handler(x)
class Datapoint( Data ):
class Datapoint(Data):
pass pass
class Datalist( Data ):
class Datalist(Data):
""" """
Response definition class for list data Response definition class for list data
This maps to a key in a JSON dictionary storing a list of data This maps to a key in a JSON dictionary storing a list of data
""" """
def __init__(self, field, handler=None, poller=None, sort=None, raw=True): def __init__(self, field, handler=None, poller=None, sort=None, raw=True, passthrough={}):
""" """
This defines how the dictionary value is to be processed by the poller This defines how the dictionary value is to be processed by the
field -- defines the dictionary key that filters what data this uses poller
handler -- (optional) callable used to process the received value field -- defines the dictionary key that filters what data
before being stored in the Element object. this uses
poller -- (optional) callable to be used if data is requested and handler -- (optional) callable used to process the received
this value has not yet been defined. the callable should value before being stored in the Element object.
return a dictionary of data from a JSON query. many poller -- (optional) callable to be used if data is requested
definitions may share a single poller, which will be and this value has not yet been defined. the
and the data used to populate all referenced definitions callable should return a dictionary of data from a
based off their defined field JSON query. many definitions may share a single
sort -- (optional) name of attribute in resultant data to be used poller, which will be and the data used to populate
to sort the list after processing. this effectively all referenced definitions based off their defined
a handler be defined to process the data into something field
that has attributes sort -- (optional) name of attribute in resultant data to be
raw -- (optional) if the specified handler is an Element class, used to sort the list after processing. this
the data will be passed into it using the 'raw' keyword effectively requires a handler be defined to process
attribute. setting this to false will force the data to the data into something that has attributes
instead be passed in as the first argument raw -- (optional) if the specified handler is an Element
class, the data will be passed into it using the
'raw' keyword attribute. setting this to false will
force the data to instead be passed in as the first
argument
""" """
super(Datalist, self).__init__(field, None, handler, poller, raw) super(Datalist, self).__init__(field, None, handler, poller, raw, passthrough=passthrough)
self.sort = sort self.sort = sort
def __set__(self, inst, value): def __set__(self, inst, value):
data = [] data = []
if value: if value:
@ -201,6 +221,10 @@ class Datalist( Data ):
if isinstance(val, Element): if isinstance(val, Element):
val._locale = inst._locale val._locale = inst._locale
val._session = inst._session val._session = inst._session
for source, dest in self.passthrough.items():
setattr(val, dest, getattr(inst, source))
data.append(val) data.append(val)
if self.sort: if self.sort:
if self.sort is True: if self.sort is True:
@ -209,45 +233,52 @@ class Datalist( Data ):
data.sort(key=lambda x: getattr(x, self.sort)) data.sort(key=lambda x: getattr(x, self.sort))
inst._data[self.field] = data inst._data[self.field] = data
class Datadict( Data ):
class Datadict(Data):
""" """
Response definition class for dictionary data Response definition class for dictionary data
This maps to a key in a JSON dictionary storing a dictionary of data This maps to a key in a JSON dictionary storing a dictionary of data
""" """
def __init__(self, field, handler=None, poller=None, raw=True, def __init__(self, field, handler=None, poller=None, raw=True,
key=None, attr=None): key=None, attr=None, passthrough={}):
""" """
This defines how the dictionary value is to be processed by the poller This defines how the dictionary value is to be processed by the
field -- defines the dictionary key that filters what data this uses poller
handler -- (optional) callable used to process the received value field -- defines the dictionary key that filters what data
before being stored in the Element object. this uses
poller -- (optional) callable to be used if data is requested and handler -- (optional) callable used to process the received
this value has not yet been defined. the callable should value before being stored in the Element object.
return a dictionary of data from a JSON query. many poller -- (optional) callable to be used if data is requested
definitions may share a single poller, which will be and this value has not yet been defined. the
and the data used to populate all referenced definitions callable should return a dictionary of data from a
based off their defined field JSON query. many definitions may share a single
key -- (optional) name of key in resultant data to be used as poller, which will be and the data used to populate
the key in the stored dictionary. if this is not the all referenced definitions based off their defined
field name from the source data is used instead field
attr -- (optional) name of attribute in resultant data to be used key -- (optional) name of key in resultant data to be used
as the key in the stored dictionary. if this is not as the key in the stored dictionary. if this is not
the field name from the source data is used instead the field name from the source data is used instead
raw -- (optional) if the specified handler is an Element class, attr -- (optional) name of attribute in resultant data to be
the data will be passed into it using the 'raw' keyword used as the key in the stored dictionary. if this is
attribute. setting this to false will force the data to not the field name from the source data is used
instead be passed in as the first argument instead
raw -- (optional) if the specified handler is an Element
class, the data will be passed into it using the
'raw' keyword attribute. setting this to false will
force the data to instead be passed in as the first
argument
""" """
if key and attr: if key and attr:
raise TypeError("`key` and `attr` cannot both be defined") raise TypeError("`key` and `attr` cannot both be defined")
super(Datadict, self).__init__(field, None, handler, poller, raw) super(Datadict, self).__init__(field, None, handler, poller, raw, passthrough=passthrough)
if key: if key:
self.getkey = lambda x: x[key] self.getkey = lambda x: x[key]
elif attr: elif attr:
self.getkey = lambda x: getattr(x, attr) self.getkey = lambda x: getattr(x, attr)
else: else:
raise TypeError("Datadict requires `key` or `attr` be defined "+\ raise TypeError("Datadict requires `key` or `attr` be defined " +
"for populating the dictionary") "for populating the dictionary")
def __set__(self, inst, value): def __set__(self, inst, value):
data = {} data = {}
if value: if value:
@ -256,6 +287,10 @@ class Datadict( Data ):
if isinstance(val, Element): if isinstance(val, Element):
val._locale = inst._locale val._locale = inst._locale
val._session = inst._session val._session = inst._session
for source, dest in self.passthrough.items():
setattr(val, dest, getattr(inst, source))
data[self.getkey(val)] = val data[self.getkey(val)] = val
inst._data[self.field] = data inst._data[self.field] = data
@ -286,7 +321,7 @@ class ElementType( type ):
# extract copies of each defined Poller function # extract copies of each defined Poller function
# from parent classes # from parent classes
pollers[k] = attr.func pollers[k] = attr.func
for k,attr in attrs.items(): for k, attr in attrs.items():
if isinstance(attr, Data): if isinstance(attr, Data):
data[k] = attr data[k] = attr
if '_populate' in attrs: if '_populate' in attrs:
@ -295,9 +330,9 @@ class ElementType( type ):
# process all defined Data attribues, testing for use as an initial # process all defined Data attribues, testing for use as an initial
# argument, and building a list of what Pollers are used to populate # argument, and building a list of what Pollers are used to populate
# which Data points # which Data points
pollermap = dict([(k,[]) for k in pollers]) pollermap = dict([(k, []) for k in pollers])
initargs = [] initargs = []
for k,v in data.items(): for k, v in data.items():
v.name = k v.name = k
if v.initarg: if v.initarg:
initargs.append(v) initargs.append(v)
@ -313,7 +348,7 @@ class ElementType( type ):
# wrap each used poller function with a Poller class, and push into # wrap each used poller function with a Poller class, and push into
# the new class attributes # the new class attributes
for k,v in pollermap.items(): for k, v in pollermap.items():
if len(v) == 0: if len(v) == 0:
continue continue
lookup = dict([(attr.field, attr.name) for attr in v]) lookup = dict([(attr.field, attr.name) for attr in v])
@ -326,8 +361,8 @@ class ElementType( type ):
attrs[attr.name] = attr attrs[attr.name] = attr
# build sorted list of arguments used for intialization # build sorted list of arguments used for intialization
attrs['_InitArgs'] = tuple([a.name for a in \ attrs['_InitArgs'] = tuple(
sorted(initargs, key=lambda x: x.initarg)]) [a.name for a in sorted(initargs, key=lambda x: x.initarg)])
return type.__new__(mcs, name, bases, attrs) return type.__new__(mcs, name, bases, attrs)
def __call__(cls, *args, **kwargs): def __call__(cls, *args, **kwargs):
@ -346,21 +381,23 @@ class ElementType( type ):
if 'raw' in kwargs: if 'raw' in kwargs:
# if 'raw' keyword is supplied, create populate object manually # if 'raw' keyword is supplied, create populate object manually
if len(args) != 0: if len(args) != 0:
raise TypeError('__init__() takes exactly 2 arguments (1 given)') raise TypeError(
'__init__() takes exactly 2 arguments (1 given)')
obj._populate.apply(kwargs['raw'], False) obj._populate.apply(kwargs['raw'], False)
else: else:
# if not, the number of input arguments must exactly match that # if not, the number of input arguments must exactly match that
# defined by the Data definitions # defined by the Data definitions
if len(args) != len(cls._InitArgs): if len(args) != len(cls._InitArgs):
raise TypeError('__init__() takes exactly {0} arguments ({1} given)'\ raise TypeError(
'__init__() takes exactly {0} arguments ({1} given)'\
.format(len(cls._InitArgs)+1, len(args)+1)) .format(len(cls._InitArgs)+1, len(args)+1))
for a,v in zip(cls._InitArgs, args): for a, v in zip(cls._InitArgs, args):
setattr(obj, a, v) setattr(obj, a, v)
obj.__init__() obj.__init__()
return obj return obj
class Element( object ): class Element( object ):
__metaclass__ = ElementType __metaclass__ = ElementType
_lang = 'en' _lang = 'en'

Loading…
Cancel
Save