tvdb_api-2.0/0000755000076500000240000000000013157176641013050 5ustar dbrstaff00000000000000tvdb_api-2.0/MANIFEST.in0000644000076500000240000000011013151446751014572 0ustar dbrstaff00000000000000include UNLICENSE include readme.md include tests/*.py include Rakefile tvdb_api-2.0/PKG-INFO0000644000076500000240000000224113157176641014144 0ustar dbrstaff00000000000000Metadata-Version: 1.1 Name: tvdb_api Version: 2.0 Summary: Interface to thetvdb.com Home-page: http://github.com/dbr/tvdb_api/tree/master Author: dbr/Ben Author-email: UNKNOWN License: unlicense Description: An easy to use API interface to TheTVDB.com Basic usage is: >>> import tvdb_api >>> t = tvdb_api.Tvdb() >>> ep = t['My Name Is Earl'][1][22] >>> ep >>> ep['episodeName'] u'Stole a Badge' Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Multimedia Classifier: Topic :: Utilities Classifier: Topic :: Software Development :: Libraries :: Python Modules tvdb_api-2.0/Rakefile0000644000076500000240000000463013151446751014514 0ustar dbrstaff00000000000000require 'fileutils' task :default => [:clean] task :clean do [".", "tests"].each do |cd| puts "Cleaning directory #{cd}" Dir.new(cd).each do |t| if t =~ /.*\.pyc$/ puts "Removing #{File.join(cd, t)}" File.delete(File.join(cd, t)) end end end end desc "Upversion files" task :upversion do puts "Upversioning" Dir.glob("*.py").each do |filename| f = File.new(filename, File::RDWR) contents = f.read() contents.gsub!(/__version__ = ".+?"/){|m| cur_version = m.scan(/\d+\.\d+/)[0].to_f new_version = cur_version + 0.1 puts "Current version: #{cur_version}" puts "New version: #{new_version}" new_line = "__version__ = \"#{new_version}\"" puts "Old line: #{m}" puts "New line: #{new_line}" m = new_line } puts contents[0] f.truncate(0) # empty the existing file f.seek(0) f.write(contents.to_s) # write modified file f.close() end end desc "Upload current version to PyPi" task :topypi => :test do cur_file = File.open("tvdb_api.py").read() tvdb_api_version = cur_file.scan(/__version__ = "(.*)"/) tvdb_api_version = tvdb_api_version[0][0] puts "Build sdist and send tvdb_api v#{tvdb_api_version} to PyPi?" if $stdin.gets.chomp == "y" puts "Sending source-dist (sdist) to PyPi" if system("python setup.py sdist register upload") puts "tvdb_api uploaded!" end else puts "Cancelled" end end desc "Profile by running unittests" task :profile do cd "tests" puts "Profiling.." `python -m cProfile -o prof_runtest.prof runtests.py` puts "Converting prof to dot" `python gprof2dot.py -o prof_runtest.dot -f pstats prof_runtest.prof` puts "Generating graph" `~/Applications/dev/graphviz.app/Contents/macOS/dot -Tpng -o profile.png prof_runtest.dot -Gbgcolor=black` puts "Cleanup" rm "prof_runtest.dot" rm "prof_runtest.prof" end task :test do puts "Nosetest'ing" if not system("nosetests -v --with-doctest") raise "Test failed!" end puts "Doctesting *.py (excluding setup.py)" Dir.glob("*.py").select{|e| ! e.match(/setup.py/)}.each do |filename| if filename =~ /^setup\.py/ skip end puts "Doctesting #{filename}" if not system("python", "-m", "doctest", filename) raise "Failed doctest" end end puts "Doctesting readme.md" if not system("python", "-m", "doctest", "readme.md") raise "Doctest" end end tvdb_api-2.0/readme.md0000644000076500000240000001117013151446751014623 0ustar dbrstaff00000000000000# `tvdb_api` `tvdb_api` is an easy to use interface to [thetvdb.com][tvdb] It supports Python 2.6, 2.7, 3.3 and 3.4 `tvnamer` has moved to a separate repository: [github.com/dbr/tvnamer][tvnamer] - it is a utility which uses `tvdb_api` to rename files from `some.show.s01e03.blah.abc.avi` to `Some Show - [01x03] - The Episode Name.avi` (which works by getting the episode name from `tvdb_api`) [![Build Status](https://secure.travis-ci.org/dbr/tvdb_api.png?branch=master)](http://travis-ci.org/dbr/tvdb_api) ## To install You can easily install `tvdb_api` via `easy_install` easy_install tvdb_api You may need to use sudo, depending on your setup: sudo easy_install tvdb_api The [`tvnamer`][tvnamer] command-line tool can also be installed via `easy_install`, this installs `tvdb_api` as a dependancy: easy_install tvnamer ## Basic usage import tvdb_api t = tvdb_api.Tvdb() episode = t['My Name Is Earl'][1][3] # get season 1, episode 3 of show print episode['episodename'] # Print episode name ## Advanced usage Most of the documentation is in docstrings. The examples are tested (using doctest) so will always be up to date and working. The docstring for `Tvdb.__init__` lists all initialisation arguments, including support for non-English searches, custom "Select Series" interfaces and enabling the retrieval of banners and extended actor information. You can also override the default API key using `apikey`, recommended if you're using `tvdb_api` in a larger script or application ### Exceptions There are several exceptions you may catch, these can be imported from `tvdb_api`: - `tvdb_error` - this is raised when there is an error communicating with [thetvdb.com][tvdb] (a network error most commonly) - `tvdb_userabort` - raised when a user aborts the Select Series dialog (by `ctrl+c`, or entering `q`) - `tvdb_shownotfound` - raised when `t['show name']` cannot find anything - `tvdb_seasonnotfound` - raised when the requested series (`t['show name][99]`) does not exist - `tvdb_episodenotfound` - raised when the requested episode (`t['show name][1][99]`) does not exist. - `tvdb_attributenotfound` - raised when the requested attribute is not found (`t['show name']['an attribute']`, `t['show name'][1]['an attribute']`, or ``t['show name'][1][1]['an attribute']``) ### Series data All data exposed by [thetvdb.com][tvdb] is accessible via the `Show` class. A Show is retrieved by doing.. >>> import tvdb_api >>> t = tvdb_api.Tvdb() >>> show = t['scrubs'] >>> type(show) For example, to find out what network Scrubs is aired: >>> t['scrubs']['network'] u'ABC' The data is stored in an attribute named `data`, within the Show instance: >>> t['scrubs'].data.keys() ['networkid', 'rating', 'airs_dayofweek', 'contentrating', 'seriesname', 'id', 'airs_time', 'network', 'fanart', 'lastupdated', 'actors', 'ratingcount', 'status', 'added', 'poster', 'tms_wanted_old', 'imdb_id', 'genre', 'banner', 'seriesid', 'language', 'zap2it_id', 'addedby', 'firstaired', 'runtime', 'overview'] Although each element is also accessible via `t['scrubs']` for ease-of-use: >>> t['scrubs']['rating'] u'9.0' This is the recommended way of retrieving "one-off" data (for example, if you are only interested in "seriesname"). If you wish to iterate over all data, or check if a particular show has a specific piece of data, use the `data` attribute, >>> 'rating' in t['scrubs'].data True ### Banners and actors Since banners and actors are separate XML files, retrieving them by default is undesirable. If you wish to retrieve banners (and other fanart), use the `banners` Tvdb initialisation argument: >>> from tvdb_api import Tvdb >>> t = Tvdb(banners = True) Then access the data using a `Show`'s `_banner` key: >>> t['scrubs']['_banners'].keys() ['fanart', 'poster', 'series', 'season'] The banner data structure will be improved in future versions. Extended actor data is accessible similarly: >>> t = Tvdb(actors = True) >>> actors = t['scrubs']['_actors'] >>> actors[0] >>> actors[0].keys() ['sortorder', 'image', 'role', 'id', 'name'] >>> actors[0]['role'] u'Dr. John Michael "J.D." Dorian' Remember a simple list of actors is accessible via the default Show data: >>> t['scrubs']['actors'] u'|Zach Braff|Donald Faison|Sarah Chalke|Judy Reyes|John C. McGinley|Neil Flynn|Ken Jenkins|Christa Miller|Aloma Wright|Robert Maschio|Sam Lloyd|Travis Schuldt|Johnny Kastl|Heather Graham|Michael Mosley|Kerry Bish\xe9|Dave Franco|Eliza Coupe|' [tvdb]: http://thetvdb.com [tvnamer]: http://github.com/dbr/tvnamer tvdb_api-2.0/setup.cfg0000644000076500000240000000007313157176641014671 0ustar dbrstaff00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 tvdb_api-2.0/setup.py0000644000076500000240000000351313157175740014563 0ustar dbrstaff00000000000000import sys from setuptools import setup from setuptools.command.test import test as TestCommand class PyTest(TestCommand): user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")] def initialize_options(self): TestCommand.initialize_options(self) self.pytest_args = [] def finalize_options(self): TestCommand.finalize_options(self) self.test_args = [] self.test_suite = True def run_tests(self): #import here, cause outside the eggs aren't loaded import pytest errno = pytest.main(self.pytest_args) sys.exit(errno) _requirements = ['requests_cache', 'requests'] _modules = ['tvdb_api', 'tvdb_ui', 'tvdb_exceptions'] setup( name = 'tvdb_api', version='2.0', author='dbr/Ben', description='Interface to thetvdb.com', url='http://github.com/dbr/tvdb_api/tree/master', license='unlicense', long_description="""\ An easy to use API interface to TheTVDB.com Basic usage is: >>> import tvdb_api >>> t = tvdb_api.Tvdb() >>> ep = t['My Name Is Earl'][1][22] >>> ep >>> ep['episodeName'] u'Stole a Badge' """, py_modules = _modules, install_requires = _requirements, tests_require=['pytest'], cmdclass = {'test': PyTest}, classifiers=[ "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Multimedia", "Topic :: Utilities", "Topic :: Software Development :: Libraries :: Python Modules", ] ) tvdb_api-2.0/tests/0000755000076500000240000000000013157176641014212 5ustar dbrstaff00000000000000tvdb_api-2.0/tests/gprof2dot.py0000644000076500000240000014774213151446751016505 0ustar dbrstaff00000000000000#!/usr/bin/env python # # Copyright 2008 Jose Fonseca # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # """Generate a dot graph from the output of several profilers.""" __author__ = "Jose Fonseca" __version__ = "1.0" import sys import math import os.path import re import textwrap import optparse try: # Debugging helper module import debug except ImportError: pass def percentage(p): return "%.02f%%" % (p*100.0,) def add(a, b): return a + b def equal(a, b): if a == b: return a else: return None def fail(a, b): assert False def ratio(numerator, denominator): numerator = float(numerator) denominator = float(denominator) assert 0.0 <= numerator assert numerator <= denominator try: return numerator/denominator except ZeroDivisionError: # 0/0 is undefined, but 1.0 yields more useful results return 1.0 class UndefinedEvent(Exception): """Raised when attempting to get an event which is undefined.""" def __init__(self, event): Exception.__init__(self) self.event = event def __str__(self): return 'unspecified event %s' % self.event.name class Event(object): """Describe a kind of event, and its basic operations.""" def __init__(self, name, null, aggregator, formatter = str): self.name = name self._null = null self._aggregator = aggregator self._formatter = formatter def __eq__(self, other): return self is other def __hash__(self): return id(self) def null(self): return self._null def aggregate(self, val1, val2): """Aggregate two event values.""" assert val1 is not None assert val2 is not None return self._aggregator(val1, val2) def format(self, val): """Format an event value.""" assert val is not None return self._formatter(val) MODULE = Event("Module", None, equal) PROCESS = Event("Process", None, equal) CALLS = Event("Calls", 0, add) SAMPLES = Event("Samples", 0, add) TIME = Event("Time", 0.0, add, lambda x: '(' + str(x) + ')') TIME_RATIO = Event("Time ratio", 0.0, add, lambda x: '(' + percentage(x) + ')') TOTAL_TIME = Event("Total time", 0.0, fail) TOTAL_TIME_RATIO = Event("Total time ratio", 0.0, fail, percentage) CALL_RATIO = Event("Call ratio", 0.0, add, percentage) PRUNE_RATIO = Event("Prune ratio", 0.0, add, percentage) class Object(object): """Base class for all objects in profile which can store events.""" def __init__(self, events=None): if events is None: self.events = {} else: self.events = events def __hash__(self): return id(self) def __eq__(self, other): return self is other def __contains__(self, event): return event in self.events def __getitem__(self, event): try: return self.events[event] except KeyError: raise UndefinedEvent(event) def __setitem__(self, event, value): if value is None: if event in self.events: del self.events[event] else: self.events[event] = value class Call(Object): """A call between functions. There should be at most one call object for every pair of functions. """ def __init__(self, callee_id): Object.__init__(self) self.callee_id = callee_id class Function(Object): """A function.""" def __init__(self, id, name): Object.__init__(self) self.id = id self.name = name self.calls = {} self.cycle = None def add_call(self, call): if call.callee_id in self.calls: sys.stderr.write('warning: overwriting call from function %s to %s\n' % (str(self.id), str(call.callee_id))) self.calls[call.callee_id] = call # TODO: write utility functions def __repr__(self): return self.name class Cycle(Object): """A cycle made from recursive function calls.""" def __init__(self): Object.__init__(self) # XXX: Do cycles need an id? self.functions = set() def add_function(self, function): assert function not in self.functions self.functions.add(function) # XXX: Aggregate events? if function.cycle is not None: for other in function.cycle.functions: if function not in self.functions: self.add_function(other) function.cycle = self class Profile(Object): """The whole profile.""" def __init__(self): Object.__init__(self) self.functions = {} self.cycles = [] def add_function(self, function): if function.id in self.functions: sys.stderr.write('warning: overwriting function %s (id %s)\n' % (function.name, str(function.id))) self.functions[function.id] = function def add_cycle(self, cycle): self.cycles.append(cycle) def validate(self): """Validate the edges.""" for function in self.functions.itervalues(): for callee_id in function.calls.keys(): assert function.calls[callee_id].callee_id == callee_id if callee_id not in self.functions: sys.stderr.write('warning: call to undefined function %s from function %s\n' % (str(callee_id), function.name)) del function.calls[callee_id] def find_cycles(self): """Find cycles using Tarjan's strongly connected components algorithm.""" # Apply the Tarjan's algorithm successively until all functions are visited visited = set() for function in self.functions.itervalues(): if function not in visited: self._tarjan(function, 0, [], {}, {}, visited) cycles = [] for function in self.functions.itervalues(): if function.cycle is not None and function.cycle not in cycles: cycles.append(function.cycle) self.cycles = cycles if 0: for cycle in cycles: sys.stderr.write("Cycle:\n") for member in cycle.functions: sys.stderr.write("\t%s\n" % member.name) def _tarjan(self, function, order, stack, orders, lowlinks, visited): """Tarjan's strongly connected components algorithm. See also: - http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm """ visited.add(function) orders[function] = order lowlinks[function] = order order += 1 pos = len(stack) stack.append(function) for call in function.calls.itervalues(): callee = self.functions[call.callee_id] # TODO: use a set to optimize lookup if callee not in orders: order = self._tarjan(callee, order, stack, orders, lowlinks, visited) lowlinks[function] = min(lowlinks[function], lowlinks[callee]) elif callee in stack: lowlinks[function] = min(lowlinks[function], orders[callee]) if lowlinks[function] == orders[function]: # Strongly connected component found members = stack[pos:] del stack[pos:] if len(members) > 1: cycle = Cycle() for member in members: cycle.add_function(member) return order def call_ratios(self, event): # Aggregate for incoming calls cycle_totals = {} for cycle in self.cycles: cycle_totals[cycle] = 0.0 function_totals = {} for function in self.functions.itervalues(): function_totals[function] = 0.0 for function in self.functions.itervalues(): for call in function.calls.itervalues(): if call.callee_id != function.id: callee = self.functions[call.callee_id] function_totals[callee] += call[event] if callee.cycle is not None and callee.cycle is not function.cycle: cycle_totals[callee.cycle] += call[event] # Compute the ratios for function in self.functions.itervalues(): for call in function.calls.itervalues(): assert CALL_RATIO not in call if call.callee_id != function.id: callee = self.functions[call.callee_id] if callee.cycle is not None and callee.cycle is not function.cycle: total = cycle_totals[callee.cycle] else: total = function_totals[callee] call[CALL_RATIO] = ratio(call[event], total) def integrate(self, outevent, inevent): """Propagate function time ratio allong the function calls. Must be called after finding the cycles. See also: - http://citeseer.ist.psu.edu/graham82gprof.html """ # Sanity checking assert outevent not in self for function in self.functions.itervalues(): assert outevent not in function assert inevent in function for call in function.calls.itervalues(): assert outevent not in call if call.callee_id != function.id: assert CALL_RATIO in call # Aggregate the input for each cycle for cycle in self.cycles: total = inevent.null() for function in self.functions.itervalues(): total = inevent.aggregate(total, function[inevent]) self[inevent] = total # Integrate along the edges total = inevent.null() for function in self.functions.itervalues(): total = inevent.aggregate(total, function[inevent]) self._integrate_function(function, outevent, inevent) self[outevent] = total def _integrate_function(self, function, outevent, inevent): if function.cycle is not None: return self._integrate_cycle(function.cycle, outevent, inevent) else: if outevent not in function: total = function[inevent] for call in function.calls.itervalues(): if call.callee_id != function.id: total += self._integrate_call(call, outevent, inevent) function[outevent] = total return function[outevent] def _integrate_call(self, call, outevent, inevent): assert outevent not in call assert CALL_RATIO in call callee = self.functions[call.callee_id] subtotal = call[CALL_RATIO]*self._integrate_function(callee, outevent, inevent) call[outevent] = subtotal return subtotal def _integrate_cycle(self, cycle, outevent, inevent): if outevent not in cycle: total = inevent.null() for member in cycle.functions: subtotal = member[inevent] for call in member.calls.itervalues(): callee = self.functions[call.callee_id] if callee.cycle is not cycle: subtotal += self._integrate_call(call, outevent, inevent) total += subtotal cycle[outevent] = total callees = {} for function in self.functions.itervalues(): if function.cycle is not cycle: for call in function.calls.itervalues(): callee = self.functions[call.callee_id] if callee.cycle is cycle: try: callees[callee] += call[CALL_RATIO] except KeyError: callees[callee] = call[CALL_RATIO] for callee, call_ratio in callees.iteritems(): ranks = {} call_ratios = {} partials = {} self._rank_cycle_function(cycle, callee, 0, ranks) self._call_ratios_cycle(cycle, callee, ranks, call_ratios, set()) partial = self._integrate_cycle_function(cycle, callee, call_ratio, partials, ranks, call_ratios, outevent, inevent) assert partial == max(partials.values()) assert not total or abs(1.0 - partial/(call_ratio*total)) <= 0.001 return cycle[outevent] def _rank_cycle_function(self, cycle, function, rank, ranks): if function not in ranks or ranks[function] > rank: ranks[function] = rank for call in function.calls.itervalues(): if call.callee_id != function.id: callee = self.functions[call.callee_id] if callee.cycle is cycle: self._rank_cycle_function(cycle, callee, rank + 1, ranks) def _call_ratios_cycle(self, cycle, function, ranks, call_ratios, visited): if function not in visited: visited.add(function) for call in function.calls.itervalues(): if call.callee_id != function.id: callee = self.functions[call.callee_id] if callee.cycle is cycle: if ranks[callee] > ranks[function]: call_ratios[callee] = call_ratios.get(callee, 0.0) + call[CALL_RATIO] self._call_ratios_cycle(cycle, callee, ranks, call_ratios, visited) def _integrate_cycle_function(self, cycle, function, partial_ratio, partials, ranks, call_ratios, outevent, inevent): if function not in partials: partial = partial_ratio*function[inevent] for call in function.calls.itervalues(): if call.callee_id != function.id: callee = self.functions[call.callee_id] if callee.cycle is not cycle: assert outevent in call partial += partial_ratio*call[outevent] else: if ranks[callee] > ranks[function]: callee_partial = self._integrate_cycle_function(cycle, callee, partial_ratio, partials, ranks, call_ratios, outevent, inevent) call_ratio = ratio(call[CALL_RATIO], call_ratios[callee]) call_partial = call_ratio*callee_partial try: call[outevent] += call_partial except UndefinedEvent: call[outevent] = call_partial partial += call_partial partials[function] = partial try: function[outevent] += partial except UndefinedEvent: function[outevent] = partial return partials[function] def aggregate(self, event): """Aggregate an event for the whole profile.""" total = event.null() for function in self.functions.itervalues(): try: total = event.aggregate(total, function[event]) except UndefinedEvent: return self[event] = total def ratio(self, outevent, inevent): assert outevent not in self assert inevent in self for function in self.functions.itervalues(): assert outevent not in function assert inevent in function function[outevent] = ratio(function[inevent], self[inevent]) for call in function.calls.itervalues(): assert outevent not in call if inevent in call: call[outevent] = ratio(call[inevent], self[inevent]) self[outevent] = 1.0 def prune(self, node_thres, edge_thres): """Prune the profile""" # compute the prune ratios for function in self.functions.itervalues(): try: function[PRUNE_RATIO] = function[TOTAL_TIME_RATIO] except UndefinedEvent: pass for call in function.calls.itervalues(): callee = self.functions[call.callee_id] if TOTAL_TIME_RATIO in call: # handle exact cases first call[PRUNE_RATIO] = call[TOTAL_TIME_RATIO] else: try: # make a safe estimate call[PRUNE_RATIO] = min(function[TOTAL_TIME_RATIO], callee[TOTAL_TIME_RATIO]) except UndefinedEvent: pass # prune the nodes for function_id in self.functions.keys(): function = self.functions[function_id] try: if function[PRUNE_RATIO] < node_thres: del self.functions[function_id] except UndefinedEvent: pass # prune the egdes for function in self.functions.itervalues(): for callee_id in function.calls.keys(): call = function.calls[callee_id] try: if callee_id not in self.functions or call[PRUNE_RATIO] < edge_thres: del function.calls[callee_id] except UndefinedEvent: pass def dump(self): for function in self.functions.itervalues(): sys.stderr.write('Function %s:\n' % (function.name,)) self._dump_events(function.events) for call in function.calls.itervalues(): callee = self.functions[call.callee_id] sys.stderr.write(' Call %s:\n' % (callee.name,)) self._dump_events(call.events) def _dump_events(self, events): for event, value in events.iteritems(): sys.stderr.write(' %s: %s\n' % (event.name, event.format(value))) class Struct: """Masquerade a dictionary with a structure-like behavior.""" def __init__(self, attrs = None): if attrs is None: attrs = {} self.__dict__['_attrs'] = attrs def __getattr__(self, name): try: return self._attrs[name] except KeyError: raise AttributeError(name) def __setattr__(self, name, value): self._attrs[name] = value def __str__(self): return str(self._attrs) def __repr__(self): return repr(self._attrs) class ParseError(Exception): """Raised when parsing to signal mismatches.""" def __init__(self, msg, line): self.msg = msg # TODO: store more source line information self.line = line def __str__(self): return '%s: %r' % (self.msg, self.line) class Parser: """Parser interface.""" def __init__(self): pass def parse(self): raise NotImplementedError class LineParser(Parser): """Base class for parsers that read line-based formats.""" def __init__(self, file): Parser.__init__(self) self._file = file self.__line = None self.__eof = False def readline(self): line = self._file.readline() if not line: self.__line = '' self.__eof = True self.__line = line.rstrip('\r\n') def lookahead(self): assert self.__line is not None return self.__line def consume(self): assert self.__line is not None line = self.__line self.readline() return line def eof(self): assert self.__line is not None return self.__eof class GprofParser(Parser): """Parser for GNU gprof output. See also: - Chapter "Interpreting gprof's Output" from the GNU gprof manual http://sourceware.org/binutils/docs-2.18/gprof/Call-Graph.html#Call-Graph - File "cg_print.c" from the GNU gprof source code http://sourceware.org/cgi-bin/cvsweb.cgi/~checkout~/src/gprof/cg_print.c?rev=1.12&cvsroot=src """ def __init__(self, fp): Parser.__init__(self) self.fp = fp self.functions = {} self.cycles = {} def readline(self): line = self.fp.readline() if not line: sys.stderr.write('error: unexpected end of file\n') sys.exit(1) line = line.rstrip('\r\n') return line _int_re = re.compile(r'^\d+$') _float_re = re.compile(r'^\d+\.\d+$') def translate(self, mo): """Extract a structure from a match object, while translating the types in the process.""" attrs = {} groupdict = mo.groupdict() for name, value in groupdict.iteritems(): if value is None: value = None elif self._int_re.match(value): value = int(value) elif self._float_re.match(value): value = float(value) attrs[name] = (value) return Struct(attrs) _cg_header_re = re.compile( # original gprof header r'^\s+called/total\s+parents\s*$|' + r'^index\s+%time\s+self\s+descendents\s+called\+self\s+name\s+index\s*$|' + r'^\s+called/total\s+children\s*$|' + # GNU gprof header r'^index\s+%\s+time\s+self\s+children\s+called\s+name\s*$' ) _cg_ignore_re = re.compile( # spontaneous r'^\s+\s*$|' # internal calls (such as "mcount") r'^.*\((\d+)\)$' ) _cg_primary_re = re.compile( r'^\[(?P\d+)\]' + r'\s+(?P\d+\.\d+)' + r'\s+(?P\d+\.\d+)' + r'\s+(?P\d+\.\d+)' + r'\s+(?:(?P\d+)(?:\+(?P\d+))?)?' + r'\s+(?P\S.*?)' + r'(?:\s+\d+)>)?' + r'\s\[(\d+)\]$' ) _cg_parent_re = re.compile( r'^\s+(?P\d+\.\d+)?' + r'\s+(?P\d+\.\d+)?' + r'\s+(?P\d+)(?:/(?P\d+))?' + r'\s+(?P\S.*?)' + r'(?:\s+\d+)>)?' + r'\s\[(?P\d+)\]$' ) _cg_child_re = _cg_parent_re _cg_cycle_header_re = re.compile( r'^\[(?P\d+)\]' + r'\s+(?P\d+\.\d+)' + r'\s+(?P\d+\.\d+)' + r'\s+(?P\d+\.\d+)' + r'\s+(?:(?P\d+)(?:\+(?P\d+))?)?' + r'\s+\d+)\sas\sa\swhole>' + r'\s\[(\d+)\]$' ) _cg_cycle_member_re = re.compile( r'^\s+(?P\d+\.\d+)?' + r'\s+(?P\d+\.\d+)?' + r'\s+(?P\d+)(?:\+(?P\d+))?' + r'\s+(?P\S.*?)' + r'(?:\s+\d+)>)?' + r'\s\[(?P\d+)\]$' ) _cg_sep_re = re.compile(r'^--+$') def parse_function_entry(self, lines): parents = [] children = [] while True: if not lines: sys.stderr.write('warning: unexpected end of entry\n') line = lines.pop(0) if line.startswith('['): break # read function parent line mo = self._cg_parent_re.match(line) if not mo: if self._cg_ignore_re.match(line): continue sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line) else: parent = self.translate(mo) parents.append(parent) # read primary line mo = self._cg_primary_re.match(line) if not mo: sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line) return else: function = self.translate(mo) while lines: line = lines.pop(0) # read function subroutine line mo = self._cg_child_re.match(line) if not mo: if self._cg_ignore_re.match(line): continue sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line) else: child = self.translate(mo) children.append(child) function.parents = parents function.children = children self.functions[function.index] = function def parse_cycle_entry(self, lines): # read cycle header line line = lines[0] mo = self._cg_cycle_header_re.match(line) if not mo: sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line) return cycle = self.translate(mo) # read cycle member lines cycle.functions = [] for line in lines[1:]: mo = self._cg_cycle_member_re.match(line) if not mo: sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line) continue call = self.translate(mo) cycle.functions.append(call) self.cycles[cycle.cycle] = cycle def parse_cg_entry(self, lines): if lines[0].startswith("["): self.parse_cycle_entry(lines) else: self.parse_function_entry(lines) def parse_cg(self): """Parse the call graph.""" # skip call graph header while not self._cg_header_re.match(self.readline()): pass line = self.readline() while self._cg_header_re.match(line): line = self.readline() # process call graph entries entry_lines = [] while line != '\014': # form feed if line and not line.isspace(): if self._cg_sep_re.match(line): self.parse_cg_entry(entry_lines) entry_lines = [] else: entry_lines.append(line) line = self.readline() def parse(self): self.parse_cg() self.fp.close() profile = Profile() profile[TIME] = 0.0 cycles = {} for index in self.cycles.iterkeys(): cycles[index] = Cycle() for entry in self.functions.itervalues(): # populate the function function = Function(entry.index, entry.name) function[TIME] = entry.self if entry.called is not None: function[CALLS] = entry.called if entry.called_self is not None: call = Call(entry.index) call[CALLS] = entry.called_self function[CALLS] += entry.called_self # populate the function calls for child in entry.children: call = Call(child.index) assert child.called is not None call[CALLS] = child.called if child.index not in self.functions: # NOTE: functions that were never called but were discovered by gprof's # static call graph analysis dont have a call graph entry so we need # to add them here missing = Function(child.index, child.name) function[TIME] = 0.0 function[CALLS] = 0 profile.add_function(missing) function.add_call(call) profile.add_function(function) if entry.cycle is not None: cycles[entry.cycle].add_function(function) profile[TIME] = profile[TIME] + function[TIME] for cycle in cycles.itervalues(): profile.add_cycle(cycle) # Compute derived events profile.validate() profile.ratio(TIME_RATIO, TIME) profile.call_ratios(CALLS) profile.integrate(TOTAL_TIME, TIME) profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME) return profile class OprofileParser(LineParser): """Parser for oprofile callgraph output. See also: - http://oprofile.sourceforge.net/doc/opreport.html#opreport-callgraph """ _fields_re = { 'samples': r'(?P\d+)', '%': r'(?P\S+)', 'linenr info': r'(?P\(no location information\)|\S+:\d+)', 'image name': r'(?P\S+(?:\s\(tgid:[^)]*\))?)', 'app name': r'(?P\S+)', 'symbol name': r'(?P\(no symbols\)|.+?)', } def __init__(self, infile): LineParser.__init__(self, infile) self.entries = {} self.entry_re = None def add_entry(self, callers, function, callees): try: entry = self.entries[function.id] except KeyError: self.entries[function.id] = (callers, function, callees) else: callers_total, function_total, callees_total = entry self.update_subentries_dict(callers_total, callers) function_total.samples += function.samples self.update_subentries_dict(callees_total, callees) def update_subentries_dict(self, totals, partials): for partial in partials.itervalues(): try: total = totals[partial.id] except KeyError: totals[partial.id] = partial else: total.samples += partial.samples def parse(self): # read lookahead self.readline() self.parse_header() while self.lookahead(): self.parse_entry() profile = Profile() reverse_call_samples = {} # populate the profile profile[SAMPLES] = 0 for _callers, _function, _callees in self.entries.itervalues(): function = Function(_function.id, _function.name) function[SAMPLES] = _function.samples profile.add_function(function) profile[SAMPLES] += _function.samples if _function.application: function[PROCESS] = os.path.basename(_function.application) if _function.image: function[MODULE] = os.path.basename(_function.image) total_callee_samples = 0 for _callee in _callees.itervalues(): total_callee_samples += _callee.samples for _callee in _callees.itervalues(): if not _callee.self: call = Call(_callee.id) call[SAMPLES] = _callee.samples function.add_call(call) # compute derived data profile.validate() profile.find_cycles() profile.ratio(TIME_RATIO, SAMPLES) profile.call_ratios(SAMPLES) profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO) return profile def parse_header(self): while not self.match_header(): self.consume() line = self.lookahead() fields = re.split(r'\s\s+', line) entry_re = r'^\s*' + r'\s+'.join([self._fields_re[field] for field in fields]) + r'(?P\s+\[self\])?$' self.entry_re = re.compile(entry_re) self.skip_separator() def parse_entry(self): callers = self.parse_subentries() if self.match_primary(): function = self.parse_subentry() if function is not None: callees = self.parse_subentries() self.add_entry(callers, function, callees) self.skip_separator() def parse_subentries(self): subentries = {} while self.match_secondary(): subentry = self.parse_subentry() subentries[subentry.id] = subentry return subentries def parse_subentry(self): entry = Struct() line = self.consume() mo = self.entry_re.match(line) if not mo: raise ParseError('failed to parse', line) fields = mo.groupdict() entry.samples = int(fields.get('samples', 0)) entry.percentage = float(fields.get('percentage', 0.0)) if 'source' in fields and fields['source'] != '(no location information)': source = fields['source'] filename, lineno = source.split(':') entry.filename = filename entry.lineno = int(lineno) else: source = '' entry.filename = None entry.lineno = None entry.image = fields.get('image', '') entry.application = fields.get('application', '') if 'symbol' in fields and fields['symbol'] != '(no symbols)': entry.symbol = fields['symbol'] else: entry.symbol = '' if entry.symbol.startswith('"') and entry.symbol.endswith('"'): entry.symbol = entry.symbol[1:-1] entry.id = ':'.join((entry.application, entry.image, source, entry.symbol)) entry.self = fields.get('self', None) != None if entry.self: entry.id += ':self' if entry.symbol: entry.name = entry.symbol else: entry.name = entry.image return entry def skip_separator(self): while not self.match_separator(): self.consume() self.consume() def match_header(self): line = self.lookahead() return line.startswith('samples') def match_separator(self): line = self.lookahead() return line == '-'*len(line) def match_primary(self): line = self.lookahead() return not line[:1].isspace() def match_secondary(self): line = self.lookahead() return line[:1].isspace() class SharkParser(LineParser): """Parser for MacOSX Shark output. Author: tom@dbservice.com """ def __init__(self, infile): LineParser.__init__(self, infile) self.stack = [] self.entries = {} def add_entry(self, function): try: entry = self.entries[function.id] except KeyError: self.entries[function.id] = (function, { }) else: function_total, callees_total = entry function_total.samples += function.samples def add_callee(self, function, callee): func, callees = self.entries[function.id] try: entry = callees[callee.id] except KeyError: callees[callee.id] = callee else: entry.samples += callee.samples def parse(self): self.readline() self.readline() self.readline() self.readline() match = re.compile(r'(?P[|+ ]*)(?P\d+), (?P[^,]+), (?P.*)') while self.lookahead(): line = self.consume() mo = match.match(line) if not mo: raise ParseError('failed to parse', line) fields = mo.groupdict() prefix = len(fields.get('prefix', 0)) / 2 - 1 symbol = str(fields.get('symbol', 0)) image = str(fields.get('image', 0)) entry = Struct() entry.id = ':'.join([symbol, image]) entry.samples = int(fields.get('samples', 0)) entry.name = symbol entry.image = image # adjust the callstack if prefix < len(self.stack): del self.stack[prefix:] if prefix == len(self.stack): self.stack.append(entry) # if the callstack has had an entry, it's this functions caller if prefix > 0: self.add_callee(self.stack[prefix - 1], entry) self.add_entry(entry) profile = Profile() profile[SAMPLES] = 0 for _function, _callees in self.entries.itervalues(): function = Function(_function.id, _function.name) function[SAMPLES] = _function.samples profile.add_function(function) profile[SAMPLES] += _function.samples if _function.image: function[MODULE] = os.path.basename(_function.image) for _callee in _callees.itervalues(): call = Call(_callee.id) call[SAMPLES] = _callee.samples function.add_call(call) # compute derived data profile.validate() profile.find_cycles() profile.ratio(TIME_RATIO, SAMPLES) profile.call_ratios(SAMPLES) profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO) return profile class PstatsParser: """Parser python profiling statistics saved with te pstats module.""" def __init__(self, *filename): import pstats self.stats = pstats.Stats(*filename) self.profile = Profile() self.function_ids = {} def get_function_name(self, (filename, line, name)): module = os.path.splitext(filename)[0] module = os.path.basename(module) return "%s:%d:%s" % (module, line, name) def get_function(self, key): try: id = self.function_ids[key] except KeyError: id = len(self.function_ids) name = self.get_function_name(key) function = Function(id, name) self.profile.functions[id] = function self.function_ids[key] = id else: function = self.profile.functions[id] return function def parse(self): self.profile[TIME] = 0.0 self.profile[TOTAL_TIME] = self.stats.total_tt for fn, (cc, nc, tt, ct, callers) in self.stats.stats.iteritems(): callee = self.get_function(fn) callee[CALLS] = nc callee[TOTAL_TIME] = ct callee[TIME] = tt self.profile[TIME] += tt self.profile[TOTAL_TIME] = max(self.profile[TOTAL_TIME], ct) for fn, value in callers.iteritems(): caller = self.get_function(fn) call = Call(callee.id) if isinstance(value, tuple): for i in xrange(0, len(value), 4): nc, cc, tt, ct = value[i:i+4] if CALLS in call: call[CALLS] += cc else: call[CALLS] = cc if TOTAL_TIME in call: call[TOTAL_TIME] += ct else: call[TOTAL_TIME] = ct else: call[CALLS] = value call[TOTAL_TIME] = ratio(value, nc)*ct caller.add_call(call) #self.stats.print_stats() #self.stats.print_callees() # Compute derived events self.profile.validate() self.profile.ratio(TIME_RATIO, TIME) self.profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME) return self.profile class Theme: def __init__(self, bgcolor = (0.0, 0.0, 1.0), mincolor = (0.0, 0.0, 0.0), maxcolor = (0.0, 0.0, 1.0), fontname = "Arial", minfontsize = 10.0, maxfontsize = 10.0, minpenwidth = 0.5, maxpenwidth = 4.0, gamma = 2.2): self.bgcolor = bgcolor self.mincolor = mincolor self.maxcolor = maxcolor self.fontname = fontname self.minfontsize = minfontsize self.maxfontsize = maxfontsize self.minpenwidth = minpenwidth self.maxpenwidth = maxpenwidth self.gamma = gamma def graph_bgcolor(self): return self.hsl_to_rgb(*self.bgcolor) def graph_fontname(self): return self.fontname def graph_fontsize(self): return self.minfontsize def node_bgcolor(self, weight): return self.color(weight) def node_fgcolor(self, weight): return self.graph_bgcolor() def node_fontsize(self, weight): return self.fontsize(weight) def edge_color(self, weight): return self.color(weight) def edge_fontsize(self, weight): return self.fontsize(weight) def edge_penwidth(self, weight): return max(weight*self.maxpenwidth, self.minpenwidth) def edge_arrowsize(self, weight): return 0.5 * math.sqrt(self.edge_penwidth(weight)) def fontsize(self, weight): return max(weight**2 * self.maxfontsize, self.minfontsize) def color(self, weight): weight = min(max(weight, 0.0), 1.0) hmin, smin, lmin = self.mincolor hmax, smax, lmax = self.maxcolor h = hmin + weight*(hmax - hmin) s = smin + weight*(smax - smin) l = lmin + weight*(lmax - lmin) return self.hsl_to_rgb(h, s, l) def hsl_to_rgb(self, h, s, l): """Convert a color from HSL color-model to RGB. See also: - http://www.w3.org/TR/css3-color/#hsl-color """ h = h % 1.0 s = min(max(s, 0.0), 1.0) l = min(max(l, 0.0), 1.0) if l <= 0.5: m2 = l*(s + 1.0) else: m2 = l + s - l*s m1 = l*2.0 - m2 r = self._hue_to_rgb(m1, m2, h + 1.0/3.0) g = self._hue_to_rgb(m1, m2, h) b = self._hue_to_rgb(m1, m2, h - 1.0/3.0) # Apply gamma correction r **= self.gamma g **= self.gamma b **= self.gamma return (r, g, b) def _hue_to_rgb(self, m1, m2, h): if h < 0.0: h += 1.0 elif h > 1.0: h -= 1.0 if h*6 < 1.0: return m1 + (m2 - m1)*h*6.0 elif h*2 < 1.0: return m2 elif h*3 < 2.0: return m1 + (m2 - m1)*(2.0/3.0 - h)*6.0 else: return m1 TEMPERATURE_COLORMAP = Theme( mincolor = (2.0/3.0, 0.80, 0.25), # dark blue maxcolor = (0.0, 1.0, 0.5), # satured red gamma = 1.0 ) PINK_COLORMAP = Theme( mincolor = (0.0, 1.0, 0.90), # pink maxcolor = (0.0, 1.0, 0.5), # satured red ) GRAY_COLORMAP = Theme( mincolor = (0.0, 0.0, 0.85), # light gray maxcolor = (0.0, 0.0, 0.0), # black ) BW_COLORMAP = Theme( minfontsize = 8.0, maxfontsize = 24.0, mincolor = (0.0, 0.0, 0.0), # black maxcolor = (0.0, 0.0, 0.0), # black minpenwidth = 0.1, maxpenwidth = 8.0, ) class DotWriter: """Writer for the DOT language. See also: - "The DOT Language" specification http://www.graphviz.org/doc/info/lang.html """ def __init__(self, fp): self.fp = fp def graph(self, profile, theme): self.begin_graph() fontname = theme.graph_fontname() self.attr('graph', fontname=fontname, ranksep=0.25, nodesep=0.125) self.attr('node', fontname=fontname, shape="box", style="filled,rounded", fontcolor="white", width=0, height=0) self.attr('edge', fontname=fontname) for function in profile.functions.itervalues(): labels = [] for event in PROCESS, MODULE: if event in function.events: label = event.format(function[event]) labels.append(label) labels.append(function.name) for event in TOTAL_TIME_RATIO, TIME_RATIO, CALLS: if event in function.events: label = event.format(function[event]) labels.append(label) try: weight = function[PRUNE_RATIO] except UndefinedEvent: weight = 0.0 label = '\n'.join(labels) self.node(function.id, label = label, color = self.color(theme.node_bgcolor(weight)), fontcolor = self.color(theme.node_fgcolor(weight)), fontsize = "%.2f" % theme.node_fontsize(weight), ) for call in function.calls.itervalues(): callee = profile.functions[call.callee_id] labels = [] for event in TOTAL_TIME_RATIO, CALLS: if event in call.events: label = event.format(call[event]) labels.append(label) try: weight = call[PRUNE_RATIO] except UndefinedEvent: try: weight = callee[PRUNE_RATIO] except UndefinedEvent: weight = 0.0 label = '\n'.join(labels) self.edge(function.id, call.callee_id, label = label, color = self.color(theme.edge_color(weight)), fontcolor = self.color(theme.edge_color(weight)), fontsize = "%.2f" % theme.edge_fontsize(weight), penwidth = "%.2f" % theme.edge_penwidth(weight), labeldistance = "%.2f" % theme.edge_penwidth(weight), arrowsize = "%.2f" % theme.edge_arrowsize(weight), ) self.end_graph() def begin_graph(self): self.write('digraph {\n') def end_graph(self): self.write('}\n') def attr(self, what, **attrs): self.write("\t") self.write(what) self.attr_list(attrs) self.write(";\n") def node(self, node, **attrs): self.write("\t") self.id(node) self.attr_list(attrs) self.write(";\n") def edge(self, src, dst, **attrs): self.write("\t") self.id(src) self.write(" -> ") self.id(dst) self.attr_list(attrs) self.write(";\n") def attr_list(self, attrs): if not attrs: return self.write(' [') first = True for name, value in attrs.iteritems(): if first: first = False else: self.write(", ") self.id(name) self.write('=') self.id(value) self.write(']') def id(self, id): if isinstance(id, (int, float)): s = str(id) elif isinstance(id, str): if id.isalnum(): s = id else: s = self.escape(id) else: raise TypeError self.write(s) def color(self, (r, g, b)): def float2int(f): if f <= 0.0: return 0 if f >= 1.0: return 255 return int(255.0*f + 0.5) return "#" + "".join(["%02x" % float2int(c) for c in (r, g, b)]) def escape(self, s): s = s.encode('utf-8') s = s.replace('\\', r'\\') s = s.replace('\n', r'\n') s = s.replace('\t', r'\t') s = s.replace('"', r'\"') return '"' + s + '"' def write(self, s): self.fp.write(s) class Main: """Main program.""" themes = { "color": TEMPERATURE_COLORMAP, "pink": PINK_COLORMAP, "gray": GRAY_COLORMAP, "bw": BW_COLORMAP, } def main(self): """Main program.""" parser = optparse.OptionParser( usage="\n\t%prog [options] [file] ...", version="%%prog %s" % __version__) parser.add_option( '-o', '--output', metavar='FILE', type="string", dest="output", help="output filename [stdout]") parser.add_option( '-n', '--node-thres', metavar='PERCENTAGE', type="float", dest="node_thres", default=0.5, help="eliminate nodes below this threshold [default: %default]") parser.add_option( '-e', '--edge-thres', metavar='PERCENTAGE', type="float", dest="edge_thres", default=0.1, help="eliminate edges below this threshold [default: %default]") parser.add_option( '-f', '--format', type="choice", choices=('prof', 'oprofile', 'pstats', 'shark'), dest="format", default="prof", help="profile format: prof, oprofile, or pstats [default: %default]") parser.add_option( '-c', '--colormap', type="choice", choices=('color', 'pink', 'gray', 'bw'), dest="theme", default="color", help="color map: color, pink, gray, or bw [default: %default]") parser.add_option( '-s', '--strip', action="store_true", dest="strip", default=False, help="strip function parameters, template parameters, and const modifiers from demangled C++ function names") parser.add_option( '-w', '--wrap', action="store_true", dest="wrap", default=False, help="wrap function names") (self.options, self.args) = parser.parse_args(sys.argv[1:]) if len(self.args) > 1 and self.options.format != 'pstats': parser.error('incorrect number of arguments') try: self.theme = self.themes[self.options.theme] except KeyError: parser.error('invalid colormap \'%s\'' % self.options.theme) if self.options.format == 'prof': if not self.args: fp = sys.stdin else: fp = open(self.args[0], 'rt') parser = GprofParser(fp) elif self.options.format == 'oprofile': if not self.args: fp = sys.stdin else: fp = open(self.args[0], 'rt') parser = OprofileParser(fp) elif self.options.format == 'pstats': if not self.args: parser.error('at least a file must be specified for pstats input') parser = PstatsParser(*self.args) elif self.options.format == 'shark': if not self.args: fp = sys.stdin else: fp = open(self.args[0], 'rt') parser = SharkParser(fp) else: parser.error('invalid format \'%s\'' % self.options.format) self.profile = parser.parse() if self.options.output is None: self.output = sys.stdout else: self.output = open(self.options.output, 'wt') self.write_graph() _parenthesis_re = re.compile(r'\([^()]*\)') _angles_re = re.compile(r'<[^<>]*>') _const_re = re.compile(r'\s+const$') def strip_function_name(self, name): """Remove extraneous information from C++ demangled function names.""" # Strip function parameters from name by recursively removing paired parenthesis while True: name, n = self._parenthesis_re.subn('', name) if not n: break # Strip const qualifier name = self._const_re.sub('', name) # Strip template parameters from name by recursively removing paired angles while True: name, n = self._angles_re.subn('', name) if not n: break return name def wrap_function_name(self, name): """Split the function name on multiple lines.""" if len(name) > 32: ratio = 2.0/3.0 height = max(int(len(name)/(1.0 - ratio) + 0.5), 1) width = max(len(name)/height, 32) # TODO: break lines in symbols name = textwrap.fill(name, width, break_long_words=False) # Take away spaces name = name.replace(", ", ",") name = name.replace("> >", ">>") name = name.replace("> >", ">>") # catch consecutive return name def compress_function_name(self, name): """Compress function name according to the user preferences.""" if self.options.strip: name = self.strip_function_name(name) if self.options.wrap: name = self.wrap_function_name(name) # TODO: merge functions with same resulting name return name def write_graph(self): dot = DotWriter(self.output) profile = self.profile profile.prune(self.options.node_thres/100.0, self.options.edge_thres/100.0) for function in profile.functions.itervalues(): function.name = self.compress_function_name(function.name) dot.graph(profile, self.theme) if __name__ == '__main__': Main().main() tvdb_api-2.0/tests/runtests.py0000755000076500000240000000105313151446751016451 0ustar dbrstaff00000000000000#!/usr/bin/env python #encoding:utf-8 #author:dbr/Ben #project:tvdb_api #repository:http://github.com/dbr/tvdb_api #license:unlicense (http://unlicense.org/) import sys import unittest import test_tvdb_api def main(): suite = unittest.TestSuite([ unittest.TestLoader().loadTestsFromModule(test_tvdb_api) ]) runner = unittest.TextTestRunner(verbosity=2) result = runner.run(suite) if result.wasSuccessful(): return 0 else: return 1 if __name__ == '__main__': sys.exit( int(main()) ) tvdb_api-2.0/tests/test_tvdb_api.py0000644000076500000240000003410313157174400017403 0ustar dbrstaff00000000000000#!/usr/bin/env python #encoding:utf-8 #author:dbr/Ben #project:tvdb_api #repository:http://github.com/dbr/tvdb_api #license:unlicense (http://unlicense.org/) """Unittests for tvdb_api """ import os import sys import datetime import pytest # Force parent directory onto path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import tvdb_api from tvdb_api import (tvdb_shownotfound, tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound) class TestTvdbBasic: # Used to store the cached instance of Tvdb() t = None @classmethod def setup_class(cls): if cls.t is None: cls.t = tvdb_api.Tvdb(cache=True, banners=False) def test_different_case(self): """Checks the auto-correction of show names is working. It should correct the weirdly capitalised 'sCruBs' to 'Scrubs' """ assert self.t['scrubs'][1][4]['episodeName'] == 'My Old Lady' assert self.t['sCruBs']['seriesName'] == 'Scrubs' def test_spaces(self): """Checks shownames with spaces """ assert self.t['My Name Is Earl']['seriesName'] == 'My Name Is Earl' assert self.t['My Name Is Earl'][1][4]['episodeName'] == 'Faked My Own Death' def test_numeric(self): """Checks numeric show names """ assert self.t['24'][2][20]['episodeName'] == 'Day 2: 3:00 A.M. - 4:00 A.M.' assert self.t['24']['seriesName'] == '24' def test_show_iter(self): """Iterating over a show returns each seasons """ assert len([season for season in self.t['Life on Mars']]) == 2 def test_season_iter(self): """Iterating over a show returns episodes """ assert len([episode for episode in self.t['Life on Mars'][1]]) == 8 def test_get_episode_overview(self): """Checks episode overview is retrieved correctly. """ assert self.t['Battlestar Galactica (2003)'][1][6]['overview'].startswith('When a new copy of Doral, a Cylon who had been previously') == True def test_get_parent(self): """Check accessing series from episode instance """ show = self.t['Battlestar Galactica (2003)'] season = show[1] episode = show[1][1] assert season.show == show assert episode.season == season assert episode.season.show == show def test_no_season(self): show = self.t['Katekyo Hitman Reborn'] print(tvdb_api) print(show[1][1]) class TestTvdbErrors: t = None @classmethod def setup_class(cls): if cls.t is None: cls.t = tvdb_api.Tvdb(cache=True, banners=False) def test_seasonnotfound(self): """Checks exception is thrown when season doesn't exist. """ with pytest.raises(tvdb_seasonnotfound): self.t['CNNNN'][10] def test_shownotfound(self): """Checks exception is thrown when episode doesn't exist. """ with pytest.raises(tvdb_shownotfound): self.t['the fake show thingy'] def test_episodenotfound(self): """Checks exception is raised for non-existent episode """ with pytest.raises(tvdb_episodenotfound): self.t['Scrubs'][1][30] def test_attributenamenotfound(self): """Checks exception is thrown for if an attribute isn't found. """ with pytest.raises(tvdb_attributenotfound): self.t['CNNNN'][1][6]['afakeattributething'] self.t['CNNNN']['afakeattributething'] class TestTvdbSearch: # Used to store the cached instance of Tvdb() t = None @classmethod def setup_class(cls): if cls.t is None: cls.t = tvdb_api.Tvdb(cache=True, banners=False) def test_search_len(self): """There should be only one result matching """ assert len(self.t['My Name Is Earl'].search('Faked My Own Death')) == 1 def test_search_checkname(self): """Checks you can get the episode name of a search result """ assert self.t['Scrubs'].search('my first')[0]['episodeName'] == 'My First Day' assert self.t['My Name Is Earl'].search('Faked My Own Death')[0]['episodeName'] == 'Faked My Own Death' def test_search_multiresults(self): """Checks search can return multiple results """ assert (len(self.t['Scrubs'].search('my first')) >= 3) == True def test_search_no_params_error(self): """Checks not supplying search info raises TypeError""" with pytest.raises(TypeError): self.t['Scrubs'].search() def test_search_season(self): """Checks the searching of a single season""" assert len(self.t['Scrubs'][1].search("First")) == 3 def test_search_show(self): """Checks the searching of an entire show""" assert len(self.t['CNNNN'].search('CNNNN', key='episodeName')) == 3 def test_aired_on(self): """Tests airedOn show method""" sr = self.t['Scrubs'].airedOn(datetime.date(2001, 10, 2)) assert len(sr) == 1 assert sr[0]['episodeName'] == u'My First Day' class TestTvdbData: # Used to store the cached instance of Tvdb() t = None @classmethod def setup_class(cls): if cls.t is None: cls.t = tvdb_api.Tvdb(cache=True, banners=False) def test_episode_data(self): """Check the firstaired value is retrieved """ assert self.t['lost']['firstAired'] == '2004-09-22' class TestTvdbMisc: # Used to store the cached instance of Tvdb() t = None @classmethod def setup_class(cls): if cls.t is None: cls.t = tvdb_api.Tvdb(cache=True, banners=False) def test_repr_show(self): """Check repr() of Season """ assert repr(self.t['CNNNN']).replace("u'", "'") == "" def test_repr_season(self): """Check repr() of Season """ assert repr(self.t['CNNNN'][1]) == "" def test_repr_episode(self): """Check repr() of Episode """ assert repr(self.t['CNNNN'][1][1]).replace("u'", "'") == "" def test_have_all_languages(self): """Check valid_languages is up-to-date (compared to languages.xml) """ et = self.t._getetsrc("https://api.thetvdb.com/languages") languages = [x['abbreviation'] for x in et] assert sorted(languages) == sorted(self.t.config['valid_languages']) class TestTvdbLanguages: def test_episode_name_french(self): """Check episode data is in French (language="fr") """ t = tvdb_api.Tvdb(cache = True, language = "fr") assert t['scrubs'][1][1]['episodeName'] == "Mon premier jour" assert t['scrubs']['overview'].startswith(u"J.D. est un jeune m\xe9decin qui d\xe9bute") def test_episode_name_spanish(self): """Check episode data is in Spanish (language="es") """ t = tvdb_api.Tvdb(cache = True, language = "es") assert t['scrubs'][1][1]['episodeName'] == u'Mi primer día' assert t['scrubs']['overview'].startswith(u'Scrubs es una divertida comedia') def test_multilanguage_selection(self): """Check selected language is used """ t_en = tvdb_api.Tvdb( cache=True, language = "en") t_it = tvdb_api.Tvdb( cache=True, language = "it") assert t_en['dexter'][1][2]['episodeName'] == "Crocodile" assert t_it['dexter'][1][2]['episodeName'] == "Lacrime di coccodrillo" class TestTvdbUnicode: def test_search_in_chinese(self): """Check searching for show with language=zh returns Chinese seriesname """ t = tvdb_api.Tvdb(cache=True, language="zh") show = t[u'T\xecnh Ng\u01b0\u1eddi Hi\u1ec7n \u0110\u1ea1i'] assert type(show) == tvdb_api.Show assert show['seriesName'] == u'T\xecnh Ng\u01b0\u1eddi Hi\u1ec7n \u0110\u1ea1i' @pytest.mark.skip('Новое API не возвращает сразу все языки') def test_search_in_all_languages(self): """Check search_all_languages returns Chinese show, with language=en """ t = tvdb_api.Tvdb(cache=True, search_all_languages=True, language="en") show = t[u'T\xecnh Ng\u01b0\u1eddi Hi\u1ec7n \u0110\u1ea1i'] assert type(show) == tvdb_api.Show assert show['seriesName'] == u'Virtues Of Harmony II' class TestTvdbBanners: # Used to store the cached instance of Tvdb() t = None @classmethod def setup_class(cls): if cls.t is None: cls.t = tvdb_api.Tvdb(cache=True, banners=True) def test_have_banners(self): """Check banners at least one banner is found """ assert (len(self.t['scrubs']['_banners']) > 0) == True def test_banner_url(self): """Checks banner URLs start with http:// """ for banner_type, banner_data in self.t['scrubs']['_banners'].items(): for res, res_data in banner_data.items(): if res != 'raw': for bid, banner_info in res_data.items(): assert banner_info['_bannerpath'].startswith("http://") == True @pytest.mark.skip('В новом API нет картинки у эпизода') def test_episode_image(self): """Checks episode 'filename' image is fully qualified URL """ assert self.t['scrubs'][1][1]['filename'].startswith("http://") == True @pytest.mark.skip('В новом API у сериала кроме банера больше нет картинок') def test_show_artwork(self): """Checks various image URLs within season data are fully qualified """ for key in ['banner', 'fanart', 'poster']: assert self.t['scrubs'][key].startswith("http://") == True class TestTvdbActors: t = None @classmethod def setup_class(cls): if cls.t is None: cls.t = tvdb_api.Tvdb(cache=True, actors=True) def test_actors_is_correct_datatype(self): """Check show/_actors key exists and is correct type""" assert isinstance(self.t['scrubs']['_actors'], tvdb_api.Actors) == True def test_actors_has_actor(self): """Check show has at least one Actor """ assert isinstance(self.t['scrubs']['_actors'][0], tvdb_api.Actor) == True def test_actor_has_name(self): """Check first actor has a name""" names = [actor['name'] for actor in self.t['scrubs']['_actors']] assert u"Zach Braff" in names def test_actor_image_corrected(self): """Check image URL is fully qualified """ for actor in self.t['scrubs']['_actors']: if actor['image'] is not None: # Actor's image can be None, it displays as the placeholder # image on thetvdb.com assert actor['image'].startswith("http://") == True class TestTvdbDoctest: def test_doctest(self): """Check docstring examples works""" import doctest doctest.testmod(tvdb_api) class TestTvdbCustomCaching: def test_true_false_string(self): """Tests setting cache to True/False/string Basic tests, only checking for errors """ tvdb_api.Tvdb(cache=True) tvdb_api.Tvdb(cache=False) tvdb_api.Tvdb(cache="/tmp") def test_invalid_cache_option(self): """Tests setting cache to invalid value """ try: tvdb_api.Tvdb(cache=2.3) except ValueError: pass else: pytest.fail("Expected ValueError from setting cache to float") def test_custom_request_session(self): from requests import Session as OriginalSession class Used(Exception): pass class CustomCacheForTest(OriginalSession): call_count = 0 def request(self, *args, **kwargs): raise Used("Hurray") c = CustomCacheForTest() t = tvdb_api.Tvdb(cache=c) try: t['scrubs'] except Used: pass else: pytest.fail("Did not use custom session") class TestTvdbById: t = None @classmethod def setup_class(cls): if cls.t is None: cls.t = tvdb_api.Tvdb(cache=True, actors=True) def test_actors_is_correct_datatype(self): """Check show/_actors key exists and is correct type""" assert self.t[76156]['seriesName'] == 'Scrubs' class TestTvdbShowOrdering: # Used to store the cached instance of Tvdb() t_dvd = None t_air = None @classmethod def setup_class(cls): if cls.t_dvd is None: cls.t_dvd = tvdb_api.Tvdb(cache=True, dvdorder=True) if cls.t_air is None: cls.t_air = tvdb_api.Tvdb(cache=True) def test_ordering(self): """Test Tvdb.search method """ assert u'The Train Job' == self.t_air['Firefly'][1][1]['episodeName'] assert u'Serenity' == self.t_dvd['Firefly'][1][1]['episodeName'] assert u'The Cat & the Claw (Part 1)' == self.t_air['Batman The Animated Series'][1][1]['episodeName'] assert u'On Leather Wings' == self.t_dvd['Batman The Animated Series'][1][1]['episodeName'] class TestTvdbShowSearch: # Used to store the cached instance of Tvdb() t = None @classmethod def setup_class(cls): if cls.t is None: cls.t = tvdb_api.Tvdb(cache=True) def test_search(self): """Test Tvdb.search method """ results = self.t.search("my name is earl") all_ids = [x['id'] for x in results] assert 75397 in all_ids class TestTvdbAltNames: t = None @classmethod def setup_class(cls): if cls.t is None: cls.t = tvdb_api.Tvdb(cache=True, actors=True) def test_1(self): """Tests basic access of series name alias """ results = self.t.search("Don't Trust the B---- in Apartment 23") series = results[0] assert 'Apartment 23' in series['aliases'] if __name__ == '__main__': pytest.main() tvdb_api-2.0/tvdb_api.egg-info/0000755000076500000240000000000013157176641016332 5ustar dbrstaff00000000000000tvdb_api-2.0/tvdb_api.egg-info/dependency_links.txt0000644000076500000240000000000113157176641022400 0ustar dbrstaff00000000000000 tvdb_api-2.0/tvdb_api.egg-info/PKG-INFO0000644000076500000240000000224113157176641017426 0ustar dbrstaff00000000000000Metadata-Version: 1.1 Name: tvdb-api Version: 2.0 Summary: Interface to thetvdb.com Home-page: http://github.com/dbr/tvdb_api/tree/master Author: dbr/Ben Author-email: UNKNOWN License: unlicense Description: An easy to use API interface to TheTVDB.com Basic usage is: >>> import tvdb_api >>> t = tvdb_api.Tvdb() >>> ep = t['My Name Is Earl'][1][22] >>> ep >>> ep['episodeName'] u'Stole a Badge' Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Multimedia Classifier: Topic :: Utilities Classifier: Topic :: Software Development :: Libraries :: Python Modules tvdb_api-2.0/tvdb_api.egg-info/requires.txt0000644000076500000240000000003013157176641020723 0ustar dbrstaff00000000000000requests_cache requests tvdb_api-2.0/tvdb_api.egg-info/SOURCES.txt0000644000076500000240000000046613157176641020224 0ustar dbrstaff00000000000000MANIFEST.in Rakefile UNLICENSE readme.md setup.py tvdb_api.py tvdb_exceptions.py tvdb_ui.py tests/gprof2dot.py tests/runtests.py tests/test_tvdb_api.py tvdb_api.egg-info/PKG-INFO tvdb_api.egg-info/SOURCES.txt tvdb_api.egg-info/dependency_links.txt tvdb_api.egg-info/requires.txt tvdb_api.egg-info/top_level.txttvdb_api-2.0/tvdb_api.egg-info/top_level.txt0000644000076500000240000000004113157176641021057 0ustar dbrstaff00000000000000tvdb_api tvdb_exceptions tvdb_ui tvdb_api-2.0/tvdb_api.py0000644000076500000240000012437613157175636015232 0ustar dbrstaff00000000000000#!/usr/bin/env python # encoding:utf-8 # author:dbr/Ben # project:tvdb_api # repository:http://github.com/dbr/tvdb_api # license:unlicense (http://unlicense.org/) """Simple-to-use Python interface to The TVDB's API (thetvdb.com) Example usage: >>> from tvdb_api import Tvdb >>> t = Tvdb() >>> t['Lost'][4][11]['episodeName'] u'Cabin Fever' """ __author__ = "dbr/Ben" __version__ = "2.0" import sys import os import time import types import getpass import tempfile import warnings import logging import datetime import hashlib import requests import requests_cache from requests_cache.backends.base import _to_bytes, _DEFAULT_HEADERS IS_PY2 = sys.version_info[0] == 2 if IS_PY2: user_input = raw_input from urllib import quote as url_quote else: from urllib.parse import quote as url_quote user_input = input if IS_PY2: int_types = (int, long) text_type = unicode else: int_types = int text_type = str lastTimeout = None def log(): return logging.getLogger("tvdb_api") ## Exceptions class tvdb_exception(Exception): """Any exception generated by tvdb_api """ pass class tvdb_error(tvdb_exception): """An error with thetvdb.com (Cannot connect, for example) """ pass class tvdb_userabort(tvdb_exception): """User aborted the interactive selection (via the q command, ^c etc) """ pass class tvdb_notauthorized(tvdb_exception): """An authorization error with thetvdb.com """ pass class tvdb_shownotfound(tvdb_exception): """Show cannot be found on thetvdb.com (non-existant show) """ pass class tvdb_seasonnotfound(tvdb_exception): """Season cannot be found on thetvdb.com """ pass class tvdb_episodenotfound(tvdb_exception): """Episode cannot be found on thetvdb.com """ pass class tvdb_resourcenotfound(tvdb_exception): """Resource cannot be found on thetvdb.com """ pass class tvdb_invalidlanguage(tvdb_exception): """invalid language given on thetvdb.com """ def __init__(self, value): self.value = value def __str__(self): return repr(self.value) class tvdb_attributenotfound(tvdb_exception): """Raised if an episode does not have the requested attribute (such as a episode name) """ pass ## UI class BaseUI(object): """Base user interface for Tvdb show selection. Selects first show. A UI is a callback. A class, it's __init__ function takes two arguments: - config, which is the Tvdb config dict, setup in tvdb_api.py - log, which is Tvdb's logger instance (which uses the logging module). You can call log.info() log.warning() etc It must have a method "selectSeries", this is passed a list of dicts, each dict contains the the keys "name" (human readable show name), and "sid" (the shows ID as on thetvdb.com). For example: [{'name': u'Lost', 'sid': u'73739'}, {'name': u'Lost Universe', 'sid': u'73181'}] The "selectSeries" method must return the appropriate dict, or it can raise tvdb_userabort (if the selection is aborted), tvdb_shownotfound (if the show cannot be found). A simple example callback, which returns a random series: >>> import random >>> from tvdb_ui import BaseUI >>> class RandomUI(BaseUI): ... def selectSeries(self, allSeries): ... import random ... return random.choice(allSeries) Then to use it.. >>> from tvdb_api import Tvdb >>> t = Tvdb(custom_ui = RandomUI) >>> random_matching_series = t['Lost'] >>> type(random_matching_series) """ def __init__(self, config, log = None): self.config = config if log is not None: warnings.warn("the UI's log parameter is deprecated, instead use\n" "use import logging; logging.getLogger('ui').info('blah')\n" "The self.log attribute will be removed in the next version") self.log = logging.getLogger(__name__) def selectSeries(self, allSeries): return allSeries[0] class ConsoleUI(BaseUI): """Interactively allows the user to select a show from a console based UI """ def _displaySeries(self, allSeries, limit = 6): """Helper function, lists series with corresponding ID """ if limit is not None: toshow = allSeries[:limit] else: toshow = allSeries print("TVDB Search Results:") for i, cshow in enumerate(toshow): i_show = i + 1 # Start at more human readable number 1 (not 0) log().debug('Showing allSeries[%s], series %s)' % (i_show, allSeries[i]['seriesName'])) if i == 0: extra = " (default)" else: extra = "" lid_map = dict((v, k) for (k, v) in self.config['langabbv_to_id'].items()) output = "%s -> %s [%s] # http://thetvdb.com/?tab=series&id=%s&lid=%s%s" % ( i_show, cshow['seriesName'], lid_map[cshow['lid']], str(cshow['id']), cshow['lid'], extra ) if IS_PY2: print(output.encode("UTF-8", "ignore")) else: print(output) def selectSeries(self, allSeries): self._displaySeries(allSeries) if len(allSeries) == 1: # Single result, return it! print("Automatically selecting only result") return allSeries[0] if self.config['select_first'] is True: print("Automatically returning first search result") return allSeries[0] while True: # return breaks this loop try: print("Enter choice (first number, return for default, 'all', ? for help):") ans = user_input() except KeyboardInterrupt: raise tvdb_userabort("User aborted (^c keyboard interupt)") except EOFError: raise tvdb_userabort("User aborted (EOF received)") log().debug('Got choice of: %s' % (ans)) try: selected_id = int(ans) - 1 # The human entered 1 as first result, not zero except ValueError: # Input was not number if len(ans.strip()) == 0: # Default option log().debug('Default option, returning first series') return allSeries[0] if ans == "q": log().debug('Got quit command (q)') raise tvdb_userabort("User aborted ('q' quit command)") elif ans == "?": print("## Help") print("# Enter the number that corresponds to the correct show.") print("# a - display all results") print("# all - display all results") print("# ? - this help") print("# q - abort tvnamer") print("# Press return with no input to select first result") elif ans.lower() in ["a", "all"]: self._displaySeries(allSeries, limit = None) else: log().debug('Unknown keypress %s' % (ans)) else: log().debug('Trying to return ID: %d' % (selected_id)) try: return allSeries[selected_id] except IndexError: log().debug('Invalid show number entered!') print("Invalid number (%s) selected!") self._displaySeries(allSeries) ## Main API class ShowContainer(dict): """Simple dict that holds a series of Show instances """ def __init__(self): self._stack = [] self._lastgc = time.time() def __setitem__(self, key, value): self._stack.append(key) # keep only the 100th latest results if time.time() - self._lastgc > 20: for o in self._stack[:-100]: del self[o] self._stack = self._stack[-100:] self._lastgc = time.time() super(ShowContainer, self).__setitem__(key, value) class Show(dict): """Holds a dict of seasons, and show data. """ def __init__(self): dict.__init__(self) self.data = {} def __repr__(self): return "" % ( self.data.get(u'seriesName', 'instance'), len(self) ) def __getitem__(self, key): v1_compatibility = { 'seriesname': 'seriesName', } if key in v1_compatibility: import warnings msg = "v1 usage is deprecated, please use new names: old: '%s', new: '%s'" % ( key, v1_compatibility[key]) key = v1_compatibility[key] if key in self: # Key is an episode, return it return dict.__getitem__(self, key) if key in self.data: # Non-numeric request is for show-data return dict.__getitem__(self.data, key) # Data wasn't found, raise appropriate error if isinstance(key, int) or key.isdigit(): # Episode number x was not found raise tvdb_seasonnotfound( "Could not find season %s" % (repr(key)) ) else: # If it's not numeric, it must be an attribute name, which # doesn't exist, so attribute error. raise tvdb_attributenotfound( "Cannot find attribute %s" % (repr(key)) ) def airedOn(self, date): """Deprecated: use aired_on instead """ warnings.warn("Show.airedOn method renamed to aired_on", category=DeprecationWarning) return self.aired_on(date) def aired_on(self, date): ret = self.search(str(date), 'firstAired') if len(ret) == 0: raise tvdb_episodenotfound( "Could not find any episodes that aired on %s" % date ) return ret def search(self, term=None, key=None): """ Search all episodes in show. Can search all data, or a specific key (for example, episodename) Always returns an array (can be empty). First index contains the first match, and so on. Each array index is an Episode() instance, so doing search_results[0]['episodename'] will retrieve the episode name of the first match. Search terms are converted to lower case (unicode) strings. # Examples These examples assume t is an instance of Tvdb(): >>> t = Tvdb() >>> To search for all episodes of Scrubs with a bit of data containing "my first day": >>> t['Scrubs'].search("my first day") [] >>> Search for "My Name Is Earl" episode named "Faked His Own Death": >>> t['My Name Is Earl'].search('Faked My Own Death', key='episodeName') [] >>> To search Scrubs for all episodes with "mentor" in the episode name: >>> t['scrubs'].search('mentor', key='episodeName') [, ] >>> # Using search results >>> results = t['Scrubs'].search("my first") >>> print results[0]['episodeName'] My First Day >>> for x in results: print x['episodeName'] My First Day My First Step My First Kill >>> """ results = [] for cur_season in self.values(): searchresult = cur_season.search(term=term, key=key) if len(searchresult) != 0: results.extend(searchresult) return results class Season(dict): def __init__(self, show=None): """The show attribute points to the parent show """ self.show = show def __repr__(self): return "" % ( len(self.keys()) ) def __getitem__(self, episode_number): if episode_number not in self: raise tvdb_episodenotfound("Could not find episode %s" % (repr(episode_number))) else: return dict.__getitem__(self, episode_number) def search(self, term=None, key=None): """Search all episodes in season, returns a list of matching Episode instances. >>> t = Tvdb() >>> t['scrubs'][1].search('first day') [] >>> See Show.search documentation for further information on search """ results = [] for ep in self.values(): searchresult = ep.search(term=term, key=key) if searchresult is not None: results.append( searchresult ) return results class Episode(dict): def __init__(self, season=None): """The season attribute points to the parent season """ self.season = season def __repr__(self): seasno = self.get(u'airedSeason', 0) epno = self.get(u'airedEpisodeNumber', 0) epname = self.get(u'episodeName') if epname is not None: return "" % (seasno, epno, epname) else: return "" % (seasno, epno) def __getitem__(self, key): try: return dict.__getitem__(self, key) except KeyError: v1_compatibility = { 'episodenumber': 'airedEpisodeNumber', 'firstaired': 'firstAired', 'seasonnumber': 'airedSeason', 'episodename': 'episodeName', } if key in v1_compatibility: msg = "v1 usage is deprecated, please use new names: old: '%s', new: '%s'" % ( key, v1_compatibility[key]) warnings.warn(msg, category=DeprecationWarning) try: value = dict.__getitem__(self, v1_compatibility[key]) if key in ['episodenumber', 'seasonnumber']: # This was a string in v1 return str(value) else: return value except KeyError: # We either return something or we get the exception below pass raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key))) def search(self, term=None, key=None): """Search episode data for term, if it matches, return the Episode (self). The key parameter can be used to limit the search to a specific element, for example, episodename. This primarily for use use by Show.search and Season.search. See Show.search for further information on search Simple example: >>> e = Episode() >>> e['episodeName'] = "An Example" >>> e.search("examp") >>> Limiting by key: >>> e.search("examp", key = "episodeName") >>> """ if term is None: raise TypeError("must supply string to search for (contents)") term = text_type(term).lower() for cur_key, cur_value in self.items(): cur_key = text_type(cur_key) cur_value = text_type(cur_value).lower() if key is not None and cur_key != key: # Do not search this key continue if cur_value.find(text_type(term)) > -1: return self class Actors(list): """Holds all Actor instances for a show """ pass class Actor(dict): """Represents a single actor. Should contain.. id, image, name, role, sortorder """ def __repr__(self): return "" % self.get("name") def create_key(self, request): """A new cache_key algo is required as the authentication token changes with each run. Also there are other header params which also change with each request (e.g. timestamp). Excluding all headers means that Accept-Language is excluded which means different language requests will return the cached response from the wrong language. The _loadurl part checks the cache before a get is performed so that an auth token can be obtained. If the response is already in the cache, the auth token is not required. This prevents the need to do a get which may access the host and fail because the session is not yet not authorized. It is not necessary to authorize if the cache is to be used thus saving host and network traffic. """ if self._ignored_parameters: url, body = self._remove_ignored_parameters(request) else: url, body = request.url, request.body key = hashlib.sha256() key.update(_to_bytes(request.method.upper())) key.update(_to_bytes(url)) if request.body: key.update(_to_bytes(body)) else: if self._include_get_headers and request.headers != _DEFAULT_HEADERS: for name, value in sorted(request.headers.items()): # include only Accept-Language as it is important for context if name in ['Accept-Language']: key.update(_to_bytes(name)) key.update(_to_bytes(value)) return key.hexdigest() class Tvdb: """Create easy-to-use interface to name of season/episode name >>> t = Tvdb() >>> t['Scrubs'][1][24]['episodeName'] u'My Last Day' """ def __init__(self, interactive=False, select_first=False, debug=False, cache=True, banners=False, actors=False, custom_ui=None, language=None, search_all_languages=False, apikey=None, username=None, userkey=None, forceConnect=False, dvdorder=False): """interactive (True/False): When True, uses built-in console UI is used to select the correct show. When False, the first search result is used. select_first (True/False): Automatically selects the first series search result (rather than showing the user a list of more than one series). Is overridden by interactive = False, or specifying a custom_ui debug (True/False) DEPRECATED: Replaced with proper use of logging module. To show debug messages: >>> import logging >>> logging.basicConfig(level = logging.DEBUG) cache (True/False/str/requests_cache.CachedSession): Retrieved URLs can be persisted to to disc. True/False enable or disable default caching. Passing string specifies the directory where to store the "tvdb.sqlite3" cache file. Alternatively a custom requests.Session instance can be passed (e.g maybe a customised instance of `requests_cache.CachedSession`) banners (True/False): Retrieves the banners for a show. These are accessed via the _banners key of a Show(), for example: >>> Tvdb(banners=True)['scrubs']['_banners'].keys() [u'fanart', u'poster', u'seasonwide', u'season', u'series'] actors (True/False): Retrieves a list of the actors for a show. These are accessed via the _actors key of a Show(), for example: >>> t = Tvdb(actors=True) >>> t['scrubs']['_actors'][0]['name'] u'John C. McGinley' custom_ui (tvdb_ui.BaseUI subclass): A callable subclass of tvdb_ui.BaseUI (overrides interactive option) language (2 character language abbreviation): The language of the returned data. Is also the language search uses. Default is "en" (English). For full list, run.. >>> Tvdb().config['valid_languages'] #doctest: +ELLIPSIS ['da', 'fi', 'nl', ...] search_all_languages (True/False): By default, Tvdb will only search in the language specified using the language option. When this is True, it will search for the show in and language apikey (str/unicode): Override the default thetvdb.com API key. By default it will use tvdb_api's own key (fine for small scripts), but you can use your own key if desired - this is recommended if you are embedding tvdb_api in a larger application) See http://thetvdb.com/?tab=apiregister to get your own key username (str/unicode): Override the default thetvdb.com username. By default it will use tvdb_api's own username (fine for small scripts), but you can use your own key if desired - this is recommended if you are embedding tvdb_api in a larger application) See http://thetvdb.com/ to register an account userkey (str/unicode): Override the default thetvdb.com userkey. By default it will use tvdb_api's own userkey (fine for small scripts), but you can use your own key if desired - this is recommended if you are embedding tvdb_api in a larger application) See http://thetvdb.com/ to register an account forceConnect (bool): If true it will always try to connect to theTVDB.com even if we recently timed out. By default it will wait one minute before trying again, and any requests within that one minute window will return an exception immediately. """ global lastTimeout # if we're given a lastTimeout that is less than 1 min just give up if not forceConnect and lastTimeout is not None and datetime.datetime.now() - lastTimeout < datetime.timedelta(minutes=1): raise tvdb_error("We recently timed out, so giving up early this time") self.shows = ShowContainer() # Holds all Show classes self.corrections = {} # Holds show-name to show_id mapping self.config = {} if apikey and username and userkey: self.config['auth_payload'] = { "apikey": apikey, "username": username, "userkey": userkey } else: self.config['auth_payload'] = { "apikey": "0629B785CE550C8D", "userkey": "", "username": "" } self.config['debug_enabled'] = debug # show debugging messages self.config['custom_ui'] = custom_ui self.config['interactive'] = interactive # prompt for correct series? self.config['select_first'] = select_first self.config['search_all_languages'] = search_all_languages self.config['dvdorder'] = dvdorder if cache is True: self.session = requests_cache.CachedSession( expire_after=21600, # 6 hours backend='sqlite', cache_name=self._getTempDir(), include_get_headers=True ) self.session.cache.create_key = types.MethodType(create_key, self.session.cache) self.session.remove_expired_responses() self.config['cache_enabled'] = True elif cache is False: self.session = requests.Session() self.config['cache_enabled'] = False elif isinstance(cache, str): # Specified cache path self.session = requests_cache.CachedSession( expire_after=21600, # 6 hours backend='sqlite', cache_name=os.path.join(cache, "tvdb_api"), include_get_headers=True ) self.session.cache.create_key = types.MethodType(create_key, self.session.cache) self.session.remove_expired_responses() else: self.session = cache try: self.session.get except AttributeError: raise ValueError("cache argument must be True/False, string as cache path or requests.Session-type object (e.g from requests_cache.CachedSession)") self.config['banners_enabled'] = banners self.config['actors_enabled'] = actors if self.config['debug_enabled']: warnings.warn( "The debug argument to tvdb_api.__init__ will be removed in the next version. " "To enable debug messages, use the following code before importing: " "import logging; logging.basicConfig(level=logging.DEBUG)" ) logging.basicConfig(level=logging.DEBUG) # List of language from http://thetvdb.com/api/0629B785CE550C8D/languages.xml # Hard-coded here as it is realtively static, and saves another HTTP request, as # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml self.config['valid_languages'] = [ "da", "fi", "nl", "de", "it", "es", "fr", "pl", "hu", "el", "tr", "ru", "he", "ja", "pt", "zh", "cs", "sl", "hr", "ko", "en", "sv", "no" ] # thetvdb.com should be based around numeric language codes, # but to link to a series like http://thetvdb.com/?tab=series&id=79349&lid=16 # requires the language ID, thus this mapping is required (mainly # for usage in tvdb_ui - internally tvdb_api will use the language abbreviations) self.config['langabbv_to_id'] = { 'el': 20, 'en': 7, 'zh': 27, 'it': 15, 'cs': 28, 'es': 16, 'ru': 22, 'nl': 13, 'pt': 26, 'no': 9, 'tr': 21, 'pl': 18, 'fr': 17, 'hr': 31, 'de': 14, 'da': 10, 'fi': 11, 'hu': 19, 'ja': 25, 'he': 24, 'ko': 32, 'sv': 8, 'sl': 30 } if language is None: self.config['language'] = 'en' else: if language not in self.config['valid_languages']: raise ValueError("Invalid language %s, options are: %s" % ( language, self.config['valid_languages'] )) else: self.config['language'] = language # The following url_ configs are based of the # http://thetvdb.com/wiki/index.php/Programmers_API self.config['base_url'] = "http://thetvdb.com" self.config['api_url'] = "https://api.thetvdb.com" self.config['url_getSeries'] = u"%(api_url)s/search/series?name=%%s" % self.config self.config['url_epInfo'] = u"%(api_url)s/series/%%s/episodes" % self.config self.config['url_seriesInfo'] = u"%(api_url)s/series/%%s" % self.config self.config['url_actorsInfo'] = u"%(api_url)s/series/%%s/actors" % self.config self.config['url_seriesBanner'] = u"%(api_url)s/series/%%s/images" % self.config self.config['url_seriesBannerInfo'] = u"%(api_url)s/series/%%s/images/query?keyType=%%s" % self.config self.config['url_artworkPrefix'] = u"%(base_url)s/banners/%%s" % self.config self.__authorized = False self.headers = {'Content-Type': 'application/json', 'Accept': 'application/json', 'Accept-Language': self.config['language']} def _getTempDir(self): """Returns the [system temp dir]/tvdb_api-u501 (or tvdb_api-myuser) """ if hasattr(os, 'getuid'): uid = "u%d" % (os.getuid()) else: # For Windows try: uid = getpass.getuser() except ImportError: return os.path.join(tempfile.gettempdir(), "tvdb_api") return os.path.join(tempfile.gettempdir(), "tvdb_api-%s" % (uid)) def _loadUrl(self, url, data=None, recache=False, language=None): """Return response from The TVDB API""" if not language: language = self.config['language'] if language not in self.config['valid_languages']: raise ValueError("Invalid language %s, options are: %s" % ( language, self.config['valid_languages'] )) self.headers['Accept-Language'] = language # TODO: обрабатывать исключения (Handle Exceptions) # TODO: обновлять токен (Update Token) # encoded url is used for hashing in the cache so # python 2 and 3 generate the same hash if not self.__authorized: # only authorize of we haven't before and we # don't have the url in the cache fake_session_for_key = requests.Session() fake_session_for_key.headers['Accept-Language'] = language cache_key = None try: # in case the session class has no cache object, fail gracefully cache_key = self.session.cache.create_key(fake_session_for_key.prepare_request(requests.Request('GET', url))) except: pass if not cache_key or not self.session.cache.has_key(cache_key): self.authorize() response = self.session.get(url, headers=self.headers) r = response.json() log().debug("loadurl: %s lid=%s" % (url, language)) log().debug("response:") log().debug(r) error = r.get('Error') errors = r.get('errors') r_data = r.get('data') links = r.get('links') if error: if error == u'Resource not found': # raise(tvdb_resourcenotfound) # handle no data at a different level so it is more specific pass if error == u'Not Authorized': raise(tvdb_notauthorized) if errors: if u'invalidLanguage' in errors: # raise(tvdb_invalidlanguage(errors[u'invalidLanguage'])) # invalidLanguage does not mean there is no data # there is just less data pass if data and isinstance(data, list): data.extend(r_data) else: data = r_data if links and links['next']: url = url.split('?')[0] _url = url + "?page=%s" % links['next'] self._loadUrl(_url, data) return data def authorize(self): log().debug("auth") r = self.session.post('https://api.thetvdb.com/login', json=self.config['auth_payload'], headers=self.headers) r_json = r.json() error = r_json.get('Error') if error: if error == u'Not Authorized': raise(tvdb_notauthorized) token = r_json.get('token') self.headers['Authorization'] = "Bearer %s" % text_type(token) self.__authorized = True def _getetsrc(self, url, language=None): """Loads a URL using caching, returns an ElementTree of the source """ src = self._loadUrl(url, language=language) return src def _setItem(self, sid, seas, ep, attrib, value): """Creates a new episode, creating Show(), Season() and Episode()s as required. Called by _getShowData to populate show Since the nice-to-use tvdb[1][24]['name] interface makes it impossible to do tvdb[1][24]['name] = "name" and still be capable of checking if an episode exists so we can raise tvdb_shownotfound, we have a slightly less pretty method of setting items.. but since the API is supposed to be read-only, this is the best way to do it! The problem is that calling tvdb[1][24]['episodename'] = "name" calls __getitem__ on tvdb[1], there is no way to check if tvdb.__dict__ should have a key "1" before we auto-create it """ if sid not in self.shows: self.shows[sid] = Show() if seas not in self.shows[sid]: self.shows[sid][seas] = Season(show=self.shows[sid]) if ep not in self.shows[sid][seas]: self.shows[sid][seas][ep] = Episode(season=self.shows[sid][seas]) self.shows[sid][seas][ep][attrib] = value def _setShowData(self, sid, key, value): """Sets self.shows[sid] to a new Show instance, or sets the data """ if sid not in self.shows: self.shows[sid] = Show() self.shows[sid].data[key] = value def search(self, series): """This searches TheTVDB.com for the series name and returns the result list """ series = url_quote(series.encode("utf-8")) log().debug("Searching for show %s" % series) seriesEt = self._getetsrc(self.config['url_getSeries'] % (series)) if not seriesEt: log().debug('Series result returned zero') raise tvdb_shownotfound("Show-name search returned zero results (cannot find show on TVDB)") allSeries = [] for series in seriesEt: series['lid'] = self.config['langabbv_to_id'][self.config['language']] series['language'] = self.config['language'] log().debug('Found series %(seriesName)s' % series) allSeries.append(series) return allSeries def _getSeries(self, series): """This searches TheTVDB.com for the series name, If a custom_ui UI is configured, it uses this to select the correct series. If not, and interactive == True, ConsoleUI is used, if not BaseUI is used to select the first result. """ allSeries = self.search(series) if self.config['custom_ui'] is not None: log().debug("Using custom UI %s" % (repr(self.config['custom_ui']))) ui = self.config['custom_ui'](config=self.config) else: if not self.config['interactive']: log().debug('Auto-selecting first search result using BaseUI') ui = BaseUI(config=self.config) else: log().debug('Interactively selecting show using ConsoleUI') ui = ConsoleUI(config=self.config) return ui.selectSeries(allSeries) def _parseBanners(self, sid): """Parses banners XML, from http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/banners.xml Banners are retrieved using t['show name]['_banners'], for example: >>> t = Tvdb(banners = True) >>> t['scrubs']['_banners'].keys() [u'fanart', u'poster', u'seasonwide', u'season', u'series'] >>> t['scrubs']['_banners']['poster']['680x1000'][35308]['_bannerpath'] u'http://thetvdb.com/banners/posters/76156-2.jpg' >>> Any key starting with an underscore has been processed (not the raw data from the XML) This interface will be improved in future versions. """ log().debug('Getting season banners for %s' % (sid)) bannersEt = self._getetsrc(self.config['url_seriesBanner'] % sid) banners = {} for cur_banner in bannersEt.keys(): banners_info = self._getetsrc(self.config['url_seriesBannerInfo'] % (sid, cur_banner)) for banner_info in banners_info: bid = banner_info.get('id') btype = banner_info.get('keyType') btype2 = banner_info.get('resolution') if btype is None or btype2 is None: continue if btype not in banners: banners[btype] = {} if btype2 not in banners[btype]: banners[btype][btype2] = {} if bid not in banners[btype][btype2]: banners[btype][btype2][bid] = {} banners[btype][btype2][bid]['bannerpath'] = banner_info['fileName'] banners[btype][btype2][bid]['resolution'] = banner_info['resolution'] banners[btype][btype2][bid]['subKey'] = banner_info['subKey'] for k, v in list(banners[btype][btype2][bid].items()): if k.endswith("path"): new_key = "_%s" % k log().debug("Transforming %s to %s" % (k, new_key)) new_url = self.config['url_artworkPrefix'] % v banners[btype][btype2][bid][new_key] = new_url banners[btype]['raw'] = banners_info self._setShowData(sid, "_banners", banners) def _parseActors(self, sid): """Parsers actors XML, from http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/actors.xml Actors are retrieved using t['show name]['_actors'], for example: >>> t = Tvdb(actors = True) >>> actors = t['scrubs']['_actors'] >>> type(actors) >>> type(actors[0]) >>> actors[0] >>> sorted(actors[0].keys()) [u'id', u'image', u'imageAdded', u'imageAuthor', u'lastUpdated', u'name', u'role', u'seriesId', u'sortOrder'] >>> actors[0]['name'] u'John C. McGinley' >>> actors[0]['image'] u'http://thetvdb.com/banners/actors/43638.jpg' Any key starting with an underscore has been processed (not the raw data from the XML) """ log().debug("Getting actors for %s" % (sid)) actorsEt = self._getetsrc(self.config['url_actorsInfo'] % (sid)) cur_actors = Actors() for curActorItem in actorsEt: curActor = Actor() for curInfo in curActorItem.keys(): tag = curInfo value = curActorItem[curInfo] if value is not None: if tag == "image": value = self.config['url_artworkPrefix'] % (value) curActor[tag] = value cur_actors.append(curActor) self._setShowData(sid, '_actors', cur_actors) def _getShowData(self, sid, language): """Takes a series ID, gets the epInfo URL and parses the TVDB XML file into the shows dict in layout: shows[series_id][season_number][episode_number] """ if self.config['language'] is None: log().debug('Config language is none, using show language') if language is None: raise tvdb_error("config['language'] was None, this should not happen") else: log().debug( 'Configured language %s override show language of %s' % ( self.config['language'], language ) ) # Parse show information log().debug('Getting all series data for %s' % (sid)) seriesInfoEt = self._getetsrc( self.config['url_seriesInfo'] % sid ) for curInfo in seriesInfoEt.keys(): tag = curInfo value = seriesInfoEt[curInfo] if value is not None: if tag in ['banner', 'fanart', 'poster']: value = self.config['url_artworkPrefix'] % (value) self._setShowData(sid, tag, value) # set language self._setShowData(sid, u'language', self.config['language']) # Parse banners if self.config['banners_enabled']: self._parseBanners(sid) # Parse actors if self.config['actors_enabled']: self._parseActors(sid) # Parse episode data log().debug('Getting all episodes of %s' % (sid)) url = self.config['url_epInfo'] % sid epsEt = self._getetsrc(url, language=language) for cur_ep in epsEt: if self.config['dvdorder']: log().debug('Using DVD ordering.') use_dvd = cur_ep.get('dvdSeason') is not None and cur_ep.get('dvdEpisodeNumber') is not None else: use_dvd = False if use_dvd: elem_seasnum, elem_epno = cur_ep.get('dvdSeason'), cur_ep.get('dvdEpisodeNumber') else: elem_seasnum, elem_epno = cur_ep['airedSeason'], cur_ep['airedEpisodeNumber'] if elem_seasnum is None or elem_epno is None: log().warning("An episode has incomplete season/episode number (season: %r, episode: %r)" % ( elem_seasnum, elem_epno)) #log().debug( # " ".join( # "%r is %r" % (child.tag, child.text) for child in cur_ep.getchildren())) # TODO: Should this happen? continue # Skip to next episode # float() is because https://github.com/dbr/tvnamer/issues/95 - should probably be fixed in TVDB data seas_no = elem_seasnum ep_no = elem_epno for cur_item in cur_ep.keys(): tag = cur_item value = cur_ep[cur_item] if value is not None: if tag == 'filename': value = self.config['url_artworkPrefix'] % (value) self._setItem(sid, seas_no, ep_no, tag, value) def _nameToSid(self, name): """Takes show name, returns the correct series ID (if the show has already been grabbed), or grabs all episodes and returns the correct SID. """ if name in self.corrections: log().debug('Correcting %s to %s' % (name, self.corrections[name])) sid = self.corrections[name] else: log().debug('Getting show %s' % name) selected_series = self._getSeries(name) sid = selected_series['id'] log().debug('Got %(seriesName)s, id %(id)s' % selected_series) self.corrections[name] = sid self._getShowData(selected_series['id'], self.config['language']) return sid def __getitem__(self, key): """Handles tvdb_instance['seriesname'] calls. The dict index should be the show id """ if isinstance(key, int_types): # Item is integer, treat as show id if key not in self.shows: self._getShowData(key, self.config['language']) return self.shows[key] sid = self._nameToSid(key) log().debug('Got series id %s' % sid) return self.shows[sid] def __repr__(self): return repr(self.shows) def main(): """Simple example of using tvdb_api - it just grabs an episode name interactively. """ import logging logging.basicConfig(level=logging.DEBUG) tvdb_instance = Tvdb(interactive=False, cache=False) print(tvdb_instance['Lost']['seriesname']) print(tvdb_instance['Lost'][1][4]['episodename']) if __name__ == '__main__': main() tvdb_api-2.0/tvdb_exceptions.py0000644000076500000240000000147313157176003016617 0ustar dbrstaff00000000000000#!/usr/bin/env python #encoding:utf-8 #author:dbr/Ben #project:tvdb_api #repository:http://github.com/dbr/tvdb_api #license:unlicense (http://unlicense.org/) """Custom exceptions used or raised by tvdb_api """ __author__ = "dbr/Ben" __version__ = "2.0" import logging __all__ = ["tvdb_error", "tvdb_userabort", "tvdb_notauthorized", "tvdb_shownotfound", "tvdb_seasonnotfound", "tvdb_episodenotfound", "tvdb_attributenotfound", "tvdb_resourcenotfound", "tvdb_invalidlanguage"] logging.getLogger(__name__).warning( "tvdb_exceptions module is deprecated - use classes directly from tvdb_api instead") from tvdb_api import ( tvdb_error, tvdb_userabort, tvdb_notauthorized, tvdb_shownotfound, tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_resourcenotfound, tvdb_invalidlanguage, tvdb_attributenotfound ) tvdb_api-2.0/tvdb_ui.py0000644000076500000240000000070113157176014015046 0ustar dbrstaff00000000000000#!/usr/bin/env python #encoding:utf-8 #author:dbr/Ben #project:tvdb_api #repository:http://github.com/dbr/tvdb_api #license:unlicense (http://unlicense.org/) __author__ = "dbr/Ben" __version__ = "2.0" import sys import logging import warnings from tvdb_exceptions import tvdb_userabort logging.getLogger(__name__).warning( "tvdb_ui module is deprecated - use classes directly from tvdb_api instead") from tvdb_api import BaseUI, ConsoleUI tvdb_api-2.0/UNLICENSE0000644000076500000240000000234213151446751014315 0ustar dbrstaff00000000000000Copyright 2011-2012 Ben Dickson (dbr) This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to