17 changed files with 2829 additions and 853 deletions
@ -1,740 +0,0 @@ |
|||||
#!/usr/bin/env python |
|
||||
#-*- coding:utf-8 -*- |
|
||||
#author:doganaydin /// forked from dbr/Ben |
|
||||
#project:themoviedb |
|
||||
#repository:http://github.com/doganaydin/themoviedb |
|
||||
#license: LGPLv2 http://www.gnu.org/licenses/lgpl.html |
|
||||
|
|
||||
"""An interface to the themoviedb.org API""" |
|
||||
|
|
||||
__author__ = "doganaydin" |
|
||||
__version__ = "0.5" |
|
||||
|
|
||||
|
|
||||
config = {} |
|
||||
|
|
||||
def configure(api_key): |
|
||||
config['apikey'] = api_key |
|
||||
config['urls'] = {} |
|
||||
config['urls']['movie.search'] = "http://api.themoviedb.org/2.1/Movie.search/en/xml/%(apikey)s/%%s" % (config) |
|
||||
config['urls']['movie.getInfo'] = "http://api.themoviedb.org/2.1/Movie.getInfo/en/xml/%(apikey)s/%%s" % (config) |
|
||||
config['urls']['media.getInfo'] = "http://api.themoviedb.org/2.1/Media.getInfo/en/xml/%(apikey)s/%%s/%%s" % (config) |
|
||||
config['urls']['imdb.lookUp'] = "http://api.themoviedb.org/2.1/Movie.imdbLookup/en/xml/%(apikey)s/%%s" % (config) |
|
||||
config['urls']['movie.browse'] = "http://api.themoviedb.org/2.1/Movie.browse/en-US/xml/%(apikey)s?%%s" % (config) |
|
||||
|
|
||||
import os, struct, urllib, urllib2, xml.etree.cElementTree as ElementTree |
|
||||
|
|
||||
class TmdBaseError(Exception): |
|
||||
pass |
|
||||
|
|
||||
class TmdNoResults(TmdBaseError): |
|
||||
pass |
|
||||
|
|
||||
class TmdHttpError(TmdBaseError): |
|
||||
pass |
|
||||
|
|
||||
class TmdXmlError(TmdBaseError): |
|
||||
pass |
|
||||
|
|
||||
class TmdConfigError(TmdBaseError): |
|
||||
pass |
|
||||
|
|
||||
def opensubtitleHashFile(name): |
|
||||
"""Hashes a file using OpenSubtitle's method. |
|
||||
> In natural language it calculates: size + 64bit chksum of the first and |
|
||||
> last 64k (even if they overlap because the file is smaller than 128k). |
|
||||
A slightly more Pythonic version of the Python solution on.. |
|
||||
http://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes |
|
||||
""" |
|
||||
longlongformat = 'q' |
|
||||
bytesize = struct.calcsize(longlongformat) |
|
||||
|
|
||||
f = open(name, "rb") |
|
||||
|
|
||||
filesize = os.path.getsize(name) |
|
||||
fhash = filesize |
|
||||
|
|
||||
if filesize < 65536 * 2: |
|
||||
raise ValueError("File size must be larger than %s bytes (is %s)" % (65536 * 2, filesize)) |
|
||||
|
|
||||
for x in range(65536 / bytesize): |
|
||||
buf = f.read(bytesize) |
|
||||
(l_value,) = struct.unpack(longlongformat, buf) |
|
||||
fhash += l_value |
|
||||
fhash = fhash & 0xFFFFFFFFFFFFFFFF # to remain as 64bit number |
|
||||
|
|
||||
f.seek(max(0, filesize - 65536), 0) |
|
||||
for x in range(65536 / bytesize): |
|
||||
buf = f.read(bytesize) |
|
||||
(l_value,) = struct.unpack(longlongformat, buf) |
|
||||
fhash += l_value |
|
||||
fhash = fhash & 0xFFFFFFFFFFFFFFFF |
|
||||
|
|
||||
f.close() |
|
||||
return "%016x" % fhash |
|
||||
|
|
||||
class XmlHandler: |
|
||||
"""Deals with retrieval of XML files from API""" |
|
||||
def __init__(self, url): |
|
||||
self.url = url |
|
||||
|
|
||||
def _grabUrl(self, url): |
|
||||
try: |
|
||||
urlhandle = urllib2.urlopen(url) |
|
||||
except IOError, errormsg: |
|
||||
raise TmdHttpError(errormsg) |
|
||||
if urlhandle.code >= 400: |
|
||||
raise TmdHttpError("HTTP status code was %d" % urlhandle.code) |
|
||||
return urlhandle.read() |
|
||||
|
|
||||
def getEt(self): |
|
||||
xml = self._grabUrl(self.url) |
|
||||
try: |
|
||||
et = ElementTree.fromstring(xml) |
|
||||
except SyntaxError, errormsg: |
|
||||
raise TmdXmlError(errormsg) |
|
||||
return et |
|
||||
|
|
||||
class SearchResults(list): |
|
||||
"""Stores a list of Movie's that matched the search""" |
|
||||
def __repr__(self): |
|
||||
return "<Search results: %s>" % (list.__repr__(self)) |
|
||||
|
|
||||
class MovieResult(dict): |
|
||||
"""A dict containing the information about a specific search result""" |
|
||||
def __repr__(self): |
|
||||
return "<MovieResult: %s (%s)>" % (self.get("name"), self.get("released")) |
|
||||
|
|
||||
def info(self): |
|
||||
"""Performs a MovieDb.getMovieInfo search on the current id, returns |
|
||||
a Movie object |
|
||||
""" |
|
||||
cur_id = self['id'] |
|
||||
info = MovieDb().getMovieInfo(cur_id) |
|
||||
return info |
|
||||
|
|
||||
class Movie(dict): |
|
||||
"""A dict containing the information about the film""" |
|
||||
def __repr__(self): |
|
||||
return "<MovieResult: %s (%s)>" % (self.get("name"), self.get("released")) |
|
||||
|
|
||||
class Categories(dict): |
|
||||
"""Stores category information""" |
|
||||
def set(self, category_et): |
|
||||
"""Takes an elementtree Element ('category') and stores the url, |
|
||||
using the type and name as the dict key. |
|
||||
For example: |
|
||||
<category type="genre" url="http://themoviedb.org/encyclopedia/category/80" name="Crime"/> |
|
||||
..becomes: |
|
||||
categories['genre']['Crime'] = 'http://themoviedb.org/encyclopedia/category/80' |
|
||||
""" |
|
||||
_type = category_et.get("type") |
|
||||
name = category_et.get("name") |
|
||||
url = category_et.get("url") |
|
||||
self.setdefault(_type, {})[name] = url |
|
||||
self[_type][name] = url |
|
||||
|
|
||||
class Studios(dict): |
|
||||
"""Stores category information""" |
|
||||
def set(self, studio_et): |
|
||||
"""Takes an elementtree Element ('studio') and stores the url, |
|
||||
using the name as the dict key. |
|
||||
For example: |
|
||||
<studio url="http://www.themoviedb.org/encyclopedia/company/20" name="Miramax Films"/> |
|
||||
..becomes: |
|
||||
studios['name'] = 'http://www.themoviedb.org/encyclopedia/company/20' |
|
||||
""" |
|
||||
name = studio_et.get("name") |
|
||||
url = studio_et.get("url") |
|
||||
self[name] = url |
|
||||
|
|
||||
class Countries(dict): |
|
||||
"""Stores country information""" |
|
||||
def set(self, country_et): |
|
||||
"""Takes an elementtree Element ('country') and stores the url, |
|
||||
using the name and code as the dict key. |
|
||||
For example: |
|
||||
<country url="http://www.themoviedb.org/encyclopedia/country/223" name="United States of America" code="US"/> |
|
||||
..becomes: |
|
||||
countries['code']['name'] = 'http://www.themoviedb.org/encyclopedia/country/223' |
|
||||
""" |
|
||||
code = country_et.get("code") |
|
||||
name = country_et.get("name") |
|
||||
url = country_et.get("url") |
|
||||
self.setdefault(code, {})[name] = url |
|
||||
|
|
||||
class Image(dict): |
|
||||
"""Stores image information for a single poster/backdrop (includes |
|
||||
multiple sizes) |
|
||||
""" |
|
||||
def __init__(self, _id, _type, size, url): |
|
||||
self['id'] = _id |
|
||||
self['type'] = _type |
|
||||
|
|
||||
def largest(self): |
|
||||
for csize in ["original", "mid", "cover", "thumb"]: |
|
||||
if csize in self: |
|
||||
return csize |
|
||||
|
|
||||
def __repr__(self): |
|
||||
return "<Image (%s for ID %s)>" % (self['type'], self['id']) |
|
||||
|
|
||||
class ImagesList(list): |
|
||||
"""Stores a list of Images, and functions to filter "only posters" etc""" |
|
||||
def set(self, image_et): |
|
||||
"""Takes an elementtree Element ('image') and stores the url, |
|
||||
along with the type, id and size. |
|
||||
Is a list containing each image as a dictionary (which includes the |
|
||||
various sizes) |
|
||||
For example: |
|
||||
<image type="poster" size="original" url="http://images.themoviedb.org/posters/4181/67926_sin-city-02-color_122_207lo.jpg" id="4181"/> |
|
||||
..becomes: |
|
||||
images[0] = {'id':4181', 'type': 'poster', 'original': 'http://images.themov...'} |
|
||||
""" |
|
||||
_type = image_et.get("type") |
|
||||
_id = image_et.get("id") |
|
||||
size = image_et.get("size") |
|
||||
url = image_et.get("url") |
|
||||
cur = self.find_by('id', _id) |
|
||||
if len(cur) == 0: |
|
||||
nimg = Image(_id = _id, _type = _type, size = size, url = url) |
|
||||
self.append(nimg) |
|
||||
elif len(cur) == 1: |
|
||||
cur[0][size] = url |
|
||||
else: |
|
||||
raise ValueError("Found more than one poster with id %s, this should never happen" % (_id)) |
|
||||
|
|
||||
def find_by(self, key, value): |
|
||||
ret = [] |
|
||||
for cur in self: |
|
||||
if cur[key] == value: |
|
||||
ret.append(cur) |
|
||||
return ret |
|
||||
|
|
||||
@property |
|
||||
def posters(self): |
|
||||
return self.find_by('type', 'poster') |
|
||||
|
|
||||
@property |
|
||||
def backdrops(self): |
|
||||
return self.find_by('type', 'backdrop') |
|
||||
|
|
||||
class CrewRoleList(dict): |
|
||||
"""Stores a list of roles, such as director, actor etc |
|
||||
>>> import tmdb |
|
||||
>>> tmdb.getMovieInfo(550)['cast'].keys()[:5] |
|
||||
['casting', 'producer', 'author', 'sound editor', 'actor'] |
|
||||
""" |
|
||||
pass |
|
||||
|
|
||||
class CrewList(list): |
|
||||
"""Stores list of crew in specific role |
|
||||
>>> import tmdb |
|
||||
>>> tmdb.getMovieInfo(550)['cast']['author'] |
|
||||
[<author (id 7468): Chuck Palahniuk>, <author (id 7469): Jim Uhls>] |
|
||||
""" |
|
||||
pass |
|
||||
|
|
||||
class Person(dict): |
|
||||
"""Stores information about a specific member of cast""" |
|
||||
def __init__(self, job, _id, name, character, url): |
|
||||
self['job'] = job |
|
||||
self['id'] = _id |
|
||||
self['name'] = name |
|
||||
self['character'] = character |
|
||||
self['url'] = url |
|
||||
|
|
||||
def __repr__(self): |
|
||||
if self['character'] is None or self['character'] == "": |
|
||||
return "<%(job)s (id %(id)s): %(name)s>" % self |
|
||||
else: |
|
||||
return "<%(job)s (id %(id)s): %(name)s (as %(character)s)>" % self |
|
||||
|
|
||||
class MovieDb: |
|
||||
"""Main interface to www.themoviedb.com |
|
||||
The search() method searches for the film by title. |
|
||||
The getMovieInfo() method retrieves information about a specific movie using themoviedb id. |
|
||||
""" |
|
||||
def _parseSearchResults(self, movie_element): |
|
||||
cur_movie = MovieResult() |
|
||||
cur_images = ImagesList() |
|
||||
for item in movie_element.getchildren(): |
|
||||
if item.tag.lower() == "images": |
|
||||
for subitem in item.getchildren(): |
|
||||
cur_images.set(subitem) |
|
||||
else: |
|
||||
cur_movie[item.tag] = item.text |
|
||||
cur_movie['images'] = cur_images |
|
||||
return cur_movie |
|
||||
|
|
||||
def _parseMovie(self, movie_element): |
|
||||
cur_movie = Movie() |
|
||||
cur_categories = Categories() |
|
||||
cur_studios = Studios() |
|
||||
cur_countries = Countries() |
|
||||
cur_images = ImagesList() |
|
||||
cur_cast = CrewRoleList() |
|
||||
for item in movie_element.getchildren(): |
|
||||
if item.tag.lower() == "categories": |
|
||||
for subitem in item.getchildren(): |
|
||||
cur_categories.set(subitem) |
|
||||
elif item.tag.lower() == "studios": |
|
||||
for subitem in item.getchildren(): |
|
||||
cur_studios.set(subitem) |
|
||||
elif item.tag.lower() == "countries": |
|
||||
for subitem in item.getchildren(): |
|
||||
cur_countries.set(subitem) |
|
||||
elif item.tag.lower() == "images": |
|
||||
for subitem in item.getchildren(): |
|
||||
cur_images.set(subitem) |
|
||||
elif item.tag.lower() == "cast": |
|
||||
for subitem in item.getchildren(): |
|
||||
job = subitem.get("job").lower() |
|
||||
p = Person( |
|
||||
job = job, |
|
||||
_id = subitem.get("id"), |
|
||||
name = subitem.get("name"), |
|
||||
character = subitem.get("character"), |
|
||||
url = subitem.get("url"), |
|
||||
) |
|
||||
cur_cast.setdefault(job, CrewList()).append(p) |
|
||||
else: |
|
||||
cur_movie[item.tag] = item.text |
|
||||
|
|
||||
cur_movie['categories'] = cur_categories |
|
||||
cur_movie['studios'] = cur_studios |
|
||||
cur_movie['countries'] = cur_countries |
|
||||
cur_movie['images'] = cur_images |
|
||||
cur_movie['cast'] = cur_cast |
|
||||
return cur_movie |
|
||||
|
|
||||
def search(self, title): |
|
||||
"""Searches for a film by its title. |
|
||||
Returns SearchResults (a list) containing all matches (Movie instances) |
|
||||
""" |
|
||||
title = urllib.quote(title.encode("utf-8")) |
|
||||
url = config['urls']['movie.search'] % (title) |
|
||||
etree = XmlHandler(url).getEt() |
|
||||
search_results = SearchResults() |
|
||||
for cur_result in etree.find("movies").findall("movie"): |
|
||||
cur_movie = self._parseSearchResults(cur_result) |
|
||||
search_results.append(cur_movie) |
|
||||
return search_results |
|
||||
|
|
||||
def getMovieInfo(self, id): |
|
||||
"""Returns movie info by it's TheMovieDb ID. |
|
||||
Returns a Movie instance |
|
||||
""" |
|
||||
url = config['urls']['movie.getInfo'] % (id) |
|
||||
etree = XmlHandler(url).getEt() |
|
||||
moviesTree = etree.find("movies").findall("movie") |
|
||||
|
|
||||
if len(moviesTree) == 0: |
|
||||
raise TmdNoResults("No results for id %s" % id) |
|
||||
return self._parseMovie(moviesTree[0]) |
|
||||
|
|
||||
def mediaGetInfo(self, hash, size): |
|
||||
"""Used to retrieve specific information about a movie but instead of |
|
||||
passing a TMDb ID, you pass a file hash and filesize in bytes |
|
||||
""" |
|
||||
url = config['urls']['media.getInfo'] % (hash, size) |
|
||||
etree = XmlHandler(url).getEt() |
|
||||
moviesTree = etree.find("movies").findall("movie") |
|
||||
if len(moviesTree) == 0: |
|
||||
raise TmdNoResults("No results for hash %s" % hash) |
|
||||
return [self._parseMovie(x) for x in moviesTree] |
|
||||
|
|
||||
def imdbLookup(self, id = 0, title = False): |
|
||||
if not config.get('apikey'): |
|
||||
raise TmdConfigError("API Key not set") |
|
||||
if id > 0: |
|
||||
url = config['urls']['imdb.lookUp'] % (id) |
|
||||
else: |
|
||||
_imdb_id = self.search(title)[0]["imdb_id"] |
|
||||
url = config['urls']['imdb.lookUp'] % (_imdb_id) |
|
||||
etree = XmlHandler(url).getEt() |
|
||||
lookup_results = SearchResults() |
|
||||
for cur_lookup in etree.find("movies").findall("movie"): |
|
||||
cur_movie = self._parseSearchResults(cur_lookup) |
|
||||
lookup_results.append(cur_movie) |
|
||||
return lookup_results |
|
||||
|
|
||||
class Browse: |
|
||||
|
|
||||
def __init__(self, params = {}): |
|
||||
""" |
|
||||
tmdb.Browse(params) |
|
||||
default params = {"order_by":"release","order":"desc"} |
|
||||
params = {"query":"some query","release_max":"1991",...} |
|
||||
all posible parameters = http://api.themoviedb.org/2.1/methods/Movie.browse |
|
||||
""" |
|
||||
if "order_by" not in params: |
|
||||
params.update({"order_by":"release"}) |
|
||||
if "order" not in params: |
|
||||
params.update({"order":"desc"}) |
|
||||
|
|
||||
self.params = urllib.urlencode(params) |
|
||||
self.movie = self.look(self.params) |
|
||||
|
|
||||
def look(self, look_for): |
|
||||
url = config['urls']['movie.browse'] % (look_for) |
|
||||
etree = XmlHandler(url).getEt() |
|
||||
look_results = SearchResults() |
|
||||
for cur_lookup in etree.find("movies").findall("movie"): |
|
||||
cur_movie = self._parseSearchResults(cur_lookup) |
|
||||
look_results.append(cur_movie) |
|
||||
return look_results |
|
||||
|
|
||||
def _parseSearchResults(self, movie_element): |
|
||||
cur_movie = MovieResult() |
|
||||
cur_images = ImagesList() |
|
||||
for item in movie_element.getchildren(): |
|
||||
if item.tag.lower() == "images": |
|
||||
for subitem in item.getchildren(): |
|
||||
cur_images.set(subitem) |
|
||||
else: |
|
||||
cur_movie[item.tag] = item.text |
|
||||
cur_movie['images'] = cur_images |
|
||||
return cur_movie |
|
||||
|
|
||||
def getTotal(self): |
|
||||
return len(self.movie) |
|
||||
|
|
||||
def getRating(self, i): |
|
||||
return self.movie[i]["rating"] |
|
||||
|
|
||||
def getVotes(self, i): |
|
||||
return self.movie[i]["votes"] |
|
||||
|
|
||||
def getName(self, i): |
|
||||
return self.movie[i]["name"] |
|
||||
|
|
||||
def getLanguage(self, i): |
|
||||
return self.movie[i]["language"] |
|
||||
|
|
||||
def getCertification(self, i): |
|
||||
return self.movie[i]["certification"] |
|
||||
|
|
||||
def getUrl(self, i): |
|
||||
return self.movie[i]["url"] |
|
||||
|
|
||||
def getOverview(self, i): |
|
||||
return self.movie[i]["overview"] |
|
||||
|
|
||||
def getPopularity(self, i): |
|
||||
return self.movie[i]["popularity"] |
|
||||
|
|
||||
def getOriginalName(self, i): |
|
||||
return self.movie[i]["original_name"] |
|
||||
|
|
||||
def getLastModified(self, i): |
|
||||
return self.movie[i]["last_modified_at"] |
|
||||
|
|
||||
def getImdbId(self, i): |
|
||||
return self.movie[i]["imdb_id"] |
|
||||
|
|
||||
def getReleased(self, i): |
|
||||
return self.movie[i]["released"] |
|
||||
|
|
||||
def getScore(self, i): |
|
||||
return self.movie[i]["score"] |
|
||||
|
|
||||
def getAdult(self, i): |
|
||||
return self.movie[i]["adult"] |
|
||||
|
|
||||
def getVersion(self, i): |
|
||||
return self.movie[i]["version"] |
|
||||
|
|
||||
def getTranslated(self, i): |
|
||||
return self.movie[i]["translated"] |
|
||||
|
|
||||
def getType(self, i): |
|
||||
return self.movie[i]["type"] |
|
||||
|
|
||||
def getId(self, i): |
|
||||
return self.movie[i]["id"] |
|
||||
|
|
||||
def getAlternativeName(self, i): |
|
||||
return self.movie[i]["alternative_name"] |
|
||||
|
|
||||
def getPoster(self, i, size): |
|
||||
if size == "thumb" or size == "t": |
|
||||
return self.movie[i]["images"][0]["thumb"] |
|
||||
elif size == "cover" or size == "c": |
|
||||
return self.movie[i]["images"][0]["cover"] |
|
||||
else: |
|
||||
return self.movie[i]["images"][0]["mid"] |
|
||||
|
|
||||
def getBackdrop(self, i, size): |
|
||||
if size == "poster" or size == "p": |
|
||||
return self.movie[i]["images"][1]["poster"] |
|
||||
else: |
|
||||
return self.movie[i]["images"][1]["thumb"] |
|
||||
|
|
||||
|
|
||||
|
|
||||
# Shortcuts for tmdb search method |
|
||||
# using: |
|
||||
# movie = tmdb.tmdb("Sin City") |
|
||||
# print movie.getRating -> 7.0 |
|
||||
class tmdb: |
|
||||
|
|
||||
def __init__(self, name): |
|
||||
"""Convenience wrapper for MovieDb.search - so you can do.. |
|
||||
>>> import tmdb |
|
||||
>>> movie = tmdb.tmdb("Fight Club") |
|
||||
>>> ranking = movie.getRanking() or votes = movie.getVotes() |
|
||||
<Search results: [<MovieResult: Fight Club (1999-09-16)>]> |
|
||||
""" |
|
||||
mdb = MovieDb() |
|
||||
self.movie = mdb.search(name) |
|
||||
|
|
||||
def getTotal(self): |
|
||||
return len(self.movie) |
|
||||
|
|
||||
def getRating(self, i): |
|
||||
return self.movie[i]["rating"] |
|
||||
|
|
||||
def getVotes(self, i): |
|
||||
return self.movie[i]["votes"] |
|
||||
|
|
||||
def getName(self, i): |
|
||||
return self.movie[i]["name"] |
|
||||
|
|
||||
def getLanguage(self, i): |
|
||||
return self.movie[i]["language"] |
|
||||
|
|
||||
def getCertification(self, i): |
|
||||
return self.movie[i]["certification"] |
|
||||
|
|
||||
def getUrl(self, i): |
|
||||
return self.movie[i]["url"] |
|
||||
|
|
||||
def getOverview(self, i): |
|
||||
return self.movie[i]["overview"] |
|
||||
|
|
||||
def getPopularity(self, i): |
|
||||
return self.movie[i]["popularity"] |
|
||||
|
|
||||
def getOriginalName(self, i): |
|
||||
return self.movie[i]["original_name"] |
|
||||
|
|
||||
def getLastModified(self, i): |
|
||||
return self.movie[i]["last_modified_at"] |
|
||||
|
|
||||
def getImdbId(self, i): |
|
||||
return self.movie[i]["imdb_id"] |
|
||||
|
|
||||
def getReleased(self, i): |
|
||||
return self.movie[i]["released"] |
|
||||
|
|
||||
def getScore(self, i): |
|
||||
return self.movie[i]["score"] |
|
||||
|
|
||||
def getAdult(self, i): |
|
||||
return self.movie[i]["adult"] |
|
||||
|
|
||||
def getVersion(self, i): |
|
||||
return self.movie[i]["version"] |
|
||||
|
|
||||
def getTranslated(self, i): |
|
||||
return self.movie[i]["translated"] |
|
||||
|
|
||||
def getType(self, i): |
|
||||
return self.movie[i]["type"] |
|
||||
|
|
||||
def getId(self, i): |
|
||||
return self.movie[i]["id"] |
|
||||
|
|
||||
def getAlternativeName(self, i): |
|
||||
return self.movie[i]["alternative_name"] |
|
||||
|
|
||||
def getPoster(self, i, size): |
|
||||
if size == "thumb" or size == "t": |
|
||||
return self.movie[i]["images"][0]["thumb"] |
|
||||
elif size == "cover" or size == "c": |
|
||||
return self.movie[i]["images"][0]["cover"] |
|
||||
else: |
|
||||
return self.movie[i]["images"][0]["mid"] |
|
||||
|
|
||||
def getBackdrop(self, i, size): |
|
||||
if size == "poster" or size == "p": |
|
||||
return self.movie[i]["images"][1]["poster"] |
|
||||
else: |
|
||||
return self.movie[i]["images"][1]["thumb"] |
|
||||
|
|
||||
# Shortcuts for imdb lookup method |
|
||||
# using: |
|
||||
# movie = tmdb.imdb("Sin City") |
|
||||
# print movie.getRating -> 7.0 |
|
||||
class imdb: |
|
||||
|
|
||||
def __init__(self, id = 0, title = False): |
|
||||
# get first movie if result=0 |
|
||||
"""Convenience wrapper for MovieDb.search - so you can do.. |
|
||||
>>> import tmdb |
|
||||
>>> movie = tmdb.imdb(title="Fight Club") # or movie = tmdb.imdb(id=imdb_id) |
|
||||
>>> ranking = movie.getRanking() or votes = movie.getVotes() |
|
||||
<Search results: [<MovieResult: Fight Club (1999-09-16)>]> |
|
||||
""" |
|
||||
self.id = id |
|
||||
self.title = title |
|
||||
self.mdb = MovieDb() |
|
||||
self.movie = self.mdb.imdbLookup(self.id, self.title) |
|
||||
|
|
||||
def getTotal(self): |
|
||||
return len(self.movie) |
|
||||
|
|
||||
def getRuntime(self, i): |
|
||||
return self.movie[i]["runtime"] |
|
||||
|
|
||||
def getCategories(self): |
|
||||
from xml.dom.minidom import parse |
|
||||
adres = config['urls']['imdb.lookUp'] % self.getImdbId() |
|
||||
d = parse(urllib2.urlopen(adres)) |
|
||||
s = d.getElementsByTagName("categories") |
|
||||
ds = [] |
|
||||
for i in range(len(s[0].childNodes)): |
|
||||
if i % 2 > 0: |
|
||||
ds.append(s[0].childNodes[i].getAttribute("name")) |
|
||||
return ds |
|
||||
|
|
||||
def getRating(self, i): |
|
||||
return self.movie[i]["rating"] |
|
||||
|
|
||||
def getVotes(self, i): |
|
||||
return self.movie[i]["votes"] |
|
||||
|
|
||||
def getName(self, i): |
|
||||
return self.movie[i]["name"] |
|
||||
|
|
||||
def getLanguage(self, i): |
|
||||
return self.movie[i]["language"] |
|
||||
|
|
||||
def getCertification(self, i): |
|
||||
return self.movie[i]["certification"] |
|
||||
|
|
||||
def getUrl(self, i): |
|
||||
return self.movie[i]["url"] |
|
||||
|
|
||||
def getOverview(self, i): |
|
||||
return self.movie[i]["overview"] |
|
||||
|
|
||||
def getPopularity(self, i): |
|
||||
return self.movie[i]["popularity"] |
|
||||
|
|
||||
def getOriginalName(self, i): |
|
||||
return self.movie[i]["original_name"] |
|
||||
|
|
||||
def getLastModified(self, i): |
|
||||
return self.movie[i]["last_modified_at"] |
|
||||
|
|
||||
def getImdbId(self, i): |
|
||||
return self.movie[i]["imdb_id"] |
|
||||
|
|
||||
def getReleased(self, i): |
|
||||
return self.movie[i]["released"] |
|
||||
|
|
||||
def getAdult(self, i): |
|
||||
return self.movie[i]["adult"] |
|
||||
|
|
||||
def getVersion(self, i): |
|
||||
return self.movie[i]["version"] |
|
||||
|
|
||||
def getTranslated(self, i): |
|
||||
return self.movie[i]["translated"] |
|
||||
|
|
||||
def getType(self, i): |
|
||||
return self.movie[i]["type"] |
|
||||
|
|
||||
def getId(self, i): |
|
||||
return self.movie[i]["id"] |
|
||||
|
|
||||
def getAlternativeName(self, i): |
|
||||
return self.movie[i]["alternative_name"] |
|
||||
|
|
||||
def getPoster(self, i, size): |
|
||||
poster = [] |
|
||||
if size == "thumb" or size == "t": |
|
||||
_size = "thumb" |
|
||||
elif size == "cover" or size == "c": |
|
||||
_size = "cover" |
|
||||
else: |
|
||||
_size = "mid" |
|
||||
for a in self.movie[i]["images"]: |
|
||||
if a["type"] == "poster": |
|
||||
poster.append(a[_size]) |
|
||||
return poster |
|
||||
del poster |
|
||||
|
|
||||
def getBackdrop(self, i, size): |
|
||||
backdrop = [] |
|
||||
if size == "thumb" or size == "t": |
|
||||
_size = "thumb" |
|
||||
elif size == "cover" or size == "c": |
|
||||
_size = "cover" |
|
||||
else: |
|
||||
_size = "mid" |
|
||||
for a in self.movie[i]["images"]: |
|
||||
if a["type"] == "backdrop": |
|
||||
backdrop.append(a[_size]) |
|
||||
return backdrop |
|
||||
del backdrop |
|
||||
|
|
||||
def imdbLookup(id = 0, title = False): |
|
||||
"""Convenience wrapper for Imdb.Lookup - so you can do.. |
|
||||
>>> import tmdb |
|
||||
>>> tmdb.imdbLookup("Fight Club") |
|
||||
<Search results: [<MovieResult: Fight Club (1999-09-16)>]> |
|
||||
""" |
|
||||
mdb = MovieDb() |
|
||||
return mdb.imdbLookup(id, title) |
|
||||
|
|
||||
def search(name): |
|
||||
"""Convenience wrapper for MovieDb.search - so you can do.. |
|
||||
>>> import tmdb |
|
||||
>>> tmdb.search("Fight Club") |
|
||||
<Search results: [<MovieResult: Fight Club (1999-09-16)>]> |
|
||||
""" |
|
||||
mdb = MovieDb() |
|
||||
return mdb.search(name) |
|
||||
|
|
||||
def getMovieInfo(id): |
|
||||
"""Convenience wrapper for MovieDb.search - so you can do.. |
|
||||
>>> import tmdb |
|
||||
>>> tmdb.getMovieInfo(187) |
|
||||
<MovieResult: Sin City (2005-04-01)> |
|
||||
""" |
|
||||
mdb = MovieDb() |
|
||||
return mdb.getMovieInfo(id) |
|
||||
|
|
||||
def mediaGetInfo(hash, size): |
|
||||
"""Convenience wrapper for MovieDb.mediaGetInfo - so you can do.. |
|
||||
|
|
||||
>>> import tmdb |
|
||||
>>> tmdb.mediaGetInfo('907172e7fe51ba57', size = 742086656)[0] |
|
||||
<MovieResult: Sin City (2005-04-01)> |
|
||||
""" |
|
||||
mdb = MovieDb() |
|
||||
return mdb.mediaGetInfo(hash, size) |
|
||||
|
|
||||
def searchByHashingFile(filename): |
|
||||
"""Searches for the specified file using the OpenSubtitle hashing method |
|
||||
""" |
|
||||
return mediaGetInfo(opensubtitleHashFile(filename), os.path.size(filename)) |
|
||||
|
|
||||
def main(): |
|
||||
results = search("Fight Club") |
|
||||
searchResult = results[0] |
|
||||
movie = getMovieInfo(searchResult['id']) |
|
||||
print movie['name'] |
|
||||
|
|
||||
print "Producers:" |
|
||||
for prodr in movie['cast']['producer']: |
|
||||
print " " * 4, prodr['name'] |
|
||||
print movie['images'] |
|
||||
for genreName in movie['categories']['genre']: |
|
||||
print "%s (%s)" % (genreName, movie['categories']['genre'][genreName]) |
|
||||
|
|
||||
if __name__ == '__main__': |
|
||||
main() |
|
@ -0,0 +1,11 @@ |
|||||
|
#!/usr/bin/env python |
||||
|
|
||||
|
from tmdb_api import Configuration, searchMovie, searchMovieWithYear, \ |
||||
|
searchPerson, searchStudio, searchList, searchCollection, \ |
||||
|
Person, Movie, Collection, Genre, List, __version__ |
||||
|
from request import set_key, set_cache |
||||
|
from locales import get_locale, set_locale |
||||
|
from tmdb_auth import get_session, set_session |
||||
|
from cache_engine import CacheEngine |
||||
|
from tmdb_exceptions import * |
||||
|
|
@ -0,0 +1,121 @@ |
|||||
|
#!/usr/bin/env python |
||||
|
# -*- coding: utf-8 -*- |
||||
|
#----------------------- |
||||
|
# Name: cache.py |
||||
|
# Python Library |
||||
|
# Author: Raymond Wagner |
||||
|
# Purpose: Caching framework to store TMDb API results |
||||
|
#----------------------- |
||||
|
|
||||
|
from tmdb_exceptions import * |
||||
|
from cache_engine import Engines |
||||
|
|
||||
|
import cache_null |
||||
|
import cache_file |
||||
|
|
||||
|
class Cache( object ): |
||||
|
""" |
||||
|
This class implements a persistent cache, backed in a file specified in |
||||
|
the object creation. The file is protected for safe, concurrent access |
||||
|
by multiple instances using flock. |
||||
|
This cache uses JSON for speed and storage efficiency, so only simple |
||||
|
data types are supported. |
||||
|
Data is stored in a simple format {key:(expiretimestamp, data)} |
||||
|
""" |
||||
|
def __init__(self, engine=None, *args, **kwargs): |
||||
|
self._engine = None |
||||
|
self._data = {} |
||||
|
self._age = 0 |
||||
|
self.configure(engine, *args, **kwargs) |
||||
|
|
||||
|
def _import(self, data=None): |
||||
|
if data is None: |
||||
|
data = self._engine.get(self._age) |
||||
|
for obj in sorted(data, key=lambda x: x.creation): |
||||
|
if not obj.expired: |
||||
|
self._data[obj.key] = obj |
||||
|
self._age = max(self._age, obj.creation) |
||||
|
|
||||
|
def _expire(self): |
||||
|
for k,v in self._data.items(): |
||||
|
if v.expired: |
||||
|
del self._data[k] |
||||
|
|
||||
|
def configure(self, engine, *args, **kwargs): |
||||
|
if engine is None: |
||||
|
engine = 'file' |
||||
|
elif engine not in Engines: |
||||
|
raise TMDBCacheError("Invalid cache engine specified: "+engine) |
||||
|
self._engine = Engines[engine](self) |
||||
|
self._engine.configure(*args, **kwargs) |
||||
|
|
||||
|
def put(self, key, data, lifetime=60*60*12): |
||||
|
# pull existing data, so cache will be fresh when written back out |
||||
|
if self._engine is None: |
||||
|
raise TMDBCacheError("No cache engine configured") |
||||
|
self._expire() |
||||
|
self._import(self._engine.put(key, data, lifetime)) |
||||
|
|
||||
|
def get(self, key): |
||||
|
if self._engine is None: |
||||
|
raise TMDBCacheError("No cache engine configured") |
||||
|
self._expire() |
||||
|
if key not in self._data: |
||||
|
self._import() |
||||
|
try: |
||||
|
return self._data[key].data |
||||
|
except: |
||||
|
return None |
||||
|
|
||||
|
def cached(self, callback): |
||||
|
""" |
||||
|
Returns a decorator that uses a callback to specify the key to use |
||||
|
for caching the responses from the decorated function. |
||||
|
""" |
||||
|
return self.Cached(self, callback) |
||||
|
|
||||
|
class Cached( object ): |
||||
|
def __init__(self, cache, callback, func=None, inst=None): |
||||
|
self.cache = cache |
||||
|
self.callback = callback |
||||
|
self.func = func |
||||
|
self.inst = inst |
||||
|
|
||||
|
if func: |
||||
|
self.__module__ = func.__module__ |
||||
|
self.__name__ = func.__name__ |
||||
|
self.__doc__ = func.__doc__ |
||||
|
|
||||
|
def __call__(self, *args, **kwargs): |
||||
|
if self.func is None: # decorator is waiting to be given a function |
||||
|
if len(kwargs) or (len(args) != 1): |
||||
|
raise TMDBCacheError('Cache.Cached decorator must be called '+\ |
||||
|
'a single callable argument before it '+\ |
||||
|
'be used.') |
||||
|
elif args[0] is None: |
||||
|
raise TMDBCacheError('Cache.Cached decorator called before '+\ |
||||
|
'being given a function to wrap.') |
||||
|
elif not callable(args[0]): |
||||
|
raise TMDBCacheError('Cache.Cached must be provided a '+\ |
||||
|
'callable object.') |
||||
|
return self.__class__(self.cache, self.callback, args[0]) |
||||
|
elif self.inst.lifetime == 0: |
||||
|
return self.func(*args, **kwargs) |
||||
|
else: |
||||
|
key = self.callback() |
||||
|
data = self.cache.get(key) |
||||
|
if data is None: |
||||
|
data = self.func(*args, **kwargs) |
||||
|
if hasattr(self.inst, 'lifetime'): |
||||
|
self.cache.put(key, data, self.inst.lifetime) |
||||
|
else: |
||||
|
self.cache.put(key, data) |
||||
|
return data |
||||
|
|
||||
|
def __get__(self, inst, owner): |
||||
|
if inst is None: |
||||
|
return self |
||||
|
func = self.func.__get__(inst, owner) |
||||
|
callback = self.callback.__get__(inst, owner) |
||||
|
return self.__class__(self.cache, callback, func, inst) |
||||
|
|
@ -0,0 +1,72 @@ |
|||||
|
#!/usr/bin/env python |
||||
|
# -*- coding: utf-8 -*- |
||||
|
#----------------------- |
||||
|
# Name: cache_engine.py |
||||
|
# Python Library |
||||
|
# Author: Raymond Wagner |
||||
|
# Purpose: Base cache engine class for collecting registered engines |
||||
|
#----------------------- |
||||
|
|
||||
|
import time |
||||
|
from weakref import ref |
||||
|
|
||||
|
class Engines( object ): |
||||
|
def __init__(self): |
||||
|
self._engines = {} |
||||
|
def register(self, engine): |
||||
|
self._engines[engine.__name__] = engine |
||||
|
self._engines[engine.name] = engine |
||||
|
def __getitem__(self, key): |
||||
|
return self._engines[key] |
||||
|
def __contains__(self, key): |
||||
|
return self._engines.__contains__(key) |
||||
|
Engines = Engines() |
||||
|
|
||||
|
class CacheEngineType( type ): |
||||
|
""" |
||||
|
Cache Engine Metaclass that registers new engines against the cache |
||||
|
for named selection and use. |
||||
|
""" |
||||
|
def __init__(mcs, name, bases, attrs): |
||||
|
super(CacheEngineType, mcs).__init__(name, bases, attrs) |
||||
|
if name != 'CacheEngine': |
||||
|
# skip base class |
||||
|
Engines.register(mcs) |
||||
|
|
||||
|
class CacheEngine( object ): |
||||
|
__metaclass__ = CacheEngineType |
||||
|
|
||||
|
name = 'unspecified' |
||||
|
def __init__(self, parent): |
||||
|
self.parent = ref(parent) |
||||
|
def configure(self): |
||||
|
raise RuntimeError |
||||
|
def get(self, date): |
||||
|
raise RuntimeError |
||||
|
def put(self, key, value, lifetime): |
||||
|
raise RuntimeError |
||||
|
def expire(self, key): |
||||
|
raise RuntimeError |
||||
|
|
||||
|
class CacheObject( object ): |
||||
|
""" |
||||
|
Cache object class, containing one stored record. |
||||
|
""" |
||||
|
|
||||
|
def __init__(self, key, data, lifetime=0, creation=None): |
||||
|
self.key = key |
||||
|
self.data = data |
||||
|
self.lifetime = lifetime |
||||
|
self.creation = creation if creation is not None else time.time() |
||||
|
|
||||
|
def __len__(self): |
||||
|
return len(self.data) |
||||
|
|
||||
|
@property |
||||
|
def expired(self): |
||||
|
return (self.remaining == 0) |
||||
|
|
||||
|
@property |
||||
|
def remaining(self): |
||||
|
return max((self.creation + self.lifetime) - time.time(), 0) |
||||
|
|
@ -0,0 +1,391 @@ |
|||||
|
#!/usr/bin/env python |
||||
|
# -*- coding: utf-8 -*- |
||||
|
#----------------------- |
||||
|
# Name: cache_file.py |
||||
|
# Python Library |
||||
|
# Author: Raymond Wagner |
||||
|
# Purpose: Persistant file-backed cache using /tmp/ to share data |
||||
|
# using flock or msvcrt.locking to allow safe concurrent |
||||
|
# access. |
||||
|
#----------------------- |
||||
|
|
||||
|
import struct |
||||
|
import errno |
||||
|
import json |
||||
|
import os |
||||
|
import io |
||||
|
|
||||
|
from cStringIO import StringIO |
||||
|
|
||||
|
from tmdb_exceptions import * |
||||
|
from cache_engine import CacheEngine, CacheObject |
||||
|
|
||||
|
#################### |
||||
|
# Cache File Format |
||||
|
#------------------ |
||||
|
# cache version (2) unsigned short |
||||
|
# slot count (2) unsigned short |
||||
|
# slot 0: timestamp (8) double |
||||
|
# slot 0: lifetime (4) unsigned int |
||||
|
# slot 0: seek point (4) unsigned int |
||||
|
# slot 1: timestamp |
||||
|
# slot 1: lifetime index slots are IDd by their query date and |
||||
|
# slot 1: seek point are filled incrementally forwards. lifetime |
||||
|
# .... is how long after query date before the item |
||||
|
# .... expires, and seek point is the location of the |
||||
|
# slot N-2: timestamp start of data for that entry. 256 empty slots |
||||
|
# slot N-2: lifetime are pre-allocated, allowing fast updates. |
||||
|
# slot N-2: seek point when all slots are filled, the cache file is |
||||
|
# slot N-1: timestamp rewritten from scrach to add more slots. |
||||
|
# slot N-1: lifetime |
||||
|
# slot N-1: seek point |
||||
|
# block 1 (?) ASCII |
||||
|
# block 2 |
||||
|
# .... blocks are just simple ASCII text, generated |
||||
|
# .... as independent objects by the JSON encoder |
||||
|
# block N-2 |
||||
|
# block N-1 |
||||
|
# |
||||
|
#################### |
||||
|
|
||||
|
|
||||
|
def _donothing(*args, **kwargs): |
||||
|
pass |
||||
|
|
||||
|
try: |
||||
|
import fcntl |
||||
|
class Flock( object ): |
||||
|
""" |
||||
|
Context manager to flock file for the duration the object exists. |
||||
|
Referenced file will be automatically unflocked as the interpreter |
||||
|
exits the context. |
||||
|
Supports an optional callback to process the error and optionally |
||||
|
suppress it. |
||||
|
""" |
||||
|
LOCK_EX = fcntl.LOCK_EX |
||||
|
LOCK_SH = fcntl.LOCK_SH |
||||
|
|
||||
|
def __init__(self, fileobj, operation, callback=None): |
||||
|
self.fileobj = fileobj |
||||
|
self.operation = operation |
||||
|
self.callback = callback |
||||
|
def __enter__(self): |
||||
|
fcntl.flock(self.fileobj, self.operation) |
||||
|
def __exit__(self, exc_type, exc_value, exc_tb): |
||||
|
suppress = False |
||||
|
if callable(self.callback): |
||||
|
suppress = self.callback(exc_type, exc_value, exc_tb) |
||||
|
fcntl.flock(self.fileobj, fcntl.LOCK_UN) |
||||
|
return suppress |
||||
|
|
||||
|
def parse_filename(filename): |
||||
|
if '$' in filename: |
||||
|
# replace any environmental variables |
||||
|
filename = os.path.expandvars(filename) |
||||
|
if filename.startswith('~'): |
||||
|
# check for home directory |
||||
|
return os.path.expanduser(filename) |
||||
|
elif filename.startswith('/'): |
||||
|
# check for absolute path |
||||
|
return filename |
||||
|
# return path with temp directory prepended |
||||
|
return '/tmp/' + filename |
||||
|
|
||||
|
except ImportError: |
||||
|
import msvcrt |
||||
|
class Flock( object ): |
||||
|
LOCK_EX = msvcrt.LK_LOCK |
||||
|
LOCK_SH = msvcrt.LK_LOCK |
||||
|
|
||||
|
def __init__(self, fileobj, operation, callback=None): |
||||
|
self.fileobj = fileobj |
||||
|
self.operation = operation |
||||
|
self.callback = callback |
||||
|
def __enter__(self): |
||||
|
self.size = os.path.getsize(self.fileobj.name) |
||||
|
msvcrt.locking(self.fileobj.fileno(), self.operation, self.size) |
||||
|
def __exit__(self, exc_type, exc_value, exc_tb): |
||||
|
suppress = False |
||||
|
if callable(self.callback): |
||||
|
suppress = self.callback(exc_type, exc_value, exc_tb) |
||||
|
msvcrt.locking(self.fileobj.fileno(), msvcrt.LK_UNLCK, self.size) |
||||
|
return suppress |
||||
|
|
||||
|
def parse_filename(filename): |
||||
|
if '%' in filename: |
||||
|
# replace any environmental variables |
||||
|
filename = os.path.expandvars(filename) |
||||
|
if filename.startswith('~'): |
||||
|
# check for home directory |
||||
|
return os.path.expanduser(filename) |
||||
|
elif (ord(filename[0]) in (range(65,91)+range(99,123))) \ |
||||
|
and (filename[1:3] == ':\\'): |
||||
|
# check for absolute drive path (e.g. C:\...) |
||||
|
return filename |
||||
|
elif (filename.count('\\') >= 3) and (filename.startswith('\\\\')): |
||||
|
# check for absolute UNC path (e.g. \\server\...) |
||||
|
return filename |
||||
|
# return path with temp directory prepended |
||||
|
return os.path.expandvars(os.path.join('%TEMP%',filename)) |
||||
|
|
||||
|
|
||||
|
class FileCacheObject( CacheObject ): |
||||
|
_struct = struct.Struct('dII') # double and two ints |
||||
|
# timestamp, lifetime, position |
||||
|
|
||||
|
@classmethod |
||||
|
def fromFile(cls, fd): |
||||
|
dat = cls._struct.unpack(fd.read(cls._struct.size)) |
||||
|
obj = cls(None, None, dat[1], dat[0]) |
||||
|
obj.position = dat[2] |
||||
|
return obj |
||||
|
|
||||
|
def __init__(self, *args, **kwargs): |
||||
|
self._key = None |
||||
|
self._data = None |
||||
|
self._size = None |
||||
|
self._buff = StringIO() |
||||
|
super(FileCacheObject, self).__init__(*args, **kwargs) |
||||
|
|
||||
|
@property |
||||
|
def size(self): |
||||
|
if self._size is None: |
||||
|
self._buff.seek(0,2) |
||||
|
size = self._buff.tell() |
||||
|
if size == 0: |
||||
|
if (self._key is None) or (self._data is None): |
||||
|
raise RuntimeError |
||||
|
json.dump([self.key, self.data], self._buff) |
||||
|
self._size = self._buff.tell() |
||||
|
self._size = size |
||||
|
return self._size |
||||
|
@size.setter |
||||
|
def size(self, value): self._size = value |
||||
|
|
||||
|
@property |
||||
|
def key(self): |
||||
|
if self._key is None: |
||||
|
try: |
||||
|
self._key, self._data = json.loads(self._buff.getvalue()) |
||||
|
except: |
||||
|
pass |
||||
|
return self._key |
||||
|
@key.setter |
||||
|
def key(self, value): self._key = value |
||||
|
|
||||
|
@property |
||||
|
def data(self): |
||||
|
if self._data is None: |
||||
|
self._key, self._data = json.loads(self._buff.getvalue()) |
||||
|
return self._data |
||||
|
@data.setter |
||||
|
def data(self, value): self._data = value |
||||
|
|
||||
|
def load(self, fd): |
||||
|
fd.seek(self.position) |
||||
|
self._buff.seek(0) |
||||
|
self._buff.write(fd.read(self.size)) |
||||
|
|
||||
|
def dumpslot(self, fd): |
||||
|
pos = fd.tell() |
||||
|
fd.write(self._struct.pack(self.creation, self.lifetime, self.position)) |
||||
|
|
||||
|
def dumpdata(self, fd): |
||||
|
self.size |
||||
|
fd.seek(self.position) |
||||
|
fd.write(self._buff.getvalue()) |
||||
|
|
||||
|
|
||||
|
class FileEngine( CacheEngine ): |
||||
|
"""Simple file-backed engine.""" |
||||
|
name = 'file' |
||||
|
_struct = struct.Struct('HH') # two shorts for version and count |
||||
|
_version = 2 |
||||
|
|
||||
|
def __init__(self, parent): |
||||
|
super(FileEngine, self).__init__(parent) |
||||
|
self.configure(None) |
||||
|
|
||||
|
def configure(self, filename, preallocate=256): |
||||
|
self.preallocate = preallocate |
||||
|
self.cachefile = filename |
||||
|
self.size = 0 |
||||
|
self.free = 0 |
||||
|
self.age = 0 |
||||
|
|
||||
|
def _init_cache(self): |
||||
|
# only run this once |
||||
|
self._init_cache = _donothing |
||||
|
|
||||
|
if self.cachefile is None: |
||||
|
raise TMDBCacheError("No cache filename given.") |
||||
|
|
||||
|
self.cachefile = parse_filename(self.cachefile) |
||||
|
|
||||
|
try: |
||||
|
# attempt to read existing cache at filename |
||||
|
# handle any errors that occur |
||||
|
self._open('r+b') |
||||
|
# seems to have read fine, make sure we have write access |
||||
|
if not os.access(self.cachefile, os.W_OK): |
||||
|
raise TMDBCacheWriteError(self.cachefile) |
||||
|
|
||||
|
except IOError as e: |
||||
|
if e.errno == errno.ENOENT: |
||||
|
# file does not exist, create a new one |
||||
|
try: |
||||
|
self._open('w+b') |
||||
|
self._write([]) |
||||
|
except IOError as e: |
||||
|
if e.errno == errno.ENOENT: |
||||
|
# directory does not exist |
||||
|
raise TMDBCacheDirectoryError(self.cachefile) |
||||
|
elif e.errno == errno.EACCES: |
||||
|
# user does not have rights to create new file |
||||
|
raise TMDBCacheWriteError(self.cachefile) |
||||
|
else: |
||||
|
# let the unhandled error continue through |
||||
|
raise |
||||
|
elif e.errno == errno.EACCESS: |
||||
|
# file exists, but we do not have permission to access it |
||||
|
raise TMDBCacheReadError(self.cachefile) |
||||
|
else: |
||||
|
# let the unhandled error continue through |
||||
|
raise |
||||
|
|
||||
|
def get(self, date): |
||||
|
self._init_cache() |
||||
|
self._open('r+b') |
||||
|
|
||||
|
with Flock(self.cachefd, Flock.LOCK_SH): # lock for shared access |
||||
|
# return any new objects in the cache |
||||
|
return self._read(date) |
||||
|
|
||||
|
def put(self, key, value, lifetime): |
||||
|
self._init_cache() |
||||
|
self._open('r+b') |
||||
|
|
||||
|
with Flock(self.cachefd, Flock.LOCK_EX): # lock for exclusive access |
||||
|
newobjs = self._read(self.age) |
||||
|
newobjs.append(FileCacheObject(key, value, lifetime)) |
||||
|
|
||||
|
# this will cause a new file object to be opened with the proper |
||||
|
# access mode, however the Flock should keep the old object open |
||||
|
# and properly locked |
||||
|
self._open('r+b') |
||||
|
self._write(newobjs) |
||||
|
return newobjs |
||||
|
|
||||
|
def _open(self, mode='r+b'): |
||||
|
# enforce binary operation |
||||
|
try: |
||||
|
if self.cachefd.mode == mode: |
||||
|
# already opened in requested mode, nothing to do |
||||
|
self.cachefd.seek(0) |
||||
|
return |
||||
|
except: pass # catch issue of no cachefile yet opened |
||||
|
self.cachefd = io.open(self.cachefile, mode) |
||||
|
|
||||
|
def _read(self, date): |
||||
|
try: |
||||
|
self.cachefd.seek(0) |
||||
|
version, count = self._struct.unpack(\ |
||||
|
self.cachefd.read(self._struct.size)) |
||||
|
if version != self._version: |
||||
|
# old version, break out and well rewrite when finished |
||||
|
raise Exception |
||||
|
|
||||
|
self.size = count |
||||
|
cache = [] |
||||
|
while count: |
||||
|
# loop through storage definitions |
||||
|
obj = FileCacheObject.fromFile(self.cachefd) |
||||
|
cache.append(obj) |
||||
|
count -= 1 |
||||
|
|
||||
|
except: |
||||
|
# failed to read information, so just discard it and return empty |
||||
|
self.size = 0 |
||||
|
self.free = 0 |
||||
|
return [] |
||||
|
|
||||
|
# get end of file |
||||
|
self.cachefd.seek(0,2) |
||||
|
position = self.cachefd.tell() |
||||
|
newobjs = [] |
||||
|
emptycount = 0 |
||||
|
|
||||
|
# walk backward through all, collecting new content and populating size |
||||
|
while len(cache): |
||||
|
obj = cache.pop() |
||||
|
if obj.creation == 0: |
||||
|
# unused slot, skip |
||||
|
emptycount += 1 |
||||
|
elif obj.expired: |
||||
|
# object has passed expiration date, no sense processing |
||||
|
continue |
||||
|
elif obj.creation > date: |
||||
|
# used slot with new data, process |
||||
|
obj.size, position = position - obj.position, obj.position |
||||
|
newobjs.append(obj) |
||||
|
# update age |
||||
|
self.age = max(self.age, obj.creation) |
||||
|
elif len(newobjs): |
||||
|
# end of new data, break |
||||
|
break |
||||
|
|
||||
|
# walk forward and load new content |
||||
|
for obj in newobjs: |
||||
|
obj.load(self.cachefd) |
||||
|
|
||||
|
self.free = emptycount |
||||
|
return newobjs |
||||
|
|
||||
|
def _write(self, data): |
||||
|
if self.free and (self.size != self.free): |
||||
|
# we only care about the last data point, since the rest are |
||||
|
# already stored in the file |
||||
|
data = data[-1] |
||||
|
|
||||
|
# determine write position of data in cache |
||||
|
self.cachefd.seek(0,2) |
||||
|
end = self.cachefd.tell() |
||||
|
data.position = end |
||||
|
|
||||
|
# write incremental update to free slot |
||||
|
self.cachefd.seek(4 + 16*(self.size-self.free)) |
||||
|
data.dumpslot(self.cachefd) |
||||
|
data.dumpdata(self.cachefd) |
||||
|
|
||||
|
else: |
||||
|
# rewrite cache file from scratch |
||||
|
# pull data from parent cache |
||||
|
data.extend(self.parent()._data.values()) |
||||
|
data.sort(key=lambda x: x.creation) |
||||
|
# write header |
||||
|
size = len(data) + self.preallocate |
||||
|
self.cachefd.seek(0) |
||||
|
self.cachefd.truncate() |
||||
|
self.cachefd.write(self._struct.pack(self._version, size)) |
||||
|
# write storage slot definitions |
||||
|
prev = None |
||||
|
for d in data: |
||||
|
if prev == None: |
||||
|
d.position = 4 + 16*size |
||||
|
else: |
||||
|
d.position = prev.position + prev.size |
||||
|
d.dumpslot(self.cachefd) |
||||
|
prev = d |
||||
|
# fill in allocated slots |
||||
|
for i in range(2**8): |
||||
|
self.cachefd.write(FileCacheObject._struct.pack(0, 0, 0)) |
||||
|
# write stored data |
||||
|
for d in data: |
||||
|
d.dumpdata(self.cachefd) |
||||
|
|
||||
|
self.cachefd.flush() |
||||
|
|
||||
|
def expire(self, key): |
||||
|
pass |
||||
|
|
||||
|
|
@ -0,0 +1,19 @@ |
|||||
|
#!/usr/bin/env python |
||||
|
# -*- coding: utf-8 -*- |
||||
|
#----------------------- |
||||
|
# Name: cache_null.py |
||||
|
# Python Library |
||||
|
# Author: Raymond Wagner |
||||
|
# Purpose: Null caching engine for debugging purposes |
||||
|
#----------------------- |
||||
|
|
||||
|
from cache_engine import CacheEngine |
||||
|
|
||||
|
class NullEngine( CacheEngine ): |
||||
|
"""Non-caching engine for debugging.""" |
||||
|
name = 'null' |
||||
|
def configure(self): pass |
||||
|
def get(self, date): return [] |
||||
|
def put(self, key, value, lifetime): return [] |
||||
|
def expire(self, key): pass |
||||
|
|
@ -0,0 +1,634 @@ |
|||||
|
#!/usr/bin/env python |
||||
|
# -*- coding: utf-8 -*- |
||||
|
#----------------------- |
||||
|
# Name: locales.py Stores locale information for filtering results |
||||
|
# Python Library |
||||
|
# Author: Raymond Wagner |
||||
|
#----------------------- |
||||
|
|
||||
|
from tmdb_exceptions import * |
||||
|
import locale |
||||
|
|
||||
|
syslocale = None |
||||
|
|
||||
|
class LocaleBase( object ): |
||||
|
__slots__ = ['__immutable'] |
||||
|
_stored = {} |
||||
|
fallthrough = False |
||||
|
|
||||
|
def __init__(self, *keys): |
||||
|
for key in keys: |
||||
|
self._stored[key.lower()] = self |
||||
|
self.__immutable = True |
||||
|
|
||||
|
def __setattr__(self, key, value): |
||||
|
if getattr(self, '__immutable', False): |
||||
|
raise NotImplementedError(self.__class__.__name__ + |
||||
|
' does not support modification.') |
||||
|
super(LocaleBase, self).__setattr__(key, value) |
||||
|
|
||||
|
def __delattr__(self, key): |
||||
|
if getattr(self, '__immutable', False): |
||||
|
raise NotImplementedError(self.__class__.__name__ + |
||||
|
' does not support modification.') |
||||
|
super(LocaleBase, self).__delattr__(key) |
||||
|
|
||||
|
def __lt__(self, other): |
||||
|
return (id(self) != id(other)) and (str(self) > str(other)) |
||||
|
def __gt__(self, other): |
||||
|
return (id(self) != id(other)) and (str(self) < str(other)) |
||||
|
def __eq__(self, other): |
||||
|
return (id(self) == id(other)) or (str(self) == str(other)) |
||||
|
|
||||
|
@classmethod |
||||
|
def getstored(cls, key): |
||||
|
if key is None: |
||||
|
return None |
||||
|
try: |
||||
|
return cls._stored[key.lower()] |
||||
|
except: |
||||
|
raise TMDBLocaleError("'{0}' is not a known valid {1} code."\ |
||||
|
.format(key, cls.__name__)) |
||||
|
|
||||
|
class Language( LocaleBase ): |
||||
|
__slots__ = ['ISO639_1', 'ISO639_2', 'ISO639_2B', 'englishname', |
||||
|
'nativename'] |
||||
|
_stored = {} |
||||
|
|
||||
|
def __init__(self, iso1, iso2, ename): |
||||
|
self.ISO639_1 = iso1 |
||||
|
self.ISO639_2 = iso2 |
||||
|
# self.ISO639_2B = iso2b |
||||
|
self.englishname = ename |
||||
|
# self.nativename = nname |
||||
|
super(Language, self).__init__(iso1, iso2) |
||||
|
|
||||
|
def __str__(self): |
||||
|
return self.ISO639_1 |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return u"<Language '{0.englishname}' ({0.ISO639_1})>".format(self) |
||||
|
|
||||
|
class Country( LocaleBase ): |
||||
|
__slots__ = ['alpha2', 'name'] |
||||
|
_stored = {} |
||||
|
|
||||
|
def __init__(self, alpha2, name): |
||||
|
self.alpha2 = alpha2 |
||||
|
self.name = name |
||||
|
super(Country, self).__init__(alpha2) |
||||
|
|
||||
|
def __str__(self): |
||||
|
return self.alpha2 |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return u"<Country '{0.name}' ({0.alpha2})>".format(self) |
||||
|
|
||||
|
class Locale( LocaleBase ): |
||||
|
__slots__ = ['language', 'country', 'encoding'] |
||||
|
|
||||
|
def __init__(self, language, country, encoding): |
||||
|
self.language = Language.getstored(language) |
||||
|
self.country = Country.getstored(country) |
||||
|
self.encoding = encoding if encoding else 'latin-1' |
||||
|
|
||||
|
def __str__(self): |
||||
|
return u"{0}_{1}".format(self.language, self.country) |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return u"<Locale {0.language}_{0.country}>".format(self) |
||||
|
|
||||
|
def encode(self, dat): |
||||
|
"""Encode using system default encoding for network/file output.""" |
||||
|
try: |
||||
|
return dat.encode(self.encoding) |
||||
|
except AttributeError: |
||||
|
# not a string type, pass along |
||||
|
return dat |
||||
|
except UnicodeDecodeError: |
||||
|
# just return unmodified and hope for the best |
||||
|
return dat |
||||
|
|
||||
|
def decode(self, dat): |
||||
|
"""Decode to system default encoding for internal use.""" |
||||
|
try: |
||||
|
return dat.decode(self.encoding) |
||||
|
except AttributeError: |
||||
|
# not a string type, pass along |
||||
|
return dat |
||||
|
except UnicodeEncodeError: |
||||
|
# just return unmodified and hope for the best |
||||
|
return dat |
||||
|
|
||||
|
def set_locale(language=None, country=None, fallthrough=False): |
||||
|
global syslocale |
||||
|
LocaleBase.fallthrough = fallthrough |
||||
|
|
||||
|
sysloc, sysenc = locale.getdefaultlocale() |
||||
|
|
||||
|
if (not language) or (not country): |
||||
|
dat = None |
||||
|
if syslocale is not None: |
||||
|
dat = (str(syslocale.language), str(syslocale.country)) |
||||
|
else: |
||||
|
if (sysloc is None) or ('_' not in sysloc): |
||||
|
dat = ('en', 'US') |
||||
|
else: |
||||
|
dat = sysloc.split('_') |
||||
|
if language is None: |
||||
|
language = dat[0] |
||||
|
if country is None: |
||||
|
country = dat[1] |
||||
|
|
||||
|
syslocale = Locale(language, country, sysenc) |
||||
|
|
||||
|
def get_locale(language=-1, country=-1): |
||||
|
"""Output locale using provided attributes, or return system locale.""" |
||||
|
global syslocale |
||||
|
# pull existing stored values |
||||
|
if syslocale is None: |
||||
|
loc = Locale(None, None, locale.getdefaultlocale()[1]) |
||||
|
else: |
||||
|
loc = syslocale |
||||
|
|
||||
|
# both options are default, return stored values |
||||
|
if language == country == -1: |
||||
|
return loc |
||||
|
|
||||
|
# supplement default option with stored values |
||||
|
if language == -1: |
||||
|
language = loc.language |
||||
|
elif country == -1: |
||||
|
country = loc.country |
||||
|
return Locale(language, country, loc.encoding) |
||||
|
|
||||
|
######## AUTOGENERATED LANGUAGE AND COUNTRY DATA BELOW HERE ######### |
||||
|
|
||||
|
Language("ab", "abk", u"Abkhazian") |
||||
|
Language("aa", "aar", u"Afar") |
||||
|
Language("af", "afr", u"Afrikaans") |
||||
|
Language("ak", "aka", u"Akan") |
||||
|
Language("sq", "alb/sqi", u"Albanian") |
||||
|
Language("am", "amh", u"Amharic") |
||||
|
Language("ar", "ara", u"Arabic") |
||||
|
Language("an", "arg", u"Aragonese") |
||||
|
Language("hy", "arm/hye", u"Armenian") |
||||
|
Language("as", "asm", u"Assamese") |
||||
|
Language("av", "ava", u"Avaric") |
||||
|
Language("ae", "ave", u"Avestan") |
||||
|
Language("ay", "aym", u"Aymara") |
||||
|
Language("az", "aze", u"Azerbaijani") |
||||
|
Language("bm", "bam", u"Bambara") |
||||
|
Language("ba", "bak", u"Bashkir") |
||||
|
Language("eu", "baq/eus", u"Basque") |
||||
|
Language("be", "bel", u"Belarusian") |
||||
|
Language("bn", "ben", u"Bengali") |
||||
|
Language("bh", "bih", u"Bihari languages") |
||||
|
Language("bi", "bis", u"Bislama") |
||||
|
Language("nb", "nob", u"Bokmål, Norwegian") |
||||
|
Language("bs", "bos", u"Bosnian") |
||||
|
Language("br", "bre", u"Breton") |
||||
|
Language("bg", "bul", u"Bulgarian") |
||||
|
Language("my", "bur/mya", u"Burmese") |
||||
|
Language("es", "spa", u"Castilian") |
||||
|
Language("ca", "cat", u"Catalan") |
||||
|
Language("km", "khm", u"Central Khmer") |
||||
|
Language("ch", "cha", u"Chamorro") |
||||
|
Language("ce", "che", u"Chechen") |
||||
|
Language("ny", "nya", u"Chewa") |
||||
|
Language("ny", "nya", u"Chichewa") |
||||
|
Language("zh", "chi/zho", u"Chinese") |
||||
|
Language("za", "zha", u"Chuang") |
||||
|
Language("cu", "chu", u"Church Slavic") |
||||
|
Language("cu", "chu", u"Church Slavonic") |
||||
|
Language("cv", "chv", u"Chuvash") |
||||
|
Language("kw", "cor", u"Cornish") |
||||
|
Language("co", "cos", u"Corsican") |
||||
|
Language("cr", "cre", u"Cree") |
||||
|
Language("hr", "hrv", u"Croatian") |
||||
|
Language("cs", "cze/ces", u"Czech") |
||||
|
Language("da", "dan", u"Danish") |
||||
|
Language("dv", "div", u"Dhivehi") |
||||
|
Language("dv", "div", u"Divehi") |
||||
|
Language("nl", "dut/nld", u"Dutch") |
||||
|
Language("dz", "dzo", u"Dzongkha") |
||||
|
Language("en", "eng", u"English") |
||||
|
Language("eo", "epo", u"Esperanto") |
||||
|
Language("et", "est", u"Estonian") |
||||
|
Language("ee", "ewe", u"Ewe") |
||||
|
Language("fo", "fao", u"Faroese") |
||||
|
Language("fj", "fij", u"Fijian") |
||||
|
Language("fi", "fin", u"Finnish") |
||||
|
Language("nl", "dut/nld", u"Flemish") |
||||
|
Language("fr", "fre/fra", u"French") |
||||
|
Language("ff", "ful", u"Fulah") |
||||
|
Language("gd", "gla", u"Gaelic") |
||||
|
Language("gl", "glg", u"Galician") |
||||
|
Language("lg", "lug", u"Ganda") |
||||
|
Language("ka", "geo/kat", u"Georgian") |
||||
|
Language("de", "ger/deu", u"German") |
||||
|
Language("ki", "kik", u"Gikuyu") |
||||
|
Language("el", "gre/ell", u"Greek, Modern (1453-)") |
||||
|
Language("kl", "kal", u"Greenlandic") |
||||
|
Language("gn", "grn", u"Guarani") |
||||
|
Language("gu", "guj", u"Gujarati") |
||||
|
Language("ht", "hat", u"Haitian") |
||||
|
Language("ht", "hat", u"Haitian Creole") |
||||
|
Language("ha", "hau", u"Hausa") |
||||
|
Language("he", "heb", u"Hebrew") |
||||
|
Language("hz", "her", u"Herero") |
||||
|
Language("hi", "hin", u"Hindi") |
||||
|
Language("ho", "hmo", u"Hiri Motu") |
||||
|
Language("hu", "hun", u"Hungarian") |
||||
|
Language("is", "ice/isl", u"Icelandic") |
||||
|
Language("io", "ido", u"Ido") |
||||
|
Language("ig", "ibo", u"Igbo") |
||||
|
Language("id", "ind", u"Indonesian") |
||||
|
Language("ia", "ina", u"Interlingua (International Auxiliary Language Association)") |
||||
|
Language("ie", "ile", u"Interlingue") |
||||
|
Language("iu", "iku", u"Inuktitut") |
||||
|
Language("ik", "ipk", u"Inupiaq") |
||||
|
Language("ga", "gle", u"Irish") |
||||
|
Language("it", "ita", u"Italian") |
||||
|
Language("ja", "jpn", u"Japanese") |
||||
|
Language("jv", "jav", u"Javanese") |
||||
|
Language("kl", "kal", u"Kalaallisut") |
||||
|
Language("kn", "kan", u"Kannada") |
||||
|
Language("kr", "kau", u"Kanuri") |
||||
|
Language("ks", "kas", u"Kashmiri") |
||||
|
Language("kk", "kaz", u"Kazakh") |
||||
|
Language("ki", "kik", u"Kikuyu") |
||||
|
Language("rw", "kin", u"Kinyarwanda") |
||||
|
Language("ky", "kir", u"Kirghiz") |
||||
|
Language("kv", "kom", u"Komi") |
||||
|
Language("kg", "kon", u"Kongo") |
||||
|
Language("ko", "kor", u"Korean") |
||||
|
Language("kj", "kua", u"Kuanyama") |
||||
|
Language("ku", "kur", u"Kurdish") |
||||
|
Language("kj", "kua", u"Kwanyama") |
||||
|
Language("ky", "kir", u"Kyrgyz") |
||||
|
Language("lo", "lao", u"Lao") |
||||
|
Language("la", "lat", u"Latin") |
||||
|
Language("lv", "lav", u"Latvian") |
||||
|
Language("lb", "ltz", u"Letzeburgesch") |
||||
|
Language("li", "lim", u"Limburgan") |
||||
|
Language("li", "lim", u"Limburger") |
||||
|
Language("li", "lim", u"Limburgish") |
||||
|
Language("ln", "lin", u"Lingala") |
||||
|
Language("lt", "lit", u"Lithuanian") |
||||
|
Language("lu", "lub", u"Luba-Katanga") |
||||
|
Language("lb", "ltz", u"Luxembourgish") |
||||
|
Language("mk", "mac/mkd", u"Macedonian") |
||||
|
Language("mg", "mlg", u"Malagasy") |
||||
|
Language("ms", "may/msa", u"Malay") |
||||
|
Language("ml", "mal", u"Malayalam") |
||||
|
Language("dv", "div", u"Maldivian") |
||||
|
Language("mt", "mlt", u"Maltese") |
||||
|
Language("gv", "glv", u"Manx") |
||||
|
Language("mi", "mao/mri", u"Maori") |
||||
|
Language("mr", "mar", u"Marathi") |
||||
|
Language("mh", "mah", u"Marshallese") |
||||
|
Language("ro", "rum/ron", u"Moldavian") |
||||
|
Language("ro", "rum/ron", u"Moldovan") |
||||
|
Language("mn", "mon", u"Mongolian") |
||||
|
Language("na", "nau", u"Nauru") |
||||
|
Language("nv", "nav", u"Navaho") |
||||
|
Language("nv", "nav", u"Navajo") |
||||
|
Language("nd", "nde", u"Ndebele, North") |
||||
|
Language("nr", "nbl", u"Ndebele, South") |
||||
|
Language("ng", "ndo", u"Ndonga") |
||||
|
Language("ne", "nep", u"Nepali") |
||||
|
Language("nd", "nde", u"North Ndebele") |
||||
|
Language("se", "sme", u"Northern Sami") |
||||
|
Language("no", "nor", u"Norwegian") |
||||
|
Language("nb", "nob", u"Norwegian Bokmål") |
||||
|
Language("nn", "nno", u"Norwegian Nynorsk") |
||||
|
Language("ii", "iii", u"Nuosu") |
||||
|
Language("ny", "nya", u"Nyanja") |
||||
|
Language("nn", "nno", u"Nynorsk, Norwegian") |
||||
|
Language("ie", "ile", u"Occidental") |
||||
|
Language("oc", "oci", u"Occitan (post 1500)") |
||||
|
Language("oj", "oji", u"Ojibwa") |
||||
|
Language("cu", "chu", u"Old Bulgarian") |
||||
|
Language("cu", "chu", u"Old Church Slavonic") |
||||
|
Language("cu", "chu", u"Old Slavonic") |
||||
|
Language("or", "ori", u"Oriya") |
||||
|
Language("om", "orm", u"Oromo") |
||||
|
Language("os", "oss", u"Ossetian") |
||||
|
Language("os", "oss", u"Ossetic") |
||||
|
Language("pi", "pli", u"Pali") |
||||
|
Language("pa", "pan", u"Panjabi") |
||||
|
Language("ps", "pus", u"Pashto") |
||||
|
Language("fa", "per/fas", u"Persian") |
||||
|
Language("pl", "pol", u"Polish") |
||||
|
Language("pt", "por", u"Portuguese") |
||||
|
Language("pa", "pan", u"Punjabi") |
||||
|
Language("ps", "pus", u"Pushto") |
||||
|
Language("qu", "que", u"Quechua") |
||||
|
Language("ro", "rum/ron", u"Romanian") |
||||
|
Language("rm", "roh", u"Romansh") |
||||
|
Language("rn", "run", u"Rundi") |
||||
|
Language("ru", "rus", u"Russian") |
||||
|
Language("sm", "smo", u"Samoan") |
||||
|
Language("sg", "sag", u"Sango") |
||||
|
Language("sa", "san", u"Sanskrit") |
||||
|
Language("sc", "srd", u"Sardinian") |
||||
|
Language("gd", "gla", u"Scottish Gaelic") |
||||
|
Language("sr", "srp", u"Serbian") |
||||
|
Language("sn", "sna", u"Shona") |
||||
|
Language("ii", "iii", u"Sichuan Yi") |
||||
|
Language("sd", "snd", u"Sindhi") |
||||
|
Language("si", "sin", u"Sinhala") |
||||
|
Language("si", "sin", u"Sinhalese") |
||||
|
Language("sk", "slo/slk", u"Slovak") |
||||
|
Language("sl", "slv", u"Slovenian") |
||||
|
Language("so", "som", u"Somali") |
||||
|
Language("st", "sot", u"Sotho, Southern") |
||||
|
Language("nr", "nbl", u"South Ndebele") |
||||
|
Language("es", "spa", u"Spanish") |
||||
|
Language("su", "sun", u"Sundanese") |
||||
|
Language("sw", "swa", u"Swahili") |
||||
|
Language("ss", "ssw", u"Swati") |
||||
|
Language("sv", "swe", u"Swedish") |
||||
|
Language("tl", "tgl", u"Tagalog") |
||||
|
Language("ty", "tah", u"Tahitian") |
||||
|
Language("tg", "tgk", u"Tajik") |
||||
|
Language("ta", "tam", u"Tamil") |
||||
|
Language("tt", "tat", u"Tatar") |
||||
|
Language("te", "tel", u"Telugu") |
||||
|
Language("th", "tha", u"Thai") |
||||
|
Language("bo", "tib/bod", u"Tibetan") |
||||
|
Language("ti", "tir", u"Tigrinya") |
||||
|
Language("to", "ton", u"Tonga (Tonga Islands)") |
||||
|
Language("ts", "tso", u"Tsonga") |
||||
|
Language("tn", "tsn", u"Tswana") |
||||
|
Language("tr", "tur", u"Turkish") |
||||
|
Language("tk", "tuk", u"Turkmen") |
||||
|
Language("tw", "twi", u"Twi") |
||||
|
Language("ug", "uig", u"Uighur") |
||||
|
Language("uk", "ukr", u"Ukrainian") |
||||
|
Language("ur", "urd", u"Urdu") |
||||
|
Language("ug", "uig", u"Uyghur") |
||||
|
Language("uz", "uzb", u"Uzbek") |
||||
|
Language("ca", "cat", u"Valencian") |
||||
|
Language("ve", "ven", u"Venda") |
||||
|
Language("vi", "vie", u"Vietnamese") |
||||
|
Language("vo", "vol", u"Volapük") |
||||
|
Language("wa", "wln", u"Walloon") |
||||
|
Language("cy", "wel/cym", u"Welsh") |
||||
|
Language("fy", "fry", u"Western Frisian") |
||||
|
Language("wo", "wol", u"Wolof") |
||||
|
Language("xh", "xho", u"Xhosa") |
||||
|
Language("yi", "yid", u"Yiddish") |
||||
|
Language("yo", "yor", u"Yoruba") |
||||
|
Language("za", "zha", u"Zhuang") |
||||
|
Language("zu", "zul", u"Zulu") |
||||
|
Country("AF", u"AFGHANISTAN") |
||||
|
Country("AX", u"ÅLAND ISLANDS") |
||||
|
Country("AL", u"ALBANIA") |
||||
|
Country("DZ", u"ALGERIA") |
||||
|
Country("AS", u"AMERICAN SAMOA") |
||||
|
Country("AD", u"ANDORRA") |
||||
|
Country("AO", u"ANGOLA") |
||||
|
Country("AI", u"ANGUILLA") |
||||
|
Country("AQ", u"ANTARCTICA") |
||||
|
Country("AG", u"ANTIGUA AND BARBUDA") |
||||
|
Country("AR", u"ARGENTINA") |
||||
|
Country("AM", u"ARMENIA") |
||||
|
Country("AW", u"ARUBA") |
||||
|
Country("AU", u"AUSTRALIA") |
||||
|
Country("AT", u"AUSTRIA") |
||||
|
Country("AZ", u"AZERBAIJAN") |
||||
|
Country("BS", u"BAHAMAS") |
||||
|
Country("BH", u"BAHRAIN") |
||||
|
Country("BD", u"BANGLADESH") |
||||
|
Country("BB", u"BARBADOS") |
||||
|
Country("BY", u"BELARUS") |
||||
|
Country("BE", u"BELGIUM") |
||||
|
Country("BZ", u"BELIZE") |
||||
|
Country("BJ", u"BENIN") |
||||
|
Country("BM", u"BERMUDA") |
||||
|
Country("BT", u"BHUTAN") |
||||
|
Country("BO", u"BOLIVIA, PLURINATIONAL STATE OF") |
||||
|
Country("BQ", u"BONAIRE, SINT EUSTATIUS AND SABA") |
||||
|
Country("BA", u"BOSNIA AND HERZEGOVINA") |
||||
|
Country("BW", u"BOTSWANA") |
||||
|
Country("BV", u"BOUVET ISLAND") |
||||
|
Country("BR", u"BRAZIL") |
||||
|
Country("IO", u"BRITISH INDIAN OCEAN TERRITORY") |
||||
|
Country("BN", u"BRUNEI DARUSSALAM") |
||||
|
Country("BG", u"BULGARIA") |
||||
|
Country("BF", u"BURKINA FASO") |
||||
|
Country("BI", u"BURUNDI") |
||||
|
Country("KH", u"CAMBODIA") |
||||
|
Country("CM", u"CAMEROON") |
||||
|
Country("CA", u"CANADA") |
||||
|
Country("CV", u"CAPE VERDE") |
||||
|
Country("KY", u"CAYMAN ISLANDS") |
||||
|
Country("CF", u"CENTRAL AFRICAN REPUBLIC") |
||||
|
Country("TD", u"CHAD") |
||||
|
Country("CL", u"CHILE") |
||||
|
Country("CN", u"CHINA") |
||||
|
Country("CX", u"CHRISTMAS ISLAND") |
||||
|
Country("CC", u"COCOS (KEELING) ISLANDS") |
||||
|
Country("CO", u"COLOMBIA") |
||||
|
Country("KM", u"COMOROS") |
||||
|
Country("CG", u"CONGO") |
||||
|
Country("CD", u"CONGO, THE DEMOCRATIC REPUBLIC OF THE") |
||||
|
Country("CK", u"COOK ISLANDS") |
||||
|
Country("CR", u"COSTA RICA") |
||||
|
Country("CI", u"CÔTE D'IVOIRE") |
||||
|
Country("HR", u"CROATIA") |
||||
|
Country("CU", u"CUBA") |
||||
|
Country("CW", u"CURAÇAO") |
||||
|
Country("CY", u"CYPRUS") |
||||
|
Country("CZ", u"CZECH REPUBLIC") |
||||
|
Country("DK", u"DENMARK") |
||||
|
Country("DJ", u"DJIBOUTI") |
||||
|
Country("DM", u"DOMINICA") |
||||
|
Country("DO", u"DOMINICAN REPUBLIC") |
||||
|
Country("EC", u"ECUADOR") |
||||
|
Country("EG", u"EGYPT") |
||||
|
Country("SV", u"EL SALVADOR") |
||||
|
Country("GQ", u"EQUATORIAL GUINEA") |
||||
|
Country("ER", u"ERITREA") |
||||
|
Country("EE", u"ESTONIA") |
||||
|
Country("ET", u"ETHIOPIA") |
||||
|
Country("FK", u"FALKLAND ISLANDS (MALVINAS)") |
||||
|
Country("FO", u"FAROE ISLANDS") |
||||
|
Country("FJ", u"FIJI") |
||||
|
Country("FI", u"FINLAND") |
||||
|
Country("FR", u"FRANCE") |
||||
|
Country("GF", u"FRENCH GUIANA") |
||||
|
Country("PF", u"FRENCH POLYNESIA") |
||||
|
Country("TF", u"FRENCH SOUTHERN TERRITORIES") |
||||
|
Country("GA", u"GABON") |
||||
|
Country("GM", u"GAMBIA") |
||||
|
Country("GE", u"GEORGIA") |
||||
|
Country("DE", u"GERMANY") |
||||
|
Country("GH", u"GHANA") |
||||
|
Country("GI", u"GIBRALTAR") |
||||
|
Country("GR", u"GREECE") |
||||
|
Country("GL", u"GREENLAND") |
||||
|
Country("GD", u"GRENADA") |
||||
|
Country("GP", u"GUADELOUPE") |
||||
|
Country("GU", u"GUAM") |
||||
|
Country("GT", u"GUATEMALA") |
||||
|
Country("GG", u"GUERNSEY") |
||||
|
Country("GN", u"GUINEA") |
||||
|
Country("GW", u"GUINEA-BISSAU") |
||||
|
Country("GY", u"GUYANA") |
||||
|
Country("HT", u"HAITI") |
||||
|
Country("HM", u"HEARD ISLAND AND MCDONALD ISLANDS") |
||||
|
Country("VA", u"HOLY SEE (VATICAN CITY STATE)") |
||||
|
Country("HN", u"HONDURAS") |
||||
|
Country("HK", u"HONG KONG") |
||||
|
Country("HU", u"HUNGARY") |
||||
|
Country("IS", u"ICELAND") |
||||
|
Country("IN", u"INDIA") |
||||
|
Country("ID", u"INDONESIA") |
||||
|
Country("IR", u"IRAN, ISLAMIC REPUBLIC OF") |
||||
|
Country("IQ", u"IRAQ") |
||||
|
Country("IE", u"IRELAND") |
||||
|
Country("IM", u"ISLE OF MAN") |
||||
|
Country("IL", u"ISRAEL") |
||||
|
Country("IT", u"ITALY") |
||||
|
Country("JM", u"JAMAICA") |
||||
|
Country("JP", u"JAPAN") |
||||
|
Country("JE", u"JERSEY") |
||||
|
Country("JO", u"JORDAN") |
||||
|
Country("KZ", u"KAZAKHSTAN") |
||||
|
Country("KE", u"KENYA") |
||||
|
Country("KI", u"KIRIBATI") |
||||
|
Country("KP", u"KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF") |
||||
|
Country("KR", u"KOREA, REPUBLIC OF") |
||||
|
Country("KW", u"KUWAIT") |
||||
|
Country("KG", u"KYRGYZSTAN") |
||||
|
Country("LA", u"LAO PEOPLE'S DEMOCRATIC REPUBLIC") |
||||
|
Country("LV", u"LATVIA") |
||||
|
Country("LB", u"LEBANON") |
||||
|
Country("LS", u"LESOTHO") |
||||
|
Country("LR", u"LIBERIA") |
||||
|
Country("LY", u"LIBYA") |
||||
|
Country("LI", u"LIECHTENSTEIN") |
||||
|
Country("LT", u"LITHUANIA") |
||||
|
Country("LU", u"LUXEMBOURG") |
||||
|
Country("MO", u"MACAO") |
||||
|
Country("MK", u"MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF") |
||||
|
Country("MG", u"MADAGASCAR") |
||||
|
Country("MW", u"MALAWI") |
||||
|
Country("MY", u"MALAYSIA") |
||||
|
Country("MV", u"MALDIVES") |
||||
|
Country("ML", u"MALI") |
||||
|
Country("MT", u"MALTA") |
||||
|
Country("MH", u"MARSHALL ISLANDS") |
||||
|
Country("MQ", u"MARTINIQUE") |
||||
|
Country("MR", u"MAURITANIA") |
||||
|
Country("MU", u"MAURITIUS") |
||||
|
Country("YT", u"MAYOTTE") |
||||
|
Country("MX", u"MEXICO") |
||||
|
Country("FM", u"MICRONESIA, FEDERATED STATES OF") |
||||
|
Country("MD", u"MOLDOVA, REPUBLIC OF") |
||||
|
Country("MC", u"MONACO") |
||||
|
Country("MN", u"MONGOLIA") |
||||
|
Country("ME", u"MONTENEGRO") |
||||
|
Country("MS", u"MONTSERRAT") |
||||
|
Country("MA", u"MOROCCO") |
||||
|
Country("MZ", u"MOZAMBIQUE") |
||||
|
Country("MM", u"MYANMAR") |
||||
|
Country("NA", u"NAMIBIA") |
||||
|
Country("NR", u"NAURU") |
||||
|
Country("NP", u"NEPAL") |
||||
|
Country("NL", u"NETHERLANDS") |
||||
|
Country("NC", u"NEW CALEDONIA") |
||||
|
Country("NZ", u"NEW ZEALAND") |
||||
|
Country("NI", u"NICARAGUA") |
||||
|
Country("NE", u"NIGER") |
||||
|
Country("NG", u"NIGERIA") |
||||
|
Country("NU", u"NIUE") |
||||
|
Country("NF", u"NORFOLK ISLAND") |
||||
|
Country("MP", u"NORTHERN MARIANA ISLANDS") |
||||
|
Country("NO", u"NORWAY") |
||||
|
Country("OM", u"OMAN") |
||||
|
Country("PK", u"PAKISTAN") |
||||
|
Country("PW", u"PALAU") |
||||
|
Country("PS", u"PALESTINIAN TERRITORY, OCCUPIED") |
||||
|
Country("PA", u"PANAMA") |
||||
|
Country("PG", u"PAPUA NEW GUINEA") |
||||
|
Country("PY", u"PARAGUAY") |
||||
|
Country("PE", u"PERU") |
||||
|
Country("PH", u"PHILIPPINES") |
||||
|
Country("PN", u"PITCAIRN") |
||||
|
Country("PL", u"POLAND") |
||||
|
Country("PT", u"PORTUGAL") |
||||
|
Country("PR", u"PUERTO RICO") |
||||
|
Country("QA", u"QATAR") |
||||
|
Country("RE", u"RÉUNION") |
||||
|
Country("RO", u"ROMANIA") |
||||
|
Country("RU", u"RUSSIAN FEDERATION") |
||||
|
Country("RW", u"RWANDA") |
||||
|
Country("BL", u"SAINT BARTHÉLEMY") |
||||
|
Country("SH", u"SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA") |
||||
|
Country("KN", u"SAINT KITTS AND NEVIS") |
||||
|
Country("LC", u"SAINT LUCIA") |
||||
|
Country("MF", u"SAINT MARTIN (FRENCH PART)") |
||||
|
Country("PM", u"SAINT PIERRE AND MIQUELON") |
||||
|
Country("VC", u"SAINT VINCENT AND THE GRENADINES") |
||||
|
Country("WS", u"SAMOA") |
||||
|
Country("SM", u"SAN MARINO") |
||||
|
Country("ST", u"SAO TOME AND PRINCIPE") |
||||
|
Country("SA", u"SAUDI ARABIA") |
||||
|
Country("SN", u"SENEGAL") |
||||
|
Country("RS", u"SERBIA") |
||||
|
Country("SC", u"SEYCHELLES") |
||||
|
Country("SL", u"SIERRA LEONE") |
||||
|
Country("SG", u"SINGAPORE") |
||||
|
Country("SX", u"SINT MAARTEN (DUTCH PART)") |
||||
|
Country("SK", u"SLOVAKIA") |
||||
|
Country("SI", u"SLOVENIA") |
||||
|
Country("SB", u"SOLOMON ISLANDS") |
||||
|
Country("SO", u"SOMALIA") |
||||
|
Country("ZA", u"SOUTH AFRICA") |
||||
|
Country("GS", u"SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS") |
||||
|
Country("SS", u"SOUTH SUDAN") |
||||
|
Country("ES", u"SPAIN") |
||||
|
Country("LK", u"SRI LANKA") |
||||
|
Country("SD", u"SUDAN") |
||||
|
Country("SR", u"SURINAME") |
||||
|
Country("SJ", u"SVALBARD AND JAN MAYEN") |
||||
|
Country("SZ", u"SWAZILAND") |
||||
|
Country("SE", u"SWEDEN") |
||||
|
Country("CH", u"SWITZERLAND") |
||||
|
Country("SY", u"SYRIAN ARAB REPUBLIC") |
||||
|
Country("TW", u"TAIWAN, PROVINCE OF CHINA") |
||||
|
Country("TJ", u"TAJIKISTAN") |
||||
|
Country("TZ", u"TANZANIA, UNITED REPUBLIC OF") |
||||
|
Country("TH", u"THAILAND") |
||||
|
Country("TL", u"TIMOR-LESTE") |
||||
|
Country("TG", u"TOGO") |
||||
|
Country("TK", u"TOKELAU") |
||||
|
Country("TO", u"TONGA") |
||||
|
Country("TT", u"TRINIDAD AND TOBAGO") |
||||
|
Country("TN", u"TUNISIA") |
||||
|
Country("TR", u"TURKEY") |
||||
|
Country("TM", u"TURKMENISTAN") |
||||
|
Country("TC", u"TURKS AND CAICOS ISLANDS") |
||||
|
Country("TV", u"TUVALU") |
||||
|
Country("UG", u"UGANDA") |
||||
|
Country("UA", u"UKRAINE") |
||||
|
Country("AE", u"UNITED ARAB EMIRATES") |
||||
|
Country("GB", u"UNITED KINGDOM") |
||||
|
Country("US", u"UNITED STATES") |
||||
|
Country("UM", u"UNITED STATES MINOR OUTLYING ISLANDS") |
||||
|
Country("UY", u"URUGUAY") |
||||
|
Country("UZ", u"UZBEKISTAN") |
||||
|
Country("VU", u"VANUATU") |
||||
|
Country("VE", u"VENEZUELA, BOLIVARIAN REPUBLIC OF") |
||||
|
Country("VN", u"VIET NAM") |
||||
|
Country("VG", u"VIRGIN ISLANDS, BRITISH") |
||||
|
Country("VI", u"VIRGIN ISLANDS, U.S.") |
||||
|
Country("WF", u"WALLIS AND FUTUNA") |
||||
|
Country("EH", u"WESTERN SAHARA") |
||||
|
Country("YE", u"YEMEN") |
||||
|
Country("ZM", u"ZAMBIA") |
||||
|
Country("ZW", u"ZIMBABWE") |
@ -0,0 +1,109 @@ |
|||||
|
#!/usr/bin/env python |
||||
|
# -*- coding: utf-8 -*- |
||||
|
#----------------------- |
||||
|
# Name: pager.py List-like structure designed for handling paged results |
||||
|
# Python Library |
||||
|
# Author: Raymond Wagner |
||||
|
#----------------------- |
||||
|
|
||||
|
from collections import Sequence, Iterator |
||||
|
|
||||
|
class PagedIterator( Iterator ): |
||||
|
def __init__(self, parent): |
||||
|
self._parent = parent |
||||
|
self._index = -1 |
||||
|
self._len = len(parent) |
||||
|
|
||||
|
def __iter__(self): |
||||
|
return self |
||||
|
|
||||
|
def next(self): |
||||
|
self._index += 1 |
||||
|
if self._index == self._len: |
||||
|
raise StopIteration |
||||
|
return self._parent[self._index] |
||||
|
|
||||
|
class UnpagedData( object ): |
||||
|
def copy(self): |
||||
|
return self.__class__() |
||||
|
|
||||
|
def __mul__(self, other): |
||||
|
return (self.copy() for a in range(other)) |
||||
|
|
||||
|
def __rmul__(self, other): |
||||
|
return (self.copy() for a in range(other)) |
||||
|
|
||||
|
class PagedList( Sequence ): |
||||
|
""" |
||||
|
List-like object, with support for automatically grabbing additional |
||||
|
pages from a data source. |
||||
|
""" |
||||
|
_iter_class = None |
||||
|
|
||||
|
def __iter__(self): |
||||
|
if self._iter_class is None: |
||||
|
self._iter_class = type(self.__class__.__name__ + 'Iterator', |
||||
|
(PagedIterator,), {}) |
||||
|
return self._iter_class(self) |
||||
|
|
||||
|
def __len__(self): |
||||
|
try: |
||||
|
return self._len |
||||
|
except: |
||||
|
return len(self._data) |
||||
|
|
||||
|
def __init__(self, iterable, pagesize=20): |
||||
|
self._data = list(iterable) |
||||
|
self._pagesize = pagesize |
||||
|
|
||||
|
def __getitem__(self, index): |
||||
|
if isinstance(index, slice): |
||||
|
return [self[x] for x in xrange(*index.indices(len(self)))] |
||||
|
if index >= len(self): |
||||
|
raise IndexError("list index outside range") |
||||
|
if (index >= len(self._data)) \ |
||||
|
or isinstance(self._data[index], UnpagedData): |
||||
|
self._populatepage(index/self._pagesize + 1) |
||||
|
return self._data[index] |
||||
|
|
||||
|
def __setitem__(self, index, value): |
||||
|
raise NotImplementedError |
||||
|
|
||||
|
def __delitem__(self, index): |
||||
|
raise NotImplementedError |
||||
|
|
||||
|
def __contains__(self, item): |
||||
|
raise NotImplementedError |
||||
|
|
||||
|
def _populatepage(self, page): |
||||
|
pagestart = (page-1) * self._pagesize |
||||
|
if len(self._data) < pagestart: |
||||
|
self._data.extend(UnpagedData()*(pagestart-len(self._data))) |
||||
|
if len(self._data) == pagestart: |
||||
|
self._data.extend(self._getpage(page)) |
||||
|
else: |
||||
|
for data in self._getpage(page): |
||||
|
self._data[pagestart] = data |
||||
|
pagestart += 1 |
||||
|
|
||||
|
def _getpage(self, page): |
||||
|
raise NotImplementedError("PagedList._getpage() must be provided "+\ |
||||
|
"by subclass") |
||||
|
|
||||
|
class PagedRequest( PagedList ): |
||||
|
""" |
||||
|
Derived PageList that provides a list-like object with automatic paging |
||||
|
intended for use with search requests. |
||||
|
""" |
||||
|
def __init__(self, request, handler=None): |
||||
|
self._request = request |
||||
|
if handler: self._handler = handler |
||||
|
super(PagedRequest, self).__init__(self._getpage(1), 20) |
||||
|
|
||||
|
def _getpage(self, page): |
||||
|
req = self._request.new(page=page) |
||||
|
res = req.readJSON() |
||||
|
self._len = res['total_results'] |
||||
|
for item in res['results']: |
||||
|
yield self._handler(item) |
||||
|
|
@ -0,0 +1,157 @@ |
|||||
|
#!/usr/bin/env python |
||||
|
# -*- coding: utf-8 -*- |
||||
|
#----------------------- |
||||
|
# Name: tmdb_request.py |
||||
|
# Python Library |
||||
|
# Author: Raymond Wagner |
||||
|
# Purpose: Wrapped urllib2.Request class pre-configured for accessing the |
||||
|
# TMDb v3 API |
||||
|
#----------------------- |
||||
|
|
||||
|
from tmdb_exceptions import * |
||||
|
from locales import get_locale |
||||
|
from cache import Cache |
||||
|
|
||||
|
from urllib import urlencode |
||||
|
import urllib2 |
||||
|
import json |
||||
|
|
||||
|
DEBUG = False |
||||
|
cache = Cache(filename='pytmdb3.cache') |
||||
|
|
||||
|
#DEBUG = True |
||||
|
#cache = Cache(engine='null') |
||||
|
|
||||
|
def set_key(key): |
||||
|
""" |
||||
|
Specify the API key to use retrieving data from themoviedb.org. This |
||||
|
key must be set before any calls will function. |
||||
|
""" |
||||
|
if len(key) != 32: |
||||
|
raise TMDBKeyInvalid("Specified API key must be 128-bit hex") |
||||
|
try: |
||||
|
int(key, 16) |
||||
|
except: |
||||
|
raise TMDBKeyInvalid("Specified API key must be 128-bit hex") |
||||
|
Request._api_key = key |
||||
|
|
||||
|
def set_cache(engine=None, *args, **kwargs): |
||||
|
"""Specify caching engine and properties.""" |
||||
|
cache.configure(engine, *args, **kwargs) |
||||
|
|
||||
|
class Request( urllib2.Request ): |
||||
|
_api_key = None |
||||
|
_base_url = "http://api.themoviedb.org/3/" |
||||
|
|
||||
|
@property |
||||
|
def api_key(self): |
||||
|
if self._api_key is None: |
||||
|
raise TMDBKeyMissing("API key must be specified before "+\ |
||||
|
"requests can be made") |
||||
|
return self._api_key |
||||
|
|
||||
|
def __init__(self, url, **kwargs): |
||||
|
"""Return a request object, using specified API path and arguments.""" |
||||
|
kwargs['api_key'] = self.api_key |
||||
|
self._url = url.lstrip('/') |
||||
|
self._kwargs = dict([(kwa,kwv) for kwa,kwv in kwargs.items() |
||||
|
if kwv is not None]) |
||||
|
|
||||
|
locale = get_locale() |
||||
|
kwargs = {} |
||||
|
for k,v in self._kwargs.items(): |
||||
|
kwargs[k] = locale.encode(v) |
||||
|
url = '{0}{1}?{2}'.format(self._base_url, self._url, urlencode(kwargs)) |
||||
|
|
||||
|
urllib2.Request.__init__(self, url) |
||||
|
self.add_header('Accept', 'application/json') |
||||
|
self.lifetime = 3600 # 1hr |
||||
|
|
||||
|
def new(self, **kwargs): |
||||
|
"""Create a new instance of the request, with tweaked arguments.""" |
||||
|
args = dict(self._kwargs) |
||||
|
for k,v in kwargs.items(): |
||||
|
if v is None: |
||||
|
if k in args: |
||||
|
del args[k] |
||||
|
else: |
||||
|
args[k] = v |
||||
|
obj = self.__class__(self._url, **args) |
||||
|
obj.lifetime = self.lifetime |
||||
|
return obj |
||||
|
|
||||
|
def add_data(self, data): |
||||
|
"""Provide data to be sent with POST.""" |
||||
|
urllib2.Request.add_data(self, urlencode(data)) |
||||
|
|
||||
|
def open(self): |
||||
|
"""Open a file object to the specified URL.""" |
||||
|
try: |
||||
|
if DEBUG: |
||||
|
print 'loading '+self.get_full_url() |
||||
|
if self.has_data(): |
||||
|
print ' '+self.get_data() |
||||
|
return urllib2.urlopen(self) |
||||
|
except urllib2.HTTPError, e: |
||||
|
raise TMDBHTTPError(e) |
||||
|
|
||||
|
def read(self): |
||||
|
"""Return result from specified URL as a string.""" |
||||
|
return self.open().read() |
||||
|
|
||||
|
@cache.cached(urllib2.Request.get_full_url) |
||||
|
def readJSON(self): |
||||
|
"""Parse result from specified URL as JSON data.""" |
||||
|
url = self.get_full_url() |
||||
|
try: |
||||
|
# catch HTTP error from open() |
||||
|
data = json.load(self.open()) |
||||
|
except TMDBHTTPError, e: |
||||
|
try: |
||||
|
# try to load whatever was returned |
||||
|
data = json.loads(e.response) |
||||
|
except: |
||||
|
# cannot parse json, just raise existing error |
||||
|
raise e |
||||
|
else: |
||||
|
# response parsed, try to raise error from TMDB |
||||
|
handle_status(data, url) |
||||
|
# no error from TMDB, just raise existing error |
||||
|
raise e |
||||
|
handle_status(data, url) |
||||
|
#if DEBUG: |
||||
|
# import pprint |
||||
|
# pprint.PrettyPrinter().pprint(data) |
||||
|
return data |
||||
|
|
||||
|
status_handlers = { |
||||
|
1: None, |
||||
|
2: TMDBRequestInvalid('Invalid service - This service does not exist.'), |
||||
|
3: TMDBRequestError('Authentication Failed - You do not have '+\ |
||||
|
'permissions to access this service.'), |
||||
|
4: TMDBRequestInvalid("Invalid format - This service doesn't exist "+\ |
||||
|
'in that format.'), |
||||
|
5: TMDBRequestInvalid('Invalid parameters - Your request parameters '+\ |
||||
|
'are incorrect.'), |
||||
|
6: TMDBRequestInvalid('Invalid id - The pre-requisite id is invalid '+\ |
||||
|
'or not found.'), |
||||
|
7: TMDBKeyInvalid('Invalid API key - You must be granted a valid key.'), |
||||
|
8: TMDBRequestError('Duplicate entry - The data you tried to submit '+\ |
||||
|
'already exists.'), |
||||
|
9: TMDBOffline('This service is tempirarily offline. Try again later.'), |
||||
|
10: TMDBKeyRevoked('Suspended API key - Access to your account has been '+\ |
||||
|
'suspended, contact TMDB.'), |
||||
|
11: TMDBError('Internal error - Something went wrong. Contact TMDb.'), |
||||
|
12: None, |
||||
|
13: None, |
||||
|
14: TMDBRequestError('Authentication Failed.'), |
||||
|
15: TMDBError('Failed'), |
||||
|
16: TMDBError('Device Denied'), |
||||
|
17: TMDBError('Session Denied')} |
||||
|
|
||||
|
def handle_status(data, query): |
||||
|
status = status_handlers[data.get('status_code', 1)] |
||||
|
if status is not None: |
||||
|
status.tmdberrno = data['status_code'] |
||||
|
status.query = query |
||||
|
raise status |
@ -0,0 +1,689 @@ |
|||||
|
#!/usr/bin/env python |
||||
|
# -*- coding: utf-8 -*- |
||||
|
#----------------------- |
||||
|
# Name: tmdb_api.py Simple-to-use Python interface to TMDB's API v3 |
||||
|
# Python Library |
||||
|
# Author: Raymond Wagner |
||||
|
# Purpose: This Python library is intended to provide a series of classes |
||||
|
# and methods for search and retrieval of text metadata and image |
||||
|
# URLs from TMDB. |
||||
|
# Preliminary API specifications can be found at |
||||
|
# http://help.themoviedb.org/kb/api/about-3 |
||||
|
# License: Creative Commons GNU GPL v2 |
||||
|
# (http://creativecommons.org/licenses/GPL/2.0/) |
||||
|
#----------------------- |
||||
|
|
||||
|
__title__ = "tmdb_api - Simple-to-use Python interface to TMDB's API v3 "+\ |
||||
|
"(www.themoviedb.org)" |
||||
|
__author__ = "Raymond Wagner" |
||||
|
__purpose__ = """ |
||||
|
This Python library is intended to provide a series of classes and methods |
||||
|
for search and retrieval of text metadata and image URLs from TMDB. |
||||
|
Preliminary API specifications can be found at |
||||
|
http://help.themoviedb.org/kb/api/about-3""" |
||||
|
|
||||
|
__version__="v0.6.17" |
||||
|
# 0.1.0 Initial development |
||||
|
# 0.2.0 Add caching mechanism for API queries |
||||
|
# 0.2.1 Temporary work around for broken search paging |
||||
|
# 0.3.0 Rework backend machinery for managing OO interface to results |
||||
|
# 0.3.1 Add collection support |
||||
|
# 0.3.2 Remove MythTV key from results.py |
||||
|
# 0.3.3 Add functional language support |
||||
|
# 0.3.4 Re-enable search paging |
||||
|
# 0.3.5 Add methods for grabbing current, popular, and top rated movies |
||||
|
# 0.3.6 Rework paging mechanism |
||||
|
# 0.3.7 Generalize caching mechanism, and allow controllability |
||||
|
# 0.4.0 Add full locale support (language and country) and optional fall through |
||||
|
# 0.4.1 Add custom classmethod for dealing with IMDB movie IDs |
||||
|
# 0.4.2 Improve cache file selection for Windows systems |
||||
|
# 0.4.3 Add a few missed Person properties |
||||
|
# 0.4.4 Add support for additional Studio information |
||||
|
# 0.4.5 Add locale fallthrough for images and alternate titles |
||||
|
# 0.4.6 Add slice support for search results |
||||
|
# 0.5.0 Rework cache framework and improve file cache performance |
||||
|
# 0.6.0 Add user authentication support |
||||
|
# 0.6.1 Add adult filtering for people searches |
||||
|
# 0.6.2 Add similar movie search for Movie objects |
||||
|
# 0.6.3 Add Studio search |
||||
|
# 0.6.4 Add Genre list and associated Movie search |
||||
|
# 0.6.5 Prevent data from being blanked out by subsequent queries |
||||
|
# 0.6.6 Turn date processing errors into mutable warnings |
||||
|
# 0.6.7 Add support for searching by year |
||||
|
# 0.6.8 Add support for collection images |
||||
|
# 0.6.9 Correct Movie image language filtering |
||||
|
# 0.6.10 Add upcoming movie classmethod |
||||
|
# 0.6.11 Fix URL for top rated Movie query |
||||
|
# 0.6.12 Add support for Movie watchlist query and editing |
||||
|
# 0.6.13 Fix URL for rating Movies |
||||
|
# 0.6.14 Add support for Lists |
||||
|
# 0.6.15 Add ability to search Collections |
||||
|
# 0.6.16 Make absent primary images return None (previously u'') |
||||
|
# 0.6.17 Add userrating/votes to Image, add overview to Collection, remove |
||||
|
# releasedate sorting from Collection Movies |
||||
|
|
||||
|
from request import set_key, Request |
||||
|
from util import Datapoint, Datalist, Datadict, Element, NameRepr, SearchRepr |
||||
|
from pager import PagedRequest |
||||
|
from locales import get_locale, set_locale |
||||
|
from tmdb_auth import get_session, set_session |
||||
|
from tmdb_exceptions import * |
||||
|
|
||||
|
import datetime |
||||
|
|
||||
|
DEBUG = False |
||||
|
|
||||
|
def process_date(datestr): |
||||
|
try: |
||||
|
return datetime.date(*[int(x) for x in datestr.split('-')]) |
||||
|
except (TypeError, ValueError): |
||||
|
import sys |
||||
|
import warnings |
||||
|
import traceback |
||||
|
_,_,tb = sys.exc_info() |
||||
|
f,l,_,_ = traceback.extract_tb(tb)[-1] |
||||
|
warnings.warn_explicit(('"{0}" is not a supported date format. ' |
||||
|
'Please fix upstream data at http://www.themoviedb.org.')\ |
||||
|
.format(datestr), Warning, f, l) |
||||
|
return None |
||||
|
|
||||
|
class Configuration( Element ): |
||||
|
images = Datapoint('images') |
||||
|
def _populate(self): |
||||
|
return Request('configuration') |
||||
|
Configuration = Configuration() |
||||
|
|
||||
|
class Account( NameRepr, Element ): |
||||
|
def _populate(self): |
||||
|
return Request('account', session_id=self._session.sessionid) |
||||
|
|
||||
|
id = Datapoint('id') |
||||
|
adult = Datapoint('include_adult') |
||||
|
country = Datapoint('iso_3166_1') |
||||
|
language = Datapoint('iso_639_1') |
||||
|
name = Datapoint('name') |
||||
|
username = Datapoint('username') |
||||
|
|
||||
|
@property |
||||
|
def locale(self): |
||||
|
return get_locale(self.language, self.country) |
||||
|
|
||||
|
def searchMovie(query, locale=None, adult=False, year=None): |
||||
|
kwargs = {'query':query, 'include_adult':adult} |
||||
|
if year is not None: |
||||
|
try: |
||||
|
kwargs['year'] = year.year |
||||
|
except AttributeError: |
||||
|
kwargs['year'] = year |
||||
|
return MovieSearchResult(Request('search/movie', **kwargs), locale=locale) |
||||
|
|
||||
|
def searchMovieWithYear(query, locale=None, adult=False): |
||||
|
year = None |
||||
|
if (len(query) > 6) and (query[-1] == ')') and (query[-6] == '('): |
||||
|
# simple syntax check, no need for regular expression |
||||
|
try: |
||||
|
year = int(query[-5:-1]) |
||||
|
except ValueError: |
||||
|
pass |
||||
|
else: |
||||
|
if 1885 < year < 2050: |
||||
|
# strip out year from search |
||||
|
query = query[:-7] |
||||
|
else: |
||||
|
# sanity check on resolved year failed, pass through |
||||
|
year = None |
||||
|
return searchMovie(query, locale, adult, year) |
||||
|
|
||||
|
class MovieSearchResult( SearchRepr, PagedRequest ): |
||||
|
"""Stores a list of search matches.""" |
||||
|
_name = None |
||||
|
def __init__(self, request, locale=None): |
||||
|
if locale is None: |
||||
|
locale = get_locale() |
||||
|
super(MovieSearchResult, self).__init__( |
||||
|
request.new(language=locale.language), |
||||
|
lambda x: Movie(raw=x, locale=locale)) |
||||
|
|
||||
|
def searchPerson(query, adult=False): |
||||
|
return PeopleSearchResult(Request('search/person', query=query, |
||||
|
include_adult=adult)) |
||||
|
|
||||
|
class PeopleSearchResult( SearchRepr, PagedRequest ): |
||||
|
"""Stores a list of search matches.""" |
||||
|
_name = None |
||||
|
def __init__(self, request): |
||||
|
super(PeopleSearchResult, self).__init__(request, |
||||
|
lambda x: Person(raw=x)) |
||||
|
|
||||
|
def searchStudio(query): |
||||
|
return StudioSearchResult(Request('search/company', query=query)) |
||||
|
|
||||
|
class StudioSearchResult( SearchRepr, PagedRequest ): |
||||
|
"""Stores a list of search matches.""" |
||||
|
_name = None |
||||
|
def __init__(self, request): |
||||
|
super(StudioSearchResult, self).__init__(request, |
||||
|
lambda x: Studio(raw=x)) |
||||
|
|
||||
|
def searchList(query, adult=False): |
||||
|
ListSearchResult(Request('search/list', query=query, include_adult=adult)) |
||||
|
|
||||
|
class ListSearchResult( SearchRepr, PagedRequest ): |
||||
|
"""Stores a list of search matches.""" |
||||
|
_name = None |
||||
|
def __init__(self, request): |
||||
|
super(ListSearchResult, self).__init__(request, |
||||
|
lambda x: List(raw=x)) |
||||
|
|
||||
|
def searchCollection(query, locale=None): |
||||
|
return CollectionSearchResult(Request('search/collection', query=query), |
||||
|
locale=locale) |
||||
|
|
||||
|
class CollectionSearchResult( SearchRepr, PagedRequest ): |
||||
|
"""Stores a list of search matches.""" |
||||
|
_name=None |
||||
|
def __init__(self, request, locale=None): |
||||
|
if locale is None: |
||||
|
locale = get_locale() |
||||
|
super(CollectionSearchResult, self).__init__( |
||||
|
request.new(language=locale.language), |
||||
|
lambda x: Collection(raw=x, locale=locale)) |
||||
|
|
||||
|
class Image( Element ): |
||||
|
filename = Datapoint('file_path', initarg=1, |
||||
|
handler=lambda x: x.lstrip('/')) |
||||
|
aspectratio = Datapoint('aspect_ratio') |
||||
|
height = Datapoint('height') |
||||
|
width = Datapoint('width') |
||||
|
language = Datapoint('iso_639_1') |
||||
|
userrating = Datapoint('vote_average') |
||||
|
votes = Datapoint('vote_count') |
||||
|
|
||||
|
def sizes(self): |
||||
|
return ['original'] |
||||
|
|
||||
|
def geturl(self, size='original'): |
||||
|
if size not in self.sizes(): |
||||
|
raise TMDBImageSizeError |
||||
|
url = Configuration.images['base_url'].rstrip('/') |
||||
|
return url+'/{0}/{1}'.format(size, self.filename) |
||||
|
|
||||
|
# sort preferring locale's language, but keep remaining ordering consistent |
||||
|
def __lt__(self, other): |
||||
|
return (self.language == self._locale.language) \ |
||||
|
and (self.language != other.language) |
||||
|
def __gt__(self, other): |
||||
|
return (self.language != other.language) \ |
||||
|
and (other.language == self._locale.language) |
||||
|
# direct match for comparison |
||||
|
def __eq__(self, other): |
||||
|
return self.filename == other.filename |
||||
|
# special handling for boolean to see if exists |
||||
|
def __nonzero__(self): |
||||
|
if len(self.filename) == 0: |
||||
|
return False |
||||
|
return True |
||||
|
|
||||
|
def __repr__(self): |
||||
|
# BASE62 encoded filename, no need to worry about unicode |
||||
|
return u"<{0.__class__.__name__} '{0.filename}'>".format(self) |
||||
|
|
||||
|
class Backdrop( Image ): |
||||
|
def sizes(self): |
||||
|
return Configuration.images['backdrop_sizes'] |
||||
|
class Poster( Image ): |
||||
|
def sizes(self): |
||||
|
return Configuration.images['poster_sizes'] |
||||
|
class Profile( Image ): |
||||
|
def sizes(self): |
||||
|
return Configuration.images['profile_sizes'] |
||||
|
class Logo( Image ): |
||||
|
def sizes(self): |
||||
|
return Configuration.images['logo_sizes'] |
||||
|
|
||||
|
class AlternateTitle( Element ): |
||||
|
country = Datapoint('iso_3166_1') |
||||
|
title = Datapoint('title') |
||||
|
|
||||
|
# sort preferring locale's country, but keep remaining ordering consistent |
||||
|
def __lt__(self, other): |
||||
|
return (self.country == self._locale.country) \ |
||||
|
and (self.country != other.country) |
||||
|
def __gt__(self, other): |
||||
|
return (self.country != other.country) \ |
||||
|
and (other.country == self._locale.country) |
||||
|
def __eq__(self, other): |
||||
|
return self.country == other.country |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return u"<{0.__class__.__name__} '{0.title}' ({0.country})>"\ |
||||
|
.format(self).encode('utf-8') |
||||
|
|
||||
|
class Person( Element ): |
||||
|
id = Datapoint('id', initarg=1) |
||||
|
name = Datapoint('name') |
||||
|
biography = Datapoint('biography') |
||||
|
dayofbirth = Datapoint('birthday', default=None, handler=process_date) |
||||
|
dayofdeath = Datapoint('deathday', default=None, handler=process_date) |
||||
|
homepage = Datapoint('homepage') |
||||
|
birthplace = Datapoint('place_of_birth') |
||||
|
profile = Datapoint('profile_path', handler=Profile, \ |
||||
|
raw=False, default=None) |
||||
|
adult = Datapoint('adult') |
||||
|
aliases = Datalist('also_known_as') |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return u"<{0.__class__.__name__} '{0.name}'>"\ |
||||
|
.format(self).encode('utf-8') |
||||
|
|
||||
|
def _populate(self): |
||||
|
return Request('person/{0}'.format(self.id)) |
||||
|
def _populate_credits(self): |
||||
|
return Request('person/{0}/credits'.format(self.id), \ |
||||
|
language=self._locale.language) |
||||
|
def _populate_images(self): |
||||
|
return Request('person/{0}/images'.format(self.id)) |
||||
|
|
||||
|
roles = Datalist('cast', handler=lambda x: ReverseCast(raw=x), \ |
||||
|
poller=_populate_credits) |
||||
|
crew = Datalist('crew', handler=lambda x: ReverseCrew(raw=x), \ |
||||
|
poller=_populate_credits) |
||||
|
profiles = Datalist('profiles', handler=Profile, poller=_populate_images) |
||||
|
|
||||
|
class Cast( Person ): |
||||
|
character = Datapoint('character') |
||||
|
order = Datapoint('order') |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return u"<{0.__class__.__name__} '{0.name}' as '{0.character}'>"\ |
||||
|
.format(self).encode('utf-8') |
||||
|
|
||||
|
class Crew( Person ): |
||||
|
job = Datapoint('job') |
||||
|
department = Datapoint('department') |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return u"<{0.__class__.__name__} '{0.name}','{0.job}'>"\ |
||||
|
.format(self).encode('utf-8') |
||||
|
|
||||
|
class Keyword( Element ): |
||||
|
id = Datapoint('id') |
||||
|
name = Datapoint('name') |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return u"<{0.__class__.__name__} {0.name}>".format(self).encode('utf-8') |
||||
|
|
||||
|
class Release( Element ): |
||||
|
certification = Datapoint('certification') |
||||
|
country = Datapoint('iso_3166_1') |
||||
|
releasedate = Datapoint('release_date', handler=process_date) |
||||
|
def __repr__(self): |
||||
|
return u"<{0.__class__.__name__} {0.country}, {0.releasedate}>"\ |
||||
|
.format(self).encode('utf-8') |
||||
|
|
||||
|
class Trailer( Element ): |
||||
|
name = Datapoint('name') |
||||
|
size = Datapoint('size') |
||||
|
source = Datapoint('source') |
||||
|
|
||||
|
class YoutubeTrailer( Trailer ): |
||||
|
def geturl(self): |
||||
|
return "http://www.youtube.com/watch?v={0}".format(self.source) |
||||
|
|
||||
|
def __repr__(self): |
||||
|
# modified BASE64 encoding, no need to worry about unicode |
||||
|
return u"<{0.__class__.__name__} '{0.name}'>".format(self) |
||||
|
|
||||
|
class AppleTrailer( Element ): |
||||
|
name = Datapoint('name') |
||||
|
sources = Datadict('sources', handler=Trailer, attr='size') |
||||
|
|
||||
|
def sizes(self): |
||||
|
return self.sources.keys() |
||||
|
|
||||
|
def geturl(self, size=None): |
||||
|
if size is None: |
||||
|
# sort assuming ###p format for now, take largest resolution |
||||
|
size = str(sorted([int(size[:-1]) for size in self.sources])[-1])+'p' |
||||
|
return self.sources[size].source |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return u"<{0.__class__.__name__} '{0.name}'>".format(self) |
||||
|
|
||||
|
class Translation( Element ): |
||||
|
name = Datapoint('name') |
||||
|
language = Datapoint('iso_639_1') |
||||
|
englishname = Datapoint('english_name') |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return u"<{0.__class__.__name__} '{0.name}' ({0.language})>"\ |
||||
|
.format(self).encode('utf-8') |
||||
|
|
||||
|
class Genre( NameRepr, Element ): |
||||
|
id = Datapoint('id') |
||||
|
name = Datapoint('name') |
||||
|
|
||||
|
def _populate_movies(self): |
||||
|
return Request('genre/{0}/movies'.format(self.id), \ |
||||
|
language=self._locale.language) |
||||
|
|
||||
|
@property |
||||
|
def movies(self): |
||||
|
if 'movies' not in self._data: |
||||
|
search = MovieSearchResult(self._populate_movies(), \ |
||||
|
locale=self._locale) |
||||
|
search._name = "{0.name} Movies".format(self) |
||||
|
self._data['movies'] = search |
||||
|
return self._data['movies'] |
||||
|
|
||||
|
@classmethod |
||||
|
def getAll(cls, locale=None): |
||||
|
class GenreList( Element ): |
||||
|
genres = Datalist('genres', handler=Genre) |
||||
|
def _populate(self): |
||||
|
return Request('genre/list', language=self._locale.language) |
||||
|
return GenreList(locale=locale).genres |
||||
|
|
||||
|
|
||||
|
class Studio( NameRepr, Element ): |
||||
|
id = Datapoint('id', initarg=1) |
||||
|
name = Datapoint('name') |
||||
|
description = Datapoint('description') |
||||
|
headquarters = Datapoint('headquarters') |
||||
|
logo = Datapoint('logo_path', handler=Logo, \ |
||||
|
raw=False, default=None) |
||||
|
# FIXME: manage not-yet-defined handlers in a way that will propogate |
||||
|
# locale information properly |
||||
|
parent = Datapoint('parent_company', \ |
||||
|
handler=lambda x: Studio(raw=x)) |
||||
|
|
||||
|
def _populate(self): |
||||
|
return Request('company/{0}'.format(self.id)) |
||||
|
def _populate_movies(self): |
||||
|
return Request('company/{0}/movies'.format(self.id), \ |
||||
|
language=self._locale.language) |
||||
|
|
||||
|
# FIXME: add a cleaner way of adding types with no additional processing |
||||
|
@property |
||||
|
def movies(self): |
||||
|
if 'movies' not in self._data: |
||||
|
search = MovieSearchResult(self._populate_movies(), \ |
||||
|
locale=self._locale) |
||||
|
search._name = "{0.name} Movies".format(self) |
||||
|
self._data['movies'] = search |
||||
|
return self._data['movies'] |
||||
|
|
||||
|
class Country( NameRepr, Element ): |
||||
|
code = Datapoint('iso_3166_1') |
||||
|
name = Datapoint('name') |
||||
|
|
||||
|
class Language( NameRepr, Element ): |
||||
|
code = Datapoint('iso_639_1') |
||||
|
name = Datapoint('name') |
||||
|
|
||||
|
class Movie( Element ): |
||||
|
@classmethod |
||||
|
def latest(cls): |
||||
|
req = Request('latest/movie') |
||||
|
req.lifetime = 600 |
||||
|
return cls(raw=req.readJSON()) |
||||
|
|
||||
|
@classmethod |
||||
|
def nowplaying(cls, locale=None): |
||||
|
res = MovieSearchResult(Request('movie/now-playing'), locale=locale) |
||||
|
res._name = 'Now Playing' |
||||
|
return res |
||||
|
|
||||
|
@classmethod |
||||
|
def mostpopular(cls, locale=None): |
||||
|
res = MovieSearchResult(Request('movie/popular'), locale=locale) |
||||
|
res._name = 'Popular' |
||||
|
return res |
||||
|
|
||||
|
@classmethod |
||||
|
def toprated(cls, locale=None): |
||||
|
res = MovieSearchResult(Request('movie/top_rated'), locale=locale) |
||||
|
res._name = 'Top Rated' |
||||
|
return res |
||||
|
|
||||
|
@classmethod |
||||
|
def upcoming(cls, locale=None): |
||||
|
res = MovieSearchResult(Request('movie/upcoming'), locale=locale) |
||||
|
res._name = 'Upcoming' |
||||
|
return res |
||||
|
|
||||
|
@classmethod |
||||
|
def favorites(cls, session=None): |
||||
|
if session is None: |
||||
|
session = get_session() |
||||
|
account = Account(session=session) |
||||
|
res = MovieSearchResult( |
||||
|
Request('account/{0}/favorite_movies'.format(account.id), |
||||
|
session_id=session.sessionid)) |
||||
|
res._name = "Favorites" |
||||
|
return res |
||||
|
|
||||
|
@classmethod |
||||
|
def ratedmovies(cls, session=None): |
||||
|
if session is None: |
||||
|
session = get_session() |
||||
|
account = Account(session=session) |
||||
|
res = MovieSearchResult( |
||||
|
Request('account/{0}/rated_movies'.format(account.id), |
||||
|
session_id=session.sessionid)) |
||||
|
res._name = "Movies You Rated" |
||||
|
return res |
||||
|
|
||||
|
@classmethod |
||||
|
def watchlist(cls, session=None): |
||||
|
if session is None: |
||||
|
session = get_session() |
||||
|
account = Account(session=session) |
||||
|
res = MovieSearchResult( |
||||
|
Request('account/{0}/movie_watchlist'.format(account.id), |
||||
|
session_id=session.sessionid)) |
||||
|
res._name = "Movies You're Watching" |
||||
|
return res |
||||
|
|
||||
|
@classmethod |
||||
|
def fromIMDB(cls, imdbid, locale=None): |
||||
|
try: |
||||
|
# assume string |
||||
|
if not imdbid.startswith('tt'): |
||||
|
imdbid = "tt{0:0>7}".format(imdbid) |
||||
|
except AttributeError: |
||||
|
# assume integer |
||||
|
imdbid = "tt{0:0>7}".format(imdbid) |
||||
|
if locale is None: |
||||
|
locale = get_locale() |
||||
|
movie = cls(imdbid, locale=locale) |
||||
|
movie._populate() |
||||
|
return movie |
||||
|
|
||||
|
id = Datapoint('id', initarg=1) |
||||
|
title = Datapoint('title') |
||||
|
originaltitle = Datapoint('original_title') |
||||
|
tagline = Datapoint('tagline') |
||||
|
overview = Datapoint('overview') |
||||
|
runtime = Datapoint('runtime') |
||||
|
budget = Datapoint('budget') |
||||
|
revenue = Datapoint('revenue') |
||||
|
releasedate = Datapoint('release_date', handler=process_date) |
||||
|
homepage = Datapoint('homepage') |
||||
|
imdb = Datapoint('imdb_id') |
||||
|
|
||||
|
backdrop = Datapoint('backdrop_path', handler=Backdrop, \ |
||||
|
raw=False, default=None) |
||||
|
poster = Datapoint('poster_path', handler=Poster, \ |
||||
|
raw=False, default=None) |
||||
|
|
||||
|
popularity = Datapoint('popularity') |
||||
|
userrating = Datapoint('vote_average') |
||||
|
votes = Datapoint('vote_count') |
||||
|
|
||||
|
adult = Datapoint('adult') |
||||
|
collection = Datapoint('belongs_to_collection', handler=lambda x: \ |
||||
|
Collection(raw=x)) |
||||
|
genres = Datalist('genres', handler=Genre) |
||||
|
studios = Datalist('production_companies', handler=Studio) |
||||
|
countries = Datalist('production_countries', handler=Country) |
||||
|
languages = Datalist('spoken_languages', handler=Language) |
||||
|
|
||||
|
def _populate(self): |
||||
|
return Request('movie/{0}'.format(self.id), \ |
||||
|
language=self._locale.language) |
||||
|
def _populate_titles(self): |
||||
|
kwargs = {} |
||||
|
if not self._locale.fallthrough: |
||||
|
kwargs['country'] = self._locale.country |
||||
|
return Request('movie/{0}/alternative_titles'.format(self.id), **kwargs) |
||||
|
def _populate_cast(self): |
||||
|
return Request('movie/{0}/casts'.format(self.id)) |
||||
|
def _populate_images(self): |
||||
|
kwargs = {} |
||||
|
if not self._locale.fallthrough: |
||||
|
kwargs['language'] = self._locale.language |
||||
|
return Request('movie/{0}/images'.format(self.id), **kwargs) |
||||
|
def _populate_keywords(self): |
||||
|
return Request('movie/{0}/keywords'.format(self.id)) |
||||
|
def _populate_releases(self): |
||||
|
return Request('movie/{0}/releases'.format(self.id)) |
||||
|
def _populate_trailers(self): |
||||
|
return Request('movie/{0}/trailers'.format(self.id), \ |
||||
|
language=self._locale.language) |
||||
|
def _populate_translations(self): |
||||
|
return Request('movie/{0}/translations'.format(self.id)) |
||||
|
|
||||
|
alternate_titles = Datalist('titles', handler=AlternateTitle, \ |
||||
|
poller=_populate_titles, sort=True) |
||||
|
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) |
||||
|
keywords = Datalist('keywords', handler=Keyword, \ |
||||
|
poller=_populate_keywords) |
||||
|
releases = Datadict('countries', handler=Release, \ |
||||
|
poller=_populate_releases, attr='country') |
||||
|
youtube_trailers = Datalist('youtube', handler=YoutubeTrailer, \ |
||||
|
poller=_populate_trailers) |
||||
|
apple_trailers = Datalist('quicktime', handler=AppleTrailer, \ |
||||
|
poller=_populate_trailers) |
||||
|
translations = Datalist('translations', handler=Translation, \ |
||||
|
poller=_populate_translations) |
||||
|
|
||||
|
def setFavorite(self, value): |
||||
|
req = Request('account/{0}/favorite'.format(\ |
||||
|
Account(session=self._session).id), |
||||
|
session_id=self._session.sessionid) |
||||
|
req.add_data({'movie_id':self.id, 'favorite':str(bool(value)).lower()}) |
||||
|
req.lifetime = 0 |
||||
|
req.readJSON() |
||||
|
|
||||
|
def setRating(self, value): |
||||
|
if not (0 <= value <= 10): |
||||
|
raise TMDBError("Ratings must be between '0' and '10'.") |
||||
|
req = Request('movie/{0}/rating'.format(self.id), \ |
||||
|
session_id=self._session.sessionid) |
||||
|
req.lifetime = 0 |
||||
|
req.add_data({'value':value}) |
||||
|
req.readJSON() |
||||
|
|
||||
|
def setWatchlist(self, value): |
||||
|
req = Request('account/{0}/movie_watchlist'.format(\ |
||||
|
Account(session=self._session).id), |
||||
|
session_id=self._session.sessionid) |
||||
|
req.lifetime = 0 |
||||
|
req.add_data({'movie_id':self.id, |
||||
|
'movie_watchlist':str(bool(value)).lower()}) |
||||
|
req.readJSON() |
||||
|
|
||||
|
def getSimilar(self): |
||||
|
return self.similar |
||||
|
|
||||
|
@property |
||||
|
def similar(self): |
||||
|
res = MovieSearchResult(Request('movie/{0}/similar_movies'\ |
||||
|
.format(self.id)), |
||||
|
locale=self._locale) |
||||
|
res._name = 'Similar to {0}'.format(self._printable_name()) |
||||
|
return res |
||||
|
|
||||
|
@property |
||||
|
def lists(self): |
||||
|
res = ListSearchResult(Request('movie/{0}/lists'.format(self.id))) |
||||
|
res._name = "Lists containing {0}".format(self._printable_name()) |
||||
|
return res |
||||
|
|
||||
|
def _printable_name(self): |
||||
|
if self.title is not None: |
||||
|
s = u"'{0}'".format(self.title) |
||||
|
elif self.originaltitle is not None: |
||||
|
s = u"'{0}'".format(self.originaltitle) |
||||
|
else: |
||||
|
s = u"'No Title'" |
||||
|
if self.releasedate: |
||||
|
s = u"{0} ({1})".format(s, self.releasedate.year) |
||||
|
return s |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return u"<{0} {1}>".format(self.__class__.__name__,\ |
||||
|
self._printable_name()).encode('utf-8') |
||||
|
|
||||
|
class ReverseCast( Movie ): |
||||
|
character = Datapoint('character') |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return u"<{0.__class__.__name__} '{0.character}' on {1}>"\ |
||||
|
.format(self, self._printable_name()).encode('utf-8') |
||||
|
|
||||
|
class ReverseCrew( Movie ): |
||||
|
department = Datapoint('department') |
||||
|
job = Datapoint('job') |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return u"<{0.__class__.__name__} '{0.job}' for {1}>"\ |
||||
|
.format(self, self._printable_name()).encode('utf-8') |
||||
|
|
||||
|
class Collection( NameRepr, Element ): |
||||
|
id = Datapoint('id', initarg=1) |
||||
|
name = Datapoint('name') |
||||
|
backdrop = Datapoint('backdrop_path', handler=Backdrop, \ |
||||
|
raw=False, default=None) |
||||
|
poster = Datapoint('poster_path', handler=Poster, \ |
||||
|
raw=False, default=None) |
||||
|
members = Datalist('parts', handler=Movie) |
||||
|
overview = Datapoint('overview') |
||||
|
|
||||
|
def _populate(self): |
||||
|
return Request('collection/{0}'.format(self.id), \ |
||||
|
language=self._locale.language) |
||||
|
def _populate_images(self): |
||||
|
kwargs = {} |
||||
|
if not self._locale.fallthrough: |
||||
|
kwargs['language'] = self._locale.language |
||||
|
return Request('collection/{0}/images'.format(self.id), **kwargs) |
||||
|
|
||||
|
backdrops = Datalist('backdrops', handler=Backdrop, \ |
||||
|
poller=_populate_images, sort=True) |
||||
|
posters = Datalist('posters', handler=Poster, \ |
||||
|
poller=_populate_images, sort=True) |
||||
|
|
||||
|
class List( NameRepr, Element ): |
||||
|
id = Datapoint('id', initarg=1) |
||||
|
name = Datapoint('name') |
||||
|
author = Datapoint('created_by') |
||||
|
description = Datapoint('description') |
||||
|
favorites = Datapoint('favorite_count') |
||||
|
language = Datapoint('iso_639_1') |
||||
|
count = Datapoint('item_count') |
||||
|
poster = Datapoint('poster_path', handler=Poster, \ |
||||
|
raw=False, default=None) |
||||
|
|
||||
|
members = Datalist('items', handler=Movie) |
||||
|
|
||||
|
def _populate(self): |
||||
|
return Request('list/{0}'.format(self.id)) |
||||
|
|
@ -0,0 +1,131 @@ |
|||||
|
#!/usr/bin/env python |
||||
|
# -*- coding: utf-8 -*- |
||||
|
#----------------------- |
||||
|
# Name: tmdb_auth.py |
||||
|
# Python Library |
||||
|
# Author: Raymond Wagner |
||||
|
# Purpose: Provide authentication and session services for |
||||
|
# calls against the TMDB v3 API |
||||
|
#----------------------- |
||||
|
|
||||
|
from datetime import datetime as _pydatetime, \ |
||||
|
tzinfo as _pytzinfo |
||||
|
import re |
||||
|
class datetime( _pydatetime ): |
||||
|
"""Customized datetime class with ISO format parsing.""" |
||||
|
_reiso = re.compile('(?P<year>[0-9]{4})' |
||||
|
'-(?P<month>[0-9]{1,2})' |
||||
|
'-(?P<day>[0-9]{1,2})' |
||||
|
'.' |
||||
|
'(?P<hour>[0-9]{2})' |
||||
|
':(?P<min>[0-9]{2})' |
||||
|
'(:(?P<sec>[0-9]{2}))?' |
||||
|
'(?P<tz>Z|' |
||||
|
'(?P<tzdirec>[-+])' |
||||
|
'(?P<tzhour>[0-9]{1,2})' |
||||
|
'(:)?' |
||||
|
'(?P<tzmin>[0-9]{2})?' |
||||
|
')?') |
||||
|
|
||||
|
class _tzinfo( _pytzinfo): |
||||
|
def __init__(self, direc='+', hr=0, min=0): |
||||
|
if direc == '-': |
||||
|
hr = -1*int(hr) |
||||
|
self._offset = timedelta(hours=int(hr), minutes=int(min)) |
||||
|
def utcoffset(self, dt): return self._offset |
||||
|
def tzname(self, dt): return '' |
||||
|
def dst(self, dt): return timedelta(0) |
||||
|
|
||||
|
@classmethod |
||||
|
def fromIso(cls, isotime, sep='T'): |
||||
|
match = cls._reiso.match(isotime) |
||||
|
if match is None: |
||||
|
raise TypeError("time data '%s' does not match ISO 8601 format" \ |
||||
|
% isotime) |
||||
|
|
||||
|
dt = [int(a) for a in match.groups()[:5]] |
||||
|
if match.group('sec') is not None: |
||||
|
dt.append(int(match.group('sec'))) |
||||
|
else: |
||||
|
dt.append(0) |
||||
|
if match.group('tz'): |
||||
|
if match.group('tz') == 'Z': |
||||
|
tz = cls._tzinfo() |
||||
|
elif match.group('tzmin'): |
||||
|
tz = cls._tzinfo(*match.group('tzdirec','tzhour','tzmin')) |
||||
|
else: |
||||
|
tz = cls._tzinfo(*match.group('tzdirec','tzhour')) |
||||
|
dt.append(0) |
||||
|
dt.append(tz) |
||||
|
return cls(*dt) |
||||
|
|
||||
|
from request import Request |
||||
|
from tmdb_exceptions import * |
||||
|
|
||||
|
syssession = None |
||||
|
|
||||
|
def set_session(sessionid): |
||||
|
global syssession |
||||
|
syssession = Session(sessionid) |
||||
|
|
||||
|
def get_session(sessionid=None): |
||||
|
global syssession |
||||
|
if sessionid: |
||||
|
return Session(sessionid) |
||||
|
elif syssession is not None: |
||||
|
return syssession |
||||
|
else: |
||||
|
return Session.new() |
||||
|
|
||||
|
class Session( object ): |
||||
|
|
||||
|
@classmethod |
||||
|
def new(cls): |
||||
|
return cls(None) |
||||
|
|
||||
|
def __init__(self, sessionid): |
||||
|
self.sessionid = sessionid |
||||
|
|
||||
|
@property |
||||
|
def sessionid(self): |
||||
|
if self._sessionid is None: |
||||
|
if self._authtoken is None: |
||||
|
raise TMDBError("No Auth Token to produce Session for") |
||||
|
# TODO: check authtokenexpiration against current time |
||||
|
req = Request('authentication/session/new', \ |
||||
|
request_token=self._authtoken) |
||||
|
req.lifetime = 0 |
||||
|
dat = req.readJSON() |
||||
|
if not dat['success']: |
||||
|
raise TMDBError("Session generation failed") |
||||
|
self._sessionid = dat['session_id'] |
||||
|
return self._sessionid |
||||
|
|
||||
|
@sessionid.setter |
||||
|
def sessionid(self, value): |
||||
|
self._sessionid = value |
||||
|
self._authtoken = None |
||||
|
self._authtokenexpiration = None |
||||
|
if value is None: |
||||
|
self.authenticated = False |
||||
|
else: |
||||
|
self.authenticated = True |
||||
|
|
||||
|
@property |
||||
|
def authtoken(self): |
||||
|
if self.authenticated: |
||||
|
raise TMDBError("Session is already authenticated") |
||||
|
if self._authtoken is None: |
||||
|
req = Request('authentication/token/new') |
||||
|
req.lifetime = 0 |
||||
|
dat = req.readJSON() |
||||
|
if not dat['success']: |
||||
|
raise TMDBError("Auth Token request failed") |
||||
|
self._authtoken = dat['request_token'] |
||||
|
self._authtokenexpiration = datetime.fromIso(dat['expires_at']) |
||||
|
return self._authtoken |
||||
|
|
||||
|
@property |
||||
|
def callbackurl(self): |
||||
|
return "http://www.themoviedb.org/authenticate/"+self._authtoken |
||||
|
|
@ -0,0 +1,89 @@ |
|||||
|
#!/usr/bin/env python |
||||
|
# -*- coding: utf-8 -*- |
||||
|
#----------------------- |
||||
|
# Name: tmdb_exceptions.py Common exceptions used in tmdbv3 API library |
||||
|
# Python Library |
||||
|
# Author: Raymond Wagner |
||||
|
#----------------------- |
||||
|
|
||||
|
class TMDBError( Exception ): |
||||
|
Error = 0 |
||||
|
KeyError = 10 |
||||
|
KeyMissing = 20 |
||||
|
KeyInvalid = 30 |
||||
|
KeyRevoked = 40 |
||||
|
RequestError = 50 |
||||
|
RequestInvalid = 51 |
||||
|
PagingIssue = 60 |
||||
|
CacheError = 70 |
||||
|
CacheReadError = 71 |
||||
|
CacheWriteError = 72 |
||||
|
CacheDirectoryError = 73 |
||||
|
ImageSizeError = 80 |
||||
|
HTTPError = 90 |
||||
|
Offline = 100 |
||||
|
LocaleError = 110 |
||||
|
|
||||
|
def __init__(self, msg=None, errno=0): |
||||
|
self.errno = errno |
||||
|
if errno == 0: |
||||
|
self.errno = getattr(self, 'TMDB'+self.__class__.__name__, errno) |
||||
|
self.args = (msg,) |
||||
|
|
||||
|
class TMDBKeyError( TMDBError ): |
||||
|
pass |
||||
|
|
||||
|
class TMDBKeyMissing( TMDBKeyError ): |
||||
|
pass |
||||
|
|
||||
|
class TMDBKeyInvalid( TMDBKeyError ): |
||||
|
pass |
||||
|
|
||||
|
class TMDBKeyRevoked( TMDBKeyInvalid ): |
||||
|
pass |
||||
|
|
||||
|
class TMDBRequestError( TMDBError ): |
||||
|
pass |
||||
|
|
||||
|
class TMDBRequestInvalid( TMDBRequestError ): |
||||
|
pass |
||||
|
|
||||
|
class TMDBPagingIssue( TMDBRequestError ): |
||||
|
pass |
||||
|
|
||||
|
class TMDBCacheError( TMDBRequestError ): |
||||
|
pass |
||||
|
|
||||
|
class TMDBCacheReadError( TMDBCacheError ): |
||||
|
def __init__(self, filename): |
||||
|
super(TMDBCacheReadError, self).__init__( |
||||
|
"User does not have permission to access cache file: {0}.".format(filename)) |
||||
|
self.filename = filename |
||||
|
|
||||
|
class TMDBCacheWriteError( TMDBCacheError ): |
||||
|
def __init__(self, filename): |
||||
|
super(TMDBCacheWriteError, self).__init__( |
||||
|
"User does not have permission to write cache file: {0}.".format(filename)) |
||||
|
self.filename = filename |
||||
|
|
||||
|
class TMDBCacheDirectoryError( TMDBCacheError ): |
||||
|
def __init__(self, filename): |
||||
|
super(TMDBCacheDirectoryError, self).__init__( |
||||
|
"Directory containing cache file does not exist: {0}.".format(filename)) |
||||
|
self.filename = filename |
||||
|
|
||||
|
class TMDBImageSizeError( TMDBError ): |
||||
|
pass |
||||
|
|
||||
|
class TMDBHTTPError( TMDBError ): |
||||
|
def __init__(self, err): |
||||
|
self.httperrno = err.code |
||||
|
self.response = err.fp.read() |
||||
|
super(TMDBHTTPError, self).__init__(str(err)) |
||||
|
|
||||
|
class TMDBOffline( TMDBError ): |
||||
|
pass |
||||
|
|
||||
|
class TMDBLocaleError( TMDBError ): |
||||
|
pass |
||||
|
|
@ -0,0 +1,366 @@ |
|||||
|
#!/usr/bin/env python |
||||
|
# -*- coding: utf-8 -*- |
||||
|
#----------------------- |
||||
|
# Name: util.py Assorted utilities used in tmdb_api |
||||
|
# Python Library |
||||
|
# Author: Raymond Wagner |
||||
|
#----------------------- |
||||
|
|
||||
|
from copy import copy |
||||
|
from locales import get_locale |
||||
|
from tmdb_auth import get_session |
||||
|
|
||||
|
class NameRepr( object ): |
||||
|
"""Mixin for __repr__ methods using 'name' attribute.""" |
||||
|
def __repr__(self): |
||||
|
return u"<{0.__class__.__name__} '{0.name}'>"\ |
||||
|
.format(self).encode('utf-8') |
||||
|
|
||||
|
class SearchRepr( object ): |
||||
|
""" |
||||
|
Mixin for __repr__ methods for classes with '_name' and |
||||
|
'_request' attributes. |
||||
|
""" |
||||
|
def __repr__(self): |
||||
|
name = self._name if self._name else self._request._kwargs['query'] |
||||
|
return u"<Search Results: {0}>".format(name).encode('utf-8') |
||||
|
|
||||
|
class Poller( object ): |
||||
|
""" |
||||
|
Wrapper for an optional callable to populate an Element derived class |
||||
|
with raw data, or data from a Request. |
||||
|
""" |
||||
|
def __init__(self, func, lookup, inst=None): |
||||
|
self.func = func |
||||
|
self.lookup = lookup |
||||
|
self.inst = inst |
||||
|
if func: |
||||
|
# with function, this allows polling data from the API |
||||
|
self.__doc__ = func.__doc__ |
||||
|
self.__name__ = func.__name__ |
||||
|
self.__module__ = func.__module__ |
||||
|
else: |
||||
|
# without function, this is just a dummy poller used for applying |
||||
|
# raw data to a new Element class with the lookup table |
||||
|
self.__name__ = '_populate' |
||||
|
|
||||
|
def __get__(self, inst, owner): |
||||
|
# normal decorator stuff |
||||
|
# return self for a class |
||||
|
# return instantiated copy of self for an object |
||||
|
if inst is None: |
||||
|
return self |
||||
|
func = None |
||||
|
if self.func: |
||||
|
func = self.func.__get__(inst, owner) |
||||
|
return self.__class__(func, self.lookup, inst) |
||||
|
|
||||
|
def __call__(self): |
||||
|
# retrieve data from callable function, and apply |
||||
|
if not callable(self.func): |
||||
|
raise RuntimeError('Poller object called without a source function') |
||||
|
req = self.func() |
||||
|
if (('language' in req._kwargs) or ('country' in req._kwargs)) \ |
||||
|
and self.inst._locale.fallthrough: |
||||
|
# request specifies a locale filter, and fallthrough is enabled |
||||
|
# run a first pass with specified filter |
||||
|
if not self.apply(req.readJSON(), False): |
||||
|
return |
||||
|
# if first pass results in missed data, run a second pass to |
||||
|
# fill in the gaps |
||||
|
self.apply(req.new(language=None, country=None).readJSON()) |
||||
|
# re-apply the filtered first pass data over top the second |
||||
|
# unfiltered set. this is to work around the issue that the |
||||
|
# properties have no way of knowing when they should or |
||||
|
# should not overwrite existing data. the cache engine will |
||||
|
# take care of the duplicate query |
||||
|
self.apply(req.readJSON()) |
||||
|
|
||||
|
def apply(self, data, set_nones=True): |
||||
|
# apply data directly, bypassing callable function |
||||
|
unfilled = False |
||||
|
for k,v in self.lookup.items(): |
||||
|
if (k in data) and \ |
||||
|
((data[k] is not None) if callable(self.func) else True): |
||||
|
# argument received data, populate it |
||||
|
setattr(self.inst, v, data[k]) |
||||
|
elif v in self.inst._data: |
||||
|
# argument did not receive data, but Element already contains |
||||
|
# some value, so skip this |
||||
|
continue |
||||
|
elif set_nones: |
||||
|
# argument did not receive data, so fill it with None |
||||
|
# to indicate such and prevent a repeat scan |
||||
|
setattr(self.inst, v, None) |
||||
|
else: |
||||
|
# argument does not need data, so ignore it allowing it to |
||||
|
# trigger a later poll. this is intended for use when |
||||
|
# initializing a class with raw data, or when performing a |
||||
|
# first pass through when performing locale fall through |
||||
|
unfilled = True |
||||
|
return unfilled |
||||
|
|
||||
|
class Data( object ): |
||||
|
""" |
||||
|
Basic response definition class |
||||
|
This maps to a single key in a JSON dictionary received from the API |
||||
|
""" |
||||
|
def __init__(self, field, initarg=None, handler=None, poller=None, |
||||
|
raw=True, default=u'', lang=False): |
||||
|
""" |
||||
|
This defines how the dictionary value is to be processed by the poller |
||||
|
field -- defines the dictionary key that filters what data this uses |
||||
|
initarg -- (optional) specifies that this field must be supplied |
||||
|
when creating a new instance of the Element class this |
||||
|
definition is mapped to. Takes an integer for the order |
||||
|
it should be used in the input arguments |
||||
|
handler -- (optional) callable used to process the received value |
||||
|
before being stored in the Element object. |
||||
|
poller -- (optional) callable to be used if data is requested and |
||||
|
this value has not yet been defined. the callable should |
||||
|
return a dictionary of data from a JSON query. many |
||||
|
definitions may share a single poller, which will be |
||||
|
and the data used to populate all referenced definitions |
||||
|
based off their defined 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.initarg = initarg |
||||
|
self.poller = poller |
||||
|
self.raw = raw |
||||
|
self.default = default |
||||
|
self.sethandler(handler) |
||||
|
|
||||
|
def __get__(self, inst, owner): |
||||
|
if inst is None: |
||||
|
return self |
||||
|
if self.field not in inst._data: |
||||
|
if self.poller is None: |
||||
|
return None |
||||
|
self.poller.__get__(inst, owner)() |
||||
|
return inst._data[self.field] |
||||
|
|
||||
|
def __set__(self, inst, value): |
||||
|
if (value is not None) and (value != ''): |
||||
|
value = self.handler(value) |
||||
|
else: |
||||
|
value = self.default |
||||
|
if isinstance(value, Element): |
||||
|
value._locale = inst._locale |
||||
|
value._session = inst._session |
||||
|
inst._data[self.field] = value |
||||
|
|
||||
|
def sethandler(self, handler): |
||||
|
# ensure handler is always callable, even for passthrough data |
||||
|
if handler is None: |
||||
|
self.handler = lambda x: x |
||||
|
elif isinstance(handler, ElementType) and self.raw: |
||||
|
self.handler = lambda x: handler(raw=x) |
||||
|
else: |
||||
|
self.handler = lambda x: handler(x) |
||||
|
|
||||
|
class Datapoint( Data ): |
||||
|
pass |
||||
|
|
||||
|
class Datalist( Data ): |
||||
|
""" |
||||
|
Response definition class for list 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): |
||||
|
""" |
||||
|
This defines how the dictionary value is to be processed by the poller |
||||
|
field -- defines the dictionary key that filters what data this uses |
||||
|
handler -- (optional) callable used to process the received value |
||||
|
before being stored in the Element object. |
||||
|
poller -- (optional) callable to be used if data is requested and |
||||
|
this value has not yet been defined. the callable should |
||||
|
return a dictionary of data from a JSON query. many |
||||
|
definitions may share a single poller, which will be |
||||
|
and the data used to populate all referenced definitions |
||||
|
based off their defined field |
||||
|
sort -- (optional) name of attribute in resultant data to be used |
||||
|
to sort the list after processing. this effectively |
||||
|
a handler be defined to process the data into something |
||||
|
that has attributes |
||||
|
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) |
||||
|
self.sort = sort |
||||
|
def __set__(self, inst, value): |
||||
|
data = [] |
||||
|
if value: |
||||
|
for val in value: |
||||
|
val = self.handler(val) |
||||
|
if isinstance(val, Element): |
||||
|
val._locale = inst._locale |
||||
|
val._session = inst._session |
||||
|
data.append(val) |
||||
|
if self.sort: |
||||
|
if self.sort is True: |
||||
|
data.sort() |
||||
|
else: |
||||
|
data.sort(key=lambda x: getattr(x, self.sort)) |
||||
|
inst._data[self.field] = data |
||||
|
|
||||
|
class Datadict( Data ): |
||||
|
""" |
||||
|
Response definition class for dictionary 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, |
||||
|
key=None, attr=None): |
||||
|
""" |
||||
|
This defines how the dictionary value is to be processed by the poller |
||||
|
field -- defines the dictionary key that filters what data this uses |
||||
|
handler -- (optional) callable used to process the received value |
||||
|
before being stored in the Element object. |
||||
|
poller -- (optional) callable to be used if data is requested and |
||||
|
this value has not yet been defined. the callable should |
||||
|
return a dictionary of data from a JSON query. many |
||||
|
definitions may share a single poller, which will be |
||||
|
and the data used to populate all referenced definitions |
||||
|
based off their defined field |
||||
|
key -- (optional) name of key in resultant data to be used as |
||||
|
the key in the stored dictionary. if this is not the |
||||
|
field name from the source data is used instead |
||||
|
attr -- (optional) name of attribute in resultant data to be used |
||||
|
as the key in the stored dictionary. if this is not |
||||
|
the field name from the source data is used 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: |
||||
|
raise TypeError("`key` and `attr` cannot both be defined") |
||||
|
super(Datadict, self).__init__(field, None, handler, poller, raw) |
||||
|
if key: |
||||
|
self.getkey = lambda x: x[key] |
||||
|
elif attr: |
||||
|
self.getkey = lambda x: getattr(x, attr) |
||||
|
else: |
||||
|
raise TypeError("Datadict requires `key` or `attr` be defined "+\ |
||||
|
"for populating the dictionary") |
||||
|
def __set__(self, inst, value): |
||||
|
data = {} |
||||
|
if value: |
||||
|
for val in value: |
||||
|
val = self.handler(val) |
||||
|
if isinstance(val, Element): |
||||
|
val._locale = inst._locale |
||||
|
val._session = inst._session |
||||
|
data[self.getkey(val)] = val |
||||
|
inst._data[self.field] = data |
||||
|
|
||||
|
class ElementType( type ): |
||||
|
""" |
||||
|
MetaClass used to pre-process Element-derived classes and set up the |
||||
|
Data definitions |
||||
|
""" |
||||
|
def __new__(mcs, name, bases, attrs): |
||||
|
# any Data or Poller object defined in parent classes must be cloned |
||||
|
# and processed in this class to function properly |
||||
|
# scan through available bases for all such definitions and insert |
||||
|
# a copy into this class's attributes |
||||
|
# run in reverse order so higher priority values overwrite lower ones |
||||
|
data = {} |
||||
|
pollers = {'_populate':None} |
||||
|
|
||||
|
for base in reversed(bases): |
||||
|
if isinstance(base, mcs): |
||||
|
for k, attr in base.__dict__.items(): |
||||
|
if isinstance(attr, Data): |
||||
|
# extract copies of each defined Data element from |
||||
|
# parent classes |
||||
|
attr = copy(attr) |
||||
|
attr.poller = attr.poller.func |
||||
|
data[k] = attr |
||||
|
elif isinstance(attr, Poller): |
||||
|
# extract copies of each defined Poller function |
||||
|
# from parent classes |
||||
|
pollers[k] = attr.func |
||||
|
for k,attr in attrs.items(): |
||||
|
if isinstance(attr, Data): |
||||
|
data[k] = attr |
||||
|
if '_populate' in attrs: |
||||
|
pollers['_populate'] = attrs['_populate'] |
||||
|
|
||||
|
# process all defined Data attribues, testing for use as an initial |
||||
|
# argument, and building a list of what Pollers are used to populate |
||||
|
# which Data points |
||||
|
pollermap = dict([(k,[]) for k in pollers]) |
||||
|
initargs = [] |
||||
|
for k,v in data.items(): |
||||
|
v.name = k |
||||
|
if v.initarg: |
||||
|
initargs.append(v) |
||||
|
if v.poller: |
||||
|
pn = v.poller.__name__ |
||||
|
if pn not in pollermap: |
||||
|
pollermap[pn] = [] |
||||
|
if pn not in pollers: |
||||
|
pollers[pn] = v.poller |
||||
|
pollermap[pn].append(v) |
||||
|
else: |
||||
|
pollermap['_populate'].append(v) |
||||
|
|
||||
|
# wrap each used poller function with a Poller class, and push into |
||||
|
# the new class attributes |
||||
|
for k,v in pollermap.items(): |
||||
|
if len(v) == 0: |
||||
|
continue |
||||
|
lookup = dict([(attr.field, attr.name) for attr in v]) |
||||
|
poller = Poller(pollers[k], lookup) |
||||
|
attrs[k] = poller |
||||
|
# backfill wrapped Poller into each mapped Data object, and ensure |
||||
|
# the data elements are defined for this new class |
||||
|
for attr in v: |
||||
|
attr.poller = poller |
||||
|
attrs[attr.name] = attr |
||||
|
|
||||
|
# build sorted list of arguments used for intialization |
||||
|
attrs['_InitArgs'] = tuple([a.name for a in \ |
||||
|
sorted(initargs, key=lambda x: x.initarg)]) |
||||
|
return type.__new__(mcs, name, bases, attrs) |
||||
|
|
||||
|
def __call__(cls, *args, **kwargs): |
||||
|
obj = cls.__new__(cls) |
||||
|
if ('locale' in kwargs) and (kwargs['locale'] is not None): |
||||
|
obj._locale = kwargs['locale'] |
||||
|
else: |
||||
|
obj._locale = get_locale() |
||||
|
|
||||
|
if 'session' in kwargs: |
||||
|
obj._session = kwargs['session'] |
||||
|
else: |
||||
|
obj._session = get_session() |
||||
|
|
||||
|
obj._data = {} |
||||
|
if 'raw' in kwargs: |
||||
|
# if 'raw' keyword is supplied, create populate object manually |
||||
|
if len(args) != 0: |
||||
|
raise TypeError('__init__() takes exactly 2 arguments (1 given)') |
||||
|
obj._populate.apply(kwargs['raw'], False) |
||||
|
else: |
||||
|
# if not, the number of input arguments must exactly match that |
||||
|
# defined by the Data definitions |
||||
|
if len(args) != len(cls._InitArgs): |
||||
|
raise TypeError('__init__() takes exactly {0} arguments ({1} given)'\ |
||||
|
.format(len(cls._InitArgs)+1, len(args)+1)) |
||||
|
for a,v in zip(cls._InitArgs, args): |
||||
|
setattr(obj, a, v) |
||||
|
|
||||
|
obj.__init__() |
||||
|
return obj |
||||
|
|
||||
|
class Element( object ): |
||||
|
__metaclass__ = ElementType |
||||
|
_lang = 'en' |
||||
|
|
Loading…
Reference in new issue