initial commit
|
@ -0,0 +1,15 @@
|
|||
<app>
|
||||
<id>mythboxy</id>
|
||||
<name>MythBoxy</name>
|
||||
<version>3.0.beta</version>
|
||||
<description>Watch all your MythTV recordings from Boxee.</description>
|
||||
<thumb>http://erikkristensen.com/project/mythboxee/mythboxee_logo.jpg</thumb>
|
||||
<media>video</media>
|
||||
<copyright>Erik Kristensen</copyright>
|
||||
<email>erik@erikkristensen.com</email>
|
||||
<type>skin</type>
|
||||
<startWindow>launch</startWindow>
|
||||
<platform>all</platform>
|
||||
<minversion>0.9.20</minversion>
|
||||
<test-app>true</test-app>
|
||||
</app>
|
|
@ -0,0 +1,39 @@
|
|||
import mc
|
||||
import sys
|
||||
|
||||
|
||||
#import mythboxee
|
||||
|
||||
import mythtv
|
||||
|
||||
# Get Access to the Apps Local Config
|
||||
#config = mc.GetApp().GetLocalConfig()
|
||||
|
||||
# For Debugging
|
||||
#config.SetValue("verified", "")
|
||||
#config.SetValue("server", "")
|
||||
|
||||
mc.ActivateWindow(14005)
|
||||
|
||||
db = mythtv.MythDB(SecurityPin=4365)
|
||||
|
||||
be = mythtv.MythBE(db=db)
|
||||
print be.getRecordings()
|
||||
|
||||
# Pull out some of the variables we need
|
||||
#server = config.GetValue("server")
|
||||
#verified = config.GetValue("verified")
|
||||
|
||||
# If the server hasn't been defined, we need to get it.
|
||||
#if not server:
|
||||
# mythboxee.GetServer()
|
||||
|
||||
# If server is set, and at this point it has been verified
|
||||
# then launch the application
|
||||
#if config.GetValue("verified") == "1":
|
||||
# mc.ActivateWindow(14000)
|
||||
|
||||
# Load all the show data from the MythTV Backend Server
|
||||
# mythboxee.LoadShows()
|
||||
#else:
|
||||
# mc.ShowDialogOk("MythBoxee Error", "You must enter the full path to the MythBoxee script or MythBoxee was unable to verify the URL provided.")
|
|
@ -0,0 +1,74 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""
|
||||
MySQL Connector/Python - MySQL drive written in Python
|
||||
"""
|
||||
|
||||
import sys
|
||||
_name = 'MySQL Connector/Python'
|
||||
if not hasattr(sys, "version_info") or sys.version_info < (2,4):
|
||||
raise RuntimeError("%s requires Python 2.4 or higher." % (_name))
|
||||
elif sys.version_info >= (3,0):
|
||||
raise RuntimeError("%s does not yet support Python v3." % (_name))
|
||||
del _name
|
||||
del sys
|
||||
|
||||
# Python Db API v2
|
||||
apilevel = '2.0'
|
||||
threadsafety = 1
|
||||
paramstyle = 'pyformat'
|
||||
|
||||
# Read the version from an generated file
|
||||
import _version
|
||||
__version__ = _version.version
|
||||
|
||||
from mysql import MySQL
|
||||
from errors import *
|
||||
from constants import FieldFlag, FieldType, CharacterSet, RefreshOption
|
||||
from dbapi import *
|
||||
|
||||
def Connect(*args, **kwargs):
|
||||
"""Shortcut for creating a mysql.MySQL object."""
|
||||
return MySQL(*args, **kwargs)
|
||||
connect = Connect
|
||||
|
||||
__all__ = [
|
||||
'MySQL', 'Connect',
|
||||
|
||||
# Some useful constants
|
||||
'FieldType','FieldFlag','CharacterSet','RefreshOption',
|
||||
|
||||
# Error handling
|
||||
'Error','Warning',
|
||||
'InterfaceError','DatabaseError',
|
||||
'NotSupportedError','DataError','IntegrityError','ProgrammingError',
|
||||
'OperationalError','InternalError',
|
||||
|
||||
# DBAPI PEP 249 required exports
|
||||
'connect','apilevel','threadsafety','paramstyle',
|
||||
'Date', 'Time', 'Timestamp', 'Binary',
|
||||
'DateFromTicks', 'DateFromTicks', 'TimestampFromTicks',
|
||||
'STRING', 'BINARY', 'NUMBER',
|
||||
'DATETIME', 'ROWID',
|
||||
]
|
|
@ -0,0 +1,28 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""Holds version of MySQL Connector/Python
|
||||
"""
|
||||
|
||||
# Next line is generated
|
||||
version = (0, 1, 0, 'devel', '')
|
|
@ -0,0 +1,148 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""Implementing communication to MySQL servers
|
||||
"""
|
||||
|
||||
import socket
|
||||
import os
|
||||
|
||||
import protocol
|
||||
import errors
|
||||
from constants import CharacterSet
|
||||
|
||||
class MySQLBaseConnection(object):
|
||||
"""Base class for MySQL Connections subclasses.
|
||||
|
||||
Should not be used directly but overloaded, changing the
|
||||
open_connection part. Examples over subclasses are
|
||||
MySQLTCPConnection
|
||||
MySQLUNIXConnection
|
||||
"""
|
||||
def __init__(self, prtcls=None):
|
||||
self.sock = None # holds the socket connection
|
||||
self.connection_timeout = None
|
||||
self.protocol = None
|
||||
self.socket_flags = 0
|
||||
try:
|
||||
self.protocol = prtcls(self)
|
||||
except:
|
||||
self.protocol = protocol.MySQLProtocol(self)
|
||||
self._set_socket_flags()
|
||||
|
||||
def open_connection(self):
|
||||
pass
|
||||
|
||||
def close_connection(self):
|
||||
try:
|
||||
self.sock.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
def send(self, buf):
|
||||
"""
|
||||
Send packets using the socket to the server.
|
||||
"""
|
||||
pktlen = len(buf)
|
||||
try:
|
||||
while pktlen:
|
||||
pktlen -= self.sock.send(buf)
|
||||
except Exception, e:
|
||||
raise errors.OperationalError('%s' % e)
|
||||
|
||||
def recv(self):
|
||||
"""
|
||||
Receive packets using the socket from the server.
|
||||
"""
|
||||
try:
|
||||
header = self.sock.recv(4, self.socket_flags)
|
||||
(pktsize, pktnr) = self.protocol.handle_header(header)
|
||||
buf = header + self.sock.recv(pktsize, self.socket_flags)
|
||||
self.protocol.is_error(buf)
|
||||
except:
|
||||
raise
|
||||
|
||||
return (buf, pktsize, pktnr)
|
||||
|
||||
def set_protocol(self, prtcls):
|
||||
try:
|
||||
self.protocol = prtcls(self, self.protocol.handshake)
|
||||
except:
|
||||
self.protocol = protocol.MySQLProtocol(self)
|
||||
|
||||
def set_connection_timeout(self, timeout):
|
||||
self.connection_timeout = timeout
|
||||
|
||||
def _set_socket_flags(self, flags=None):
|
||||
self.socket_flags = 0
|
||||
if flags is None:
|
||||
if os.name == 'nt':
|
||||
flags = 0
|
||||
else:
|
||||
flags = socket.MSG_WAITALL
|
||||
|
||||
if flags is not None:
|
||||
self.socket_flags = flags
|
||||
|
||||
|
||||
class MySQLUnixConnection(MySQLBaseConnection):
|
||||
"""Opens a connection through the UNIX socket of the MySQL Server."""
|
||||
|
||||
def __init__(self, prtcls=None,unix_socket='/tmp/mysql.sock'):
|
||||
MySQLBaseConnection.__init__(self, prtcls=prtcls)
|
||||
self.unix_socket = unix_socket
|
||||
self.socket_flags = socket.MSG_WAITALL
|
||||
|
||||
def open_connection(self):
|
||||
"""Opens a UNIX socket and checks the MySQL handshake."""
|
||||
try:
|
||||
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.sock.settimeout(self.connection_timeout)
|
||||
self.sock.connect(self.unix_socket)
|
||||
except StandardError, e:
|
||||
raise errors.OperationalError('%s' % e)
|
||||
|
||||
buf = self.recv()[0]
|
||||
self.protocol.handle_handshake(buf)
|
||||
|
||||
class MySQLTCPConnection(MySQLBaseConnection):
|
||||
"""Opens a TCP connection to the MySQL Server."""
|
||||
|
||||
def __init__(self, prtcls=None, host='127.0.0.1', port=3306):
|
||||
MySQLBaseConnection.__init__(self, prtcls=prtcls)
|
||||
self.server_host = host
|
||||
self.server_port = port
|
||||
|
||||
def open_connection(self):
|
||||
"""Opens a TCP Connection and checks the MySQL handshake."""
|
||||
try:
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.settimeout(self.connection_timeout)
|
||||
self.sock.connect( (self.server_host, self.server_port) )
|
||||
except StandardError, e:
|
||||
raise errors.OperationalError('%s' % e)
|
||||
|
||||
buf = self.recv()[0]
|
||||
self.protocol.handle_handshake(buf)
|
||||
|
||||
|
|
@ -0,0 +1,571 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""Various MySQL constants and character sets
|
||||
"""
|
||||
|
||||
from errors import ProgrammingError
|
||||
|
||||
class _constants(object):
|
||||
|
||||
prefix = ''
|
||||
desc = {}
|
||||
|
||||
def __new__(cls):
|
||||
raise TypeError, "Can not instanciate from %s" % cls.__name__
|
||||
|
||||
@classmethod
|
||||
def get_desc(cls,name):
|
||||
res = ''
|
||||
try:
|
||||
res = cls.desc[name][1]
|
||||
except KeyError, e:
|
||||
raise KeyError, e
|
||||
else:
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def get_info(cls,n):
|
||||
res = ()
|
||||
for k,v in cls.desc.items():
|
||||
if v[0] == n:
|
||||
return v[1]
|
||||
raise KeyError, e
|
||||
|
||||
@classmethod
|
||||
def get_full_info(cls):
|
||||
res = ()
|
||||
try:
|
||||
res = ["%s : %s" % (k,v[1]) for k,v in cls.desc.items()]
|
||||
except StandardError, e:
|
||||
res = ('No information found in constant class.%s' % e)
|
||||
|
||||
return res
|
||||
|
||||
class FieldType(_constants):
|
||||
|
||||
prefix = 'FIELD_TYPE_'
|
||||
DECIMAL = 0x00
|
||||
TINY = 0x01
|
||||
SHORT = 0x02
|
||||
LONG = 0x03
|
||||
FLOAT = 0x04
|
||||
DOUBLE = 0x05
|
||||
NULL = 0x06
|
||||
TIMESTAMP = 0x07
|
||||
LONGLONG = 0x08
|
||||
INT24 = 0x09
|
||||
DATE = 0x0a
|
||||
TIME = 0x0b
|
||||
DATETIME = 0x0c
|
||||
YEAR = 0x0d
|
||||
NEWDATE = 0x0e
|
||||
VARCHAR = 0x0f
|
||||
BIT = 0x10
|
||||
NEWDECIMAL = 0xf6
|
||||
ENUM = 0xf7
|
||||
SET = 0xf8
|
||||
TINY_BLOB = 0xf9
|
||||
MEDIUM_BLOB = 0xfa
|
||||
LONG_BLOB = 0xfb
|
||||
BLOB = 0xfc
|
||||
VAR_STRING = 0xfd
|
||||
STRING = 0xfe
|
||||
GEOMETRY = 0xff
|
||||
|
||||
desc = {
|
||||
'DECIMAL': (0x00, 'DECIMAL'),
|
||||
'TINY': (0x01, 'TINY'),
|
||||
'SHORT': (0x02, 'SHORT'),
|
||||
'LONG': (0x03, 'LONG'),
|
||||
'FLOAT': (0x04, 'FLOAT'),
|
||||
'DOUBLE': (0x05, 'DOUBLE'),
|
||||
'NULL': (0x06, 'NULL'),
|
||||
'TIMESTAMP': (0x07, 'TIMESTAMP'),
|
||||
'LONGLONG': (0x08, 'LONGLONG'),
|
||||
'INT24': (0x09, 'INT24'),
|
||||
'DATE': (0x0a, 'DATE'),
|
||||
'TIME': (0x0b, 'TIME'),
|
||||
'DATETIME': (0x0c, 'DATETIME'),
|
||||
'YEAR': (0x0d, 'YEAR'),
|
||||
'NEWDATE': (0x0e, 'NEWDATE'),
|
||||
'VARCHAR': (0x0f, 'VARCHAR'),
|
||||
'BIT': (0x10, 'BIT'),
|
||||
'NEWDECIMAL': (0xf6, 'NEWDECIMAL'),
|
||||
'ENUM': (0xf7, 'ENUM'),
|
||||
'SET': (0xf8, 'SET'),
|
||||
'TINY_BLOB': (0xf9, 'TINY_BLOB'),
|
||||
'MEDIUM_BLOB': (0xfa, 'MEDIUM_BLOB'),
|
||||
'LONG_BLOB': (0xfb, 'LONG_BLOB'),
|
||||
'BLOB': (0xfc, 'BLOB'),
|
||||
'VAR_STRING': (0xfd, 'VAR_STRING'),
|
||||
'STRING': (0xfe, 'STRING'),
|
||||
'GEOMETRY': (0xff, 'GEOMETRY'),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_string_types(cls):
|
||||
return [
|
||||
cls.VARCHAR,
|
||||
cls.ENUM,
|
||||
cls.VAR_STRING, cls.STRING,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_binary_types(cls):
|
||||
return [
|
||||
cls.TINY_BLOB, cls.MEDIUM_BLOB,
|
||||
cls.LONG_BLOB, cls.BLOB,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_number_types(cls):
|
||||
return [
|
||||
cls.DECIMAL, cls.NEWDECIMAL,
|
||||
cls.TINY, cls.SHORT, cls.LONG,
|
||||
cls.FLOAT, cls.DOUBLE,
|
||||
cls.LONGLONG, cls.INT24,
|
||||
cls.BIT,
|
||||
cls.YEAR,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_timestamp_types(cls):
|
||||
return [
|
||||
cls.DATETIME, cls.TIMESTAMP,
|
||||
]
|
||||
|
||||
class FieldFlag(_constants):
|
||||
"""
|
||||
Field flags as found in MySQL sources mysql-src/include/mysql_com.h
|
||||
"""
|
||||
_prefix = ''
|
||||
NOT_NULL = 1 << 0
|
||||
PRI_KEY = 1 << 1
|
||||
UNIQUE_KEY = 1 << 2
|
||||
MULTIPLE_KEY = 1 << 3
|
||||
BLOB = 1 << 4
|
||||
UNSIGNED = 1 << 5
|
||||
ZEROFILL = 1 << 6
|
||||
BINARY = 1 << 7
|
||||
|
||||
ENUM = 1 << 8
|
||||
AUTO_INCREMENT = 1 << 9
|
||||
TIMESTAMP = 1 << 10
|
||||
SET = 1 << 11
|
||||
|
||||
NO_DEFAULT_VALUE = 1 << 12
|
||||
ON_UPDATE_NOW = 1 << 13
|
||||
NUM = 1 << 14
|
||||
PART_KEY = 1 << 15
|
||||
GROUP = 1 << 14 # SAME AS NUM !!!!!!!????
|
||||
UNIQUE = 1 << 16
|
||||
BINCMP = 1 << 17
|
||||
|
||||
GET_FIXED_FIELDS = 1 << 18
|
||||
FIELD_IN_PART_FUNC = 1 << 19
|
||||
FIELD_IN_ADD_INDEX = 1 << 20
|
||||
FIELD_IS_RENAMED = 1 << 21
|
||||
|
||||
desc = {
|
||||
'NOT_NULL': (1 << 0, "Field can't be NULL"),
|
||||
'PRI_KEY': (1 << 1, "Field is part of a primary key"),
|
||||
'UNIQUE_KEY': (1 << 2, "Field is part of a unique key"),
|
||||
'MULTIPLE_KEY': (1 << 3, "Field is part of a key"),
|
||||
'BLOB': (1 << 4, "Field is a blob"),
|
||||
'UNSIGNED': (1 << 5, "Field is unsigned"),
|
||||
'ZEROFILL': (1 << 6, "Field is zerofill"),
|
||||
'BINARY': (1 << 7, "Field is binary "),
|
||||
'ENUM': (1 << 8, "field is an enum"),
|
||||
'AUTO_INCREMENT': (1 << 9, "field is a autoincrement field"),
|
||||
'TIMESTAMP': (1 << 10, "Field is a timestamp"),
|
||||
'SET': (1 << 11, "field is a set"),
|
||||
'NO_DEFAULT_VALUE': (1 << 12, "Field doesn't have default value"),
|
||||
'ON_UPDATE_NOW': (1 << 13, "Field is set to NOW on UPDATE"),
|
||||
'NUM': (1 << 14, "Field is num (for clients)"),
|
||||
|
||||
'PART_KEY': (1 << 15, "Intern; Part of some key"),
|
||||
'GROUP': (1 << 14, "Intern: Group field"), # Same as NUM
|
||||
'UNIQUE': (1 << 16, "Intern: Used by sql_yacc"),
|
||||
'BINCMP': (1 << 17, "Intern: Used by sql_yacc"),
|
||||
'GET_FIXED_FIELDS': (1 << 18, "Used to get fields in item tree"),
|
||||
'FIELD_IN_PART_FUNC': (1 << 19, "Field part of partition func"),
|
||||
'FIELD_IN_ADD_INDEX': (1 << 20, "Intern: Field used in ADD INDEX"),
|
||||
'FIELD_IS_RENAMED': (1 << 21, "Intern: Field is being renamed"),
|
||||
}
|
||||
|
||||
|
||||
class ServerCmd(_constants):
|
||||
_prefix = 'COM_'
|
||||
SLEEP = 0
|
||||
QUIT = 1
|
||||
INIT_DB = 2
|
||||
QUERY = 3
|
||||
FIELD_LIST = 4
|
||||
CREATE_DB = 5
|
||||
DROP_DB = 6
|
||||
REFRESH = 7
|
||||
SHUTDOWN = 8
|
||||
STATISTICS = 9
|
||||
PROCESS_INFO = 10
|
||||
CONNECT = 11
|
||||
PROCESS_KILL = 12
|
||||
DEBUG = 13
|
||||
PING = 14
|
||||
TIME = 15
|
||||
DELAYED_INSERT = 16
|
||||
CHANGE_USER = 17
|
||||
BINLOG_DUMP = 18
|
||||
TABLE_DUMP = 19
|
||||
CONNECT_OUT = 20
|
||||
REGISTER_SLAVE = 21
|
||||
STMT_PREPARE = 22
|
||||
STMT_EXECUTE = 23
|
||||
STMT_SEND_LONG_DATA = 24
|
||||
STMT_CLOSE = 25
|
||||
STMT_RESET = 26
|
||||
SET_OPTION = 27
|
||||
STMT_FETCH = 28
|
||||
DAEMON = 29
|
||||
|
||||
class ClientFlag(_constants):
|
||||
"""
|
||||
Client Options as found in the MySQL sources mysql-src/include/mysql_com.h
|
||||
"""
|
||||
LONG_PASSWD = 1 << 0
|
||||
FOUND_ROWS = 1 << 1
|
||||
LONG_FLAG = 1 << 2
|
||||
CONNECT_WITH_DB = 1 << 3
|
||||
NO_SCHEMA = 1 << 4
|
||||
COMPRESS = 1 << 5
|
||||
ODBC = 1 << 6
|
||||
LOCAL_FILES = 1 << 7
|
||||
IGNORE_SPACE = 1 << 8
|
||||
PROTOCOL_41 = 1 << 9
|
||||
INTERACTIVE = 1 << 10
|
||||
SSL = 1 << 11
|
||||
IGNORE_SIGPIPE = 1 << 12
|
||||
TRANSACTIONS = 1 << 13
|
||||
RESERVED = 1 << 14
|
||||
SECURE_CONNECTION = 1 << 15
|
||||
MULTI_STATEMENTS = 1 << 16
|
||||
MULTI_RESULTS = 1 << 17
|
||||
SSL_VERIFY_SERVER_CERT = 1 << 30
|
||||
REMEMBER_OPTIONS = 1 << 31
|
||||
|
||||
desc = {
|
||||
'LONG_PASSWD': (1 << 0, 'New more secure passwords'),
|
||||
'FOUND_ROWS': (1 << 1, 'Found instead of affected rows'),
|
||||
'LONG_FLAG': (1 << 2, 'Get all column flags'),
|
||||
'CONNECT_WITH_DB': (1 << 3, 'One can specify db on connect'),
|
||||
'NO_SCHEMA': (1 << 4, "Don't allow database.table.column"),
|
||||
'COMPRESS': (1 << 5, 'Can use compression protocol'),
|
||||
'ODBC': (1 << 6, 'ODBC client'),
|
||||
'LOCAL_FILES': (1 << 7, 'Can use LOAD DATA LOCAL'),
|
||||
'IGNORE_SPACE': (1 << 8, "Ignore spaces before ''"),
|
||||
'PROTOCOL_41': (1 << 9, 'New 4.1 protocol'),
|
||||
'INTERACTIVE': (1 << 10, 'This is an interactive client'),
|
||||
'SSL': (1 << 11, 'Switch to SSL after handshake'),
|
||||
'IGNORE_SIGPIPE': (1 << 12, 'IGNORE sigpipes'),
|
||||
'TRANSACTIONS': (1 << 13, 'Client knows about transactions'),
|
||||
'RESERVED': (1 << 14, 'Old flag for 4.1 protocol'),
|
||||
'SECURE_CONNECTION': (1 << 15, 'New 4.1 authentication'),
|
||||
'MULTI_STATEMENTS': (1 << 16, 'Enable/disable multi-stmt support'),
|
||||
'MULTI_RESULTS': (1 << 17, 'Enable/disable multi-results'),
|
||||
'SSL_VERIFY_SERVER_CERT': (1 << 30, ''),
|
||||
'REMEMBER_OPTIONS': (1 << 31, ''),
|
||||
}
|
||||
|
||||
default = [
|
||||
LONG_PASSWD,
|
||||
LONG_FLAG,
|
||||
CONNECT_WITH_DB,
|
||||
PROTOCOL_41,
|
||||
TRANSACTIONS,
|
||||
SECURE_CONNECTION,
|
||||
MULTI_STATEMENTS,
|
||||
MULTI_RESULTS,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_default(cls):
|
||||
flags = 0
|
||||
for f in cls.default:
|
||||
flags |= f
|
||||
return flags
|
||||
|
||||
class ServerFlag(_constants):
|
||||
"""
|
||||
Server flags as found in the MySQL sources mysql-src/include/mysql_com.h
|
||||
"""
|
||||
_prefix = 'SERVER_'
|
||||
STATUS_IN_TRANS = 1 << 0
|
||||
STATUS_AUTOCOMMIT = 1 << 1
|
||||
MORE_RESULTS_EXISTS = 1 << 3
|
||||
QUERY_NO_GOOD_INDEX_USED = 1 << 4
|
||||
QUERY_NO_INDEX_USED = 1 << 5
|
||||
STATUS_CURSOR_EXISTS = 1 << 6
|
||||
STATUS_LAST_ROW_SENT = 1 << 7
|
||||
STATUS_DB_DROPPED = 1 << 8
|
||||
STATUS_NO_BACKSLASH_ESCAPES = 1 << 9
|
||||
|
||||
desc = {
|
||||
'SERVER_STATUS_IN_TRANS': (1 << 0, 'Transaction has started'),
|
||||
'SERVER_STATUS_AUTOCOMMIT': (1 << 1, 'Server in auto_commit mode'),
|
||||
'SERVER_MORE_RESULTS_EXISTS': (1 << 3, 'Multi query - next query exists'),
|
||||
'SERVER_QUERY_NO_GOOD_INDEX_USED': (1 << 4, ''),
|
||||
'SERVER_QUERY_NO_INDEX_USED': (1 << 5, ''),
|
||||
'SERVER_STATUS_CURSOR_EXISTS': (1 << 6, ''),
|
||||
'SERVER_STATUS_LAST_ROW_SENT': (1 << 7, ''),
|
||||
'SERVER_STATUS_DB_DROPPED': (1 << 8, 'A database was dropped'),
|
||||
'SERVER_STATUS_NO_BACKSLASH_ESCAPES': (1 << 9, ''),
|
||||
}
|
||||
|
||||
class RefreshOption(_constants):
|
||||
"""Options used when sending the COM_REFRESH server command."""
|
||||
|
||||
_prefix = 'REFRESH_'
|
||||
GRANT = 1 << 0
|
||||
LOG = 1 << 1
|
||||
TABLES = 1 << 2
|
||||
HOST = 1 << 3
|
||||
STATUS = 1 << 4
|
||||
THREADS = 1 << 5
|
||||
SLAVE = 1 << 6
|
||||
|
||||
desc = {
|
||||
'GRANT': (1 << 0, 'Refresh grant tables'),
|
||||
'LOG': (1 << 1, 'Start on new log file'),
|
||||
'TABLES': (1 << 2, 'close all tables'),
|
||||
'HOSTS': (1 << 3, 'Flush host cache'),
|
||||
'STATUS': (1 << 4, 'Flush status variables'),
|
||||
'THREADS': (1 << 5, 'Flush thread cache'),
|
||||
'SLAVE': (1 << 6, 'Reset master info and restart slave thread'),
|
||||
}
|
||||
|
||||
class CharacterSet(_constants):
|
||||
"""
|
||||
List of supported character sets with their collations. This maps to the
|
||||
character set we get from the server within the handshake packet.
|
||||
|
||||
To update this list, use the following query:
|
||||
SELECT ID,CHARACTER_SET_NAME, COLLATION_NAME
|
||||
FROM INFORMATION_SCHEMA.COLLATIONS
|
||||
ORDER BY ID
|
||||
|
||||
This list is hardcoded because we want to avoid doing each time the above
|
||||
query to get the name of the character set used.
|
||||
"""
|
||||
|
||||
_max_id = 211 # SELECT MAX(ID)+1 FROM INFORMATION_SCHEMA.COLLATIONS
|
||||
|
||||
@classmethod
|
||||
def _init_desc(cls):
|
||||
if not cls.__dict__.has_key('desc'):
|
||||
|
||||
# Do not forget to update the tests in test_constants!
|
||||
cls.desc = [ None for i in range(cls._max_id)]
|
||||
cls.desc[1] = ('big5','big5_chinese_ci')
|
||||
cls.desc[2] = ('latin2','latin2_czech_cs')
|
||||
cls.desc[3] = ('dec8','dec8_swedish_ci')
|
||||
cls.desc[4] = ('cp850','cp850_general_ci')
|
||||
cls.desc[5] = ('latin1','latin1_german1_ci')
|
||||
cls.desc[6] = ('hp8','hp8_english_ci')
|
||||
cls.desc[7] = ('koi8r','koi8r_general_ci')
|
||||
cls.desc[8] = ('latin1','latin1_swedish_ci')
|
||||
cls.desc[9] = ('latin2','latin2_general_ci')
|
||||
cls.desc[10] = ('swe7','swe7_swedish_ci')
|
||||
cls.desc[11] = ('ascii','ascii_general_ci')
|
||||
cls.desc[12] = ('ujis','ujis_japanese_ci')
|
||||
cls.desc[13] = ('sjis','sjis_japanese_ci')
|
||||
cls.desc[14] = ('cp1251','cp1251_bulgarian_ci')
|
||||
cls.desc[15] = ('latin1','latin1_danish_ci')
|
||||
cls.desc[16] = ('hebrew','hebrew_general_ci')
|
||||
cls.desc[18] = ('tis620','tis620_thai_ci')
|
||||
cls.desc[19] = ('euckr','euckr_korean_ci')
|
||||
cls.desc[20] = ('latin7','latin7_estonian_cs')
|
||||
cls.desc[21] = ('latin2','latin2_hungarian_ci')
|
||||
cls.desc[22] = ('koi8u','koi8u_general_ci')
|
||||
cls.desc[23] = ('cp1251','cp1251_ukrainian_ci')
|
||||
cls.desc[24] = ('gb2312','gb2312_chinese_ci')
|
||||
cls.desc[25] = ('greek','greek_general_ci')
|
||||
cls.desc[26] = ('cp1250','cp1250_general_ci')
|
||||
cls.desc[27] = ('latin2','latin2_croatian_ci')
|
||||
cls.desc[28] = ('gbk','gbk_chinese_ci')
|
||||
cls.desc[29] = ('cp1257','cp1257_lithuanian_ci')
|
||||
cls.desc[30] = ('latin5','latin5_turkish_ci')
|
||||
cls.desc[31] = ('latin1','latin1_german2_ci')
|
||||
cls.desc[32] = ('armscii8','armscii8_general_ci')
|
||||
cls.desc[33] = ('utf8','utf8_general_ci')
|
||||
cls.desc[34] = ('cp1250','cp1250_czech_cs')
|
||||
cls.desc[35] = ('ucs2','ucs2_general_ci')
|
||||
cls.desc[36] = ('cp866','cp866_general_ci')
|
||||
cls.desc[37] = ('keybcs2','keybcs2_general_ci')
|
||||
cls.desc[38] = ('macce','macce_general_ci')
|
||||
cls.desc[39] = ('macroman','macroman_general_ci')
|
||||
cls.desc[40] = ('cp852','cp852_general_ci')
|
||||
cls.desc[41] = ('latin7','latin7_general_ci')
|
||||
cls.desc[42] = ('latin7','latin7_general_cs')
|
||||
cls.desc[43] = ('macce','macce_bin')
|
||||
cls.desc[44] = ('cp1250','cp1250_croatian_ci')
|
||||
cls.desc[47] = ('latin1','latin1_bin')
|
||||
cls.desc[48] = ('latin1','latin1_general_ci')
|
||||
cls.desc[49] = ('latin1','latin1_general_cs')
|
||||
cls.desc[50] = ('cp1251','cp1251_bin')
|
||||
cls.desc[51] = ('cp1251','cp1251_general_ci')
|
||||
cls.desc[52] = ('cp1251','cp1251_general_cs')
|
||||
cls.desc[53] = ('macroman','macroman_bin')
|
||||
cls.desc[57] = ('cp1256','cp1256_general_ci')
|
||||
cls.desc[58] = ('cp1257','cp1257_bin')
|
||||
cls.desc[59] = ('cp1257','cp1257_general_ci')
|
||||
cls.desc[63] = ('binary','binary')
|
||||
cls.desc[64] = ('armscii8','armscii8_bin')
|
||||
cls.desc[65] = ('ascii','ascii_bin')
|
||||
cls.desc[66] = ('cp1250','cp1250_bin')
|
||||
cls.desc[67] = ('cp1256','cp1256_bin')
|
||||
cls.desc[68] = ('cp866','cp866_bin')
|
||||
cls.desc[69] = ('dec8','dec8_bin')
|
||||
cls.desc[70] = ('greek','greek_bin')
|
||||
cls.desc[71] = ('hebrew','hebrew_bin')
|
||||
cls.desc[72] = ('hp8','hp8_bin')
|
||||
cls.desc[73] = ('keybcs2','keybcs2_bin')
|
||||
cls.desc[74] = ('koi8r','koi8r_bin')
|
||||
cls.desc[75] = ('koi8u','koi8u_bin')
|
||||
cls.desc[77] = ('latin2','latin2_bin')
|
||||
cls.desc[78] = ('latin5','latin5_bin')
|
||||
cls.desc[79] = ('latin7','latin7_bin')
|
||||
cls.desc[80] = ('cp850','cp850_bin')
|
||||
cls.desc[81] = ('cp852','cp852_bin')
|
||||
cls.desc[82] = ('swe7','swe7_bin')
|
||||
cls.desc[83] = ('utf8','utf8_bin')
|
||||
cls.desc[84] = ('big5','big5_bin')
|
||||
cls.desc[85] = ('euckr','euckr_bin')
|
||||
cls.desc[86] = ('gb2312','gb2312_bin')
|
||||
cls.desc[87] = ('gbk','gbk_bin')
|
||||
cls.desc[88] = ('sjis','sjis_bin')
|
||||
cls.desc[89] = ('tis620','tis620_bin')
|
||||
cls.desc[90] = ('ucs2','ucs2_bin')
|
||||
cls.desc[91] = ('ujis','ujis_bin')
|
||||
cls.desc[92] = ('geostd8','geostd8_general_ci')
|
||||
cls.desc[93] = ('geostd8','geostd8_bin')
|
||||
cls.desc[94] = ('latin1','latin1_spanish_ci')
|
||||
cls.desc[95] = ('cp932','cp932_japanese_ci')
|
||||
cls.desc[96] = ('cp932','cp932_bin')
|
||||
cls.desc[97] = ('eucjpms','eucjpms_japanese_ci')
|
||||
cls.desc[98] = ('eucjpms','eucjpms_bin')
|
||||
cls.desc[128] = ('ucs2','ucs2_unicode_ci')
|
||||
cls.desc[129] = ('ucs2','ucs2_icelandic_ci')
|
||||
cls.desc[130] = ('ucs2','ucs2_latvian_ci')
|
||||
cls.desc[131] = ('ucs2','ucs2_romanian_ci')
|
||||
cls.desc[132] = ('ucs2','ucs2_slovenian_ci')
|
||||
cls.desc[133] = ('ucs2','ucs2_polish_ci')
|
||||
cls.desc[134] = ('ucs2','ucs2_estonian_ci')
|
||||
cls.desc[135] = ('ucs2','ucs2_spanish_ci')
|
||||
cls.desc[136] = ('ucs2','ucs2_swedish_ci')
|
||||
cls.desc[137] = ('ucs2','ucs2_turkish_ci')
|
||||
cls.desc[138] = ('ucs2','ucs2_czech_ci')
|
||||
cls.desc[139] = ('ucs2','ucs2_danish_ci')
|
||||
cls.desc[140] = ('ucs2','ucs2_lithuanian_ci')
|
||||
cls.desc[141] = ('ucs2','ucs2_slovak_ci')
|
||||
cls.desc[142] = ('ucs2','ucs2_spanish2_ci')
|
||||
cls.desc[143] = ('ucs2','ucs2_roman_ci')
|
||||
cls.desc[144] = ('ucs2','ucs2_persian_ci')
|
||||
cls.desc[145] = ('ucs2','ucs2_esperanto_ci')
|
||||
cls.desc[146] = ('ucs2','ucs2_hungarian_ci')
|
||||
cls.desc[192] = ('utf8','utf8_unicode_ci')
|
||||
cls.desc[193] = ('utf8','utf8_icelandic_ci')
|
||||
cls.desc[194] = ('utf8','utf8_latvian_ci')
|
||||
cls.desc[195] = ('utf8','utf8_romanian_ci')
|
||||
cls.desc[196] = ('utf8','utf8_slovenian_ci')
|
||||
cls.desc[197] = ('utf8','utf8_polish_ci')
|
||||
cls.desc[198] = ('utf8','utf8_estonian_ci')
|
||||
cls.desc[199] = ('utf8','utf8_spanish_ci')
|
||||
cls.desc[200] = ('utf8','utf8_swedish_ci')
|
||||
cls.desc[201] = ('utf8','utf8_turkish_ci')
|
||||
cls.desc[202] = ('utf8','utf8_czech_ci')
|
||||
cls.desc[203] = ('utf8','utf8_danish_ci')
|
||||
cls.desc[204] = ('utf8','utf8_lithuanian_ci')
|
||||
cls.desc[205] = ('utf8','utf8_slovak_ci')
|
||||
cls.desc[206] = ('utf8','utf8_spanish2_ci')
|
||||
cls.desc[207] = ('utf8','utf8_roman_ci')
|
||||
cls.desc[208] = ('utf8','utf8_persian_ci')
|
||||
cls.desc[209] = ('utf8','utf8_esperanto_ci')
|
||||
cls.desc[210] = ('utf8','utf8_hungarian_ci')
|
||||
|
||||
@classmethod
|
||||
def get_info(cls,setid):
|
||||
"""Returns information about the charset for given MySQL ID."""
|
||||
cls._init_desc()
|
||||
res = ()
|
||||
errmsg = "Character set with id '%d' unsupported." % (setid)
|
||||
try:
|
||||
res = cls.desc[setid]
|
||||
except:
|
||||
raise ProgrammingError, errmsg
|
||||
|
||||
if res is None:
|
||||
raise ProgrammingError, errmsg
|
||||
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def get_desc(cls,setid):
|
||||
"""Returns info string about the charset for given MySQL ID."""
|
||||
res = ()
|
||||
try:
|
||||
res = "%s/%s" % self.get_info(setid)
|
||||
except ProgrammingError, e:
|
||||
raise
|
||||
else:
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def get_charset_info(cls, name, collation=None):
|
||||
"""Returns information about the charset and optional collation."""
|
||||
cls._init_desc()
|
||||
l = len(cls.desc)
|
||||
errmsg = "Character set '%s' unsupported." % (name)
|
||||
|
||||
if collation is None:
|
||||
collation = '%s_general_ci' % (name)
|
||||
|
||||
# Search the list and return when found
|
||||
idx = 0
|
||||
for info in cls.desc:
|
||||
if info and info[0] == name and info[1] == collation:
|
||||
return (idx,info[0],info[1])
|
||||
idx += 1
|
||||
|
||||
# If we got here, we didn't find the charset
|
||||
raise ProgrammingError, errmsg
|
||||
|
||||
@classmethod
|
||||
def get_supported(cls):
|
||||
"""Returns a list with names of all supproted character sets."""
|
||||
res = []
|
||||
for info in cls.desc:
|
||||
if info and info[0] not in res:
|
||||
res.append(info[0])
|
||||
return tuple(res)
|
||||
|
||||
|
|
@ -0,0 +1,401 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""Converting MySQL and Python types
|
||||
"""
|
||||
|
||||
from types import NoneType
|
||||
import re
|
||||
import datetime
|
||||
import time
|
||||
from decimal import Decimal
|
||||
|
||||
import errors
|
||||
from constants import FieldType, FieldFlag
|
||||
|
||||
class ConverterBase(object):
|
||||
|
||||
def __init__(self, charset='utf8', use_unicode=True):
|
||||
self.python_types = None
|
||||
self.mysql_types = None
|
||||
self.set_charset(charset)
|
||||
self.set_unicode(use_unicode)
|
||||
|
||||
def set_charset(self, charset):
|
||||
if charset is not None:
|
||||
self.charset = charset
|
||||
else:
|
||||
# default to utf8
|
||||
self.charset = 'utf8'
|
||||
|
||||
def set_unicode(self, value=True):
|
||||
self.use_unicode = value
|
||||
|
||||
def to_mysql(self, value):
|
||||
return value
|
||||
|
||||
def to_python(self, vtype, value):
|
||||
return value
|
||||
|
||||
def escape(self, buf):
|
||||
return buf
|
||||
|
||||
def quote(self, buf):
|
||||
return str(buf)
|
||||
|
||||
class MySQLConverter(ConverterBase):
|
||||
"""
|
||||
A converted class grouping:
|
||||
o escape method: for escpaing values send to MySQL
|
||||
o quoting method: for quoting values send to MySQL in statements
|
||||
o conversion mapping: maps Python and MySQL data types to
|
||||
function for converting them.
|
||||
|
||||
This class should be overloaded whenever one needs differences
|
||||
in how values are to be converted. Each MySQLConnection object
|
||||
has a default_converter property, which can be set like
|
||||
MySQL.converter(CustomMySQLConverter)
|
||||
|
||||
"""
|
||||
def __init__(self, charset=None, use_unicode=True):
|
||||
ConverterBase.__init__(self, charset, use_unicode)
|
||||
|
||||
# Python types
|
||||
self.python_types = {
|
||||
int : int,
|
||||
str : self._str_to_mysql,
|
||||
long : long,
|
||||
float : float,
|
||||
unicode : self._unicode_to_mysql,
|
||||
bool : self._bool_to_mysql,
|
||||
NoneType : self._none_to_mysql,
|
||||
datetime.datetime : self._datetime_to_mysql,
|
||||
datetime.date : self._date_to_mysql,
|
||||
datetime.time : self._time_to_mysql,
|
||||
time.struct_time : self._struct_time_to_mysql,
|
||||
datetime.timedelta : self._timedelta_to_mysql,
|
||||
Decimal : self._decimal_to_mysql,
|
||||
}
|
||||
|
||||
# MySQL types
|
||||
self.mysql_types = {
|
||||
FieldType.TINY : self._int,
|
||||
FieldType.SHORT : self._int,
|
||||
FieldType.INT24 : self._int,
|
||||
FieldType.LONG : self._long,
|
||||
FieldType.LONGLONG : self._long,
|
||||
FieldType.FLOAT : self._float,
|
||||
FieldType.DOUBLE : self._float,
|
||||
FieldType.DECIMAL : self._decimal,
|
||||
FieldType.NEWDECIMAL : self._decimal,
|
||||
FieldType.VAR_STRING : self._STRING_to_python,
|
||||
FieldType.STRING : self._STRING_to_python,
|
||||
FieldType.SET : self._SET_to_python,
|
||||
FieldType.TIME : self._TIME_to_python,
|
||||
FieldType.DATE : self._DATE_to_python,
|
||||
FieldType.NEWDATE : self._DATE_to_python,
|
||||
FieldType.DATETIME : self._DATETIME_to_python,
|
||||
FieldType.TIMESTAMP : self._DATETIME_to_python,
|
||||
FieldType.BLOB : self._STRING_to_python,
|
||||
}
|
||||
|
||||
def escape(self, value):
|
||||
"""
|
||||
Escapes special characters as they are expected to by when MySQL
|
||||
receives them.
|
||||
As found in MySQL source mysys/charset.c
|
||||
|
||||
Returns the value if not a string, or the escaped string.
|
||||
"""
|
||||
if value is None:
|
||||
return value
|
||||
elif isinstance(value, (int,float,long,Decimal)):
|
||||
return value
|
||||
backslash = re.compile(r'\134')
|
||||
res = value
|
||||
res = backslash.sub(r'\\\\', res)
|
||||
res = res.replace('\n','\\n')
|
||||
res = res.replace('\r','\\r')
|
||||
res = res.replace('\047','\134\047') # single quotes
|
||||
res = res.replace('\042','\134\042') # double quotes
|
||||
res = res.replace('\032','\134\032') # for Win32
|
||||
return res
|
||||
|
||||
def quote(self, buf):
|
||||
"""
|
||||
Quote the parameters for commands. General rules:
|
||||
o numbers are returns as str type (because operation expect it)
|
||||
o None is returned as str('NULL')
|
||||
o String are quoted with single quotes '<string>'
|
||||
|
||||
Returns a string.
|
||||
"""
|
||||
if isinstance(buf, (int,float,long,Decimal)):
|
||||
return str(buf)
|
||||
elif isinstance(buf, NoneType):
|
||||
return "NULL"
|
||||
else:
|
||||
# Anything else would be a string
|
||||
return "'%s'" % buf
|
||||
|
||||
def to_mysql(self, value):
|
||||
vtype = type(value)
|
||||
return self.python_types[vtype](value)
|
||||
|
||||
def _str_to_mysql(self, value):
|
||||
return str(value)
|
||||
|
||||
def _unicode_to_mysql(self, value):
|
||||
"""
|
||||
Encodes value, a Python unicode string, to whatever the
|
||||
character set for this converter is set too.
|
||||
"""
|
||||
return value.encode(self.charset)
|
||||
|
||||
def _bool_to_mysql(self, value):
|
||||
if value:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def _none_to_mysql(self, value):
|
||||
"""
|
||||
This would return what None would be in MySQL, but instead we
|
||||
leave it None and return it right away. The actual convertion
|
||||
from None to NULL happens in the quoting functionality.
|
||||
|
||||
Return None.
|
||||
"""
|
||||
return None
|
||||
|
||||
def _datetime_to_mysql(self, value):
|
||||
"""
|
||||
Converts a datetime instance to a string suitable for MySQL.
|
||||
The returned string has format: %Y-%m-%d %H:%M:%S
|
||||
|
||||
If the instance isn't a datetime.datetime type, it return None.
|
||||
|
||||
Returns a string or None when not valid.
|
||||
"""
|
||||
if isinstance(value, datetime.datetime):
|
||||
return value.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
return None
|
||||
|
||||
def _date_to_mysql(self, value):
|
||||
"""
|
||||
Converts a date instance to a string suitable for MySQL.
|
||||
The returned string has format: %Y-%m-%d
|
||||
|
||||
If the instance isn't a datetime.date type, it return None.
|
||||
|
||||
Returns a string or None when not valid.
|
||||
"""
|
||||
if isinstance(value, datetime.date):
|
||||
return value.strftime('%Y-%m-%d')
|
||||
|
||||
|
||||
return None
|
||||
|
||||
def _time_to_mysql(self, value):
|
||||
"""
|
||||
Converts a time instance to a string suitable for MySQL.
|
||||
The returned string has format: %H:%M:%S
|
||||
|
||||
If the instance isn't a datetime.time type, it return None.
|
||||
|
||||
Returns a string or None when not valid.
|
||||
"""
|
||||
if isinstance(value, datetime.time):
|
||||
return value.strftime('%H:%M:%S')
|
||||
|
||||
return None
|
||||
|
||||
def _struct_time_to_mysql(self, value):
|
||||
"""
|
||||
Converts a time.struct_time sequence to a string suitable
|
||||
for MySQL.
|
||||
The returned string has format: %Y-%m-%d %H:%M:%S
|
||||
|
||||
Returns a string or None when not valid.
|
||||
"""
|
||||
if isinstance(value, time.struct_time):
|
||||
return time.strftime('%Y-%m-%d %H:%M:%S',value)
|
||||
return None
|
||||
|
||||
def _timedelta_to_mysql(self, value):
|
||||
"""
|
||||
Converts a timedelta instance to a string suitable for MySQL.
|
||||
The returned string has format: %H:%M:%S
|
||||
|
||||
Returns a string or None when not valid.
|
||||
"""
|
||||
if isinstance(value, datetime.timedelta):
|
||||
secs = value.seconds%60
|
||||
mins = value.seconds%3600/60
|
||||
hours = value.seconds/3600+(value.days*24)
|
||||
return '%d:%02d:%02d' % (hours,mins,secs)
|
||||
|
||||
return None
|
||||
|
||||
def _decimal_to_mysql(self, value):
|
||||
"""
|
||||
Converts a decimal.Decimal instance to a string suitable for
|
||||
MySQL.
|
||||
|
||||
Returns a string or None when not valid.
|
||||
"""
|
||||
if isinstance(value, Decimal):
|
||||
return str(value)
|
||||
|
||||
return None
|
||||
|
||||
def to_python(self, flddsc, value):
|
||||
"""
|
||||
Converts a given value coming from MySQL to a certain type in Python.
|
||||
The flddsc contains additional information for the field in the
|
||||
table. It's an element from MySQLCursor.description.
|
||||
|
||||
Returns a mixed value.
|
||||
"""
|
||||
res = value
|
||||
|
||||
if value == '\x00':
|
||||
# Don't go further when we hit a NULL value
|
||||
return None
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
res = self.mysql_types[flddsc[1]](value, flddsc)
|
||||
except KeyError:
|
||||
# If one type is not defined, we just return the value as str
|
||||
return str(value)
|
||||
except ValueError, e:
|
||||
raise ValueError, "%s (field %s)" % (e, flddsc[0])
|
||||
except TypeError, e:
|
||||
raise TypeError, "%s (field %s)" % (e, flddsc[0])
|
||||
except:
|
||||
raise
|
||||
|
||||
return res
|
||||
|
||||
def _float(self, v, desc=None):
|
||||
"""
|
||||
Returns v as float type.
|
||||
"""
|
||||
return float(v)
|
||||
|
||||
def _int(self, v, desc=None):
|
||||
"""
|
||||
Returns v as int type.
|
||||
"""
|
||||
return int(v)
|
||||
|
||||
def _long(self, v, desc=None):
|
||||
"""
|
||||
Returns v as long type.
|
||||
"""
|
||||
return long(v)
|
||||
|
||||
def _decimal(self, v, desc=None):
|
||||
"""
|
||||
Returns v as a decimal.Decimal.
|
||||
"""
|
||||
return Decimal(v)
|
||||
|
||||
def _str(self, v, desc=None):
|
||||
"""
|
||||
Returns v as str type.
|
||||
"""
|
||||
return str(v)
|
||||
|
||||
def _DATE_to_python(self, v, dsc=None):
|
||||
"""
|
||||
Returns DATE column type as datetime.date type.
|
||||
"""
|
||||
pv = None
|
||||
try:
|
||||
pv = datetime.date(*[ int(s) for s in v.split('-')])
|
||||
except ValueError:
|
||||
return None
|
||||
else:
|
||||
return pv
|
||||
|
||||
def _TIME_to_python(self, v, dsc=None):
|
||||
"""
|
||||
Returns TIME column type as datetime.time type.
|
||||
"""
|
||||
pv = None
|
||||
try:
|
||||
(h, m, s) = [ int(s) for s in v.split(':')]
|
||||
pv = datetime.timedelta(hours=h,minutes=m,seconds=s)
|
||||
except ValueError:
|
||||
raise ValueError, "Could not convert %s to python datetime.timedelta" % v
|
||||
else:
|
||||
return pv
|
||||
|
||||
def _DATETIME_to_python(self, v, dsc=None):
|
||||
"""
|
||||
Returns DATETIME column type as datetime.datetime type.
|
||||
"""
|
||||
pv = None
|
||||
try:
|
||||
pv = datetime.datetime(*time.strptime(v, "%Y-%m-%d %H:%M:%S")[0:6])
|
||||
except ValueError:
|
||||
pv = None
|
||||
|
||||
return pv
|
||||
|
||||
def _SET_to_python(self, v, dsc=None):
|
||||
"""
|
||||
Actually, MySQL protocol sees a SET as a string type field. So this
|
||||
code isn't called directly, but used by STRING_to_python() method.
|
||||
|
||||
Returns SET column type as string splitted using a comma.
|
||||
"""
|
||||
pv = None
|
||||
try:
|
||||
pv = v.split(',')
|
||||
except ValueError:
|
||||
raise ValueError, "Could not convert set %s to a sequence." % v
|
||||
return pv
|
||||
|
||||
def _STRING_to_python(self, v, dsc=None):
|
||||
"""
|
||||
Note that a SET is a string too, but using the FieldFlag we can see
|
||||
whether we have to split it.
|
||||
|
||||
Returns string typed columns as string type.
|
||||
"""
|
||||
if dsc is not None:
|
||||
# Check if we deal with a SET
|
||||
if dsc[7] & FieldFlag.SET:
|
||||
return self._SET_to_python(v, dsc)
|
||||
|
||||
if self.use_unicode:
|
||||
try:
|
||||
return unicode(v, self.charset)
|
||||
except:
|
||||
raise
|
||||
return str(v)
|
|
@ -0,0 +1,542 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""Cursor classes
|
||||
"""
|
||||
|
||||
import exceptions
|
||||
|
||||
import mysql
|
||||
import connection
|
||||
import protocol
|
||||
import errors
|
||||
import utils
|
||||
|
||||
class CursorBase(object):
|
||||
"""
|
||||
Base for defining MySQLCursor. This class is a skeleton and defines
|
||||
methods and members as required for the Python Database API
|
||||
Specification v2.0.
|
||||
|
||||
It's better to inherite from MySQLCursor.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.description = None
|
||||
self.rowcount = -1
|
||||
self.arraysize = 1
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def callproc(self, procname, args=()):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def execute(self, operation, params=()):
|
||||
pass
|
||||
|
||||
def executemany(self, operation, seqparams):
|
||||
pass
|
||||
|
||||
def fetchone(self):
|
||||
pass
|
||||
|
||||
def fetchmany(self, size=1):
|
||||
pass
|
||||
|
||||
def fetchall(self):
|
||||
pass
|
||||
|
||||
def nextset(self):
|
||||
pass
|
||||
|
||||
def setinputsizes(self, sizes):
|
||||
pass
|
||||
|
||||
def setoutputsize(self, size, column=None):
|
||||
pass
|
||||
|
||||
class MySQLCursor(CursorBase):
|
||||
"""
|
||||
Default cursor which fetches all rows and stores it for later
|
||||
usage. It uses the converter set for the MySQLConnection to map
|
||||
MySQL types to Python types automatically.
|
||||
|
||||
This class should be inherited whenever other functionallity is
|
||||
required. An example would to change the fetch* member functions
|
||||
to return dictionaries instead of lists of values.
|
||||
|
||||
Implements the Python Database API Specification v2.0.
|
||||
|
||||
Possible parameters are:
|
||||
|
||||
db
|
||||
A MySQLConnection instance.
|
||||
"""
|
||||
|
||||
def __init__(self, db=None):
|
||||
CursorBase.__init__(self)
|
||||
self.db = None
|
||||
self.fields = ()
|
||||
self.nrflds = 0
|
||||
self._result = []
|
||||
self._nextrow = (None, None)
|
||||
self.lastrowid = None
|
||||
self._warnings = None
|
||||
self._warning_count = 0
|
||||
self._executed = None
|
||||
self._have_result = False
|
||||
self._get_warnings = False
|
||||
|
||||
if db is not None:
|
||||
self.set_connection(db)
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Iteration over the result set which calls self.fetchone()
|
||||
and returns the next row.
|
||||
"""
|
||||
return iter(self.fetchone, None)
|
||||
|
||||
def _valid_protocol(self,db):
|
||||
if not hasattr(db,'conn'):
|
||||
raise errors.InterfaceError(
|
||||
"MySQL connection object connection not valid.")
|
||||
|
||||
try:
|
||||
if not isinstance(db.conn.protocol,protocol.MySQLProtocol):
|
||||
raise errors.InterfaceError(
|
||||
"MySQL connection has no protocol set.")
|
||||
except AttributeError:
|
||||
raise errors.InterfaceError(
|
||||
"MySQL connection object connection not valid.")
|
||||
|
||||
return True
|
||||
|
||||
def set_connection(self, db):
|
||||
if isinstance(db,mysql.MySQLBase):
|
||||
if self._valid_protocol(db):
|
||||
self.db = db
|
||||
self.protocol = db.conn.protocol
|
||||
self.db.register_cursor(self)
|
||||
self._get_warnings = self.db.get_warnings
|
||||
else:
|
||||
raise errors.InterfaceError(
|
||||
"MySQLCursor db-argument must subclass of mysql.MySQLBase")
|
||||
|
||||
def _reset_result(self):
|
||||
del self._result[:]
|
||||
self.rowcount = -1
|
||||
self._nextrow = (None, None)
|
||||
self._have_result = False
|
||||
self._warnings = None
|
||||
self._warning_count = 0
|
||||
self._fields = ()
|
||||
|
||||
def next(self):
|
||||
"""
|
||||
Used for iterating over the result set. Calles self.fetchone()
|
||||
to get the next row.
|
||||
"""
|
||||
try:
|
||||
row = self.fetchone()
|
||||
except errors.InterfaceError:
|
||||
raise StopIteration
|
||||
if not row:
|
||||
raise StopIteration
|
||||
return row
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close the cursor, disconnecting it from the MySQL object.
|
||||
|
||||
Returns True when succesful, otherwise False.
|
||||
"""
|
||||
if self.db is None:
|
||||
return False
|
||||
try:
|
||||
self.db.remove_cursor(self)
|
||||
self.db = None
|
||||
except:
|
||||
return False
|
||||
|
||||
del self._result[:]
|
||||
return True
|
||||
|
||||
def _process_params_dict(self, params):
|
||||
try:
|
||||
to_mysql = self.db.converter.to_mysql
|
||||
escape = self.db.converter.escape
|
||||
quote = self.db.converter.quote
|
||||
res = {}
|
||||
for k,v in params.items():
|
||||
c = v
|
||||
c = to_mysql(c)
|
||||
c = escape(c)
|
||||
c = quote(c)
|
||||
res[k] = c
|
||||
except StandardError, e:
|
||||
raise errors.ProgrammingError(
|
||||
"Failed processing pyformat-parameters; %s" % e)
|
||||
else:
|
||||
return res
|
||||
|
||||
return None
|
||||
|
||||
def _process_params(self, params):
|
||||
"""
|
||||
Process the parameters which were given when self.execute() was
|
||||
called. It does following using the MySQLConnection converter:
|
||||
* Convert Python types to MySQL types
|
||||
* Escapes characters required for MySQL.
|
||||
* Quote values when needed.
|
||||
|
||||
Returns a list.
|
||||
"""
|
||||
if isinstance(params,dict):
|
||||
return self._process_params_dict(params)
|
||||
|
||||
try:
|
||||
res = params
|
||||
|
||||
to_mysql = self.db.converter.to_mysql
|
||||
escape = self.db.converter.escape
|
||||
quote = self.db.converter.quote
|
||||
|
||||
res = map(to_mysql,res)
|
||||
res = map(escape,res)
|
||||
res = map(quote,res)
|
||||
except StandardError, e:
|
||||
raise errors.ProgrammingError(
|
||||
"Failed processing format-parameters; %s" % e)
|
||||
else:
|
||||
return tuple(res)
|
||||
return None
|
||||
|
||||
def _get_description(self, res=None):
|
||||
"""
|
||||
Gets the description of the fields out of a result we got from
|
||||
the MySQL Server. If res is None then self.description is
|
||||
returned (which can be None).
|
||||
|
||||
Returns a list or None when no descriptions are available.
|
||||
"""
|
||||
if not res:
|
||||
return self.description
|
||||
|
||||
desc = []
|
||||
try:
|
||||
for fld in res[1]:
|
||||
if not isinstance(fld, protocol.FieldPacket):
|
||||
raise errors.ProgrammingError(
|
||||
"Can only get description from protocol.FieldPacket")
|
||||
desc.append(fld.get_description())
|
||||
except TypeError:
|
||||
raise errors.ProgrammingError(
|
||||
"_get_description needs a list as argument."
|
||||
)
|
||||
return desc
|
||||
|
||||
def _row_to_python(self, rowdata, desc=None):
|
||||
res = ()
|
||||
try:
|
||||
to_python = self.db.converter.to_python
|
||||
if not desc:
|
||||
desc = self.description
|
||||
for idx,v in enumerate(rowdata):
|
||||
flddsc = desc[idx]
|
||||
res += (to_python(flddsc, v),)
|
||||
except StandardError, e:
|
||||
raise errors.InterfaceError(
|
||||
"Failed converting row to Python types; %s" % e)
|
||||
else:
|
||||
return res
|
||||
|
||||
return None
|
||||
|
||||
def _handle_noresultset(self, res):
|
||||
"""Handles result of execute() when there is no result set."""
|
||||
try:
|
||||
self.rowcount = res.affected_rows
|
||||
self.lastrowid = res.insert_id
|
||||
self._warning_count = res.warning_count
|
||||
if self._get_warnings is True and self._warning_count:
|
||||
self._warnings = self._fetch_warnings()
|
||||
except StandardError, e:
|
||||
raise errors.ProgrammingError(
|
||||
"Failed handling non-resultset; %s" % e)
|
||||
|
||||
def _handle_resultset(self):
|
||||
pass
|
||||
|
||||
def execute(self, operation, params=None):
|
||||
"""
|
||||
Executes the given operation. The parameters given through params
|
||||
are used to substitute %%s in the operation string.
|
||||
For example, getting all rows where id is 5:
|
||||
cursor.execute("SELECT * FROM t1 WHERE id = %s", (5,))
|
||||
|
||||
If warnings where generated, and db.get_warnings is True, then
|
||||
self._warnings will be a list containing these warnings.
|
||||
|
||||
Raises exceptions when any error happens.
|
||||
"""
|
||||
if not operation:
|
||||
return 0
|
||||
self._reset_result()
|
||||
stmt = ''
|
||||
|
||||
# Make sure we send the query in correct character set
|
||||
try:
|
||||
if isinstance(operation, unicode):
|
||||
operation.encode(self.db.charset_name)
|
||||
if params is not None:
|
||||
stmt = operation % self._process_params(params)
|
||||
else:
|
||||
stmt = operation
|
||||
res = self.protocol.cmd_query(stmt)
|
||||
if isinstance(res, protocol.OKResultPacket):
|
||||
self._have_result = False
|
||||
self._handle_noresultset(res)
|
||||
else:
|
||||
self.description = self._get_description(res)
|
||||
self._have_result = True
|
||||
self._handle_resultset()
|
||||
except errors.ProgrammingError:
|
||||
raise
|
||||
except errors.OperationalError:
|
||||
raise
|
||||
except StandardError, e:
|
||||
raise errors.InterfaceError(
|
||||
"Failed executing the operation; %s" % e)
|
||||
else:
|
||||
self._executed = stmt
|
||||
return self.rowcount
|
||||
|
||||
return 0
|
||||
|
||||
def executemany(self, operation, seq_params):
|
||||
"""Loops over seq_params and calls excute()"""
|
||||
if not operation:
|
||||
return 0
|
||||
|
||||
rowcnt = 0
|
||||
try:
|
||||
for params in seq_params:
|
||||
self.execute(operation, params)
|
||||
if self._have_result:
|
||||
self.fetchall()
|
||||
rowcnt += self.rowcount
|
||||
except (ValueError,TypeError), e:
|
||||
raise errors.InterfaceError(
|
||||
"Failed executing the operation; %s" % e)
|
||||
except:
|
||||
# Raise whatever execute() raises
|
||||
raise
|
||||
|
||||
return rowcnt
|
||||
|
||||
def callproc(self, procname, args=()):
|
||||
"""Calls a stored procedue with the given arguments
|
||||
|
||||
The arguments will be set during this session, meaning
|
||||
they will be called like _<procname>__arg<nr> where
|
||||
<nr> is an enumeration (+1) of the arguments.
|
||||
|
||||
Coding Example:
|
||||
1) Definining the Stored Routine in MySQL:
|
||||
CREATE PROCEDURE multiply(IN pFac1 INT, IN pFac2 INT, OUT pProd INT)
|
||||
BEGIN
|
||||
SET pProd := pFac1 * pFac2;
|
||||
END
|
||||
|
||||
2) Executing in Python:
|
||||
args = (5,5,0) # 0 is to hold pprod
|
||||
cursor.callproc(multiply, args)
|
||||
print cursor.fetchone()
|
||||
|
||||
The last print should output ('5', '5', 25L)
|
||||
|
||||
Does not return a value, but a result set will be
|
||||
available when the CALL-statement execute succesfully.
|
||||
Raises exceptions when something is wrong.
|
||||
"""
|
||||
argfmt = "@_%s_arg%d"
|
||||
|
||||
try:
|
||||
procargs = self._process_params(args)
|
||||
argnames = []
|
||||
|
||||
for idx,arg in enumerate(procargs):
|
||||
argname = argfmt % (procname, idx+1)
|
||||
argnames.append(argname)
|
||||
setquery = "SET %s=%%s" % argname
|
||||
self.execute(setquery, (arg,))
|
||||
|
||||
call = "CALL %s(%s)" % (procname,','.join(argnames))
|
||||
res = self.protocol.cmd_query(call)
|
||||
|
||||
select = "SELECT %s" % ','.join(argnames)
|
||||
self.execute(select)
|
||||
|
||||
except errors.ProgrammingError:
|
||||
raise
|
||||
except StandardError, e:
|
||||
raise errors.InterfaceError(
|
||||
"Failed calling stored routine; %s" % e)
|
||||
|
||||
def getlastrowid(self):
|
||||
return self.lastrowid
|
||||
|
||||
def _fetch_warnings(self):
|
||||
"""
|
||||
Fetch warnings doing a SHOW WARNINGS. Can be called after getting
|
||||
the result.
|
||||
|
||||
Returns a result set or None when there were no warnings.
|
||||
"""
|
||||
res = []
|
||||
try:
|
||||
c = self.db.cursor()
|
||||
cnt = c.execute("SHOW WARNINGS")
|
||||
res = c.fetchall()
|
||||
c.close()
|
||||
except StandardError, e:
|
||||
raise errors.ProgrammingError(
|
||||
"Failed getting warnings; %s" % e)
|
||||
else:
|
||||
if len(res):
|
||||
return res
|
||||
|
||||
return None
|
||||
|
||||
def _handle_eof(self, eof):
|
||||
self._have_result = False
|
||||
self._nextrow = (None, None)
|
||||
self._warning_count = eof.warning_count
|
||||
if self.db.get_warnings is True and eof.warning_count:
|
||||
self._warnings = self._fetch_warnings()
|
||||
|
||||
def _fetch_row(self):
|
||||
if self._have_result is False:
|
||||
return None
|
||||
row = None
|
||||
try:
|
||||
if self._nextrow == (None, None):
|
||||
(row, eof) = self.protocol.result_get_row()
|
||||
else:
|
||||
(row, eof) = self._nextrow
|
||||
if row:
|
||||
(foo, eof) = self._nextrow = self.protocol.result_get_row()
|
||||
if eof is not None:
|
||||
self._handle_eof(eof)
|
||||
if self.rowcount == -1:
|
||||
self.rowcount = 1
|
||||
else:
|
||||
self.rowcount += 1
|
||||
if eof:
|
||||
self._handle_eof(eof)
|
||||
except:
|
||||
raise
|
||||
else:
|
||||
return row
|
||||
|
||||
return None
|
||||
|
||||
def fetchwarnings(self):
|
||||
return self._warnings
|
||||
|
||||
def fetchone(self):
|
||||
row = self._fetch_row()
|
||||
if row:
|
||||
return self._row_to_python(row)
|
||||
return None
|
||||
|
||||
def fetchmany(self,size=None):
|
||||
res = []
|
||||
cnt = (size or self.arraysize)
|
||||
while cnt > 0 and self._have_result:
|
||||
cnt -= 1
|
||||
row = self.fetchone()
|
||||
if row:
|
||||
res.append(row)
|
||||
|
||||
return res
|
||||
|
||||
def fetchall(self):
|
||||
if self._have_result is False:
|
||||
raise errors.InterfaceError("No result set to fetch from.")
|
||||
res = []
|
||||
row = None
|
||||
while self._have_result:
|
||||
row = self.fetchone()
|
||||
if row:
|
||||
res.append(row)
|
||||
return res
|
||||
|
||||
def __unicode__(self):
|
||||
fmt = "MySQLCursor: %s"
|
||||
if self._executed:
|
||||
if len(self._executed) > 30:
|
||||
res = fmt % (self._executed[:30] + '..')
|
||||
else:
|
||||
res = fmt % (self._executed)
|
||||
else:
|
||||
res = fmt % '(Nothing executed yet)'
|
||||
return res
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.__unicode__())
|
||||
|
||||
class MySQLCursorBuffered(MySQLCursor):
|
||||
"""Cursor which fetches rows within execute()"""
|
||||
|
||||
def __init__(self, db=None):
|
||||
MySQLCursor.__init__(self, db)
|
||||
self._rows = []
|
||||
self._next_row = 0
|
||||
|
||||
def _handle_resultset(self):
|
||||
self._get_all_rows()
|
||||
|
||||
def _get_all_rows(self):
|
||||
(self._rows, eof) = self.protocol.result_get_rows()
|
||||
self.rowcount = len(self._rows)
|
||||
self._handle_eof(eof)
|
||||
self._next_row = 0
|
||||
self._have_result = True
|
||||
|
||||
def _fetch_row(self):
|
||||
row = None
|
||||
try:
|
||||
row = self._rows[self._next_row]
|
||||
except:
|
||||
self._have_result = False
|
||||
return None
|
||||
else:
|
||||
self._next_row += 1
|
||||
return row
|
||||
return None
|
|
@ -0,0 +1,64 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""DB API v2.0 required
|
||||
"""
|
||||
|
||||
import time
|
||||
import datetime
|
||||
|
||||
import constants
|
||||
|
||||
class _DBAPITypeObject:
|
||||
|
||||
def __init__(self,*values):
|
||||
self.values = values
|
||||
|
||||
def __cmp__(self,other):
|
||||
if other in self.values:
|
||||
return 0
|
||||
if other < self.values:
|
||||
return 1
|
||||
else:
|
||||
return -1
|
||||
|
||||
Date = datetime.date
|
||||
Time = datetime.time
|
||||
Timestamp = datetime.datetime
|
||||
|
||||
def DateFromTicks(ticks):
|
||||
return Date(*time.localtime(ticks)[:3])
|
||||
|
||||
def TimeFromTicks(ticks):
|
||||
return Time(*time.localtime(ticks)[3:6])
|
||||
|
||||
def TimestampFromTicks(ticks):
|
||||
return Timestamp(*time.localtime(ticks)[:6])
|
||||
|
||||
Binary = str
|
||||
|
||||
STRING = _DBAPITypeObject(constants.FieldType.get_string_types())
|
||||
BINARY = _DBAPITypeObject(constants.FieldType.get_binary_types())
|
||||
NUMBER = _DBAPITypeObject(constants.FieldType.get_number_types())
|
||||
DATETIME = _DBAPITypeObject(constants.FieldType.get_timestamp_types())
|
||||
ROWID = _DBAPITypeObject()
|
|
@ -0,0 +1,87 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""Python exceptions
|
||||
"""
|
||||
|
||||
import exceptions
|
||||
import protocol
|
||||
|
||||
class Error(StandardError):
|
||||
|
||||
def __init__(self, m):
|
||||
if isinstance(m,protocol.ErrorResultPacket):
|
||||
# process MySQL error packet
|
||||
self._process_packet(m)
|
||||
else:
|
||||
# else the message should be a string
|
||||
self.errno = -1
|
||||
self.errmsg = str(m)
|
||||
self.sqlstate = -1
|
||||
self.msg = str(m)
|
||||
|
||||
def _process_packet(self, packet):
|
||||
self.errno = packet.errno
|
||||
self.errmsg = packet.errmsg
|
||||
self.sqlstate = packet.sqlstate
|
||||
if self.sqlstate:
|
||||
m = '%d (%s): %s' % (self.errno, self.sqlstate, self.errmsg)
|
||||
else:
|
||||
m = '%d: %s' % (self.errno, self.errmsg)
|
||||
self.errmsglong = m
|
||||
self.msg = m
|
||||
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
|
||||
def __unicode__(self):
|
||||
return self.msg
|
||||
|
||||
class Warning(StandardError):
|
||||
pass
|
||||
|
||||
class InterfaceError(Error):
|
||||
def __init__(self, msg):
|
||||
Error.__init__(self, msg)
|
||||
|
||||
class DatabaseError(Error):
|
||||
def __init__(self, msg):
|
||||
Error.__init__(self, msg)
|
||||
|
||||
class InternalError(DatabaseError):
|
||||
pass
|
||||
|
||||
class OperationalError(DatabaseError):
|
||||
pass
|
||||
|
||||
class ProgrammingError(DatabaseError):
|
||||
pass
|
||||
|
||||
class IntegrityError(DatabaseError):
|
||||
pass
|
||||
|
||||
class DataError(DatabaseError):
|
||||
pass
|
||||
|
||||
class NotSupportedError(DatabaseError):
|
||||
pass
|
|
@ -0,0 +1,414 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""Main classes for interacting with MySQL
|
||||
"""
|
||||
|
||||
import socket, string, os
|
||||
|
||||
from connection import *
|
||||
import constants
|
||||
import conversion
|
||||
import protocol
|
||||
import errors
|
||||
import utils
|
||||
import cursor
|
||||
|
||||
|
||||
class MySQLBase(object):
|
||||
"""MySQLBase"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initializing"""
|
||||
self.conn = None # Holding the connection
|
||||
self.converter = None
|
||||
|
||||
self.client_flags = constants.ClientFlag.get_default()
|
||||
(self.charset,
|
||||
self.charset_name,
|
||||
self.collation_name) = constants.CharacterSet.get_charset_info('utf8')
|
||||
|
||||
self.username = ''
|
||||
self.password = ''
|
||||
self.database = ''
|
||||
self.client_host = ''
|
||||
self.client_port = 0
|
||||
|
||||
self.affected_rows = 0
|
||||
self.server_status = 0
|
||||
self.warning_count = 0
|
||||
self.field_count = 0
|
||||
self.insert_id = 0
|
||||
self.info_msg = ''
|
||||
self.use_unicode = True
|
||||
self.get_warnings = False
|
||||
self.autocommit = False
|
||||
self.connection_timeout = None
|
||||
self.buffered = False
|
||||
|
||||
def connect(self):
|
||||
"""To be implemented while subclassing MySQLBase."""
|
||||
pass
|
||||
|
||||
def _set_connection(self, prtcls=None):
|
||||
"""Automatically chooses based on configuration which connection type to setup."""
|
||||
if self.unix_socket and os.name != 'nt':
|
||||
self.conn = MySQLUnixConnection(prtcls=prtcls,
|
||||
unix_socket=self.unix_socket)
|
||||
else:
|
||||
self.conn = MySQLTCPConnection(prtcls=prtcls,
|
||||
host=self.server_host, port=self.server_port)
|
||||
self.conn.set_connection_timeout(self.connection_timeout)
|
||||
|
||||
def _open_connection(self):
|
||||
"""Opens the connection and sets the appropriated protocol."""
|
||||
# We don't know yet the MySQL version we connect too
|
||||
self._set_connection()
|
||||
try:
|
||||
self.conn.open_connection()
|
||||
version = self.conn.protocol.server_version
|
||||
if version < (4,1):
|
||||
raise InterfaceError("MySQL Version %s is not supported." % version)
|
||||
else:
|
||||
self.conn.set_protocol(protocol.MySQLProtocol)
|
||||
self.protocol = self.conn.protocol
|
||||
self.protocol.do_auth(username=self.username, password=self.password,
|
||||
database=self.database)
|
||||
except:
|
||||
raise
|
||||
|
||||
def _post_connection(self):
|
||||
"""Should be called after a connection was established"""
|
||||
self.get_characterset_info()
|
||||
self.set_converter_class(conversion.MySQLConverter)
|
||||
|
||||
try:
|
||||
self.set_charset(self.charset_name)
|
||||
self.set_autocommit(self.autocommit)
|
||||
except:
|
||||
raise
|
||||
|
||||
def is_connected(self):
|
||||
"""
|
||||
Check whether we are connected to the MySQL server.
|
||||
"""
|
||||
return self.protocol.cmd_ping()
|
||||
ping = is_connected
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
Disconnect from the MySQL server.
|
||||
"""
|
||||
if not self.conn:
|
||||
return
|
||||
|
||||
if self.conn.sock is not None:
|
||||
self.protocol.cmd_quit()
|
||||
try:
|
||||
self.conn.close_connection()
|
||||
except:
|
||||
pass
|
||||
self.protocol = None
|
||||
self.conn = None
|
||||
|
||||
def set_converter_class(self, convclass):
|
||||
"""
|
||||
Set the converter class to be used. This should be a class overloading
|
||||
methods and members of conversion.MySQLConverter.
|
||||
"""
|
||||
self.converter_class = convclass
|
||||
self.converter = self.converter_class(self.charset_name, self.use_unicode)
|
||||
|
||||
def get_characterset_info(self):
|
||||
try:
|
||||
(self.charset_name, self.collation_name) = constants.CharacterSet.get_info(self.charset)
|
||||
except:
|
||||
raise ProgrammingError, "Illegal character set information (id=%d)" % self.charset
|
||||
return (self.charset_name, self.collation_name)
|
||||
|
||||
def get_server_version(self):
|
||||
"""Returns the server version as a tuple"""
|
||||
try:
|
||||
return self.protocol.server_version
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def get_server_info(self):
|
||||
"""Returns the server version as a string"""
|
||||
return self.protocol.server_version_original
|
||||
|
||||
def get_server_threadid(self):
|
||||
"""Returns the MySQL threadid of the connection."""
|
||||
threadid = None
|
||||
try:
|
||||
threadid = self.protocol.server_threadid
|
||||
except:
|
||||
pass
|
||||
|
||||
return threadid
|
||||
|
||||
def set_host(self, host):
|
||||
"""
|
||||
Set the host for connection to the MySQL server.
|
||||
"""
|
||||
self.server_host = host
|
||||
|
||||
def set_port(self, port):
|
||||
"""
|
||||
Set the TCP port to be used when connecting to the server, usually 3306.
|
||||
"""
|
||||
self.server_port = port
|
||||
|
||||
def set_login(self, username=None, password=None):
|
||||
"""
|
||||
Set the username and/or password for the user connecting to the MySQL Server.
|
||||
"""
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def set_unicode(self, value=True):
|
||||
"""
|
||||
Set whether we return string fields as unicode or not.
|
||||
Default is True.
|
||||
"""
|
||||
self.use_unicode = value
|
||||
if self.converter:
|
||||
self.converter.set_unicode(value)
|
||||
|
||||
def set_database(self, database):
|
||||
"""
|
||||
Set the database to be used after connection succeeded.
|
||||
"""
|
||||
self.database = database
|
||||
|
||||
def set_charset(self, name):
|
||||
"""
|
||||
Set the character set used for the connection. This is the recommended
|
||||
way of change it per connection basis. It does execute SET NAMES
|
||||
internally, but it's good not to use this command directly, since we
|
||||
are setting some other members accordingly.
|
||||
"""
|
||||
if name not in constants.CharacterSet.get_supported():
|
||||
raise errors.ProgrammingError, "Character set '%s' not supported." % name
|
||||
return
|
||||
try:
|
||||
info = constants.CharacterSet.get_charset_info(name)
|
||||
except errors.ProgrammingError, e:
|
||||
raise
|
||||
|
||||
try:
|
||||
self.protocol.cmd_query("SET NAMES '%s'" % name)
|
||||
except:
|
||||
raise
|
||||
else:
|
||||
(self.charset, self.charset_name, self.collation_name) = info
|
||||
self.converter.set_charset(self.charset_name)
|
||||
|
||||
def set_getwarnings(self, bool):
|
||||
"""
|
||||
Set wheter we should get warnings whenever an operation produced some.
|
||||
"""
|
||||
self.get_warnings = bool
|
||||
|
||||
def set_autocommit(self, switch):
|
||||
"""
|
||||
Set auto commit on or off. The argument 'switch' must be a boolean type.
|
||||
"""
|
||||
if not isinstance(switch, bool):
|
||||
raise ValueError, "The switch argument must be boolean."
|
||||
|
||||
s = 'OFF'
|
||||
if switch:
|
||||
s = 'ON'
|
||||
|
||||
try:
|
||||
self.protocol.cmd_query("SET AUTOCOMMIT = %s" % s)
|
||||
except:
|
||||
raise
|
||||
else:
|
||||
self.autocommit = switch
|
||||
|
||||
def set_unixsocket(self, loc):
|
||||
"""Set the UNIX Socket location. Does not check if it exists."""
|
||||
self.unix_socket = loc
|
||||
|
||||
def set_connection_timeout(self, timeout):
|
||||
self.connection_timeout = timeout
|
||||
|
||||
def set_client_flags(self, flags):
|
||||
self.client_flags = flags
|
||||
|
||||
def set_buffered(self, val=False):
|
||||
"""Sets whether cursor .execute() fetches rows"""
|
||||
self.buffered = val
|
||||
|
||||
class MySQL(MySQLBase):
|
||||
"""
|
||||
Class implementing Python DB API v2.0.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Initializes the MySQL object. Calls connect() to open the connection
|
||||
when an instance is created.
|
||||
"""
|
||||
MySQLBase.__init__(self)
|
||||
self.cursors = []
|
||||
self.affected_rows = 0
|
||||
self.server_status = 0
|
||||
self.warning_count = 0
|
||||
self.field_count = 0
|
||||
self.insert_id = 0
|
||||
self.info_msg = ''
|
||||
|
||||
self.connect(*args, **kwargs)
|
||||
|
||||
def connect(self, dsn='', user='', password='', host='127.0.0.1',
|
||||
port=3306, db=None, database=None, use_unicode=True, charset='utf8', get_warnings=False,
|
||||
autocommit=False, unix_socket=None,
|
||||
connection_timeout=None, client_flags=None, buffered=False):
|
||||
"""
|
||||
Establishes a connection to the MySQL Server. Called also when instansiating
|
||||
a new MySQLConnection object through the __init__ method.
|
||||
|
||||
Possible parameters are:
|
||||
|
||||
dsn
|
||||
(not used)
|
||||
user
|
||||
The username used to authenticate with the MySQL Server.
|
||||
|
||||
password
|
||||
The password to authenticate the user with the MySQL Server.
|
||||
|
||||
host
|
||||
The hostname or the IP address of the MySQL Server we are connecting with.
|
||||
(default 127.0.0.1)
|
||||
|
||||
port
|
||||
TCP port to use for connecting to the MySQL Server.
|
||||
(default 3306)
|
||||
|
||||
database
|
||||
db
|
||||
Initial database to use once we are connected with the MySQL Server.
|
||||
The db argument is synonym, but database takes precedence.
|
||||
|
||||
use_unicode
|
||||
If set to true, string values received from MySQL will be returned
|
||||
as Unicode strings.
|
||||
Default: True
|
||||
|
||||
charset
|
||||
Which character shall we use for sending data to MySQL. One can still
|
||||
override this by using the SET NAMES command directly, but this is
|
||||
discouraged. Instead, use the set_charset() method if you
|
||||
want to change it.
|
||||
Default: Whatever the MySQL server has default.
|
||||
|
||||
get_warnings
|
||||
If set to true, whenever a query gives a warning, a SHOW WARNINGS will
|
||||
be done to fetch them. They will be available as MySQLCursor.warnings.
|
||||
The default is to ignore these warnings, for debugging it's good to
|
||||
enable it though, or use strict mode in MySQL to make most of these
|
||||
warnings errors.
|
||||
Default: False
|
||||
|
||||
autocommit
|
||||
Auto commit is OFF by default, which is required by the Python Db API
|
||||
2.0 specification.
|
||||
Default: False
|
||||
|
||||
unix_socket
|
||||
Full path to the MySQL Server UNIX socket. By default TCP connection will
|
||||
be used using the address specified by the host argument.
|
||||
|
||||
connection_timeout
|
||||
Timeout for the TCP and UNIX socket connection.
|
||||
|
||||
client_flags
|
||||
Allows to set flags for the connection. Check following for possible flags:
|
||||
>>> from mysql.connector.constants import ClientFlag
|
||||
>>> print '\n'.join(ClientFlag.get_full_info())
|
||||
|
||||
buffered
|
||||
When set to True .execute() will fetch the rows immediatly.
|
||||
|
||||
"""
|
||||
# db is not part of Db API v2.0, but MySQLdb supports it.
|
||||
if db and not database:
|
||||
database = db
|
||||
|
||||
self.set_host(host)
|
||||
self.set_port(port)
|
||||
self.set_database(database)
|
||||
self.set_getwarnings(get_warnings)
|
||||
self.set_unixsocket(unix_socket)
|
||||
self.set_connection_timeout(connection_timeout)
|
||||
self.set_client_flags(client_flags)
|
||||
self.set_buffered(buffered)
|
||||
|
||||
if user or password:
|
||||
self.set_login(user, password)
|
||||
|
||||
self.disconnect()
|
||||
self._open_connection()
|
||||
self._post_connection()
|
||||
|
||||
def close(self):
|
||||
del self.cursors[:]
|
||||
self.disconnect()
|
||||
|
||||
def remove_cursor(self, c):
|
||||
try:
|
||||
self.cursors.remove(c)
|
||||
except ValueError:
|
||||
raise errors.ProgrammingError(
|
||||
"Cursor could not be removed.")
|
||||
|
||||
def register_cursor(self, c):
|
||||
try:
|
||||
self.cursors.append(c)
|
||||
except:
|
||||
raise
|
||||
|
||||
def cursor(self):
|
||||
if self.buffered:
|
||||
c = (cursor.MySQLCursorBuffered)(self)
|
||||
else:
|
||||
c = (cursor.MySQLCursor)(self)
|
||||
|
||||
self.register_cursor(c)
|
||||
return c
|
||||
|
||||
def commit(self):
|
||||
"""Shortcut for executing COMMIT."""
|
||||
self.protocol.cmd_query("COMMIT")
|
||||
|
||||
def rollback(self):
|
||||
"""Shortcut for executing ROLLBACK"""
|
||||
self.protocol.cmd_query("ROLLBACK")
|
||||
|
||||
|
|
@ -0,0 +1,863 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USAs
|
||||
|
||||
"""Implementing the MySQL Client/Server protocol
|
||||
"""
|
||||
|
||||
import string
|
||||
import socket
|
||||
import re
|
||||
import struct
|
||||
|
||||
try:
|
||||
from hashlib import sha1
|
||||
except ImportError:
|
||||
from sha import new as sha1
|
||||
|
||||
from datetime import datetime
|
||||
from time import strptime
|
||||
from decimal import Decimal
|
||||
|
||||
from constants import *
|
||||
import errors
|
||||
import utils
|
||||
|
||||
class MySQLProtocol(object):
|
||||
"""Class handling the MySQL Protocol.
|
||||
|
||||
MySQL v4.1 Client/Server Protocol is currently supported.
|
||||
"""
|
||||
def __init__(self, conn, handshake=None):
|
||||
self.client_flags = 0
|
||||
self.conn = conn # MySQL Connection
|
||||
if handshake:
|
||||
self.set_handshake(handshake)
|
||||
|
||||
def handle_header(self, buf):
|
||||
"""Takes a buffer and readers information from header.
|
||||
|
||||
Returns a tuple (pktsize, pktnr)
|
||||
"""
|
||||
pktsize = utils.int3read(buf[0:3])
|
||||
pktnr = utils.int1read(buf[3])
|
||||
|
||||
return (pktsize, pktnr)
|
||||
|
||||
def do_auth(self, username=None, password=None, database=None,
|
||||
client_flags=None):
|
||||
"""
|
||||
Make and send the authentication using information found in the
|
||||
handshake packet.
|
||||
"""
|
||||
if not client_flags:
|
||||
client_flags = ClientFlag.get_default()
|
||||
|
||||
auth = Auth(client_flags=client_flags,
|
||||
pktnr=self.handshake.pktnr+1)
|
||||
auth.create(username=username, password=password,
|
||||
seed=self.handshake.info['seed'], database=database)
|
||||
|
||||
self.conn.send(auth.get())
|
||||
buf = self.conn.recv()[0]
|
||||
if self.is_eof(buf):
|
||||
raise errors.InterfaceError("Found EOF after Auth, expecting OK. Using old passwords?")
|
||||
|
||||
connect_with_db = client_flags & ClientFlag.CONNECT_WITH_DB
|
||||
if self.is_ok(buf) and database and not connect_with_db:
|
||||
self.cmd_init_db(database)
|
||||
|
||||
def handle_handshake(self, buf):
|
||||
"""
|
||||
Check whether the buffer is a valid handshake. If it is, we set some
|
||||
member variables for later usage. The handshake packet is returned for later
|
||||
usuage, e.g. authentication.
|
||||
"""
|
||||
|
||||
if self.is_error(buf):
|
||||
# an ErrorPacket is returned by the server
|
||||
self._handle_error(buf)
|
||||
|
||||
handshake = None
|
||||
try:
|
||||
handshake = Handshake(buf)
|
||||
except errors.InterfaceError, msg:
|
||||
raise errors.InterfaceError(msg)
|
||||
self.set_handshake(handshake)
|
||||
|
||||
def set_handshake(self, handshake):
|
||||
"""Gather data from the given handshake."""
|
||||
ver = re.compile("^(\d{1,2})\.(\d{1,2})\.(\d{1,3})(.*)")
|
||||
version = handshake.info['version']
|
||||
m = ver.match(version)
|
||||
if not m:
|
||||
raise errors.InterfaceError("Could not parse MySQL version, was '%s'" % version)
|
||||
else:
|
||||
self.server_version = tuple([ int(v) for v in m.groups()[0:3]])
|
||||
|
||||
self.server_version_original = handshake.info['version']
|
||||
self.server_threadid = handshake.info['thrdid']
|
||||
self.capabilities = handshake.info['capabilities']
|
||||
self.charset = handshake.info['charset']
|
||||
self.threadid = handshake.info['thrdid']
|
||||
self.handshake = handshake
|
||||
|
||||
def _handle_error(self, buf):
|
||||
"""Raise an OperationalError if result is an error
|
||||
"""
|
||||
try:
|
||||
err = ErrorResultPacket(buf)
|
||||
except errors.InterfaceError, e:
|
||||
raise e
|
||||
else:
|
||||
raise errors.OperationalError(err)
|
||||
|
||||
def is_error(self, buf):
|
||||
"""Check if the given buffer is a MySQL Error Packet.
|
||||
|
||||
Buffer should start with \xff.
|
||||
|
||||
Returns boolean.
|
||||
"""
|
||||
if buf and buf[4] == '\xff':
|
||||
self._handle_error(buf)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _handle_ok(self, buf):
|
||||
"""
|
||||
Handle an OK Result Packet. If we got an InterfaceError, raise that
|
||||
instead.
|
||||
"""
|
||||
try:
|
||||
ok = OKResultPacket(buf)
|
||||
except errors.InterfaceError, e:
|
||||
raise e
|
||||
else:
|
||||
self.server_status = ok.server_status
|
||||
self.warning_count = ok.warning_count
|
||||
self.field_count = ok.field_count
|
||||
self.affected_rows = ok.affected_rows
|
||||
self.info_msg = ok.info_msg
|
||||
|
||||
def is_ok(self, buf):
|
||||
"""
|
||||
Check if the given buffer is a MySQL OK Packet. It should
|
||||
start with \x00.
|
||||
|
||||
Returns boolean.
|
||||
"""
|
||||
if buf and buf[4] == '\x00':
|
||||
self._handle_ok(buf)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _handle_fields(self, nrflds):
|
||||
"""Reads a number of fields from a result set."""
|
||||
i = 0
|
||||
fields = []
|
||||
while i < nrflds:
|
||||
buf = self.conn.recv()[0]
|
||||
fld = FieldPacket(buf)
|
||||
fields.append(fld)
|
||||
i += 1
|
||||
return fields
|
||||
|
||||
def is_eof(self, buf):
|
||||
"""
|
||||
Check if the given buffer is a MySQL EOF Packet. It should
|
||||
start with \xfe and be smaller 9 bytes.
|
||||
|
||||
Returns boolean.
|
||||
"""
|
||||
l = utils.read_int(buf, 3)[1]
|
||||
if buf and buf[4] == '\xfe' and l < 9:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _handle_resultset(self, pkt):
|
||||
"""Processes a resultset getting fields information.
|
||||
|
||||
The argument pkt must be a protocol.Packet with length 1, a byte
|
||||
which contains the number of fields.
|
||||
"""
|
||||
if not isinstance(pkt, PacketIn):
|
||||
raise ValueError("%s is not a protocol.PacketIn" % pkt)
|
||||
|
||||
if len(pkt) == 1:
|
||||
(buf,nrflds) = utils.read_lc_int(pkt.data)
|
||||
|
||||
# Get the fields
|
||||
fields = self._handle_fields(nrflds)
|
||||
|
||||
buf = self.conn.recv()[0]
|
||||
eof = EOFPacket(buf)
|
||||
|
||||
return (nrflds, fields, eof)
|
||||
else:
|
||||
raise errors.InterfaceError('Something wrong reading result after query.')
|
||||
|
||||
def result_get_row(self):
|
||||
"""Get data for 1 row
|
||||
|
||||
Get one row's data. Should be called after getting the field
|
||||
descriptions.
|
||||
|
||||
Returns a tuple with 2 elements: a row's data and the
|
||||
EOF packet.
|
||||
"""
|
||||
buf = self.conn.recv()[0]
|
||||
if self.is_eof(buf):
|
||||
eof = EOFPacket(buf)
|
||||
rowdata = None
|
||||
else:
|
||||
eof = None
|
||||
rowdata = utils.read_lc_string_list(buf[4:])
|
||||
return (rowdata, eof)
|
||||
|
||||
def result_get_rows(self, cnt=None):
|
||||
"""Get all rows
|
||||
|
||||
Returns a tuple with 2 elements: a list with all rows and
|
||||
the EOF packet.
|
||||
"""
|
||||
rows = []
|
||||
eof = None
|
||||
rowdata = None
|
||||
while eof is None:
|
||||
(rowdata,eof) = self.result_get_row()
|
||||
if eof is None and rowdata is not None:
|
||||
rows.append(rowdata)
|
||||
return (rows,eof)
|
||||
|
||||
def cmd_query(self, query):
|
||||
"""
|
||||
Sends a query to the server.
|
||||
|
||||
Returns a tuple, when the query returns a result. The tuple
|
||||
consist number of fields and a list containing their descriptions.
|
||||
If the query doesn't return a result set, the an OKResultPacket
|
||||
will be returned.
|
||||
"""
|
||||
try:
|
||||
cmd = CommandPacket()
|
||||
cmd.set_command(ServerCmd.QUERY)
|
||||
cmd.set_argument(query)
|
||||
cmd.create()
|
||||
self.conn.send(cmd.get()) # Errors handled in _handle_error()
|
||||
|
||||
buf = self.conn.recv()[0]
|
||||
if self.is_ok(buf):
|
||||
# Query does not return a result (INSERT/DELETE/..)
|
||||
return OKResultPacket(buf)
|
||||
|
||||
p = PacketIn(buf)
|
||||
(nrflds, fields, eof) = self._handle_resultset(p)
|
||||
except:
|
||||
raise
|
||||
else:
|
||||
return (nrflds, fields)
|
||||
|
||||
return (0, ())
|
||||
|
||||
def _cmd_simple(self, servercmd, arg=''):
|
||||
"""Makes a simple CommandPacket with no arguments"""
|
||||
cmd = CommandPacket()
|
||||
cmd.set_command(servercmd)
|
||||
cmd.set_argument(arg)
|
||||
cmd.create()
|
||||
|
||||
return cmd
|
||||
|
||||
def cmd_refresh(self, opts):
|
||||
"""Send the Refresh command to the MySQL server.
|
||||
|
||||
The argument should be a bitwise value using the protocol.RefreshOption
|
||||
constants.
|
||||
|
||||
Usage:
|
||||
|
||||
RefreshOption = mysql.connector.RefreshOption
|
||||
refresh = RefreshOption.LOG | RefreshOption.THREADS
|
||||
db.cmd_refresh(refresh)
|
||||
|
||||
"""
|
||||
cmd = self._cmd_simple(ServerCmd.REFRESH, opts)
|
||||
try:
|
||||
self.conn.send(cmd.get())
|
||||
buf = self.conn.recv()[0]
|
||||
except:
|
||||
raise
|
||||
|
||||
if self.is_ok(buf):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def cmd_quit(self):
|
||||
"""Closes the current connection with the server."""
|
||||
cmd = self._cmd_simple(ServerCmd.QUIT)
|
||||
self.conn.send(cmd.get())
|
||||
|
||||
def cmd_init_db(self, database):
|
||||
"""
|
||||
Send command to server to change databases.
|
||||
"""
|
||||
cmd = self._cmd_simple(ServerCmd.INIT_DB, database)
|
||||
self.conn.send(cmd.get())
|
||||
self.conn.recv()[0]
|
||||
|
||||
def cmd_shutdown(self):
|
||||
"""Shuts down the MySQL Server.
|
||||
|
||||
Careful with this command if you have SUPER privileges! (Which your
|
||||
scripts probably don't need!)
|
||||
|
||||
Returns True if it succeeds.
|
||||
"""
|
||||
cmd = self._cmd_simple(ServerCmd.SHUTDOWN)
|
||||
try:
|
||||
self.conn.send(cmd.get())
|
||||
buf = self.conn.recv()[0]
|
||||
except:
|
||||
raise
|
||||
|
||||
return True
|
||||
|
||||
def cmd_statistics(self):
|
||||
"""Sends statistics command to the MySQL Server
|
||||
|
||||
Returns a dictionary with various statistical information.
|
||||
"""
|
||||
cmd = self._cmd_simple(ServerCmd.STATISTICS)
|
||||
try:
|
||||
self.conn.send(cmd.get())
|
||||
buf = self.conn.recv()[0]
|
||||
except:
|
||||
raise
|
||||
|
||||
p = Packet(buf)
|
||||
info = str(p.data)
|
||||
|
||||
res = {}
|
||||
pairs = info.split('\x20\x20') # Information is separated by 2 spaces
|
||||
for pair in pairs:
|
||||
(lbl,val) = [ v.strip() for v in pair.split(':') ]
|
||||
# It's either an integer or a decimal
|
||||
try:
|
||||
res[lbl] = long(val)
|
||||
except:
|
||||
try:
|
||||
res[lbl] = Decimal(val)
|
||||
except:
|
||||
raise ValueError(
|
||||
"Got wrong value in COM_STATISTICS information (%s : %s)." % (lbl, val))
|
||||
return res
|
||||
|
||||
def cmd_process_info(self):
|
||||
"""Gets the process list from the MySQL Server.
|
||||
|
||||
Returns a list of dictionaries which corresponds to the output of
|
||||
SHOW PROCESSLIST of MySQL. The data is converted to Python types.
|
||||
"""
|
||||
raise errors.NotSupportedError(
|
||||
"Not implemented. Use a cursor to get processlist information.")
|
||||
|
||||
def cmd_process_kill(self, mypid):
|
||||
"""Kills a MySQL process using it's ID.
|
||||
|
||||
The mypid must be an integer.
|
||||
|
||||
"""
|
||||
cmd = KillPacket(mypid)
|
||||
cmd.create()
|
||||
try:
|
||||
self.conn.send(cmd.get())
|
||||
buf = self.conn.recv()[0]
|
||||
except:
|
||||
raise
|
||||
|
||||
if self.is_eof(buf):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def cmd_debug(self):
|
||||
"""Send DEBUG command to the MySQL Server
|
||||
|
||||
Needs SUPER privileges. The output will go to the MySQL server error log.
|
||||
|
||||
Returns True when it was succesful.
|
||||
"""
|
||||
cmd = self._cmd_simple(ServerCmd.DEBUG)
|
||||
try:
|
||||
self.conn.send(cmd.get())
|
||||
buf = self.conn.recv()[0]
|
||||
except:
|
||||
raise
|
||||
|
||||
if self.is_eof(buf):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def cmd_ping(self):
|
||||
"""
|
||||
Ping the MySQL server to check if the connection is still alive.
|
||||
|
||||
Returns True when alive, False when server doesn't respond.
|
||||
"""
|
||||
cmd = self._cmd_simple(ServerCmd.PING)
|
||||
try:
|
||||
self.conn.send(cmd.get())
|
||||
buf = self.conn.recv()[0]
|
||||
except:
|
||||
return False
|
||||
else:
|
||||
if self.is_ok(buf):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def cmd_change_user(self, username, password, database=None):
|
||||
"""Change the user with given username and password to another optional database.
|
||||
"""
|
||||
if not database:
|
||||
database = self.database
|
||||
|
||||
cmd = ChangeUserPacket()
|
||||
cmd.create(username=username, password=password, database=database,
|
||||
charset=self.charset, seed=self.handshake.info['seed'])
|
||||
try:
|
||||
self.conn.send(cmd.get())
|
||||
buf = self.conn.recv()[0]
|
||||
except:
|
||||
raise
|
||||
|
||||
if not self.is_ok(buf):
|
||||
raise errors.OperationalError(
|
||||
"Failed getting OK Packet after changing user")
|
||||
|
||||
return True
|
||||
|
||||
class BasePacket(object):
|
||||
|
||||
def __len__(self):
|
||||
try:
|
||||
return len(self.data)
|
||||
except:
|
||||
return 0
|
||||
|
||||
def is_valid(self, buf=None):
|
||||
if buf is None:
|
||||
buf = self.data
|
||||
|
||||
(l, n) = (buf[0:3], buf[3])
|
||||
hlength = utils.int3read(l)
|
||||
rlength = len(buf) - 4
|
||||
|
||||
if hlength != rlength:
|
||||
return False
|
||||
|
||||
res = self._is_valid_extra(buf)
|
||||
if res != None:
|
||||
return res
|
||||
|
||||
return True
|
||||
|
||||
def _is_valid_extra(self, buf):
|
||||
return True
|
||||
|
||||
class PacketIn(BasePacket):
|
||||
def __init__(self, buf=None, pktnr=0):
|
||||
self.data = ''
|
||||
self.pktnr = pktnr
|
||||
self.protocol = 10
|
||||
|
||||
if buf:
|
||||
self.is_valid(buf)
|
||||
self.data = buf[4:]
|
||||
|
||||
if self.data:
|
||||
self._parse()
|
||||
|
||||
def _parse(self):
|
||||
pass
|
||||
|
||||
class PacketOut(BasePacket):
|
||||
"""
|
||||
Each packet type used in the MySQL Client Protocol is build on the Packet
|
||||
class. It defines lots of useful functions for parsing and sending
|
||||
data to and from the MySQL Server.
|
||||
"""
|
||||
|
||||
def __init__(self, buf=None, pktnr=0):
|
||||
self.data = ''
|
||||
self.pktnr = pktnr
|
||||
self.protocol = 10
|
||||
|
||||
if buf:
|
||||
self.set(buf)
|
||||
|
||||
if self.data:
|
||||
self._parse()
|
||||
|
||||
def _make_header(self):
|
||||
h = utils.int3store(len(self)) + utils.int1store(self.pktnr)
|
||||
return h
|
||||
|
||||
def _parse(self):
|
||||
pass
|
||||
|
||||
def add(self, s):
|
||||
if not s:
|
||||
self.add_null()
|
||||
else:
|
||||
self.data = self.data + s
|
||||
|
||||
def add_1_int(self, i):
|
||||
self.add(utils.int1store(i))
|
||||
|
||||
def add_2_int(self, i):
|
||||
self.add(utils.int2store(i))
|
||||
|
||||
def add_3_int(self, i):
|
||||
self.add(utils.int3store(i))
|
||||
|
||||
def add_4_int(self, i):
|
||||
self.add(utils.int4store(i))
|
||||
|
||||
def add_null(self, nr=1):
|
||||
self.add('\x00'*nr)
|
||||
|
||||
def get(self):
|
||||
return self._make_header() + self.data
|
||||
|
||||
def get_header(self):
|
||||
return self._make_header()
|
||||
|
||||
def set(self, buf):
|
||||
if not self.is_valid(buf):
|
||||
raise errors.InterfaceError('Packet not valid.')
|
||||
|
||||
self.data = buf[4:]
|
||||
|
||||
def _is_valid_extra(self, buf=None):
|
||||
return None
|
||||
|
||||
class Handshake(PacketIn):
|
||||
|
||||
def __init__(self, buf=None):
|
||||
PacketIn.__init__(self, buf)
|
||||
|
||||
def _parse(self):
|
||||
version = ''
|
||||
options = 0
|
||||
srvstatus = 0
|
||||
|
||||
buf = self.data
|
||||
(buf,self.protocol) = utils.read_int(buf,1)
|
||||
(buf,version) = utils.read_string(buf,end='\x00')
|
||||
(buf,thrdid) = utils.read_int(buf,4)
|
||||
(buf,scramble) = utils.read_bytes(buf, 8)
|
||||
buf = buf[1:] # Filler 1 * \x00
|
||||
(buf,srvcap) = utils.read_int(buf,2)
|
||||
(buf,charset) = utils.read_int(buf,1)
|
||||
(buf,serverstatus) = utils.read_int(buf,2)
|
||||
buf = buf[13:] # Filler 13 * \x00
|
||||
(buf,scramble_next) = utils.read_bytes(buf,12)
|
||||
scramble += scramble_next
|
||||
|
||||
self.info = {
|
||||
'version' : version,
|
||||
'thrdid' : thrdid,
|
||||
'seed' : scramble,
|
||||
'capabilities' : srvcap,
|
||||
'charset' : charset,
|
||||
'serverstatus' : serverstatus,
|
||||
}
|
||||
|
||||
def get_dict(self):
|
||||
self._parse()
|
||||
return self.info
|
||||
|
||||
def _is_valid_extra(self, buf):
|
||||
|
||||
if buf[3] != '\x00':
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
class Auth(PacketOut):
|
||||
|
||||
def __init__(self, packet=None, client_flags=0, pktnr=0):
|
||||
PacketOut.__init__(self, packet, pktnr)
|
||||
self.client_flags = 0
|
||||
self.username = None
|
||||
self.password = None
|
||||
self.database = None
|
||||
if client_flags:
|
||||
self.set_client_flags(client_flags)
|
||||
|
||||
def set_client_flags(self, flags):
|
||||
self.client_flags = flags
|
||||
|
||||
def set_login(self, username, password, database=None):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.database = database
|
||||
|
||||
def scramble(self, passwd, seed):
|
||||
|
||||
hash4 = None
|
||||
try:
|
||||
hash1 = sha1(passwd).digest()
|
||||
hash2 = sha1(hash1).digest() # Password as found in mysql.user()
|
||||
hash3 = sha1(seed + hash2).digest()
|
||||
xored = [ utils.int1read(h1) ^ utils.int1read(h3)
|
||||
for (h1,h3) in zip(hash1, hash3) ]
|
||||
hash4 = struct.pack('20B', *xored)
|
||||
except StandardError, e:
|
||||
raise errors.ProgrammingError('Failed scrambling password; %s' % e)
|
||||
else:
|
||||
return hash4
|
||||
return None
|
||||
|
||||
def create(self, username=None, password=None, database=None, seed=None):
|
||||
self.add_4_int(self.client_flags)
|
||||
self.add_4_int(10 * 1024 * 1024)
|
||||
self.add_1_int(8)
|
||||
self.add_null(23)
|
||||
self.add(username + '\x00')
|
||||
if password:
|
||||
self.add_1_int(20)
|
||||
self.add(self.scramble(password,seed))
|
||||
else:
|
||||
self.add_null(1)
|
||||
|
||||
if database:
|
||||
self.add(database + '\x00')
|
||||
else:
|
||||
self.add_null()
|
||||
|
||||
|
||||
class ChangeUserPacket(Auth):
|
||||
def __init__(self):
|
||||
self.command = ServerCmd.CHANGE_USER
|
||||
Auth.__init__(self)
|
||||
|
||||
def create(self, username=None, password=None, database=None, charset=8, seed=None):
|
||||
self.add_1_int(self.command)
|
||||
self.add(username + '\x00')
|
||||
if password:
|
||||
self.add_1_int(20)
|
||||
self.add(self.scramble(password,seed))
|
||||
else:
|
||||
self.add_null(1)
|
||||
if database:
|
||||
self.add(database + '\x00')
|
||||
else:
|
||||
self.add_null()
|
||||
|
||||
self.add_2_int(charset)
|
||||
|
||||
class ErrorResultPacket(PacketIn):
|
||||
|
||||
def __init__(self, buf=None):
|
||||
self.errno = 0
|
||||
self.errmsg = ''
|
||||
self.sqlstate = None
|
||||
PacketIn.__init__(self, buf)
|
||||
|
||||
def _parse(self):
|
||||
buf = self.data
|
||||
|
||||
if buf[0] != '\xff':
|
||||
raise errors.InterfaceError('Expected an Error Packet.')
|
||||
buf = buf[1:]
|
||||
|
||||
(buf,self.errno) = utils.read_int(buf, 2)
|
||||
|
||||
if buf[0] != '\x23':
|
||||
# Error without SQLState
|
||||
self.errmsg = buf
|
||||
else:
|
||||
(buf,self.sqlstate) = utils.read_bytes(buf[1:],5)
|
||||
self.errmsg = buf
|
||||
|
||||
class OKResultPacket(PacketIn):
|
||||
def __init__(self, buf=None):
|
||||
self.affected_rows = None
|
||||
self.insert_id = None
|
||||
self.server_status = 0
|
||||
self.warning_count = 0
|
||||
self.field_count = 0
|
||||
self.info_msg = ''
|
||||
PacketIn.__init__(self, buf)
|
||||
|
||||
def __str__(self):
|
||||
if self.affected_rows == 1:
|
||||
lbl_rows = 'row'
|
||||
else:
|
||||
lbl_rows = 'rows'
|
||||
|
||||
xtr = []
|
||||
if self.insert_id:
|
||||
xtr.append('last insert: %d ' % self.insert_id)
|
||||
if self.warning_count:
|
||||
xtr.append('warnings: %d' % self.warning_count)
|
||||
|
||||
return "Query OK, %d %s affected %s( sec)" % (self.affected_rows,
|
||||
lbl_rows, ', '.join(xtr))
|
||||
|
||||
def _parse(self):
|
||||
buf = self.data
|
||||
(buf,self.field_count) = utils.read_int(buf,1)
|
||||
(buf,self.affected_rows) = utils.read_lc_int(buf)
|
||||
(buf,self.insert_id) = utils.read_lc_int(buf)
|
||||
(buf,self.server_status) = utils.read_int(buf,2)
|
||||
(buf,self.warning_count) = utils.read_int(buf,2)
|
||||
if buf:
|
||||
(buf,self.info_msg) = utils.read_lc_string(buf)
|
||||
|
||||
class CommandPacket(PacketOut):
|
||||
def __init__(self, cmd=None, arg=None):
|
||||
self.command = cmd
|
||||
self.argument = arg
|
||||
PacketOut.__init__(self)
|
||||
|
||||
def create(self):
|
||||
self.add_1_int(self.command)
|
||||
self.add(str(self.argument))
|
||||
|
||||
def set_command(self, cmd):
|
||||
self.command = cmd
|
||||
|
||||
def set_argument(self, arg):
|
||||
self.argument = arg
|
||||
|
||||
class KillPacket(CommandPacket):
|
||||
|
||||
def __init__(self, arg):
|
||||
CommandPacket.__init__(self)
|
||||
self.set_command(ServerCmd.PROCESS_KILL)
|
||||
self.set_argument(arg)
|
||||
|
||||
def create(self):
|
||||
""""""
|
||||
self.add_1_int(self.command)
|
||||
self.add_4_int(self.argument)
|
||||
|
||||
def set_argument(self, arg):
|
||||
if arg and not isinstance(int, long) and arg > 2**32:
|
||||
raise ValueError, "KillPacket needs integer value as argument not larger than 2^32."
|
||||
self.argument = arg
|
||||
|
||||
class FieldPacket(PacketIn):
|
||||
def __init__(self, buf=None):
|
||||
self.catalog = None
|
||||
self.db = None
|
||||
self.table = None
|
||||
self.org_table = None
|
||||
self.name = None
|
||||
self.length = None
|
||||
self.org_name = None
|
||||
self.charset = None
|
||||
self.type = None
|
||||
self.flags = None
|
||||
PacketIn.__init__(self, buf)
|
||||
|
||||
def __str__(self):
|
||||
flags = []
|
||||
for k,f in FieldFlag.desc.items():
|
||||
if int(self.flags) & f[0]:
|
||||
flags.append(k)
|
||||
return """
|
||||
Field: catalog: %s ; db:%s ; table:%s ; org_table: %s ;
|
||||
name: %s ; org_name: %s ;
|
||||
charset: %s ; lenght: %s ;
|
||||
type: %02x ;
|
||||
flags(%d): %s;
|
||||
""" % (self.catalog,self.db,self.table,self.org_table,self.name,self.org_name,
|
||||
self.charset, len(self), self.type,
|
||||
self.flags, '|'.join(flags))
|
||||
|
||||
def _parse(self):
|
||||
buf = self.data
|
||||
|
||||
(buf,self.catalog) = utils.read_lc_string(buf)
|
||||
(buf,self.db) = utils.read_lc_string(buf)
|
||||
(buf,self.table) = utils.read_lc_string(buf)
|
||||
(buf,self.org_table) = utils.read_lc_string(buf)
|
||||
(buf,self.name) = utils.read_lc_string(buf)
|
||||
(buf,self.org_name) = utils.read_lc_string(buf)
|
||||
buf = buf[1:] # filler 1 * \x00
|
||||
(buf,self.charset) = utils.read_int(buf, 2)
|
||||
(buf,self.length) = utils.read_int(buf, 4)
|
||||
(buf,self.type) = utils.read_int(buf, 1)
|
||||
(buf,self.flags) = utils.read_int(buf, 2)
|
||||
(buf,self.decimal) = utils.read_int(buf, 1)
|
||||
buf = buf[2:] # filler 2 * \x00
|
||||
|
||||
def get_description(self):
|
||||
"""Returns a description as a list useful for cursors.
|
||||
|
||||
This function returns a list as defined in the Python Db API v2.0
|
||||
specification.
|
||||
|
||||
"""
|
||||
return (
|
||||
self.name,
|
||||
self.type,
|
||||
None, # display_size
|
||||
None, # internal_size
|
||||
None, # precision
|
||||
None, # scale
|
||||
~self.flags & FieldFlag.NOT_NULL, # null_ok
|
||||
self.flags, # MySQL specific
|
||||
)
|
||||
|
||||
class EOFPacket(PacketIn):
|
||||
def __init__(self, buf=None):
|
||||
self.warning_count = None
|
||||
self.status_flag = None
|
||||
PacketIn.__init__(self, buf)
|
||||
|
||||
def __str__(self):
|
||||
return "EOFPacket: warnings %d / status: %d" % (self.warning_count,self.status_flag)
|
||||
|
||||
def _is_valid_extra(self, buf=None):
|
||||
if not buf:
|
||||
buf = self.data
|
||||
else:
|
||||
buf = buf[4:]
|
||||
if buf[0] == '\xfe' and len(buf) == 5:
|
||||
# An EOF should always start with \xfe and smaller than 9 bytes
|
||||
return True
|
||||
return False
|
||||
|
||||
def _parse(self):
|
||||
buf = self.data
|
||||
|
||||
buf = buf[1:] # disregard the first checking byte
|
||||
(buf, self.warning_count) = utils.read_int(buf, 2)
|
||||
(buf, self.status_flag) = utils.read_int(buf, 2)
|
|
@ -0,0 +1,392 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""Utilities
|
||||
"""
|
||||
|
||||
__MYSQL_DEBUG__ = False
|
||||
|
||||
import struct
|
||||
|
||||
def int1read(c):
|
||||
"""
|
||||
Takes a bytes and returns it was an integer.
|
||||
|
||||
Returns integer.
|
||||
"""
|
||||
if isinstance(c,int):
|
||||
if c < 0 or c > 254:
|
||||
raise ValueError('excepts int 0 <= x <= 254')
|
||||
return c
|
||||
elif len(c) > 1:
|
||||
raise ValueError('excepts 1 byte long bytes-object or int')
|
||||
|
||||
return int('%02x' % ord(c),16)
|
||||
|
||||
def int2read(s):
|
||||
"""
|
||||
Takes a string of 2 bytes and unpacks it as unsigned integer.
|
||||
|
||||
Returns integer.
|
||||
"""
|
||||
if len(s) > 2:
|
||||
raise ValueError('int2read require s length of maximum 3 bytes')
|
||||
elif len(s) < 2:
|
||||
s = s + '\x00'
|
||||
return struct.unpack('<H', s)[0]
|
||||
|
||||
def int3read(s):
|
||||
"""
|
||||
Takes a string of 3 bytes and unpacks it as integer.
|
||||
|
||||
Returns integer.
|
||||
"""
|
||||
if len(s) > 3:
|
||||
raise ValueError('int3read require s length of maximum 3 bytes')
|
||||
elif len(s) < 4:
|
||||
s = s + '\x00'*(4-len(s))
|
||||
return struct.unpack('<I', s)[0]
|
||||
|
||||
def int4read(s):
|
||||
"""
|
||||
Takes a string of 4 bytes and unpacks it as integer.
|
||||
|
||||
Returns integer.
|
||||
"""
|
||||
if len(s) > 4:
|
||||
raise ValueError('int4read require s length of maximum 4 bytes')
|
||||
elif len(s) < 4:
|
||||
s = s + '\x00'*(4-len(s))
|
||||
return struct.unpack('<I', s)[0]
|
||||
|
||||
def int8read(s):
|
||||
"""
|
||||
Takes a string of 8 bytes and unpacks it as integer.
|
||||
|
||||
Returns integer.
|
||||
"""
|
||||
if len(s) > 8:
|
||||
raise ValueError('int4read require s length of maximum 8 bytes')
|
||||
elif len(s) < 8:
|
||||
s = s + '\x00'*(8-len(s))
|
||||
return struct.unpack('<Q', s)[0]
|
||||
|
||||
def intread(s):
|
||||
"""
|
||||
Takes a string and unpacks it as an integer.
|
||||
|
||||
This function uses int1read, int2read, int3read and int4read by
|
||||
checking the length of the given string.
|
||||
|
||||
Returns integer.
|
||||
"""
|
||||
l = len(s)
|
||||
if l < 1 or l > 4:
|
||||
raise ValueError('intread expects a string not longer than 4 bytes')
|
||||
if not isinstance(s, str):
|
||||
raise ValueError('intread expects a string')
|
||||
fs = {
|
||||
1 : int1read,
|
||||
2 : int2read,
|
||||
3 : int3read,
|
||||
4 : int4read,
|
||||
8 : int8read,
|
||||
}
|
||||
return fs[l](s)
|
||||
|
||||
def int1store(i):
|
||||
"""
|
||||
Takes an unsigned byte (1 byte) and packs it as string.
|
||||
|
||||
Returns string.
|
||||
"""
|
||||
if i < 0 or i > 255:
|
||||
raise ValueError('int1store requires 0 <= i <= 255')
|
||||
else:
|
||||
return struct.pack('<B',i)
|
||||
|
||||
def int2store(i):
|
||||
"""
|
||||
Takes an unsigned short (2 bytes) and packs it as string.
|
||||
|
||||
Returns string.
|
||||
"""
|
||||
if i < 0 or i > 65535:
|
||||
raise ValueError('int2store requires 0 <= i <= 65535')
|
||||
else:
|
||||
return struct.pack('<H',i)
|
||||
|
||||
def int3store(i):
|
||||
"""
|
||||
Takes an unsigned integer (3 bytes) and packs it as string.
|
||||
|
||||
Returns string.
|
||||
"""
|
||||
if i < 0 or i > 16777215:
|
||||
raise ValueError('int3store requires 0 <= i <= 16777215')
|
||||
else:
|
||||
return struct.pack('<I',i)[0:3]
|
||||
|
||||
def int4store(i):
|
||||
"""
|
||||
Takes an unsigned integer (4 bytes) and packs it as string.
|
||||
|
||||
Returns string.
|
||||
"""
|
||||
if i < 0 or i > 4294967295L:
|
||||
raise ValueError('int4store requires 0 <= i <= 4294967295')
|
||||
else:
|
||||
return struct.pack('<I',i)
|
||||
|
||||
def intstore(i):
|
||||
"""
|
||||
Takes an unsigned integers and packs it as a string.
|
||||
|
||||
This function uses int1store, int2store, int3store and
|
||||
int4store depending on the integer value.
|
||||
|
||||
returns string.
|
||||
"""
|
||||
if i < 0 or i > 4294967295L:
|
||||
raise ValueError('intstore requires 0 <= i <= 4294967295')
|
||||
|
||||
if i <= 255:
|
||||
fs = int1store
|
||||
elif i <= 65535:
|
||||
fs = int2store
|
||||
elif i <= 16777215:
|
||||
fs = int3store
|
||||
else:
|
||||
fs = int4store
|
||||
|
||||
return fs(i)
|
||||
|
||||
def read_bytes(buf, size):
|
||||
"""
|
||||
Reads bytes from a buffer.
|
||||
|
||||
Returns a tuple with buffer less the read bytes, and the bytes.
|
||||
"""
|
||||
s = buf[0:size]
|
||||
return (buf[size:], s)
|
||||
|
||||
def read_lc_string(buf):
|
||||
"""
|
||||
Takes a buffer and reads a length coded string from the start.
|
||||
|
||||
This is how Length coded strings work
|
||||
|
||||
If the string is 250 bytes long or smaller, then it looks like this:
|
||||
|
||||
<-- 1b -->
|
||||
+----------+-------------------------
|
||||
| length | a string goes here
|
||||
+----------+-------------------------
|
||||
|
||||
If the string is bigger than 250, then it looks like this:
|
||||
|
||||
<- 1b -><- 2/3/4 ->
|
||||
+------+-----------+-------------------------
|
||||
| type | length | a string goes here
|
||||
+------+-----------+-------------------------
|
||||
|
||||
if type == \xfc:
|
||||
length is code in next 2 bytes
|
||||
elif type == \xfd:
|
||||
length is code in next 3 bytes
|
||||
elif type == \xfe:
|
||||
length is code in next 4 bytes
|
||||
|
||||
NULL has a special value. If the buffer starts with \xfb then
|
||||
it's a NULL and we return None as value.
|
||||
|
||||
Returns a tuple (trucated buffer, string).
|
||||
"""
|
||||
if buf[0] == '\xfb':
|
||||
# NULL value
|
||||
return (buf[1:], None)
|
||||
|
||||
l = lsize = start = 0
|
||||
fst = buf[0]
|
||||
# Remove the type byte, we got the length information.
|
||||
buf = buf[1:]
|
||||
|
||||
if fst <= '\xFA':
|
||||
# Returns result right away.
|
||||
l = ord(fst)
|
||||
s = buf[:l]
|
||||
return (buf[l:], s)
|
||||
elif fst == '\xFC':
|
||||
lsize = 2
|
||||
elif fst == '\xFD':
|
||||
lsize = 3
|
||||
elif fst == '\xFE':
|
||||
lsize = 4
|
||||
|
||||
l = intread(buf[0:lsize])
|
||||
# Chop of the bytes which hold the length
|
||||
buf = buf[lsize:]
|
||||
# Get the actual string
|
||||
s = buf[0:l]
|
||||
# Set the buffer so we can return it
|
||||
buf = buf[l:]
|
||||
|
||||
return (buf, s)
|
||||
|
||||
def read_lc_string_list(buf):
|
||||
"""
|
||||
Reads all length encoded strings from the given buffer.
|
||||
|
||||
This is exact same function as read_lc_string() but duplicated
|
||||
in hopes for performance gain when reading results.
|
||||
"""
|
||||
strlst = []
|
||||
|
||||
while buf:
|
||||
if buf[0] == '\xfb':
|
||||
# NULL value
|
||||
buf = buf[1:]
|
||||
strlst.append(None)
|
||||
continue
|
||||
|
||||
l = lsize = start = 0
|
||||
fst = buf[0]
|
||||
# Remove the type byte, we got the length information.
|
||||
buf = buf[1:]
|
||||
|
||||
if fst <= '\xFA':
|
||||
# Returns result right away.
|
||||
l = ord(fst)
|
||||
strlst.append(buf[:l])
|
||||
buf = buf[l:]
|
||||
continue
|
||||
elif fst == '\xFC':
|
||||
lsize = 2
|
||||
elif fst == '\xFD':
|
||||
lsize = 3
|
||||
elif fst == '\xFE':
|
||||
lsize = 4
|
||||
|
||||
l = intread(buf[0:lsize])
|
||||
# Chop of the bytes which hold the length
|
||||
buf = buf[lsize:]
|
||||
# Get the actual string
|
||||
s = buf[0:l]
|
||||
# Set the buffer so we can return it
|
||||
buf = buf[l:]
|
||||
|
||||
strlst.append(s)
|
||||
|
||||
return strlst
|
||||
|
||||
def read_string(buf, end=None, size=None):
|
||||
"""
|
||||
Reads a string up until a character or for a given size.
|
||||
|
||||
Returns a tuple (trucated buffer, string).
|
||||
"""
|
||||
if end is None and size is None:
|
||||
raise ValueError('read_string() needs either end or size')
|
||||
|
||||
if end is not None:
|
||||
try:
|
||||
idx = buf.index(end)
|
||||
except (ValueError), e:
|
||||
raise ValueError("end byte not precent in buffer")
|
||||
return (buf[idx+1:], buf[0:idx])
|
||||
elif size is not None:
|
||||
return read_bytes(buf,size)
|
||||
|
||||
raise ValueError('read_string() needs either end or size (weird)')
|
||||
|
||||
def read_int(buf, size):
|
||||
"""
|
||||
Take a buffer and reads an integer of a certain size (1 <= size <= 4).
|
||||
|
||||
Returns a tuple (truncated buffer, int)
|
||||
"""
|
||||
if len(buf) == 0:
|
||||
raise ValueError("Empty buffer.")
|
||||
if not isinstance(size,int) or (size not in [1,2,3,4,8]):
|
||||
raise ValueError('size should be int in range of 1..4 or 8')
|
||||
|
||||
i = None
|
||||
if size == 1:
|
||||
i = int1read(buf[0])
|
||||
elif size == 2:
|
||||
i = int2read(buf[0:2])
|
||||
elif size == 3:
|
||||
i = int3read(buf[0:3])
|
||||
elif size == 4:
|
||||
i = int4read(buf[0:4])
|
||||
elif size == 8:
|
||||
i = int8read(buf[0:8])
|
||||
else:
|
||||
raise ValueError('size should be int in range of 1..4 or 8 (weird)')
|
||||
|
||||
return (buf[size:], int(i))
|
||||
|
||||
def read_lc_int(buf):
|
||||
"""
|
||||
Takes a buffer and reads an length code string from the start.
|
||||
|
||||
Returns a tuple with buffer less the integer and the integer read.
|
||||
"""
|
||||
if len(buf) == 0:
|
||||
raise ValueError("Empty buffer.")
|
||||
|
||||
(buf,s) = read_int(buf,1)
|
||||
if s == 251:
|
||||
l = 0
|
||||
return (buf,None)
|
||||
elif s == 252:
|
||||
(buf,i) = read_int(buf,2)
|
||||
elif s == 253:
|
||||
(buf,i) = read_int(buf,3)
|
||||
elif s == 254:
|
||||
(buf,i) = read_int(buf,8)
|
||||
else:
|
||||
i = s
|
||||
|
||||
return (buf, int(i))
|
||||
|
||||
#
|
||||
# For debugging
|
||||
#
|
||||
def _dump_buffer(buf, label=None):
|
||||
import __main__
|
||||
if not __main__.__dict__.has_key('__MYSQL_DEBUG__'):
|
||||
return
|
||||
else:
|
||||
debug = __main__.__dict__['__MYSQL_DEBUG__']
|
||||
|
||||
try:
|
||||
if debug:
|
||||
if len(buf) == 0:
|
||||
print "%s : EMPTY BUFFER" % label
|
||||
import string
|
||||
print "%s: %s" % (label,string.join( [ "%02x" % ord(c) for c in buf ], ' '))
|
||||
if debug > 1:
|
||||
print "%s: %s" % (label,string.join( [ "%s" % chr(ord(c)) for c in buf ], ''))
|
||||
except:
|
||||
raise
|
|
@ -0,0 +1,212 @@
|
|||
import mc
|
||||
import re
|
||||
from operator import itemgetter, attrgetter
|
||||
|
||||
config = mc.GetApp().GetLocalConfig()
|
||||
|
||||
titles = []
|
||||
recordings = []
|
||||
idbanners = {}
|
||||
shows = {}
|
||||
|
||||
def LoadShows():
|
||||
del titles[:]
|
||||
del recordings[:]
|
||||
idbanners.clear()
|
||||
shows.clear()
|
||||
|
||||
config = mc.GetApp().GetLocalConfig()
|
||||
sg = mc.Http()
|
||||
html = sg.Get("http://" + config.GetValue("server") + ":6544/Myth/GetRecorded")
|
||||
results = re.compile("<Program title=\"(.*?)\" subTitle=\"(.*?)\".*?endTime=\"(.*?)\" airdate=\"(.*?)\" startTime=\"(.*?)\".*?>(.*?)<Channel.*?chanId=\"(.*?)\".*?>").findall(html)
|
||||
for title,subtitle,endtime,airdate,starttime,desc,chanid in results:
|
||||
if title not in titles:
|
||||
titles.append(title)
|
||||
idbanners[title] = GetSeriesIDBanner(title)
|
||||
shows[title] = []
|
||||
|
||||
single = [title,subtitle,desc,chanid,airdate,starttime,endtime]
|
||||
recordings.append(single)
|
||||
|
||||
shows[title].append(single)
|
||||
|
||||
titles.sort()
|
||||
|
||||
items = mc.ListItems()
|
||||
for title in titles:
|
||||
item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN )
|
||||
item.SetLabel(title)
|
||||
item.SetThumbnail(idbanners[title][1])
|
||||
item.SetProperty("seriesid", idbanners[title][0])
|
||||
items.append(item)
|
||||
|
||||
mc.GetWindow(14000).GetList(13).SetItems(items)
|
||||
|
||||
|
||||
def LoadSingleShow():
|
||||
config = mc.GetApp().GetLocalConfig()
|
||||
ilist = mc.GetActiveWindow().GetList(13)
|
||||
item = ilist.GetItem(ilist.GetFocusedItem())
|
||||
name = item.GetLabel()
|
||||
config.SetValue("seriesid", item.GetProperty("seriesid"))
|
||||
config.SetValue("show", name)
|
||||
mc.ActivateWindow(14001)
|
||||
|
||||
SetSortables()
|
||||
GetSetSeriesDetails(name, item.GetProperty("seriesid"))
|
||||
LoadSeriesEpisodes(name)
|
||||
|
||||
|
||||
def SetSortables():
|
||||
config.SetValue("SortBy", "Recorded Date")
|
||||
config.SetValue("SortDir", "Descending")
|
||||
sortable = ['Original Air Date', 'Recorded Date', 'Title']
|
||||
items = mc.ListItems()
|
||||
for sorttype in sortable:
|
||||
item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN )
|
||||
item.SetLabel(sorttype)
|
||||
items.append(item)
|
||||
|
||||
mc.GetActiveWindow().GetList(2014).SetItems(items)
|
||||
mc.GetActiveWindow().GetList(2014).SetSelected(1, True)
|
||||
|
||||
sortableby = ['Ascending', 'Descending']
|
||||
items = mc.ListItems()
|
||||
for sorttype in sortableby:
|
||||
item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN )
|
||||
item.SetLabel(sorttype)
|
||||
items.append(item)
|
||||
|
||||
mc.GetActiveWindow().GetList(2015).SetItems(items)
|
||||
mc.GetActiveWindow().GetList(2015).SetSelected(1, True)
|
||||
|
||||
|
||||
def ShowEpisodeDetails():
|
||||
print "ShowEpisodeDetails"
|
||||
|
||||
|
||||
def SortBySeriesEpisodes():
|
||||
sortByItems = sortByItemNumber = mc.GetWindow(14001).GetList(2014).GetSelected()
|
||||
sortDirectionItems = sortDirectionItemNumber = mc.GetWindow(14001).GetList(2015).GetSelected()
|
||||
|
||||
mc.GetActiveWindow().GetList(2014).UnselectAll()
|
||||
mc.GetActiveWindow().GetList(2014).SetSelected(mc.GetActiveWindow().GetList(2014).GetFocusedItem(), True)
|
||||
|
||||
config.SetValue("SortBy", mc.GetActiveWindow().GetList(2014).GetItem(mc.GetActiveWindow().GetList(2014).GetFocusedItem()).GetLabel())
|
||||
|
||||
LoadSeriesEpisodes(config.GetValue("name"))
|
||||
|
||||
|
||||
def SortDirSeriesEpisodes():
|
||||
sortByItems = sortByItemNumber = mc.GetWindow(14001).GetList(2014).GetSelected()
|
||||
|
||||
mc.GetActiveWindow().GetList(2015).UnselectAll()
|
||||
mc.GetActiveWindow().GetList(2015).SetSelected(mc.GetActiveWindow().GetList(2015).GetFocusedItem(), True)
|
||||
|
||||
config.SetValue("SortDir", mc.GetActiveWindow().GetList(2015).GetItem(mc.GetActiveWindow().GetList(2015).GetFocusedItem()).GetLabel())
|
||||
|
||||
LoadSeriesEpisodes(config.GetValue("name"))
|
||||
|
||||
def GetSeriesIDBanner(name):
|
||||
sg = mc.Http()
|
||||
sg.SetUserAgent('MythBoxee v3.0.beta')
|
||||
html = sg.Get("http://www.thetvdb.com/api/GetSeries.php?seriesname=" + name.replace(" ", "%20"))
|
||||
series = re.compile("<seriesid>(.*?)</seriesid>").findall(html)
|
||||
banners = re.compile("<banner>(.*?)</banner>").findall(html)
|
||||
show = []
|
||||
if series:
|
||||
show.append(series[0])
|
||||
show.append("http://www.thetvdb.com/banners/" + banners[0])
|
||||
else:
|
||||
show.append("00000")
|
||||
show.append("http://192.168.1.210/")
|
||||
return show
|
||||
|
||||
|
||||
def GetSetSeriesDetails(name, seriesid):
|
||||
sg = mc.Http()
|
||||
sg.SetUserAgent('MythBoxee v3.0.beta')
|
||||
html = sg.Get("http://thetvdb.com/api/6BEAB4CB5157AAE0/series/" + seriesid + "/")
|
||||
overview = re.compile("<Overview>(.*?)</Overview>").findall(html)
|
||||
poster = re.compile("<poster>(.*?)</poster>").findall(html)
|
||||
items = mc.ListItems()
|
||||
item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN )
|
||||
item.SetLabel(name)
|
||||
item.SetTitle(name)
|
||||
if overview:
|
||||
item.SetDescription(overview[0])
|
||||
item.SetProperty("description", overview[0])
|
||||
item.SetThumbnail("http://www.thetvdb.com/banners/" + poster[0])
|
||||
items.append(item)
|
||||
|
||||
mc.GetWindow(14001).GetList(21).SetItems(items)
|
||||
|
||||
|
||||
def LoadSeriesEpisodes(name):
|
||||
config = mc.GetApp().GetLocalConfig()
|
||||
config.SetValue("name", name)
|
||||
showitems = mc.ListItems()
|
||||
|
||||
sortBy = config.GetValue("SortBy")
|
||||
sortDir = config.GetValue("SortDir")
|
||||
|
||||
print shows[name]
|
||||
|
||||
if sortBy == "Original Air Date" and sortDir == "Ascending":
|
||||
episodes = sorted(shows[name], key=itemgetter(4))
|
||||
elif sortBy == "Original Air Date" and sortDir == "Descending":
|
||||
episodes = sorted(shows[name], key=itemgetter(4), reverse=True)
|
||||
elif sortBy == "Recorded Date" and sortDir == "Ascending":
|
||||
episodes = sorted(shows[name], key=itemgetter(5))
|
||||
elif sortBy == "Recorded Date" and sortDir == "Descending":
|
||||
episodes = sorted(shows[name], key=itemgetter(5), reverse=True)
|
||||
elif sortBy == "Title" and sortDir == "Ascending":
|
||||
episodes = sorted(shows[name], key=itemgetter(1))
|
||||
elif sortBy == "Title" and sortDir == "Descending":
|
||||
episodes = sorted(shows[name], key=itemgetter(1), reverse=True)
|
||||
else:
|
||||
episodes = shows[name]
|
||||
|
||||
for title,subtitle,desc,chanid,airdate,starttime,endtime in episodes:
|
||||
showitem = mc.ListItem( mc.ListItem.MEDIA_VIDEO_EPISODE )
|
||||
showitem.SetLabel(subtitle)
|
||||
showitem.SetTitle(subtitle)
|
||||
showitem.SetTVShowTitle(name)
|
||||
showitem.SetDescription(desc)
|
||||
date = airdate.split("-")
|
||||
showitem.SetProperty("starttime", starttime)
|
||||
showitem.SetDate(int(date[0]), int(date[1]), int(date[2]))
|
||||
showitem.SetThumbnail("http://" + config.GetValue("server") + ":6544/Myth/GetPreviewImage?ChanId=" + chanid + "&StartTime=" + starttime.replace("T", "%20"))
|
||||
showitem.SetPath("http://" + config.GetValue("server") + ":6544/Myth/GetRecording?ChanId=" + chanid + "&StartTime=" + starttime.replace("T", "%20"))
|
||||
showitems.append(showitem)
|
||||
|
||||
mc.GetActiveWindow().GetList(2013).SetItems(showitems)
|
||||
|
||||
|
||||
|
||||
def GetServer():
|
||||
config = mc.GetApp().GetLocalConfig()
|
||||
server = config.GetValue("server")
|
||||
response = mc.ShowDialogKeyboard("Enter IP Address of MythTV Backend Server", server, False)
|
||||
url = "http://" + response + ":6544/Myth/GetServDesc"
|
||||
if VerifyServer(url) == True:
|
||||
config.SetValue("server", response)
|
||||
|
||||
def VerifyServer(url):
|
||||
config = mc.GetApp().GetLocalConfig()
|
||||
http = mc.Http()
|
||||
data = http.Get(url)
|
||||
if http.GetHttpResponseCode() == 200:
|
||||
config.SetValue("verified", "1")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 67
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV
|
||||
END
|
||||
MythStatic.py
|
||||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 81
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/MythStatic.py
|
||||
END
|
||||
MythData.py
|
||||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 79
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/MythData.py
|
||||
END
|
||||
MythBase.py
|
||||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 79
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/MythBase.py
|
||||
END
|
||||
MythFunc.py
|
||||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 79
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/MythFunc.py
|
||||
END
|
||||
__init__.py
|
||||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 79
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/__init__.py
|
||||
END
|
|
@ -0,0 +1,204 @@
|
|||
10
|
||||
|
||||
dir
|
||||
25361
|
||||
http://svn.mythtv.org/svn/tags/release-0-23/mythtv/bindings/python/MythTV
|
||||
http://svn.mythtv.org/svn
|
||||
|
||||
|
||||
|
||||
2010-05-05T00:45:58.150174Z
|
||||
24420
|
||||
wagnerrp
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
7dbf422c-18fa-0310-86e9-fd20926502f2
|
||||
|
||||
MythStatic.py
|
||||
file
|
||||
|
||||
|
||||
|
||||
|
||||
2010-07-16T22:31:04.000000Z
|
||||
c362740b39721bf47554a27b8dcb7255
|
||||
2010-01-30T01:25:12.140142Z
|
||||
23365
|
||||
wagnerrp
|
||||
has-props
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
233
|
||||
|
||||
MythData.py
|
||||
file
|
||||
|
||||
|
||||
|
||||
|
||||
2010-07-16T22:31:04.000000Z
|
||||
ea13afe13bb398e11e7895be6654e06e
|
||||
2010-04-21T04:46:55.439637Z
|
||||
24220
|
||||
wagnerrp
|
||||
has-props
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
57313
|
||||
|
||||
ttvdb
|
||||
dir
|
||||
|
||||
MythBase.py
|
||||
file
|
||||
|
||||
|
||||
|
||||
|
||||
2010-07-16T22:31:04.000000Z
|
||||
4e2f2010af14b04d6ebb3ead0606faa1
|
||||
2010-05-05T00:45:58.150174Z
|
||||
24420
|
||||
wagnerrp
|
||||
has-props
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
69983
|
||||
|
||||
MythFunc.py
|
||||
file
|
||||
|
||||
|
||||
|
||||
|
||||
2010-07-16T22:31:04.000000Z
|
||||
80c438b974b0dadaed423f9308c04912
|
||||
2010-03-31T19:07:44.868318Z
|
||||
23878
|
||||
wagnerrp
|
||||
has-props
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
41463
|
||||
|
||||
tmdb
|
||||
dir
|
||||
|
||||
__init__.py
|
||||
file
|
||||
|
||||
|
||||
|
||||
|
||||
2010-07-16T22:31:04.000000Z
|
||||
5c15a4df6a8f262fb7bdd3f91b32119a
|
||||
2010-05-03T05:03:57.011023Z
|
||||
24346
|
||||
wagnerrp
|
||||
has-props
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
1464
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
K 14
|
||||
svn:executable
|
||||
V 1
|
||||
*
|
||||
END
|
|
@ -0,0 +1,5 @@
|
|||
K 14
|
||||
svn:executable
|
||||
V 1
|
||||
*
|
||||
END
|
|
@ -0,0 +1,5 @@
|
|||
K 14
|
||||
svn:executable
|
||||
V 1
|
||||
*
|
||||
END
|
|
@ -0,0 +1,5 @@
|
|||
K 14
|
||||
svn:executable
|
||||
V 1
|
||||
*
|
||||
END
|
|
@ -0,0 +1,17 @@
|
|||
K 13
|
||||
svn:eol-style
|
||||
V 6
|
||||
native
|
||||
K 14
|
||||
svn:executable
|
||||
V 1
|
||||
*
|
||||
K 12
|
||||
svn:keywords
|
||||
V 31
|
||||
Id Date Revision Author HeadURL
|
||||
K 13
|
||||
svn:mime-type
|
||||
V 13
|
||||
text/x-python
|
||||
END
|
|
@ -0,0 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Contains any static and global variables for MythTV Python Bindings
|
||||
"""
|
||||
|
||||
SCHEMA_VERSION = 1254
|
||||
MVSCHEMA_VERSION = 1032
|
||||
NVSCHEMA_VERSION = 1004
|
||||
PROTO_VERSION = 56
|
||||
PROGRAM_FIELDS = 47
|
||||
BACKEND_SEP = '[]:[]'
|
|
@ -0,0 +1,51 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
__all__ = ['MythStatic', \
|
||||
\
|
||||
'DictData', 'DBData', 'DBDataWrite', 'DBDataCRef', 'MythDBConn', \
|
||||
'MythBEConn', 'MythXMLConn', 'MythLog', 'MythError', \
|
||||
'StorageGroup', 'Grabber', \
|
||||
\
|
||||
'ftopen', 'FileTransfer', 'FreeSpace', 'Program', 'Record', \
|
||||
'Recorded', 'RecordedProgram', 'OldRecorded', 'Job', 'Channel', \
|
||||
'Guide', 'Video', 'VideoGrabber', 'NetVisionRSSItem', \
|
||||
'NetVisionTreeItem', 'NetVisionSite', 'NetVisionGrabber', \
|
||||
\
|
||||
'MythBE', 'Frontend', 'MythDB', 'MythVideo', 'MythXML']
|
||||
|
||||
import26 = """
|
||||
import warnings
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore')
|
||||
from MythStatic import *
|
||||
from MythBase import *
|
||||
from MythData import *
|
||||
from MythFunc import *
|
||||
"""
|
||||
|
||||
import25 = """
|
||||
from MythStatic import *
|
||||
from MythBase import *
|
||||
from MythData import *
|
||||
from MythFunc import *
|
||||
"""
|
||||
|
||||
from sys import version_info
|
||||
if version_info >= (2, 6): # 2.6 or newer
|
||||
exec(import26)
|
||||
else:
|
||||
exec(import25)
|
||||
|
||||
if __name__ == '__main__':
|
||||
banner = 'MythTV Python interactive shell.'
|
||||
import code
|
||||
try:
|
||||
import readline, rlcompleter
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
readline.parse_and_bind("tab: complete")
|
||||
banner += ' TAB completion available.'
|
||||
namespace = globals().copy()
|
||||
namespace.update(locals())
|
||||
code.InteractiveConsole(namespace).interact(banner)
|
|
@ -0,0 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Contains any static and global variables for MythTV Python Bindings
|
||||
"""
|
||||
|
||||
SCHEMA_VERSION = 1254
|
||||
MVSCHEMA_VERSION = 1032
|
||||
NVSCHEMA_VERSION = 1004
|
||||
PROTO_VERSION = 56
|
||||
PROGRAM_FIELDS = 47
|
||||
BACKEND_SEP = '[]:[]'
|
|
@ -0,0 +1,51 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
__all__ = ['MythStatic', \
|
||||
\
|
||||
'DictData', 'DBData', 'DBDataWrite', 'DBDataCRef', 'MythDBConn', \
|
||||
'MythBEConn', 'MythXMLConn', 'MythLog', 'MythError', \
|
||||
'StorageGroup', 'Grabber', \
|
||||
\
|
||||
'ftopen', 'FileTransfer', 'FreeSpace', 'Program', 'Record', \
|
||||
'Recorded', 'RecordedProgram', 'OldRecorded', 'Job', 'Channel', \
|
||||
'Guide', 'Video', 'VideoGrabber', 'NetVisionRSSItem', \
|
||||
'NetVisionTreeItem', 'NetVisionSite', 'NetVisionGrabber', \
|
||||
\
|
||||
'MythBE', 'Frontend', 'MythDB', 'MythVideo', 'MythXML']
|
||||
|
||||
import26 = """
|
||||
import warnings
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore')
|
||||
from MythStatic import *
|
||||
from MythBase import *
|
||||
from MythData import *
|
||||
from MythFunc import *
|
||||
"""
|
||||
|
||||
import25 = """
|
||||
from MythStatic import *
|
||||
from MythBase import *
|
||||
from MythData import *
|
||||
from MythFunc import *
|
||||
"""
|
||||
|
||||
from sys import version_info
|
||||
if version_info >= (2, 6): # 2.6 or newer
|
||||
exec(import26)
|
||||
else:
|
||||
exec(import25)
|
||||
|
||||
if __name__ == '__main__':
|
||||
banner = 'MythTV Python interactive shell.'
|
||||
import code
|
||||
try:
|
||||
import readline, rlcompleter
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
readline.parse_and_bind("tab: complete")
|
||||
banner += ' TAB completion available.'
|
||||
namespace = globals().copy()
|
||||
namespace.update(locals())
|
||||
code.InteractiveConsole(namespace).interact(banner)
|
|
@ -0,0 +1,29 @@
|
|||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 72
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/tmdb
|
||||
END
|
||||
__init__.py
|
||||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 84
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/tmdb/__init__.py
|
||||
END
|
||||
tmdb_api.py
|
||||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 84
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/tmdb/tmdb_api.py
|
||||
END
|
||||
tmdb_ui.py
|
||||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 83
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/tmdb/tmdb_ui.py
|
||||
END
|
||||
tmdb_exceptions.py
|
||||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 91
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/tmdb/tmdb_exceptions.py
|
||||
END
|
|
@ -0,0 +1,164 @@
|
|||
10
|
||||
|
||||
dir
|
||||
25361
|
||||
http://svn.mythtv.org/svn/tags/release-0-23/mythtv/bindings/python/MythTV/tmdb
|
||||
http://svn.mythtv.org/svn
|
||||
|
||||
|
||||
|
||||
2010-04-29T22:38:50.878564Z
|
||||
24305
|
||||
robertm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
7dbf422c-18fa-0310-86e9-fd20926502f2
|
||||
|
||||
__init__.py
|
||||
file
|
||||
|
||||
|
||||
|
||||
|
||||
2010-07-16T22:31:04.000000Z
|
||||
d41d8cd98f00b204e9800998ecf8427e
|
||||
2010-01-29T01:39:37.380922Z
|
||||
23354
|
||||
wagnerrp
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
0
|
||||
|
||||
tmdb_api.py
|
||||
file
|
||||
|
||||
|
||||
|
||||
|
||||
2010-07-16T22:31:04.000000Z
|
||||
80afb5dbadea4de7d5ed5d0e1fa956bf
|
||||
2010-04-29T22:38:50.878564Z
|
||||
24305
|
||||
robertm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
43403
|
||||
|
||||
tmdb_ui.py
|
||||
file
|
||||
|
||||
|
||||
|
||||
|
||||
2010-07-16T22:31:04.000000Z
|
||||
89285d26d830a7a0fc71f92e4f60f815
|
||||
2010-04-12T22:36:01.726283Z
|
||||
24097
|
||||
robertm
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
10747
|
||||
|
||||
tmdb_exceptions.py
|
||||
file
|
||||
|
||||
|
||||
|
||||
|
||||
2010-07-16T22:31:04.000000Z
|
||||
d6d57ccfa5baa9fd79eb04eb3b6e7aff
|
||||
2010-01-29T01:39:37.380922Z
|
||||
23354
|
||||
wagnerrp
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
1384
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
# ----------------------
|
||||
# Name: tmdb_exceptions - Custom exceptions used or raised by tmdb_api
|
||||
# Python Script
|
||||
# Author: dbr/Ben modified by R.D. Vaughan
|
||||
# Purpose: Custom exceptions used or raised by tmdb_api
|
||||
#
|
||||
# License:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
#-------------------------------------
|
||||
__title__ ="tmdb_exceptions - Custom exceptions used or raised by tmdb_api";
|
||||
__author__="dbr/Ben modified by R.D. Vaughan"
|
||||
__version__="v0.1.4"
|
||||
# 0.1.0 Initial development
|
||||
# 0.1.1 Alpha Release
|
||||
# 0.1.2 Release bump - no changes to this code
|
||||
# 0.1.3 Release bump - no changes to this code
|
||||
# 0.1.4 Release bump - no changes to this code
|
||||
|
||||
__all__ = ["TmdBaseError", "TmdHttpError", "TmdXmlError", "TmdbUiAbort", "TmdbMovieOrPersonNotFound", ]
|
||||
|
||||
# Start of code used to access themoviedb.org api
|
||||
class TmdBaseError(Exception):
|
||||
pass
|
||||
|
||||
class TmdHttpError(TmdBaseError):
|
||||
def __repr__(self): # Display the type of error
|
||||
return None
|
||||
# end __repr__
|
||||
|
||||
class TmdXmlError(TmdBaseError):
|
||||
def __repr__(self): # Display the type of error
|
||||
return None
|
||||
# end __repr__
|
||||
|
||||
class TmdbMovieOrPersonNotFound(TmdBaseError):
|
||||
def __repr__(self):
|
||||
return None
|
||||
# end __repr__
|
||||
|
||||
class TmdbUiAbort(TmdBaseError):
|
||||
def __repr__(self):
|
||||
return None
|
||||
# end __repr__
|
|
@ -0,0 +1,266 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
# ----------------------
|
||||
# Name: tmdb_ui.py Is a simple console user interface for the tmdb_api. The interface is used selection
|
||||
# of a movie from themoviesdb.org.
|
||||
# Python Script
|
||||
# Author: dbr/Ben modified by R.D. Vaughan
|
||||
# Purpose: Console interface for selecting a movie from themoviedb.org. This interface would be invoked when
|
||||
# an exact match is not found and the invoking script has specified the "interactive = True" when
|
||||
# creating an instance of MovieDb().
|
||||
#
|
||||
# License:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
#-------------------------------------
|
||||
__title__ ="tmdb_ui - Is a simple console user interface for the tmdb_api. The interface is used selection of a movie oe person from themoviesdb.org.";
|
||||
__author__="dbr/Ben modified by R.D. Vaughan"
|
||||
__purpose__='''Console interface for selecting a movie from themoviedb.org. This interface would be invoked when an exact match is not found and the invoking script has specified the "interactive = True" when creating an instance of MovieDb().
|
||||
'''
|
||||
__version__="v0.1.5"
|
||||
# 0.1.0 Initial development
|
||||
# 0.1.1 Alpha Release
|
||||
# 0.1.2 Release bump - no changes to this code
|
||||
# 0.1.3 Release bump - no changes to this code
|
||||
# 0.1.4 Release bump - no changes to this code
|
||||
# 0.1.5 Modified automated selection when there was only one search result. It was causing
|
||||
# too many incorrect selections.
|
||||
# Also removed a duplicate initialization of the version number.
|
||||
|
||||
|
||||
"""Contains included user interfaces for tmdb movie/person selection.
|
||||
|
||||
A UI is a callback. A class, it's __init__ function takes two arguments:
|
||||
|
||||
- config, which is the tmdb config dict, setup in tmdb_api.py
|
||||
- log, which is tmdb's logger instance (which uses the logging module). You can
|
||||
call log.info() log.warning() etc
|
||||
|
||||
It pass a dictionary "allElements", this is passed a list of dicts, each dict
|
||||
contains at least the keys "name" (human readable movie name), and "id" (the movies/person)
|
||||
ID as on themoviedb.org). for movies only if the key 'released' is included the year will be added
|
||||
to the movie title like 'Avatar (2009)' all other keys will be ignored.
|
||||
For example:
|
||||
|
||||
[{'name': u'Avatar', 'id': u'19995', 'released': '2009-12-25'},
|
||||
{'name': u'Avatar - Sequel', 'id': u'73181'}]
|
||||
OR
|
||||
[{'name': u'Tom Cruise', 'id': u'500'},
|
||||
{'name': u'Cruise Moylan', 'id': u'77716'}]
|
||||
|
||||
|
||||
The "selectMovieOrPerson" method must return the appropriate dict, or it can raise
|
||||
TmdbUiAbort (if the selection is aborted), TmdbMovieOrPersonNotFound (if the movie
|
||||
or person cannot be found).
|
||||
|
||||
A simple example callback, which returns a random movie:
|
||||
|
||||
>>> import random
|
||||
>>> from tmdb_api_ui import BaseUI
|
||||
>>> class RandomUI(BaseUI):
|
||||
... def selectMovieOrPerson(self, allElements):
|
||||
... import random
|
||||
... return random.choice(allElements)
|
||||
|
||||
Then to use it..
|
||||
|
||||
>>> from tmdb_api import MovieDb
|
||||
>>> t = MovieDb(custom_ui = RandomUI)
|
||||
>>> random_matching_movie = t.searchTitle('Avatar')
|
||||
>>> type(random_matching_movie)
|
||||
[{"Avatar",'19995'}]
|
||||
"""
|
||||
|
||||
import os, struct, sys, string
|
||||
from tmdb_exceptions import TmdbUiAbort
|
||||
|
||||
class OutStreamEncoder(object):
|
||||
"""Wraps a stream with an encoder"""
|
||||
def __init__(self, outstream, encoding=None):
|
||||
self.out = outstream
|
||||
if not encoding:
|
||||
self.encoding = sys.getfilesystemencoding()
|
||||
else:
|
||||
self.encoding = encoding
|
||||
|
||||
def write(self, obj):
|
||||
"""Wraps the output stream, encoding Unicode strings with the specified encoding"""
|
||||
if isinstance(obj, unicode):
|
||||
try:
|
||||
self.out.write(obj.encode(self.encoding))
|
||||
except IOError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
self.out.write(obj)
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
def __getattr__(self, attr):
|
||||
"""Delegate everything but write to the stream"""
|
||||
return getattr(self.out, attr)
|
||||
sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
|
||||
sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
|
||||
|
||||
# Two routines used for movie title search and matching
|
||||
def is_punct_char(char):
|
||||
'''check if char is punctuation char
|
||||
return True if char is punctuation
|
||||
return False if char is not punctuation
|
||||
'''
|
||||
return char in string.punctuation
|
||||
|
||||
def is_not_punct_char(char):
|
||||
'''check if char is not punctuation char
|
||||
return True if char is not punctuation
|
||||
return False if chaar is punctuation
|
||||
'''
|
||||
return not is_punct_char(char)
|
||||
|
||||
|
||||
def makeDict(movieORperson):
|
||||
'''Make a dictionary out of the chosen movie data
|
||||
return a dictionary of the movie and ancillary data
|
||||
'''
|
||||
selection = {}
|
||||
for key in movieORperson.keys():
|
||||
selection[key] = movieORperson[key]
|
||||
return selection
|
||||
# end makeDict()
|
||||
|
||||
|
||||
class BaseUI:
|
||||
"""Default non-interactive UI, which auto-selects first results
|
||||
"""
|
||||
def __init__(self, config, log, searchTerm=None):
|
||||
self.config = config
|
||||
self.log = log
|
||||
self.searchTerm = searchTerm
|
||||
|
||||
def selectMovieOrPerson(self, allElements):
|
||||
return makeDict([allElements[0]])
|
||||
|
||||
|
||||
class ConsoleUI(BaseUI):
|
||||
"""Interactively allows the user to select a movie or person from a console based UI
|
||||
"""
|
||||
|
||||
def removeCommonWords(self, title):
|
||||
'''Remove common words from a title
|
||||
return title striped of common words
|
||||
'''
|
||||
if not title:
|
||||
return u' '
|
||||
wordList = [u'the ', u'a ', u' '] # common word list. Leave double space as the last value.
|
||||
title = title.lower()
|
||||
for word in wordList:
|
||||
title = title.replace(word, u'')
|
||||
if not title:
|
||||
return u' '
|
||||
title = title.strip()
|
||||
return filter(is_not_punct_char, title)
|
||||
# end removeCommonWords()
|
||||
|
||||
|
||||
def _displayMovie(self, allElements):
|
||||
"""Helper function, lists movies or people with corresponding ID
|
||||
"""
|
||||
print u"themoviedb.org Search Results:"
|
||||
for i in range(len(allElements[:15])): # list first 15 search results
|
||||
i_show = i + 1 # Start at more human readable number 1 (not 0)
|
||||
self.log.debug('Showing allElements[%s] = %s)' % (i_show, allElements[i]))
|
||||
if allElements[i]['name'] == u'User choses to ignore video':
|
||||
print u"% 2s -> %s # %s" % (
|
||||
i_show,
|
||||
'99999999', "Set this video to be ignored by Jamu with a reference number of '99999999'"
|
||||
)
|
||||
continue
|
||||
if not allElements[i].has_key('released'):
|
||||
title = allElements[i]['name']
|
||||
elif len(allElements[i]['released']) > 3:
|
||||
title = u"%s (%s)" % (allElements[i]['name'], allElements[i]['released'][:4])
|
||||
else:
|
||||
title = allElements[i]['name']
|
||||
if allElements[i]['url'].find('/person/') > -1:
|
||||
format = u"%2s -> %-30s # http://www.themoviedb.org/person/%s"
|
||||
else:
|
||||
format = u"%2s -> %-50s # http://www.themoviedb.org/movie/%s"
|
||||
print format % (
|
||||
i_show,
|
||||
title,
|
||||
allElements[i]['id']
|
||||
)
|
||||
print u"Direct search of themoviedb.org # http://themoviedb.org/"
|
||||
# end _displayMovie()
|
||||
|
||||
def selectMovieOrPerson(self, allElements):
|
||||
if allElements[0]['url'].find('/movie/') > -1:
|
||||
morp = u'movie'
|
||||
else:
|
||||
morp = u'person'
|
||||
# Add the ability to select the skip inetref of '99999999' for movies only
|
||||
if morp == u'movie':
|
||||
allElements.append( {'id': '99999999', 'name': u'User choses to ignore video'} )
|
||||
|
||||
self._displayMovie(allElements)
|
||||
|
||||
refsize = 5 # The number of digits required in a TMDB number is directly entered
|
||||
if len(allElements) == 2 and morp == u'movie':
|
||||
data = makeDict(allElements[0])
|
||||
if self.removeCommonWords(data['name']) == self.removeCommonWords(self.searchTerm) and data.has_key('released'):
|
||||
# Single result, return it!
|
||||
print u"Automatically selecting only result"
|
||||
return [data]
|
||||
|
||||
if len(allElements) == 1 and morp == u'person':
|
||||
data = makeDict(allElements[0])
|
||||
if self.removeCommonWords(data['name']) == self.removeCommonWords(self.searchTerm):
|
||||
# Single result, return it!
|
||||
print u"Automatically selecting only result"
|
||||
return [data]
|
||||
|
||||
if self.config['select_first'] is True:
|
||||
print u"Automatically returning first search result"
|
||||
return [makeDict(allElements[0])]
|
||||
|
||||
while True: # return breaks this loop
|
||||
try:
|
||||
print u'Enter choice:\n("Enter" key equals first selection (1)) or input a zero padded 5 digit %s TMDB id number, ? for help):' % morp
|
||||
ans = raw_input()
|
||||
except KeyboardInterrupt:
|
||||
raise TmdbUiAbort("User aborted (^c keyboard interupt)")
|
||||
except EOFError:
|
||||
raise TmdbUiAbort("User aborted (EOF received)")
|
||||
|
||||
self.log.debug(u'Got choice of: %s' % (ans))
|
||||
try:
|
||||
if ans == '': # Enter pressed which equates to the first selection
|
||||
selected_id = 0
|
||||
else:
|
||||
if int(ans) == 0:
|
||||
raise ValueError
|
||||
selected_id = int(ans) - 1 # The human entered 1 as first result, not zero
|
||||
except ValueError: # Input was not number
|
||||
if ans == "q":
|
||||
self.log.debug(u'Got quit command (q)')
|
||||
raise TmdbUiAbort("User aborted ('q' quit command)")
|
||||
elif ans == "?":
|
||||
print u"## Help"
|
||||
print u"# Enter the number that corresponds to the correct movie."
|
||||
print u"# Paste a TMDB %s ID number (pad with leading zeros to make 5 digits) from themoviedb.org and hit 'Enter'" % morp
|
||||
print u"# ? - this help"
|
||||
print u"# q - abort/skip movie selection"
|
||||
else:
|
||||
print '! Unknown/Invalid keypress "%s"\n' % (ans)
|
||||
self.log.debug('Unknown keypress %s' % (ans))
|
||||
else:
|
||||
self.log.debug('Trying to return ID: %d' % (selected_id))
|
||||
try:
|
||||
data = makeDict(allElements[selected_id])
|
||||
data['userResponse'] = u'User selected'
|
||||
return [data]
|
||||
except IndexError:
|
||||
if len(ans) == refsize:
|
||||
return [{'userResponse': u'User input', 'id': u'%d' % int(ans)}]
|
||||
#end try
|
||||
#end while not valid_input
|
||||
# end selectMovieOrPerson()
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
# ----------------------
|
||||
# Name: tmdb_exceptions - Custom exceptions used or raised by tmdb_api
|
||||
# Python Script
|
||||
# Author: dbr/Ben modified by R.D. Vaughan
|
||||
# Purpose: Custom exceptions used or raised by tmdb_api
|
||||
#
|
||||
# License:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
#-------------------------------------
|
||||
__title__ ="tmdb_exceptions - Custom exceptions used or raised by tmdb_api";
|
||||
__author__="dbr/Ben modified by R.D. Vaughan"
|
||||
__version__="v0.1.4"
|
||||
# 0.1.0 Initial development
|
||||
# 0.1.1 Alpha Release
|
||||
# 0.1.2 Release bump - no changes to this code
|
||||
# 0.1.3 Release bump - no changes to this code
|
||||
# 0.1.4 Release bump - no changes to this code
|
||||
|
||||
__all__ = ["TmdBaseError", "TmdHttpError", "TmdXmlError", "TmdbUiAbort", "TmdbMovieOrPersonNotFound", ]
|
||||
|
||||
# Start of code used to access themoviedb.org api
|
||||
class TmdBaseError(Exception):
|
||||
pass
|
||||
|
||||
class TmdHttpError(TmdBaseError):
|
||||
def __repr__(self): # Display the type of error
|
||||
return None
|
||||
# end __repr__
|
||||
|
||||
class TmdXmlError(TmdBaseError):
|
||||
def __repr__(self): # Display the type of error
|
||||
return None
|
||||
# end __repr__
|
||||
|
||||
class TmdbMovieOrPersonNotFound(TmdBaseError):
|
||||
def __repr__(self):
|
||||
return None
|
||||
# end __repr__
|
||||
|
||||
class TmdbUiAbort(TmdBaseError):
|
||||
def __repr__(self):
|
||||
return None
|
||||
# end __repr__
|
|
@ -0,0 +1,266 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
# ----------------------
|
||||
# Name: tmdb_ui.py Is a simple console user interface for the tmdb_api. The interface is used selection
|
||||
# of a movie from themoviesdb.org.
|
||||
# Python Script
|
||||
# Author: dbr/Ben modified by R.D. Vaughan
|
||||
# Purpose: Console interface for selecting a movie from themoviedb.org. This interface would be invoked when
|
||||
# an exact match is not found and the invoking script has specified the "interactive = True" when
|
||||
# creating an instance of MovieDb().
|
||||
#
|
||||
# License:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
#-------------------------------------
|
||||
__title__ ="tmdb_ui - Is a simple console user interface for the tmdb_api. The interface is used selection of a movie oe person from themoviesdb.org.";
|
||||
__author__="dbr/Ben modified by R.D. Vaughan"
|
||||
__purpose__='''Console interface for selecting a movie from themoviedb.org. This interface would be invoked when an exact match is not found and the invoking script has specified the "interactive = True" when creating an instance of MovieDb().
|
||||
'''
|
||||
__version__="v0.1.5"
|
||||
# 0.1.0 Initial development
|
||||
# 0.1.1 Alpha Release
|
||||
# 0.1.2 Release bump - no changes to this code
|
||||
# 0.1.3 Release bump - no changes to this code
|
||||
# 0.1.4 Release bump - no changes to this code
|
||||
# 0.1.5 Modified automated selection when there was only one search result. It was causing
|
||||
# too many incorrect selections.
|
||||
# Also removed a duplicate initialization of the version number.
|
||||
|
||||
|
||||
"""Contains included user interfaces for tmdb movie/person selection.
|
||||
|
||||
A UI is a callback. A class, it's __init__ function takes two arguments:
|
||||
|
||||
- config, which is the tmdb config dict, setup in tmdb_api.py
|
||||
- log, which is tmdb's logger instance (which uses the logging module). You can
|
||||
call log.info() log.warning() etc
|
||||
|
||||
It pass a dictionary "allElements", this is passed a list of dicts, each dict
|
||||
contains at least the keys "name" (human readable movie name), and "id" (the movies/person)
|
||||
ID as on themoviedb.org). for movies only if the key 'released' is included the year will be added
|
||||
to the movie title like 'Avatar (2009)' all other keys will be ignored.
|
||||
For example:
|
||||
|
||||
[{'name': u'Avatar', 'id': u'19995', 'released': '2009-12-25'},
|
||||
{'name': u'Avatar - Sequel', 'id': u'73181'}]
|
||||
OR
|
||||
[{'name': u'Tom Cruise', 'id': u'500'},
|
||||
{'name': u'Cruise Moylan', 'id': u'77716'}]
|
||||
|
||||
|
||||
The "selectMovieOrPerson" method must return the appropriate dict, or it can raise
|
||||
TmdbUiAbort (if the selection is aborted), TmdbMovieOrPersonNotFound (if the movie
|
||||
or person cannot be found).
|
||||
|
||||
A simple example callback, which returns a random movie:
|
||||
|
||||
>>> import random
|
||||
>>> from tmdb_api_ui import BaseUI
|
||||
>>> class RandomUI(BaseUI):
|
||||
... def selectMovieOrPerson(self, allElements):
|
||||
... import random
|
||||
... return random.choice(allElements)
|
||||
|
||||
Then to use it..
|
||||
|
||||
>>> from tmdb_api import MovieDb
|
||||
>>> t = MovieDb(custom_ui = RandomUI)
|
||||
>>> random_matching_movie = t.searchTitle('Avatar')
|
||||
>>> type(random_matching_movie)
|
||||
[{"Avatar",'19995'}]
|
||||
"""
|
||||
|
||||
import os, struct, sys, string
|
||||
from tmdb_exceptions import TmdbUiAbort
|
||||
|
||||
class OutStreamEncoder(object):
|
||||
"""Wraps a stream with an encoder"""
|
||||
def __init__(self, outstream, encoding=None):
|
||||
self.out = outstream
|
||||
if not encoding:
|
||||
self.encoding = sys.getfilesystemencoding()
|
||||
else:
|
||||
self.encoding = encoding
|
||||
|
||||
def write(self, obj):
|
||||
"""Wraps the output stream, encoding Unicode strings with the specified encoding"""
|
||||
if isinstance(obj, unicode):
|
||||
try:
|
||||
self.out.write(obj.encode(self.encoding))
|
||||
except IOError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
self.out.write(obj)
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
def __getattr__(self, attr):
|
||||
"""Delegate everything but write to the stream"""
|
||||
return getattr(self.out, attr)
|
||||
sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
|
||||
sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
|
||||
|
||||
# Two routines used for movie title search and matching
|
||||
def is_punct_char(char):
|
||||
'''check if char is punctuation char
|
||||
return True if char is punctuation
|
||||
return False if char is not punctuation
|
||||
'''
|
||||
return char in string.punctuation
|
||||
|
||||
def is_not_punct_char(char):
|
||||
'''check if char is not punctuation char
|
||||
return True if char is not punctuation
|
||||
return False if chaar is punctuation
|
||||
'''
|
||||
return not is_punct_char(char)
|
||||
|
||||
|
||||
def makeDict(movieORperson):
|
||||
'''Make a dictionary out of the chosen movie data
|
||||
return a dictionary of the movie and ancillary data
|
||||
'''
|
||||
selection = {}
|
||||
for key in movieORperson.keys():
|
||||
selection[key] = movieORperson[key]
|
||||
return selection
|
||||
# end makeDict()
|
||||
|
||||
|
||||
class BaseUI:
|
||||
"""Default non-interactive UI, which auto-selects first results
|
||||
"""
|
||||
def __init__(self, config, log, searchTerm=None):
|
||||
self.config = config
|
||||
self.log = log
|
||||
self.searchTerm = searchTerm
|
||||
|
||||
def selectMovieOrPerson(self, allElements):
|
||||
return makeDict([allElements[0]])
|
||||
|
||||
|
||||
class ConsoleUI(BaseUI):
|
||||
"""Interactively allows the user to select a movie or person from a console based UI
|
||||
"""
|
||||
|
||||
def removeCommonWords(self, title):
|
||||
'''Remove common words from a title
|
||||
return title striped of common words
|
||||
'''
|
||||
if not title:
|
||||
return u' '
|
||||
wordList = [u'the ', u'a ', u' '] # common word list. Leave double space as the last value.
|
||||
title = title.lower()
|
||||
for word in wordList:
|
||||
title = title.replace(word, u'')
|
||||
if not title:
|
||||
return u' '
|
||||
title = title.strip()
|
||||
return filter(is_not_punct_char, title)
|
||||
# end removeCommonWords()
|
||||
|
||||
|
||||
def _displayMovie(self, allElements):
|
||||
"""Helper function, lists movies or people with corresponding ID
|
||||
"""
|
||||
print u"themoviedb.org Search Results:"
|
||||
for i in range(len(allElements[:15])): # list first 15 search results
|
||||
i_show = i + 1 # Start at more human readable number 1 (not 0)
|
||||
self.log.debug('Showing allElements[%s] = %s)' % (i_show, allElements[i]))
|
||||
if allElements[i]['name'] == u'User choses to ignore video':
|
||||
print u"% 2s -> %s # %s" % (
|
||||
i_show,
|
||||
'99999999', "Set this video to be ignored by Jamu with a reference number of '99999999'"
|
||||
)
|
||||
continue
|
||||
if not allElements[i].has_key('released'):
|
||||
title = allElements[i]['name']
|
||||
elif len(allElements[i]['released']) > 3:
|
||||
title = u"%s (%s)" % (allElements[i]['name'], allElements[i]['released'][:4])
|
||||
else:
|
||||
title = allElements[i]['name']
|
||||
if allElements[i]['url'].find('/person/') > -1:
|
||||
format = u"%2s -> %-30s # http://www.themoviedb.org/person/%s"
|
||||
else:
|
||||
format = u"%2s -> %-50s # http://www.themoviedb.org/movie/%s"
|
||||
print format % (
|
||||
i_show,
|
||||
title,
|
||||
allElements[i]['id']
|
||||
)
|
||||
print u"Direct search of themoviedb.org # http://themoviedb.org/"
|
||||
# end _displayMovie()
|
||||
|
||||
def selectMovieOrPerson(self, allElements):
|
||||
if allElements[0]['url'].find('/movie/') > -1:
|
||||
morp = u'movie'
|
||||
else:
|
||||
morp = u'person'
|
||||
# Add the ability to select the skip inetref of '99999999' for movies only
|
||||
if morp == u'movie':
|
||||
allElements.append( {'id': '99999999', 'name': u'User choses to ignore video'} )
|
||||
|
||||
self._displayMovie(allElements)
|
||||
|
||||
refsize = 5 # The number of digits required in a TMDB number is directly entered
|
||||
if len(allElements) == 2 and morp == u'movie':
|
||||
data = makeDict(allElements[0])
|
||||
if self.removeCommonWords(data['name']) == self.removeCommonWords(self.searchTerm) and data.has_key('released'):
|
||||
# Single result, return it!
|
||||
print u"Automatically selecting only result"
|
||||
return [data]
|
||||
|
||||
if len(allElements) == 1 and morp == u'person':
|
||||
data = makeDict(allElements[0])
|
||||
if self.removeCommonWords(data['name']) == self.removeCommonWords(self.searchTerm):
|
||||
# Single result, return it!
|
||||
print u"Automatically selecting only result"
|
||||
return [data]
|
||||
|
||||
if self.config['select_first'] is True:
|
||||
print u"Automatically returning first search result"
|
||||
return [makeDict(allElements[0])]
|
||||
|
||||
while True: # return breaks this loop
|
||||
try:
|
||||
print u'Enter choice:\n("Enter" key equals first selection (1)) or input a zero padded 5 digit %s TMDB id number, ? for help):' % morp
|
||||
ans = raw_input()
|
||||
except KeyboardInterrupt:
|
||||
raise TmdbUiAbort("User aborted (^c keyboard interupt)")
|
||||
except EOFError:
|
||||
raise TmdbUiAbort("User aborted (EOF received)")
|
||||
|
||||
self.log.debug(u'Got choice of: %s' % (ans))
|
||||
try:
|
||||
if ans == '': # Enter pressed which equates to the first selection
|
||||
selected_id = 0
|
||||
else:
|
||||
if int(ans) == 0:
|
||||
raise ValueError
|
||||
selected_id = int(ans) - 1 # The human entered 1 as first result, not zero
|
||||
except ValueError: # Input was not number
|
||||
if ans == "q":
|
||||
self.log.debug(u'Got quit command (q)')
|
||||
raise TmdbUiAbort("User aborted ('q' quit command)")
|
||||
elif ans == "?":
|
||||
print u"## Help"
|
||||
print u"# Enter the number that corresponds to the correct movie."
|
||||
print u"# Paste a TMDB %s ID number (pad with leading zeros to make 5 digits) from themoviedb.org and hit 'Enter'" % morp
|
||||
print u"# ? - this help"
|
||||
print u"# q - abort/skip movie selection"
|
||||
else:
|
||||
print '! Unknown/Invalid keypress "%s"\n' % (ans)
|
||||
self.log.debug('Unknown keypress %s' % (ans))
|
||||
else:
|
||||
self.log.debug('Trying to return ID: %d' % (selected_id))
|
||||
try:
|
||||
data = makeDict(allElements[selected_id])
|
||||
data['userResponse'] = u'User selected'
|
||||
return [data]
|
||||
except IndexError:
|
||||
if len(ans) == refsize:
|
||||
return [{'userResponse': u'User input', 'id': u'%d' % int(ans)}]
|
||||
#end try
|
||||
#end while not valid_input
|
||||
# end selectMovieOrPerson()
|
|
@ -0,0 +1,41 @@
|
|||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 73
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/ttvdb
|
||||
END
|
||||
tvdb_api.py
|
||||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 85
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py
|
||||
END
|
||||
ttvdb-example.conf
|
||||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 92
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/ttvdb/ttvdb-example.conf
|
||||
END
|
||||
tvdb_ui.py
|
||||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 84
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/ttvdb/tvdb_ui.py
|
||||
END
|
||||
__init__.py
|
||||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 85
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/ttvdb/__init__.py
|
||||
END
|
||||
tvdb_exceptions.py
|
||||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 92
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/ttvdb/tvdb_exceptions.py
|
||||
END
|
||||
cache.py
|
||||
K 25
|
||||
svn:wc:ra_dav:version-url
|
||||
V 82
|
||||
/svn/!svn/ver/24521/tags/release-0-23/mythtv/bindings/python/MythTV/ttvdb/cache.py
|
||||
END
|
|
@ -0,0 +1,232 @@
|
|||
10
|
||||
|
||||
dir
|
||||
25361
|
||||
http://svn.mythtv.org/svn/tags/release-0-23/mythtv/bindings/python/MythTV/ttvdb
|
||||
http://svn.mythtv.org/svn
|
||||
|
||||
|
||||
|
||||
2010-02-01T05:23:09.925099Z
|
||||
23416
|
||||
wagnerrp
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
7dbf422c-18fa-0310-86e9-fd20926502f2
|
||||
|
||||
tvdb_api.py
|
||||
file
|
||||
|
||||
|
||||
|
||||
|
||||
2010-07-16T22:31:04.000000Z
|
||||
554f84950a1f7cc2bab09477c3798f18
|
||||
2010-01-29T01:39:37.380922Z
|
||||
23354
|
||||
wagnerrp
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
29233
|
||||
|
||||
ttvdb-example.conf
|
||||
file
|
||||
|
||||
|
||||
|
||||
|
||||
2010-07-16T22:31:04.000000Z
|
||||
8a9a6d831a1892828eb38318b114f0b0
|
||||
2010-01-29T01:39:37.380922Z
|
||||
23354
|
||||
wagnerrp
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
5590
|
||||
|
||||
tvdb_ui.py
|
||||
file
|
||||
|
||||
|
||||
|
||||
|
||||
2010-07-16T22:31:04.000000Z
|
||||
305a22b10a8be8dfef8ff90c399d9a38
|
||||
2010-01-29T01:39:37.380922Z
|
||||
23354
|
||||
wagnerrp
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
4387
|
||||
|
||||
__init__.py
|
||||
file
|
||||
|
||||
|
||||
|
||||
|
||||
2010-07-16T22:31:04.000000Z
|
||||
d41d8cd98f00b204e9800998ecf8427e
|
||||
2010-01-29T01:39:37.380922Z
|
||||
23354
|
||||
wagnerrp
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
0
|
||||
|
||||
tvdb_exceptions.py
|
||||
file
|
||||
|
||||
|
||||
|
||||
|
||||
2010-07-16T22:31:04.000000Z
|
||||
90ce82c602de1cc41d603c01563d3bfb
|
||||
2010-01-29T01:39:37.380922Z
|
||||
23354
|
||||
wagnerrp
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
1168
|
||||
|
||||
cache.py
|
||||
file
|
||||
|
||||
|
||||
|
||||
|
||||
2010-07-16T22:31:04.000000Z
|
||||
e904998a5a3c1e088dc5b00a57947127
|
||||
2010-01-29T01:39:37.380922Z
|
||||
23354
|
||||
wagnerrp
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
7345
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
#!/usr/bin/env python
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben
|
||||
#project:tvdb_api
|
||||
#repository:http://github.com/dbr/tvdb_api
|
||||
#license:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
|
||||
"""
|
||||
urllib2 caching handler
|
||||
Modified from http://code.activestate.com/recipes/491261/
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
|
||||
__author__ = "dbr/Ben"
|
||||
__version__ = "1.2.1"
|
||||
|
||||
import os
|
||||
import time
|
||||
import errno
|
||||
import httplib
|
||||
import urllib2
|
||||
import StringIO
|
||||
from hashlib import md5
|
||||
from threading import RLock
|
||||
|
||||
cache_lock = RLock()
|
||||
|
||||
def locked_function(origfunc):
|
||||
"""Decorator to execute function under lock"""
|
||||
def wrapped(*args, **kwargs):
|
||||
cache_lock.acquire()
|
||||
try:
|
||||
return origfunc(*args, **kwargs)
|
||||
finally:
|
||||
cache_lock.release()
|
||||
return wrapped
|
||||
|
||||
def calculate_cache_path(cache_location, url):
|
||||
"""Checks if [cache_location]/[hash_of_url].headers and .body exist
|
||||
"""
|
||||
thumb = md5(url).hexdigest()
|
||||
header = os.path.join(cache_location, thumb + ".headers")
|
||||
body = os.path.join(cache_location, thumb + ".body")
|
||||
return header, body
|
||||
|
||||
def check_cache_time(path, max_age):
|
||||
"""Checks if a file has been created/modified in the [last max_age] seconds.
|
||||
False means the file is too old (or doesn't exist), True means it is
|
||||
up-to-date and valid"""
|
||||
if not os.path.isfile(path):
|
||||
return False
|
||||
cache_modified_time = os.stat(path).st_mtime
|
||||
time_now = time.time()
|
||||
if cache_modified_time < time_now - max_age:
|
||||
# Cache is old
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
@locked_function
|
||||
def exists_in_cache(cache_location, url, max_age):
|
||||
"""Returns if header AND body cache file exist (and are up-to-date)"""
|
||||
hpath, bpath = calculate_cache_path(cache_location, url)
|
||||
if os.path.exists(hpath) and os.path.exists(bpath):
|
||||
return(
|
||||
check_cache_time(hpath, max_age)
|
||||
and check_cache_time(bpath, max_age)
|
||||
)
|
||||
else:
|
||||
# File does not exist
|
||||
return False
|
||||
|
||||
@locked_function
|
||||
def store_in_cache(cache_location, url, response):
|
||||
"""Tries to store response in cache."""
|
||||
hpath, bpath = calculate_cache_path(cache_location, url)
|
||||
try:
|
||||
outf = open(hpath, "w")
|
||||
headers = str(response.info())
|
||||
outf.write(headers)
|
||||
outf.close()
|
||||
|
||||
outf = open(bpath, "w")
|
||||
outf.write(response.read())
|
||||
outf.close()
|
||||
except IOError:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
class CacheHandler(urllib2.BaseHandler):
|
||||
"""Stores responses in a persistant on-disk cache.
|
||||
|
||||
If a subsequent GET request is made for the same URL, the stored
|
||||
response is returned, saving time, resources and bandwidth
|
||||
"""
|
||||
@locked_function
|
||||
def __init__(self, cache_location, max_age = 21600):
|
||||
"""The location of the cache directory"""
|
||||
self.max_age = max_age
|
||||
self.cache_location = cache_location
|
||||
if not os.path.exists(self.cache_location):
|
||||
try:
|
||||
os.mkdir(self.cache_location)
|
||||
except OSError, e:
|
||||
if e.errno == errno.EEXIST and os.path.isdir(self.cache_location):
|
||||
# File exists, and it's a directory,
|
||||
# another process beat us to creating this dir, that's OK.
|
||||
pass
|
||||
else:
|
||||
# Our target dir is already a file, or different error,
|
||||
# relay the error!
|
||||
raise OSError(e)
|
||||
|
||||
def default_open(self, request):
|
||||
"""Handles GET requests, if the response is cached it returns it
|
||||
"""
|
||||
if request.get_method() is not "GET":
|
||||
return None # let the next handler try to handle the request
|
||||
|
||||
if exists_in_cache(
|
||||
self.cache_location, request.get_full_url(), self.max_age
|
||||
):
|
||||
return CachedResponse(
|
||||
self.cache_location,
|
||||
request.get_full_url(),
|
||||
set_cache_header = True
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
def http_response(self, request, response):
|
||||
"""Gets a HTTP response, if it was a GET request and the status code
|
||||
starts with 2 (200 OK etc) it caches it and returns a CachedResponse
|
||||
"""
|
||||
if (request.get_method() == "GET"
|
||||
and str(response.code).startswith("2")
|
||||
):
|
||||
if 'x-local-cache' not in response.info():
|
||||
# Response is not cached
|
||||
set_cache_header = store_in_cache(
|
||||
self.cache_location,
|
||||
request.get_full_url(),
|
||||
response
|
||||
)
|
||||
else:
|
||||
set_cache_header = True
|
||||
#end if x-cache in response
|
||||
|
||||
return CachedResponse(
|
||||
self.cache_location,
|
||||
request.get_full_url(),
|
||||
set_cache_header = set_cache_header
|
||||
)
|
||||
else:
|
||||
return response
|
||||
|
||||
class CachedResponse(StringIO.StringIO):
|
||||
"""An urllib2.response-like object for cached responses.
|
||||
|
||||
To determine if a response is cached or coming directly from
|
||||
the network, check the x-local-cache header rather than the object type.
|
||||
"""
|
||||
|
||||
@locked_function
|
||||
def __init__(self, cache_location, url, set_cache_header=True):
|
||||
self.cache_location = cache_location
|
||||
hpath, bpath = calculate_cache_path(cache_location, url)
|
||||
|
||||
StringIO.StringIO.__init__(self, file(bpath).read())
|
||||
|
||||
self.url = url
|
||||
self.code = 200
|
||||
self.msg = "OK"
|
||||
headerbuf = file(hpath).read()
|
||||
if set_cache_header:
|
||||
headerbuf += "x-local-cache: %s\r\n" % (bpath)
|
||||
self.headers = httplib.HTTPMessage(StringIO.StringIO(headerbuf))
|
||||
|
||||
def info(self):
|
||||
"""Returns headers
|
||||
"""
|
||||
return self.headers
|
||||
|
||||
def geturl(self):
|
||||
"""Returns original URL
|
||||
"""
|
||||
return self.url
|
||||
|
||||
@locked_function
|
||||
def recache(self):
|
||||
new_request = urllib2.urlopen(self.url)
|
||||
set_cache_header = store_in_cache(
|
||||
self.cache_location,
|
||||
new_request.url,
|
||||
new_request
|
||||
)
|
||||
CachedResponse.__init__(self, self.cache_location, self.url, True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
def main():
|
||||
"""Quick test/example of CacheHandler"""
|
||||
opener = urllib2.build_opener(CacheHandler("/tmp/"))
|
||||
response = opener.open("http://google.com")
|
||||
print response.headers
|
||||
print "Response:", response.read()
|
||||
|
||||
response.recache()
|
||||
print response.headers
|
||||
print "After recache:", response.read()
|
||||
|
||||
# Test usage in threads
|
||||
from threading import Thread
|
||||
class CacheThreadTest(Thread):
|
||||
lastdata = None
|
||||
def run(self):
|
||||
req = opener.open("http://google.com")
|
||||
newdata = req.read()
|
||||
if self.lastdata is None:
|
||||
self.lastdata = newdata
|
||||
assert self.lastdata == newdata, "Data was not consistent, uhoh"
|
||||
req.recache()
|
||||
threads = [CacheThreadTest() for x in range(50)]
|
||||
print "Starting threads"
|
||||
[t.start() for t in threads]
|
||||
print "..done"
|
||||
print "Joining threads"
|
||||
[t.join() for t in threads]
|
||||
print "..done"
|
||||
main()
|
|
@ -0,0 +1,109 @@
|
|||
[File ttvdb-example.conf]
|
||||
#-------------------------------------
|
||||
# Name: ttvdb-example.conf
|
||||
# Project: ttvdb
|
||||
# Configuration file
|
||||
# Author: R.D. Vaughan
|
||||
# Version: 0.1.0 - Initial alpha release
|
||||
# Version: 0.8.9 - version changed to match the current ttvdb release number
|
||||
# Version: 0.9.5 - Changed regex pattern strings to support multi-langiage file names
|
||||
#
|
||||
# License:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
#-------------------------------------
|
||||
#
|
||||
# START season name overrides section --------------------------------------------------------------
|
||||
#####
|
||||
# PURPOSE: TV program sources such as Schedule Direct series names do not always match the series
|
||||
# names on thetvdb.com. This section allow you to override series names to reduce the
|
||||
# need for manual interaction and more accurate matching with thetvdb.com TV series wiki.
|
||||
#
|
||||
# FORMAT:
|
||||
# Any line starting with "#" is treated as a comment
|
||||
# Any blank line is ignored
|
||||
# All other lines must have two fields in this specific order:
|
||||
# 1st) The series name or the %TITLE% field as passed by MythTV and a trailing ':' character
|
||||
# 2nd) The thetvdb.com series id (SID) as specified for the series on thetvdb.com wiki. This will
|
||||
# override the series name to eliminate the need for manual interaction.
|
||||
# NOTE: Included here. but disabled are examples of current (2009) TV shows that Schedule Direct
|
||||
# has different TV series names then what is on thetvdb.com wiki. If you are searching for
|
||||
# series based on the Schedule Direct names as MythTV does then the wrong TV series will be
|
||||
# identified. You can pick the correct series by using interactive mode but this does not
|
||||
# help with unattended processing. This section allows you to specify the correct TV series
|
||||
# on thetvbd.com despite the name in Schedule Direct. Luckily there are not many TV series
|
||||
# that require overrides.
|
||||
#####
|
||||
[series_name_override]
|
||||
# Specify recorded "Life On Mars" shows as the US version
|
||||
#Life on Mars:82289
|
||||
# For overnight episode updates when a filename is used
|
||||
#Life on Mars (US):82289
|
||||
|
||||
# Specify recorded "Eleventh Hour" shows as the US version
|
||||
#Eleventh Hour:83066
|
||||
# For overnight episode updates when a filename is used
|
||||
#Eleventh Hour (US):83066
|
||||
|
||||
# Specify recorded "Frontline" or "Frontline/World" shows as the "Frontline PBS" version
|
||||
#Frontline/World:80646
|
||||
#Frontline:80646
|
||||
|
||||
# Specify recorded "The Beast" shows are the 2009 version
|
||||
#The Beast:83805
|
||||
# For overnight episode updates when a filename is used
|
||||
#The Beast (2009):83805
|
||||
|
||||
# Specify recorded "Castle" shows are the 2009 version
|
||||
#Castle:83462
|
||||
# For overnight episode updates when a filename is used
|
||||
#Castle (2009):83462
|
||||
|
||||
# Specify recorded "Battlestar Galactica" shows are the 2003 version
|
||||
#Battlestar Galactica:73545
|
||||
# For overnight episode updates when a filename is used
|
||||
#Battlestar Galactica (2003):73545
|
||||
# END season name overrides section --------------------------------------------------------------
|
||||
|
||||
# START episode name massaging section --------------------------------------------------------------
|
||||
#####
|
||||
# PURPOSE: TV program sources such as Schedule Direct episode names do not always match the episode
|
||||
# names on thetvdb.com. This section allow you to massage episode names to reduce the
|
||||
# need for manual interaction and more accurate matching with thetvdb.com TV series wiki.
|
||||
# Alter the episode names for a series to reduce the need for manual interaction
|
||||
# and more accurate matching with thetvdb.com TV series wiki. See example below.
|
||||
#
|
||||
# FORMAT:
|
||||
# All lines must in the following format:
|
||||
# 1st) The series name or the %TITLE% field as passed by MythTV and a trailing ':' character
|
||||
# 2nd) Pairs of values separated by commas. The first value is the search text to match to text within
|
||||
# the episode name such as the %SUBTITLE% field passed by MythTV and the text to replace the
|
||||
# matched searched text. If the replacement text contains a space then surround that text with
|
||||
# the '"' double quotation characters.
|
||||
# E.g. "24": PM," PM", AM, " AM" will turn the episode name
|
||||
# "Day 7: 11:00AM to 12:00PM" into "Day 7: 11:00 AM to 12:00 PM"
|
||||
#
|
||||
#####
|
||||
[ep_name_massage]
|
||||
#24: PM," PM", AM, " AM",M-,"M - "
|
||||
# END episode name massaging section --------------------------------------------------------------
|
||||
|
||||
# START regex section------------------------------------------------------------------------------
|
||||
#####
|
||||
# NOTE: If you do not understand regex expressions DO NOT USE this section.
|
||||
# PURPOSE: This section adds regex strings used to parse video file names when extracting
|
||||
# the following: series name, season number, episode number. Essential when downloading
|
||||
# metadata from mythtvfrontend->mythvideo when using ttvdb. You only need to add a regex
|
||||
# string if ttvdb cannot extract the required information from your video file names.
|
||||
# NOTE: ANY % percent sign in the expression must be doubled (e.g. a % must be changed to %% )
|
||||
# NOTE: The key value (e.g. "regex##") must be unique for each value.
|
||||
# NOTE: DO NOT surround the regex pattern string with the traditional ''' three single quotes
|
||||
#
|
||||
# "regex01" is an example video file name "foo_S01_12" where:
|
||||
# series name is "foo", season number starts at "S" and episode number starts after '_'
|
||||
# foo_[s01]_[e01]
|
||||
#####
|
||||
[regex]
|
||||
# foo_S01_12
|
||||
regex01: ^(.+?)[ \._\-][Ss]([0-9]+)_([0-9]+)[^\\/]*$
|
||||
# END regex section------------------------------------------------------------------------------
|
||||
|
|
@ -0,0 +1,805 @@
|
|||
#!/usr/bin/env python
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben
|
||||
#project:tvdb_api
|
||||
#repository:http://github.com/dbr/tvdb_api
|
||||
#license:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
|
||||
"""Simple-to-use Python interface to The TVDB's API (www.thetvdb.com)
|
||||
|
||||
Example usage:
|
||||
|
||||
>>> from tvdb_api import Tvdb
|
||||
>>> t = Tvdb()
|
||||
>>> t['Lost'][4][11]['episodename']
|
||||
u'Cabin Fever'
|
||||
"""
|
||||
__author__ = "dbr/Ben"
|
||||
__version__ = "1.2.1"
|
||||
|
||||
import os
|
||||
import sys
|
||||
import urllib
|
||||
import urllib2
|
||||
import tempfile
|
||||
import logging
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as ElementTree
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ElementTree
|
||||
|
||||
from cache import CacheHandler
|
||||
|
||||
from tvdb_ui import BaseUI, ConsoleUI
|
||||
from tvdb_exceptions import (tvdb_error, tvdb_userabort, tvdb_shownotfound,
|
||||
tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound)
|
||||
|
||||
class ShowContainer(dict):
|
||||
"""Simple dict that holds a series of Show instances
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Show(dict):
|
||||
"""Holds a dict of seasons, and show data.
|
||||
"""
|
||||
def __init__(self):
|
||||
dict.__init__(self)
|
||||
self.data = {}
|
||||
|
||||
def __repr__(self):
|
||||
return "<Show %s (containing %s seasons)>" % (
|
||||
self.data.get(u'seriesname', 'instance'),
|
||||
len(self)
|
||||
)
|
||||
|
||||
def __getitem__(self, 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 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")
|
||||
[<Episode 01x01 - My First Day>]
|
||||
>>>
|
||||
|
||||
Search for "My Name Is Earl" episode named "Faked His Own Death":
|
||||
|
||||
>>> t['My Name Is Earl'].search('Faked His Own Death', key = 'episodename')
|
||||
[<Episode 01x04 - Faked His Own Death>]
|
||||
>>>
|
||||
|
||||
To search Scrubs for all episodes with "mentor" in the episode name:
|
||||
|
||||
>>> t['scrubs'].search('mentor', key = 'episodename')
|
||||
[<Episode 01x02 - My Mentor>, <Episode 03x15 - My Tormented Mentor>]
|
||||
>>>
|
||||
|
||||
# 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)
|
||||
#end for cur_season
|
||||
return results
|
||||
|
||||
|
||||
class Season(dict):
|
||||
def __repr__(self):
|
||||
return "<Season instance (containing %s episodes)>" % (
|
||||
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')
|
||||
[<Episode 01x01 - My 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 __repr__(self):
|
||||
seasno = int(self.get(u'seasonnumber', 0))
|
||||
epno = int(self.get(u'episodenumber', 0))
|
||||
epname = self.get(u'episodename')
|
||||
if epname is not None:
|
||||
return "<Episode %02dx%02d - %s>" % (seasno, epno, epname)
|
||||
else:
|
||||
return "<Episode %02dx%02d>" % (seasno, epno)
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return dict.__getitem__(self, key)
|
||||
except KeyError:
|
||||
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")
|
||||
<Episode 00x00 - An Example>
|
||||
>>>
|
||||
|
||||
Limiting by key:
|
||||
|
||||
>>> e.search("examp", key = "episodename")
|
||||
<Episode 00x00 - An Example>
|
||||
>>>
|
||||
"""
|
||||
if term == None:
|
||||
raise TypeError("must supply string to search for (contents)")
|
||||
|
||||
term = unicode(term).lower()
|
||||
for cur_key, cur_value in self.items():
|
||||
cur_key, cur_value = unicode(cur_key).lower(), unicode(cur_value).lower()
|
||||
if key is not None and cur_key != key:
|
||||
# Do not search this key
|
||||
continue
|
||||
if cur_value.find( unicode(term).lower() ) > -1:
|
||||
return self
|
||||
#end if cur_value.find()
|
||||
#end for cur_key, cur_value
|
||||
|
||||
|
||||
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 "<Actor \"%s\">" % (self.get("name"))
|
||||
|
||||
|
||||
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):
|
||||
"""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):
|
||||
shows verbose debugging information
|
||||
|
||||
cache (True/False/str/unicode):
|
||||
Retrieved XML are persisted to to disc. If true, stores in tvdb_api
|
||||
folder under your systems TEMP_DIR, if set to str/unicode instance it
|
||||
will use this as the cache location. If False, disables caching.
|
||||
|
||||
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()
|
||||
['fanart', 'poster', 'series', 'season']
|
||||
|
||||
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'Zach Braff'
|
||||
|
||||
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
|
||||
"""
|
||||
self.shows = ShowContainer() # Holds all Show classes
|
||||
self.corrections = {} # Holds show-name to show_id mapping
|
||||
|
||||
self.config = {}
|
||||
|
||||
if apikey is not None:
|
||||
self.config['apikey'] = apikey
|
||||
else:
|
||||
self.config['apikey'] = "0629B785CE550C8D" # tvdb_api's API key
|
||||
|
||||
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
|
||||
|
||||
if cache is True:
|
||||
self.config['cache_enabled'] = True
|
||||
self.config['cache_location'] = self._getTempDir()
|
||||
elif isinstance(cache, basestring):
|
||||
self.config['cache_enabled'] = True
|
||||
self.config['cache_location'] = cache
|
||||
else:
|
||||
self.config['cache_enabled'] = False
|
||||
|
||||
if self.config['cache_enabled']:
|
||||
self.urlopener = urllib2.build_opener(
|
||||
CacheHandler(self.config['cache_location'])
|
||||
)
|
||||
else:
|
||||
self.urlopener = urllib2.build_opener()
|
||||
|
||||
self.config['banners_enabled'] = banners
|
||||
self.config['actors_enabled'] = actors
|
||||
|
||||
self.log = self._initLogger() # Setups the logger (self.log.debug() etc)
|
||||
|
||||
# List of language from http://www.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"
|
||||
]
|
||||
|
||||
if language is None:
|
||||
self.config['language'] = "en"
|
||||
elif 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://www.thetvdb.com"
|
||||
|
||||
if self.config['search_all_languages']:
|
||||
self.config['url_getSeries'] = "%(base_url)s/api/GetSeries.php?seriesname=%%s&language=all" % self.config
|
||||
else:
|
||||
self.config['url_getSeries'] = "%(base_url)s/api/GetSeries.php?seriesname=%%s&language=%(language)s" % self.config
|
||||
|
||||
self.config['url_epInfo'] = "%(base_url)s/api/%(apikey)s/series/%%s/all/%(language)s.xml" % self.config
|
||||
|
||||
self.config['url_seriesInfo'] = "%(base_url)s/api/%(apikey)s/series/%%s/%(language)s.xml" % self.config
|
||||
self.config['url_actorsInfo'] = "%(base_url)s/api/%(apikey)s/series/%%s/actors.xml" % self.config
|
||||
|
||||
self.config['url_seriesBanner'] = "%(base_url)s/api/%(apikey)s/series/%%s/banners.xml" % self.config
|
||||
self.config['url_artworkPrefix'] = "%(base_url)s/banners/%%s" % self.config
|
||||
|
||||
#end __init__
|
||||
|
||||
def _initLogger(self):
|
||||
"""Setups a logger using the logging module, returns a log object
|
||||
"""
|
||||
logger = logging.getLogger("tvdb")
|
||||
formatter = logging.Formatter('%(asctime)s) %(levelname)s %(message)s')
|
||||
|
||||
hdlr = logging.StreamHandler(sys.stdout)
|
||||
|
||||
hdlr.setFormatter(formatter)
|
||||
logger.addHandler(hdlr)
|
||||
|
||||
if self.config['debug_enabled']:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
logger.setLevel(logging.WARNING)
|
||||
return logger
|
||||
#end initLogger
|
||||
|
||||
def _getTempDir(self):
|
||||
"""Returns the [system temp dir]/tvdb_api
|
||||
"""
|
||||
return os.path.join(tempfile.gettempdir(), "tvdb_api")
|
||||
|
||||
def _loadUrl(self, url, recache = False):
|
||||
try:
|
||||
self.log.debug("Retrieving URL %s" % url)
|
||||
resp = self.urlopener.open(url)
|
||||
if 'x-local-cache' in resp.headers:
|
||||
self.log.debug("URL %s was cached in %s" % (
|
||||
url,
|
||||
resp.headers['x-local-cache'])
|
||||
)
|
||||
if recache:
|
||||
self.log.debug("Attempting to recache %s" % url)
|
||||
resp.recache()
|
||||
except urllib2.URLError, errormsg:
|
||||
raise tvdb_error("Could not connect to server: %s" % (errormsg))
|
||||
#end try
|
||||
|
||||
return resp.read()
|
||||
|
||||
def _getetsrc(self, url):
|
||||
"""Loads a URL sing caching, returns an ElementTree of the source
|
||||
"""
|
||||
src = self._loadUrl(url)
|
||||
try:
|
||||
return ElementTree.fromstring(src)
|
||||
except SyntaxError:
|
||||
src = self._loadUrl(url, recache=True)
|
||||
try:
|
||||
return ElementTree.fromstring(src)
|
||||
except SyntaxError, exceptionmsg:
|
||||
errormsg = "There was an error with the XML retrieved from thetvdb.com:\n%s" % (
|
||||
exceptionmsg
|
||||
)
|
||||
|
||||
if self.config['cache_enabled']:
|
||||
errormsg += "\nFirst try emptying the cache folder at..\n%s" % (
|
||||
self.config['cache_location']
|
||||
)
|
||||
|
||||
errormsg += "\nIf this does not resolve the issue, please try again later. If the error persists, report a bug on"
|
||||
errormsg += "\nhttp://dbr.lighthouseapp.com/projects/13342-tvdb_api/overview\n"
|
||||
raise tvdb_error(errormsg)
|
||||
#end _getetsrc
|
||||
|
||||
def _setItem(self, sid, seas, ep, attrib, value):
|
||||
"""Creates a new episode, creating Show(), Season() and
|
||||
Episode()s as required. Called by _getShowData to populute
|
||||
|
||||
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()
|
||||
if ep not in self.shows[sid][seas]:
|
||||
self.shows[sid][seas][ep] = Episode()
|
||||
self.shows[sid][seas][ep][attrib] = value
|
||||
#end _set_item
|
||||
|
||||
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 _cleanData(self, data):
|
||||
"""Cleans up strings returned by TheTVDB.com
|
||||
|
||||
Issues corrected:
|
||||
- Replaces & with &
|
||||
- Trailing whitespace
|
||||
"""
|
||||
data = data.replace(u"&", u"&")
|
||||
data = data.strip()
|
||||
return data
|
||||
#end _cleanData
|
||||
|
||||
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.
|
||||
"""
|
||||
series = urllib.quote(series.encode("utf-8"))
|
||||
self.log.debug("Searching for show %s" % series)
|
||||
seriesEt = self._getetsrc(self.config['url_getSeries'] % (series))
|
||||
allSeries = []
|
||||
for series in seriesEt:
|
||||
sn = series.find('SeriesName')
|
||||
value = self._cleanData(sn.text)
|
||||
cur_sid = series.find('id').text
|
||||
self.log.debug('Found series %s (id: %s)' % (value, cur_sid))
|
||||
allSeries.append( {'sid':cur_sid, 'name':value} )
|
||||
#end for series
|
||||
|
||||
if len(allSeries) == 0:
|
||||
self.log.debug('Series result returned zero')
|
||||
raise tvdb_shownotfound("Show-name search returned zero results (cannot find show on TVDB)")
|
||||
|
||||
if self.config['custom_ui'] is not None:
|
||||
self.log.debug("Using custom UI %s" % (repr(self.config['custom_ui'])))
|
||||
ui = self.config['custom_ui'](config = self.config, log = self.log)
|
||||
else:
|
||||
if not self.config['interactive']:
|
||||
self.log.debug('Auto-selecting first search result using BaseUI')
|
||||
ui = BaseUI(config = self.config, log = self.log)
|
||||
else:
|
||||
self.log.debug('Interactivily selecting show using ConsoleUI')
|
||||
ui = ConsoleUI(config = self.config, log = self.log)
|
||||
#end if config['interactive]
|
||||
#end if custom_ui != None
|
||||
|
||||
return ui.selectSeries(allSeries)
|
||||
|
||||
#end _getSeries
|
||||
|
||||
def _parseBanners(self, sid):
|
||||
"""Parses banners XML, from
|
||||
http://www.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()
|
||||
['fanart', 'poster', 'series', 'season']
|
||||
>>> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath']
|
||||
'http://www.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.
|
||||
"""
|
||||
self.log.debug('Getting season banners for %s' % (sid))
|
||||
bannersEt = self._getetsrc( self.config['url_seriesBanner'] % (sid) )
|
||||
banners = {}
|
||||
for cur_banner in bannersEt.findall('Banner'):
|
||||
bid = cur_banner.find('id').text
|
||||
btype = cur_banner.find('BannerType')
|
||||
btype2 = cur_banner.find('BannerType2')
|
||||
if btype is None or btype2 is None:
|
||||
continue
|
||||
btype, btype2 = btype.text, btype2.text
|
||||
if not btype in banners:
|
||||
banners[btype] = {}
|
||||
if not btype2 in banners[btype]:
|
||||
banners[btype][btype2] = {}
|
||||
if not bid in banners[btype][btype2]:
|
||||
banners[btype][btype2][bid] = {}
|
||||
|
||||
self.log.debug("Banner: %s", bid)
|
||||
for cur_element in cur_banner.getchildren():
|
||||
tag = cur_element.tag.lower()
|
||||
value = cur_element.text
|
||||
if tag is None or value is None:
|
||||
continue
|
||||
tag, value = tag.lower(), value.lower()
|
||||
self.log.debug("Banner info: %s = %s" % (tag, value))
|
||||
banners[btype][btype2][bid][tag] = value
|
||||
|
||||
for k, v in banners[btype][btype2][bid].items():
|
||||
if k.endswith("path"):
|
||||
new_key = "_%s" % (k)
|
||||
self.log.debug("Transforming %s to %s" % (k, new_key))
|
||||
new_url = self.config['url_artworkPrefix'] % (v)
|
||||
self.log.debug("New banner URL: %s" % (new_url))
|
||||
banners[btype][btype2][bid][new_key] = new_url
|
||||
|
||||
self._setShowData(sid, "_banners", banners)
|
||||
|
||||
|
||||
# Alternate tvdb_api's method for retrieving graphics URLs but returned as a list that preserves
|
||||
# the user rating order highest rated to lowest rated
|
||||
def ttvdb_parseBanners(self, sid):
|
||||
"""Parses banners XML, from
|
||||
http://www.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()
|
||||
['fanart', 'poster', 'series', 'season']
|
||||
>>> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath']
|
||||
'http://www.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.
|
||||
Changed in this interface is that a list or URLs is created to preserve the user rating order from
|
||||
top rated to lowest rated.
|
||||
"""
|
||||
|
||||
self.log.debug('Getting season banners for %s' % (sid))
|
||||
bannersEt = self._getetsrc( self.config['url_seriesBanner'] % (sid) )
|
||||
banners = {}
|
||||
bid_order = {'fanart': [], 'poster': [], 'series': [], 'season': []}
|
||||
for cur_banner in bannersEt.findall('Banner'):
|
||||
bid = cur_banner.find('id').text
|
||||
btype = cur_banner.find('BannerType')
|
||||
btype2 = cur_banner.find('BannerType2')
|
||||
if btype is None or btype2 is None:
|
||||
continue
|
||||
btype, btype2 = btype.text, btype2.text
|
||||
if not btype in banners:
|
||||
banners[btype] = {}
|
||||
if not btype2 in banners[btype]:
|
||||
banners[btype][btype2] = {}
|
||||
if not bid in banners[btype][btype2]:
|
||||
banners[btype][btype2][bid] = {}
|
||||
if btype in bid_order.keys():
|
||||
if btype2 != u'blank':
|
||||
bid_order[btype].append([bid, btype2])
|
||||
|
||||
self.log.debug("Banner: %s", bid)
|
||||
for cur_element in cur_banner.getchildren():
|
||||
tag = cur_element.tag.lower()
|
||||
value = cur_element.text
|
||||
if tag is None or value is None:
|
||||
continue
|
||||
tag, value = tag.lower(), value.lower()
|
||||
self.log.debug("Banner info: %s = %s" % (tag, value))
|
||||
banners[btype][btype2][bid][tag] = value
|
||||
|
||||
for k, v in banners[btype][btype2][bid].items():
|
||||
if k.endswith("path"):
|
||||
new_key = "_%s" % (k)
|
||||
self.log.debug("Transforming %s to %s" % (k, new_key))
|
||||
new_url = self.config['url_artworkPrefix'] % (v)
|
||||
self.log.debug("New banner URL: %s" % (new_url))
|
||||
banners[btype][btype2][bid][new_key] = new_url
|
||||
|
||||
graphics_in_order = {'fanart': [], 'poster': [], 'series': [], 'season': []}
|
||||
for key in bid_order.keys():
|
||||
for bid in bid_order[key]:
|
||||
graphics_in_order[key].append(banners[key][bid[1]][bid[0]])
|
||||
return graphics_in_order
|
||||
# end ttvdb_parseBanners()
|
||||
|
||||
|
||||
def _parseActors(self, sid):
|
||||
"""Parsers actors XML, from
|
||||
http://www.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)
|
||||
<class 'tvdb_api.Actors'>
|
||||
>>> type(actors[0])
|
||||
<class 'tvdb_api.Actor'>
|
||||
>>> actors[0]
|
||||
<Actor "Zach Braff">
|
||||
>>> sorted(actors[0].keys())
|
||||
['id', 'image', 'name', 'role', 'sortorder']
|
||||
>>> actors[0]['name']
|
||||
u'Zach Braff'
|
||||
>>> actors[0]['image']
|
||||
'http://www.thetvdb.com/banners/actors/43640.jpg'
|
||||
|
||||
Any key starting with an underscore has been processed (not the raw
|
||||
data from the XML)
|
||||
"""
|
||||
self.log.debug("Getting actors for %s" % (sid))
|
||||
actorsEt = self._getetsrc(self.config['url_actorsInfo'] % (sid))
|
||||
|
||||
cur_actors = Actors()
|
||||
for curActorItem in actorsEt.findall("Actor"):
|
||||
curActor = Actor()
|
||||
for curInfo in curActorItem:
|
||||
tag = curInfo.tag.lower()
|
||||
value = curInfo.text
|
||||
if value is not None:
|
||||
if tag == "image":
|
||||
value = self.config['url_artworkPrefix'] % (value)
|
||||
else:
|
||||
value = self._cleanData(value)
|
||||
curActor[tag] = value
|
||||
cur_actors.append(curActor)
|
||||
self._setShowData(sid, '_actors', cur_actors)
|
||||
|
||||
def _getShowData(self, sid):
|
||||
"""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]
|
||||
"""
|
||||
|
||||
# Parse show information
|
||||
self.log.debug('Getting all series data for %s' % (sid))
|
||||
seriesInfoEt = self._getetsrc(self.config['url_seriesInfo'] % (sid))
|
||||
for curInfo in seriesInfoEt.findall("Series")[0]:
|
||||
tag = curInfo.tag.lower()
|
||||
value = curInfo.text
|
||||
|
||||
if value is not None:
|
||||
if tag in ['banner', 'fanart', 'poster']:
|
||||
value = self.config['url_artworkPrefix'] % (value)
|
||||
else:
|
||||
value = self._cleanData(value)
|
||||
|
||||
self._setShowData(sid, tag, value)
|
||||
self.log.debug("Got info: %s = %s" % (tag, value))
|
||||
#end for series
|
||||
|
||||
# Parse banners
|
||||
if self.config['banners_enabled']:
|
||||
self._parseBanners(sid)
|
||||
|
||||
# Parse actors
|
||||
if self.config['actors_enabled']:
|
||||
self._parseActors(sid)
|
||||
|
||||
# Parse episode data
|
||||
self.log.debug('Getting all episodes of %s' % (sid))
|
||||
epsEt = self._getetsrc( self.config['url_epInfo'] % (sid) )
|
||||
|
||||
for cur_ep in epsEt.findall("Episode"):
|
||||
seas_no = int(cur_ep.find('SeasonNumber').text)
|
||||
ep_no = int(cur_ep.find('EpisodeNumber').text)
|
||||
for cur_item in cur_ep.getchildren():
|
||||
tag = cur_item.tag.lower()
|
||||
value = cur_item.text
|
||||
if value is not None:
|
||||
if tag == 'filename':
|
||||
value = self.config['url_artworkPrefix'] % (value)
|
||||
else:
|
||||
value = self._cleanData(value)
|
||||
self._setItem(sid, seas_no, ep_no, tag, value)
|
||||
#end for cur_ep
|
||||
#end _geEps
|
||||
|
||||
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:
|
||||
self.log.debug('Correcting %s to %s' % (name, self.corrections[name]) )
|
||||
sid = self.corrections[name]
|
||||
else:
|
||||
self.log.debug('Getting show %s' % (name))
|
||||
selected_series = self._getSeries( name )
|
||||
sname, sid = selected_series['name'], selected_series['sid']
|
||||
self.log.debug('Got %s, sid %s' % (sname, sid))
|
||||
|
||||
self.corrections[name] = sid
|
||||
self._getShowData(sid)
|
||||
#end if name in self.corrections
|
||||
return sid
|
||||
#end _nameToSid
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Handles tvdb_instance['seriesname'] calls.
|
||||
The dict index should be the show id
|
||||
"""
|
||||
if isinstance(key, (int, long)):
|
||||
# Item is integer, treat as show id
|
||||
if key not in self.shows:
|
||||
self._getShowData(key)
|
||||
return self.shows[key]
|
||||
|
||||
key = key.lower() # make key lower case
|
||||
sid = self._nameToSid(key)
|
||||
self.log.debug('Got series id %s' % (sid))
|
||||
return self.shows[sid]
|
||||
#end __getitem__
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.shows)
|
||||
#end __repr__
|
||||
#end Tvdb
|
||||
|
||||
def main():
|
||||
"""Simple example of using tvdb_api - it just
|
||||
grabs an episode name interactively.
|
||||
"""
|
||||
tvdb_instance = Tvdb(interactive=True, debug=True, cache=False)
|
||||
print tvdb_instance['Lost']['seriesname']
|
||||
print tvdb_instance['Lost'][1][4]['episodename']
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env python
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben
|
||||
#project:tvdb_api
|
||||
#repository:http://github.com/dbr/tvdb_api
|
||||
#license:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
|
||||
"""Custom exceptions used or raised by tvdb_api
|
||||
"""
|
||||
|
||||
__author__ = "dbr/Ben"
|
||||
__version__ = "1.2.1"
|
||||
|
||||
__all__ = ["tvdb_error", "tvdb_userabort", "tvdb_shownotfound",
|
||||
"tvdb_seasonnotfound", "tvdb_episodenotfound", "tvdb_attributenotfound"]
|
||||
|
||||
class tvdb_error(Exception):
|
||||
"""An error with www.thetvdb.com (Cannot connect, for example)
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_userabort(Exception):
|
||||
"""User aborted the interactive selection (via
|
||||
the q command, ^c etc)
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_shownotfound(Exception):
|
||||
"""Show cannot be found on www.thetvdb.com (non-existant show)
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_seasonnotfound(Exception):
|
||||
"""Season cannot be found on www.thetvdb.com
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_episodenotfound(Exception):
|
||||
"""Episode cannot be found on www.thetvdb.com
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_attributenotfound(Exception):
|
||||
"""Raised if an episode does not have the requested
|
||||
attribute (such as a episode name)
|
||||
"""
|
||||
pass
|
|
@ -0,0 +1,124 @@
|
|||
#!/usr/bin/env python
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben
|
||||
#project:tvdb_api
|
||||
#repository:http://github.com/dbr/tvdb_api
|
||||
#license:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
|
||||
"""Contains included user interfaces for Tvdb show selection.
|
||||
|
||||
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)
|
||||
<class 'tvdb_api.Show'>
|
||||
"""
|
||||
|
||||
__author__ = "dbr/Ben"
|
||||
__version__ = "1.2.1"
|
||||
|
||||
from tvdb_exceptions import tvdb_userabort
|
||||
|
||||
class BaseUI:
|
||||
"""Default non-interactive UI, which auto-selects first results
|
||||
"""
|
||||
def __init__(self, config, log):
|
||||
self.config = config
|
||||
self.log = log
|
||||
|
||||
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):
|
||||
"""Helper function, lists series with corresponding ID
|
||||
"""
|
||||
print "TVDB Search Results:"
|
||||
for i in range(len(allSeries[:6])): # list first 6 search results
|
||||
i_show = i + 1 # Start at more human readable number 1 (not 0)
|
||||
self.log.debug('Showing allSeries[%s] = %s)' % (i_show, allSeries[i]))
|
||||
print "%s -> %s # http://thetvdb.com/?tab=series&id=%s" % (
|
||||
i_show,
|
||||
allSeries[i]['name'].encode("UTF-8","ignore"),
|
||||
allSeries[i]['sid'].encode("UTF-8","ignore")
|
||||
)
|
||||
|
||||
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, ? for help):"
|
||||
ans = raw_input()
|
||||
except KeyboardInterrupt:
|
||||
raise tvdb_userabort("User aborted (^c keyboard interupt)")
|
||||
except EOFError:
|
||||
raise tvdb_userabort("User aborted (EOF received)")
|
||||
|
||||
self.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 ans == "q":
|
||||
self.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 "# ? - this help"
|
||||
print "# q - abort tvnamer"
|
||||
else:
|
||||
self.log.debug('Unknown keypress %s' % (ans))
|
||||
else:
|
||||
self.log.debug('Trying to return ID: %d' % (selected_id))
|
||||
try:
|
||||
return allSeries[ selected_id ]
|
||||
except IndexError:
|
||||
self.log.debug('Invalid show number entered!')
|
||||
print "Invalid number (%s) selected!"
|
||||
self._displaySeries(allSeries)
|
||||
#end try
|
||||
#end while not valid_input
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
#!/usr/bin/env python
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben
|
||||
#project:tvdb_api
|
||||
#repository:http://github.com/dbr/tvdb_api
|
||||
#license:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
|
||||
"""
|
||||
urllib2 caching handler
|
||||
Modified from http://code.activestate.com/recipes/491261/
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
|
||||
__author__ = "dbr/Ben"
|
||||
__version__ = "1.2.1"
|
||||
|
||||
import os
|
||||
import time
|
||||
import errno
|
||||
import httplib
|
||||
import urllib2
|
||||
import StringIO
|
||||
from hashlib import md5
|
||||
from threading import RLock
|
||||
|
||||
cache_lock = RLock()
|
||||
|
||||
def locked_function(origfunc):
|
||||
"""Decorator to execute function under lock"""
|
||||
def wrapped(*args, **kwargs):
|
||||
cache_lock.acquire()
|
||||
try:
|
||||
return origfunc(*args, **kwargs)
|
||||
finally:
|
||||
cache_lock.release()
|
||||
return wrapped
|
||||
|
||||
def calculate_cache_path(cache_location, url):
|
||||
"""Checks if [cache_location]/[hash_of_url].headers and .body exist
|
||||
"""
|
||||
thumb = md5(url).hexdigest()
|
||||
header = os.path.join(cache_location, thumb + ".headers")
|
||||
body = os.path.join(cache_location, thumb + ".body")
|
||||
return header, body
|
||||
|
||||
def check_cache_time(path, max_age):
|
||||
"""Checks if a file has been created/modified in the [last max_age] seconds.
|
||||
False means the file is too old (or doesn't exist), True means it is
|
||||
up-to-date and valid"""
|
||||
if not os.path.isfile(path):
|
||||
return False
|
||||
cache_modified_time = os.stat(path).st_mtime
|
||||
time_now = time.time()
|
||||
if cache_modified_time < time_now - max_age:
|
||||
# Cache is old
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
@locked_function
|
||||
def exists_in_cache(cache_location, url, max_age):
|
||||
"""Returns if header AND body cache file exist (and are up-to-date)"""
|
||||
hpath, bpath = calculate_cache_path(cache_location, url)
|
||||
if os.path.exists(hpath) and os.path.exists(bpath):
|
||||
return(
|
||||
check_cache_time(hpath, max_age)
|
||||
and check_cache_time(bpath, max_age)
|
||||
)
|
||||
else:
|
||||
# File does not exist
|
||||
return False
|
||||
|
||||
@locked_function
|
||||
def store_in_cache(cache_location, url, response):
|
||||
"""Tries to store response in cache."""
|
||||
hpath, bpath = calculate_cache_path(cache_location, url)
|
||||
try:
|
||||
outf = open(hpath, "w")
|
||||
headers = str(response.info())
|
||||
outf.write(headers)
|
||||
outf.close()
|
||||
|
||||
outf = open(bpath, "w")
|
||||
outf.write(response.read())
|
||||
outf.close()
|
||||
except IOError:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
class CacheHandler(urllib2.BaseHandler):
|
||||
"""Stores responses in a persistant on-disk cache.
|
||||
|
||||
If a subsequent GET request is made for the same URL, the stored
|
||||
response is returned, saving time, resources and bandwidth
|
||||
"""
|
||||
@locked_function
|
||||
def __init__(self, cache_location, max_age = 21600):
|
||||
"""The location of the cache directory"""
|
||||
self.max_age = max_age
|
||||
self.cache_location = cache_location
|
||||
if not os.path.exists(self.cache_location):
|
||||
try:
|
||||
os.mkdir(self.cache_location)
|
||||
except OSError, e:
|
||||
if e.errno == errno.EEXIST and os.path.isdir(self.cache_location):
|
||||
# File exists, and it's a directory,
|
||||
# another process beat us to creating this dir, that's OK.
|
||||
pass
|
||||
else:
|
||||
# Our target dir is already a file, or different error,
|
||||
# relay the error!
|
||||
raise OSError(e)
|
||||
|
||||
def default_open(self, request):
|
||||
"""Handles GET requests, if the response is cached it returns it
|
||||
"""
|
||||
if request.get_method() is not "GET":
|
||||
return None # let the next handler try to handle the request
|
||||
|
||||
if exists_in_cache(
|
||||
self.cache_location, request.get_full_url(), self.max_age
|
||||
):
|
||||
return CachedResponse(
|
||||
self.cache_location,
|
||||
request.get_full_url(),
|
||||
set_cache_header = True
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
def http_response(self, request, response):
|
||||
"""Gets a HTTP response, if it was a GET request and the status code
|
||||
starts with 2 (200 OK etc) it caches it and returns a CachedResponse
|
||||
"""
|
||||
if (request.get_method() == "GET"
|
||||
and str(response.code).startswith("2")
|
||||
):
|
||||
if 'x-local-cache' not in response.info():
|
||||
# Response is not cached
|
||||
set_cache_header = store_in_cache(
|
||||
self.cache_location,
|
||||
request.get_full_url(),
|
||||
response
|
||||
)
|
||||
else:
|
||||
set_cache_header = True
|
||||
#end if x-cache in response
|
||||
|
||||
return CachedResponse(
|
||||
self.cache_location,
|
||||
request.get_full_url(),
|
||||
set_cache_header = set_cache_header
|
||||
)
|
||||
else:
|
||||
return response
|
||||
|
||||
class CachedResponse(StringIO.StringIO):
|
||||
"""An urllib2.response-like object for cached responses.
|
||||
|
||||
To determine if a response is cached or coming directly from
|
||||
the network, check the x-local-cache header rather than the object type.
|
||||
"""
|
||||
|
||||
@locked_function
|
||||
def __init__(self, cache_location, url, set_cache_header=True):
|
||||
self.cache_location = cache_location
|
||||
hpath, bpath = calculate_cache_path(cache_location, url)
|
||||
|
||||
StringIO.StringIO.__init__(self, file(bpath).read())
|
||||
|
||||
self.url = url
|
||||
self.code = 200
|
||||
self.msg = "OK"
|
||||
headerbuf = file(hpath).read()
|
||||
if set_cache_header:
|
||||
headerbuf += "x-local-cache: %s\r\n" % (bpath)
|
||||
self.headers = httplib.HTTPMessage(StringIO.StringIO(headerbuf))
|
||||
|
||||
def info(self):
|
||||
"""Returns headers
|
||||
"""
|
||||
return self.headers
|
||||
|
||||
def geturl(self):
|
||||
"""Returns original URL
|
||||
"""
|
||||
return self.url
|
||||
|
||||
@locked_function
|
||||
def recache(self):
|
||||
new_request = urllib2.urlopen(self.url)
|
||||
set_cache_header = store_in_cache(
|
||||
self.cache_location,
|
||||
new_request.url,
|
||||
new_request
|
||||
)
|
||||
CachedResponse.__init__(self, self.cache_location, self.url, True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
def main():
|
||||
"""Quick test/example of CacheHandler"""
|
||||
opener = urllib2.build_opener(CacheHandler("/tmp/"))
|
||||
response = opener.open("http://google.com")
|
||||
print response.headers
|
||||
print "Response:", response.read()
|
||||
|
||||
response.recache()
|
||||
print response.headers
|
||||
print "After recache:", response.read()
|
||||
|
||||
# Test usage in threads
|
||||
from threading import Thread
|
||||
class CacheThreadTest(Thread):
|
||||
lastdata = None
|
||||
def run(self):
|
||||
req = opener.open("http://google.com")
|
||||
newdata = req.read()
|
||||
if self.lastdata is None:
|
||||
self.lastdata = newdata
|
||||
assert self.lastdata == newdata, "Data was not consistent, uhoh"
|
||||
req.recache()
|
||||
threads = [CacheThreadTest() for x in range(50)]
|
||||
print "Starting threads"
|
||||
[t.start() for t in threads]
|
||||
print "..done"
|
||||
print "Joining threads"
|
||||
[t.join() for t in threads]
|
||||
print "..done"
|
||||
main()
|
|
@ -0,0 +1,109 @@
|
|||
[File ttvdb-example.conf]
|
||||
#-------------------------------------
|
||||
# Name: ttvdb-example.conf
|
||||
# Project: ttvdb
|
||||
# Configuration file
|
||||
# Author: R.D. Vaughan
|
||||
# Version: 0.1.0 - Initial alpha release
|
||||
# Version: 0.8.9 - version changed to match the current ttvdb release number
|
||||
# Version: 0.9.5 - Changed regex pattern strings to support multi-langiage file names
|
||||
#
|
||||
# License:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
#-------------------------------------
|
||||
#
|
||||
# START season name overrides section --------------------------------------------------------------
|
||||
#####
|
||||
# PURPOSE: TV program sources such as Schedule Direct series names do not always match the series
|
||||
# names on thetvdb.com. This section allow you to override series names to reduce the
|
||||
# need for manual interaction and more accurate matching with thetvdb.com TV series wiki.
|
||||
#
|
||||
# FORMAT:
|
||||
# Any line starting with "#" is treated as a comment
|
||||
# Any blank line is ignored
|
||||
# All other lines must have two fields in this specific order:
|
||||
# 1st) The series name or the %TITLE% field as passed by MythTV and a trailing ':' character
|
||||
# 2nd) The thetvdb.com series id (SID) as specified for the series on thetvdb.com wiki. This will
|
||||
# override the series name to eliminate the need for manual interaction.
|
||||
# NOTE: Included here. but disabled are examples of current (2009) TV shows that Schedule Direct
|
||||
# has different TV series names then what is on thetvdb.com wiki. If you are searching for
|
||||
# series based on the Schedule Direct names as MythTV does then the wrong TV series will be
|
||||
# identified. You can pick the correct series by using interactive mode but this does not
|
||||
# help with unattended processing. This section allows you to specify the correct TV series
|
||||
# on thetvbd.com despite the name in Schedule Direct. Luckily there are not many TV series
|
||||
# that require overrides.
|
||||
#####
|
||||
[series_name_override]
|
||||
# Specify recorded "Life On Mars" shows as the US version
|
||||
#Life on Mars:82289
|
||||
# For overnight episode updates when a filename is used
|
||||
#Life on Mars (US):82289
|
||||
|
||||
# Specify recorded "Eleventh Hour" shows as the US version
|
||||
#Eleventh Hour:83066
|
||||
# For overnight episode updates when a filename is used
|
||||
#Eleventh Hour (US):83066
|
||||
|
||||
# Specify recorded "Frontline" or "Frontline/World" shows as the "Frontline PBS" version
|
||||
#Frontline/World:80646
|
||||
#Frontline:80646
|
||||
|
||||
# Specify recorded "The Beast" shows are the 2009 version
|
||||
#The Beast:83805
|
||||
# For overnight episode updates when a filename is used
|
||||
#The Beast (2009):83805
|
||||
|
||||
# Specify recorded "Castle" shows are the 2009 version
|
||||
#Castle:83462
|
||||
# For overnight episode updates when a filename is used
|
||||
#Castle (2009):83462
|
||||
|
||||
# Specify recorded "Battlestar Galactica" shows are the 2003 version
|
||||
#Battlestar Galactica:73545
|
||||
# For overnight episode updates when a filename is used
|
||||
#Battlestar Galactica (2003):73545
|
||||
# END season name overrides section --------------------------------------------------------------
|
||||
|
||||
# START episode name massaging section --------------------------------------------------------------
|
||||
#####
|
||||
# PURPOSE: TV program sources such as Schedule Direct episode names do not always match the episode
|
||||
# names on thetvdb.com. This section allow you to massage episode names to reduce the
|
||||
# need for manual interaction and more accurate matching with thetvdb.com TV series wiki.
|
||||
# Alter the episode names for a series to reduce the need for manual interaction
|
||||
# and more accurate matching with thetvdb.com TV series wiki. See example below.
|
||||
#
|
||||
# FORMAT:
|
||||
# All lines must in the following format:
|
||||
# 1st) The series name or the %TITLE% field as passed by MythTV and a trailing ':' character
|
||||
# 2nd) Pairs of values separated by commas. The first value is the search text to match to text within
|
||||
# the episode name such as the %SUBTITLE% field passed by MythTV and the text to replace the
|
||||
# matched searched text. If the replacement text contains a space then surround that text with
|
||||
# the '"' double quotation characters.
|
||||
# E.g. "24": PM," PM", AM, " AM" will turn the episode name
|
||||
# "Day 7: 11:00AM to 12:00PM" into "Day 7: 11:00 AM to 12:00 PM"
|
||||
#
|
||||
#####
|
||||
[ep_name_massage]
|
||||
#24: PM," PM", AM, " AM",M-,"M - "
|
||||
# END episode name massaging section --------------------------------------------------------------
|
||||
|
||||
# START regex section------------------------------------------------------------------------------
|
||||
#####
|
||||
# NOTE: If you do not understand regex expressions DO NOT USE this section.
|
||||
# PURPOSE: This section adds regex strings used to parse video file names when extracting
|
||||
# the following: series name, season number, episode number. Essential when downloading
|
||||
# metadata from mythtvfrontend->mythvideo when using ttvdb. You only need to add a regex
|
||||
# string if ttvdb cannot extract the required information from your video file names.
|
||||
# NOTE: ANY % percent sign in the expression must be doubled (e.g. a % must be changed to %% )
|
||||
# NOTE: The key value (e.g. "regex##") must be unique for each value.
|
||||
# NOTE: DO NOT surround the regex pattern string with the traditional ''' three single quotes
|
||||
#
|
||||
# "regex01" is an example video file name "foo_S01_12" where:
|
||||
# series name is "foo", season number starts at "S" and episode number starts after '_'
|
||||
# foo_[s01]_[e01]
|
||||
#####
|
||||
[regex]
|
||||
# foo_S01_12
|
||||
regex01: ^(.+?)[ \._\-][Ss]([0-9]+)_([0-9]+)[^\\/]*$
|
||||
# END regex section------------------------------------------------------------------------------
|
||||
|
|
@ -0,0 +1,805 @@
|
|||
#!/usr/bin/env python
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben
|
||||
#project:tvdb_api
|
||||
#repository:http://github.com/dbr/tvdb_api
|
||||
#license:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
|
||||
"""Simple-to-use Python interface to The TVDB's API (www.thetvdb.com)
|
||||
|
||||
Example usage:
|
||||
|
||||
>>> from tvdb_api import Tvdb
|
||||
>>> t = Tvdb()
|
||||
>>> t['Lost'][4][11]['episodename']
|
||||
u'Cabin Fever'
|
||||
"""
|
||||
__author__ = "dbr/Ben"
|
||||
__version__ = "1.2.1"
|
||||
|
||||
import os
|
||||
import sys
|
||||
import urllib
|
||||
import urllib2
|
||||
import tempfile
|
||||
import logging
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as ElementTree
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ElementTree
|
||||
|
||||
from cache import CacheHandler
|
||||
|
||||
from tvdb_ui import BaseUI, ConsoleUI
|
||||
from tvdb_exceptions import (tvdb_error, tvdb_userabort, tvdb_shownotfound,
|
||||
tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound)
|
||||
|
||||
class ShowContainer(dict):
|
||||
"""Simple dict that holds a series of Show instances
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Show(dict):
|
||||
"""Holds a dict of seasons, and show data.
|
||||
"""
|
||||
def __init__(self):
|
||||
dict.__init__(self)
|
||||
self.data = {}
|
||||
|
||||
def __repr__(self):
|
||||
return "<Show %s (containing %s seasons)>" % (
|
||||
self.data.get(u'seriesname', 'instance'),
|
||||
len(self)
|
||||
)
|
||||
|
||||
def __getitem__(self, 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 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")
|
||||
[<Episode 01x01 - My First Day>]
|
||||
>>>
|
||||
|
||||
Search for "My Name Is Earl" episode named "Faked His Own Death":
|
||||
|
||||
>>> t['My Name Is Earl'].search('Faked His Own Death', key = 'episodename')
|
||||
[<Episode 01x04 - Faked His Own Death>]
|
||||
>>>
|
||||
|
||||
To search Scrubs for all episodes with "mentor" in the episode name:
|
||||
|
||||
>>> t['scrubs'].search('mentor', key = 'episodename')
|
||||
[<Episode 01x02 - My Mentor>, <Episode 03x15 - My Tormented Mentor>]
|
||||
>>>
|
||||
|
||||
# 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)
|
||||
#end for cur_season
|
||||
return results
|
||||
|
||||
|
||||
class Season(dict):
|
||||
def __repr__(self):
|
||||
return "<Season instance (containing %s episodes)>" % (
|
||||
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')
|
||||
[<Episode 01x01 - My 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 __repr__(self):
|
||||
seasno = int(self.get(u'seasonnumber', 0))
|
||||
epno = int(self.get(u'episodenumber', 0))
|
||||
epname = self.get(u'episodename')
|
||||
if epname is not None:
|
||||
return "<Episode %02dx%02d - %s>" % (seasno, epno, epname)
|
||||
else:
|
||||
return "<Episode %02dx%02d>" % (seasno, epno)
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return dict.__getitem__(self, key)
|
||||
except KeyError:
|
||||
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")
|
||||
<Episode 00x00 - An Example>
|
||||
>>>
|
||||
|
||||
Limiting by key:
|
||||
|
||||
>>> e.search("examp", key = "episodename")
|
||||
<Episode 00x00 - An Example>
|
||||
>>>
|
||||
"""
|
||||
if term == None:
|
||||
raise TypeError("must supply string to search for (contents)")
|
||||
|
||||
term = unicode(term).lower()
|
||||
for cur_key, cur_value in self.items():
|
||||
cur_key, cur_value = unicode(cur_key).lower(), unicode(cur_value).lower()
|
||||
if key is not None and cur_key != key:
|
||||
# Do not search this key
|
||||
continue
|
||||
if cur_value.find( unicode(term).lower() ) > -1:
|
||||
return self
|
||||
#end if cur_value.find()
|
||||
#end for cur_key, cur_value
|
||||
|
||||
|
||||
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 "<Actor \"%s\">" % (self.get("name"))
|
||||
|
||||
|
||||
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):
|
||||
"""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):
|
||||
shows verbose debugging information
|
||||
|
||||
cache (True/False/str/unicode):
|
||||
Retrieved XML are persisted to to disc. If true, stores in tvdb_api
|
||||
folder under your systems TEMP_DIR, if set to str/unicode instance it
|
||||
will use this as the cache location. If False, disables caching.
|
||||
|
||||
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()
|
||||
['fanart', 'poster', 'series', 'season']
|
||||
|
||||
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'Zach Braff'
|
||||
|
||||
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
|
||||
"""
|
||||
self.shows = ShowContainer() # Holds all Show classes
|
||||
self.corrections = {} # Holds show-name to show_id mapping
|
||||
|
||||
self.config = {}
|
||||
|
||||
if apikey is not None:
|
||||
self.config['apikey'] = apikey
|
||||
else:
|
||||
self.config['apikey'] = "0629B785CE550C8D" # tvdb_api's API key
|
||||
|
||||
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
|
||||
|
||||
if cache is True:
|
||||
self.config['cache_enabled'] = True
|
||||
self.config['cache_location'] = self._getTempDir()
|
||||
elif isinstance(cache, basestring):
|
||||
self.config['cache_enabled'] = True
|
||||
self.config['cache_location'] = cache
|
||||
else:
|
||||
self.config['cache_enabled'] = False
|
||||
|
||||
if self.config['cache_enabled']:
|
||||
self.urlopener = urllib2.build_opener(
|
||||
CacheHandler(self.config['cache_location'])
|
||||
)
|
||||
else:
|
||||
self.urlopener = urllib2.build_opener()
|
||||
|
||||
self.config['banners_enabled'] = banners
|
||||
self.config['actors_enabled'] = actors
|
||||
|
||||
self.log = self._initLogger() # Setups the logger (self.log.debug() etc)
|
||||
|
||||
# List of language from http://www.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"
|
||||
]
|
||||
|
||||
if language is None:
|
||||
self.config['language'] = "en"
|
||||
elif 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://www.thetvdb.com"
|
||||
|
||||
if self.config['search_all_languages']:
|
||||
self.config['url_getSeries'] = "%(base_url)s/api/GetSeries.php?seriesname=%%s&language=all" % self.config
|
||||
else:
|
||||
self.config['url_getSeries'] = "%(base_url)s/api/GetSeries.php?seriesname=%%s&language=%(language)s" % self.config
|
||||
|
||||
self.config['url_epInfo'] = "%(base_url)s/api/%(apikey)s/series/%%s/all/%(language)s.xml" % self.config
|
||||
|
||||
self.config['url_seriesInfo'] = "%(base_url)s/api/%(apikey)s/series/%%s/%(language)s.xml" % self.config
|
||||
self.config['url_actorsInfo'] = "%(base_url)s/api/%(apikey)s/series/%%s/actors.xml" % self.config
|
||||
|
||||
self.config['url_seriesBanner'] = "%(base_url)s/api/%(apikey)s/series/%%s/banners.xml" % self.config
|
||||
self.config['url_artworkPrefix'] = "%(base_url)s/banners/%%s" % self.config
|
||||
|
||||
#end __init__
|
||||
|
||||
def _initLogger(self):
|
||||
"""Setups a logger using the logging module, returns a log object
|
||||
"""
|
||||
logger = logging.getLogger("tvdb")
|
||||
formatter = logging.Formatter('%(asctime)s) %(levelname)s %(message)s')
|
||||
|
||||
hdlr = logging.StreamHandler(sys.stdout)
|
||||
|
||||
hdlr.setFormatter(formatter)
|
||||
logger.addHandler(hdlr)
|
||||
|
||||
if self.config['debug_enabled']:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
logger.setLevel(logging.WARNING)
|
||||
return logger
|
||||
#end initLogger
|
||||
|
||||
def _getTempDir(self):
|
||||
"""Returns the [system temp dir]/tvdb_api
|
||||
"""
|
||||
return os.path.join(tempfile.gettempdir(), "tvdb_api")
|
||||
|
||||
def _loadUrl(self, url, recache = False):
|
||||
try:
|
||||
self.log.debug("Retrieving URL %s" % url)
|
||||
resp = self.urlopener.open(url)
|
||||
if 'x-local-cache' in resp.headers:
|
||||
self.log.debug("URL %s was cached in %s" % (
|
||||
url,
|
||||
resp.headers['x-local-cache'])
|
||||
)
|
||||
if recache:
|
||||
self.log.debug("Attempting to recache %s" % url)
|
||||
resp.recache()
|
||||
except urllib2.URLError, errormsg:
|
||||
raise tvdb_error("Could not connect to server: %s" % (errormsg))
|
||||
#end try
|
||||
|
||||
return resp.read()
|
||||
|
||||
def _getetsrc(self, url):
|
||||
"""Loads a URL sing caching, returns an ElementTree of the source
|
||||
"""
|
||||
src = self._loadUrl(url)
|
||||
try:
|
||||
return ElementTree.fromstring(src)
|
||||
except SyntaxError:
|
||||
src = self._loadUrl(url, recache=True)
|
||||
try:
|
||||
return ElementTree.fromstring(src)
|
||||
except SyntaxError, exceptionmsg:
|
||||
errormsg = "There was an error with the XML retrieved from thetvdb.com:\n%s" % (
|
||||
exceptionmsg
|
||||
)
|
||||
|
||||
if self.config['cache_enabled']:
|
||||
errormsg += "\nFirst try emptying the cache folder at..\n%s" % (
|
||||
self.config['cache_location']
|
||||
)
|
||||
|
||||
errormsg += "\nIf this does not resolve the issue, please try again later. If the error persists, report a bug on"
|
||||
errormsg += "\nhttp://dbr.lighthouseapp.com/projects/13342-tvdb_api/overview\n"
|
||||
raise tvdb_error(errormsg)
|
||||
#end _getetsrc
|
||||
|
||||
def _setItem(self, sid, seas, ep, attrib, value):
|
||||
"""Creates a new episode, creating Show(), Season() and
|
||||
Episode()s as required. Called by _getShowData to populute
|
||||
|
||||
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()
|
||||
if ep not in self.shows[sid][seas]:
|
||||
self.shows[sid][seas][ep] = Episode()
|
||||
self.shows[sid][seas][ep][attrib] = value
|
||||
#end _set_item
|
||||
|
||||
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 _cleanData(self, data):
|
||||
"""Cleans up strings returned by TheTVDB.com
|
||||
|
||||
Issues corrected:
|
||||
- Replaces & with &
|
||||
- Trailing whitespace
|
||||
"""
|
||||
data = data.replace(u"&", u"&")
|
||||
data = data.strip()
|
||||
return data
|
||||
#end _cleanData
|
||||
|
||||
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.
|
||||
"""
|
||||
series = urllib.quote(series.encode("utf-8"))
|
||||
self.log.debug("Searching for show %s" % series)
|
||||
seriesEt = self._getetsrc(self.config['url_getSeries'] % (series))
|
||||
allSeries = []
|
||||
for series in seriesEt:
|
||||
sn = series.find('SeriesName')
|
||||
value = self._cleanData(sn.text)
|
||||
cur_sid = series.find('id').text
|
||||
self.log.debug('Found series %s (id: %s)' % (value, cur_sid))
|
||||
allSeries.append( {'sid':cur_sid, 'name':value} )
|
||||
#end for series
|
||||
|
||||
if len(allSeries) == 0:
|
||||
self.log.debug('Series result returned zero')
|
||||
raise tvdb_shownotfound("Show-name search returned zero results (cannot find show on TVDB)")
|
||||
|
||||
if self.config['custom_ui'] is not None:
|
||||
self.log.debug("Using custom UI %s" % (repr(self.config['custom_ui'])))
|
||||
ui = self.config['custom_ui'](config = self.config, log = self.log)
|
||||
else:
|
||||
if not self.config['interactive']:
|
||||
self.log.debug('Auto-selecting first search result using BaseUI')
|
||||
ui = BaseUI(config = self.config, log = self.log)
|
||||
else:
|
||||
self.log.debug('Interactivily selecting show using ConsoleUI')
|
||||
ui = ConsoleUI(config = self.config, log = self.log)
|
||||
#end if config['interactive]
|
||||
#end if custom_ui != None
|
||||
|
||||
return ui.selectSeries(allSeries)
|
||||
|
||||
#end _getSeries
|
||||
|
||||
def _parseBanners(self, sid):
|
||||
"""Parses banners XML, from
|
||||
http://www.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()
|
||||
['fanart', 'poster', 'series', 'season']
|
||||
>>> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath']
|
||||
'http://www.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.
|
||||
"""
|
||||
self.log.debug('Getting season banners for %s' % (sid))
|
||||
bannersEt = self._getetsrc( self.config['url_seriesBanner'] % (sid) )
|
||||
banners = {}
|
||||
for cur_banner in bannersEt.findall('Banner'):
|
||||
bid = cur_banner.find('id').text
|
||||
btype = cur_banner.find('BannerType')
|
||||
btype2 = cur_banner.find('BannerType2')
|
||||
if btype is None or btype2 is None:
|
||||
continue
|
||||
btype, btype2 = btype.text, btype2.text
|
||||
if not btype in banners:
|
||||
banners[btype] = {}
|
||||
if not btype2 in banners[btype]:
|
||||
banners[btype][btype2] = {}
|
||||
if not bid in banners[btype][btype2]:
|
||||
banners[btype][btype2][bid] = {}
|
||||
|
||||
self.log.debug("Banner: %s", bid)
|
||||
for cur_element in cur_banner.getchildren():
|
||||
tag = cur_element.tag.lower()
|
||||
value = cur_element.text
|
||||
if tag is None or value is None:
|
||||
continue
|
||||
tag, value = tag.lower(), value.lower()
|
||||
self.log.debug("Banner info: %s = %s" % (tag, value))
|
||||
banners[btype][btype2][bid][tag] = value
|
||||
|
||||
for k, v in banners[btype][btype2][bid].items():
|
||||
if k.endswith("path"):
|
||||
new_key = "_%s" % (k)
|
||||
self.log.debug("Transforming %s to %s" % (k, new_key))
|
||||
new_url = self.config['url_artworkPrefix'] % (v)
|
||||
self.log.debug("New banner URL: %s" % (new_url))
|
||||
banners[btype][btype2][bid][new_key] = new_url
|
||||
|
||||
self._setShowData(sid, "_banners", banners)
|
||||
|
||||
|
||||
# Alternate tvdb_api's method for retrieving graphics URLs but returned as a list that preserves
|
||||
# the user rating order highest rated to lowest rated
|
||||
def ttvdb_parseBanners(self, sid):
|
||||
"""Parses banners XML, from
|
||||
http://www.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()
|
||||
['fanart', 'poster', 'series', 'season']
|
||||
>>> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath']
|
||||
'http://www.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.
|
||||
Changed in this interface is that a list or URLs is created to preserve the user rating order from
|
||||
top rated to lowest rated.
|
||||
"""
|
||||
|
||||
self.log.debug('Getting season banners for %s' % (sid))
|
||||
bannersEt = self._getetsrc( self.config['url_seriesBanner'] % (sid) )
|
||||
banners = {}
|
||||
bid_order = {'fanart': [], 'poster': [], 'series': [], 'season': []}
|
||||
for cur_banner in bannersEt.findall('Banner'):
|
||||
bid = cur_banner.find('id').text
|
||||
btype = cur_banner.find('BannerType')
|
||||
btype2 = cur_banner.find('BannerType2')
|
||||
if btype is None or btype2 is None:
|
||||
continue
|
||||
btype, btype2 = btype.text, btype2.text
|
||||
if not btype in banners:
|
||||
banners[btype] = {}
|
||||
if not btype2 in banners[btype]:
|
||||
banners[btype][btype2] = {}
|
||||
if not bid in banners[btype][btype2]:
|
||||
banners[btype][btype2][bid] = {}
|
||||
if btype in bid_order.keys():
|
||||
if btype2 != u'blank':
|
||||
bid_order[btype].append([bid, btype2])
|
||||
|
||||
self.log.debug("Banner: %s", bid)
|
||||
for cur_element in cur_banner.getchildren():
|
||||
tag = cur_element.tag.lower()
|
||||
value = cur_element.text
|
||||
if tag is None or value is None:
|
||||
continue
|
||||
tag, value = tag.lower(), value.lower()
|
||||
self.log.debug("Banner info: %s = %s" % (tag, value))
|
||||
banners[btype][btype2][bid][tag] = value
|
||||
|
||||
for k, v in banners[btype][btype2][bid].items():
|
||||
if k.endswith("path"):
|
||||
new_key = "_%s" % (k)
|
||||
self.log.debug("Transforming %s to %s" % (k, new_key))
|
||||
new_url = self.config['url_artworkPrefix'] % (v)
|
||||
self.log.debug("New banner URL: %s" % (new_url))
|
||||
banners[btype][btype2][bid][new_key] = new_url
|
||||
|
||||
graphics_in_order = {'fanart': [], 'poster': [], 'series': [], 'season': []}
|
||||
for key in bid_order.keys():
|
||||
for bid in bid_order[key]:
|
||||
graphics_in_order[key].append(banners[key][bid[1]][bid[0]])
|
||||
return graphics_in_order
|
||||
# end ttvdb_parseBanners()
|
||||
|
||||
|
||||
def _parseActors(self, sid):
|
||||
"""Parsers actors XML, from
|
||||
http://www.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)
|
||||
<class 'tvdb_api.Actors'>
|
||||
>>> type(actors[0])
|
||||
<class 'tvdb_api.Actor'>
|
||||
>>> actors[0]
|
||||
<Actor "Zach Braff">
|
||||
>>> sorted(actors[0].keys())
|
||||
['id', 'image', 'name', 'role', 'sortorder']
|
||||
>>> actors[0]['name']
|
||||
u'Zach Braff'
|
||||
>>> actors[0]['image']
|
||||
'http://www.thetvdb.com/banners/actors/43640.jpg'
|
||||
|
||||
Any key starting with an underscore has been processed (not the raw
|
||||
data from the XML)
|
||||
"""
|
||||
self.log.debug("Getting actors for %s" % (sid))
|
||||
actorsEt = self._getetsrc(self.config['url_actorsInfo'] % (sid))
|
||||
|
||||
cur_actors = Actors()
|
||||
for curActorItem in actorsEt.findall("Actor"):
|
||||
curActor = Actor()
|
||||
for curInfo in curActorItem:
|
||||
tag = curInfo.tag.lower()
|
||||
value = curInfo.text
|
||||
if value is not None:
|
||||
if tag == "image":
|
||||
value = self.config['url_artworkPrefix'] % (value)
|
||||
else:
|
||||
value = self._cleanData(value)
|
||||
curActor[tag] = value
|
||||
cur_actors.append(curActor)
|
||||
self._setShowData(sid, '_actors', cur_actors)
|
||||
|
||||
def _getShowData(self, sid):
|
||||
"""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]
|
||||
"""
|
||||
|
||||
# Parse show information
|
||||
self.log.debug('Getting all series data for %s' % (sid))
|
||||
seriesInfoEt = self._getetsrc(self.config['url_seriesInfo'] % (sid))
|
||||
for curInfo in seriesInfoEt.findall("Series")[0]:
|
||||
tag = curInfo.tag.lower()
|
||||
value = curInfo.text
|
||||
|
||||
if value is not None:
|
||||
if tag in ['banner', 'fanart', 'poster']:
|
||||
value = self.config['url_artworkPrefix'] % (value)
|
||||
else:
|
||||
value = self._cleanData(value)
|
||||
|
||||
self._setShowData(sid, tag, value)
|
||||
self.log.debug("Got info: %s = %s" % (tag, value))
|
||||
#end for series
|
||||
|
||||
# Parse banners
|
||||
if self.config['banners_enabled']:
|
||||
self._parseBanners(sid)
|
||||
|
||||
# Parse actors
|
||||
if self.config['actors_enabled']:
|
||||
self._parseActors(sid)
|
||||
|
||||
# Parse episode data
|
||||
self.log.debug('Getting all episodes of %s' % (sid))
|
||||
epsEt = self._getetsrc( self.config['url_epInfo'] % (sid) )
|
||||
|
||||
for cur_ep in epsEt.findall("Episode"):
|
||||
seas_no = int(cur_ep.find('SeasonNumber').text)
|
||||
ep_no = int(cur_ep.find('EpisodeNumber').text)
|
||||
for cur_item in cur_ep.getchildren():
|
||||
tag = cur_item.tag.lower()
|
||||
value = cur_item.text
|
||||
if value is not None:
|
||||
if tag == 'filename':
|
||||
value = self.config['url_artworkPrefix'] % (value)
|
||||
else:
|
||||
value = self._cleanData(value)
|
||||
self._setItem(sid, seas_no, ep_no, tag, value)
|
||||
#end for cur_ep
|
||||
#end _geEps
|
||||
|
||||
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:
|
||||
self.log.debug('Correcting %s to %s' % (name, self.corrections[name]) )
|
||||
sid = self.corrections[name]
|
||||
else:
|
||||
self.log.debug('Getting show %s' % (name))
|
||||
selected_series = self._getSeries( name )
|
||||
sname, sid = selected_series['name'], selected_series['sid']
|
||||
self.log.debug('Got %s, sid %s' % (sname, sid))
|
||||
|
||||
self.corrections[name] = sid
|
||||
self._getShowData(sid)
|
||||
#end if name in self.corrections
|
||||
return sid
|
||||
#end _nameToSid
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Handles tvdb_instance['seriesname'] calls.
|
||||
The dict index should be the show id
|
||||
"""
|
||||
if isinstance(key, (int, long)):
|
||||
# Item is integer, treat as show id
|
||||
if key not in self.shows:
|
||||
self._getShowData(key)
|
||||
return self.shows[key]
|
||||
|
||||
key = key.lower() # make key lower case
|
||||
sid = self._nameToSid(key)
|
||||
self.log.debug('Got series id %s' % (sid))
|
||||
return self.shows[sid]
|
||||
#end __getitem__
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.shows)
|
||||
#end __repr__
|
||||
#end Tvdb
|
||||
|
||||
def main():
|
||||
"""Simple example of using tvdb_api - it just
|
||||
grabs an episode name interactively.
|
||||
"""
|
||||
tvdb_instance = Tvdb(interactive=True, debug=True, cache=False)
|
||||
print tvdb_instance['Lost']['seriesname']
|
||||
print tvdb_instance['Lost'][1][4]['episodename']
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env python
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben
|
||||
#project:tvdb_api
|
||||
#repository:http://github.com/dbr/tvdb_api
|
||||
#license:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
|
||||
"""Custom exceptions used or raised by tvdb_api
|
||||
"""
|
||||
|
||||
__author__ = "dbr/Ben"
|
||||
__version__ = "1.2.1"
|
||||
|
||||
__all__ = ["tvdb_error", "tvdb_userabort", "tvdb_shownotfound",
|
||||
"tvdb_seasonnotfound", "tvdb_episodenotfound", "tvdb_attributenotfound"]
|
||||
|
||||
class tvdb_error(Exception):
|
||||
"""An error with www.thetvdb.com (Cannot connect, for example)
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_userabort(Exception):
|
||||
"""User aborted the interactive selection (via
|
||||
the q command, ^c etc)
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_shownotfound(Exception):
|
||||
"""Show cannot be found on www.thetvdb.com (non-existant show)
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_seasonnotfound(Exception):
|
||||
"""Season cannot be found on www.thetvdb.com
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_episodenotfound(Exception):
|
||||
"""Episode cannot be found on www.thetvdb.com
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_attributenotfound(Exception):
|
||||
"""Raised if an episode does not have the requested
|
||||
attribute (such as a episode name)
|
||||
"""
|
||||
pass
|
|
@ -0,0 +1,124 @@
|
|||
#!/usr/bin/env python
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben
|
||||
#project:tvdb_api
|
||||
#repository:http://github.com/dbr/tvdb_api
|
||||
#license:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
|
||||
"""Contains included user interfaces for Tvdb show selection.
|
||||
|
||||
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)
|
||||
<class 'tvdb_api.Show'>
|
||||
"""
|
||||
|
||||
__author__ = "dbr/Ben"
|
||||
__version__ = "1.2.1"
|
||||
|
||||
from tvdb_exceptions import tvdb_userabort
|
||||
|
||||
class BaseUI:
|
||||
"""Default non-interactive UI, which auto-selects first results
|
||||
"""
|
||||
def __init__(self, config, log):
|
||||
self.config = config
|
||||
self.log = log
|
||||
|
||||
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):
|
||||
"""Helper function, lists series with corresponding ID
|
||||
"""
|
||||
print "TVDB Search Results:"
|
||||
for i in range(len(allSeries[:6])): # list first 6 search results
|
||||
i_show = i + 1 # Start at more human readable number 1 (not 0)
|
||||
self.log.debug('Showing allSeries[%s] = %s)' % (i_show, allSeries[i]))
|
||||
print "%s -> %s # http://thetvdb.com/?tab=series&id=%s" % (
|
||||
i_show,
|
||||
allSeries[i]['name'].encode("UTF-8","ignore"),
|
||||
allSeries[i]['sid'].encode("UTF-8","ignore")
|
||||
)
|
||||
|
||||
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, ? for help):"
|
||||
ans = raw_input()
|
||||
except KeyboardInterrupt:
|
||||
raise tvdb_userabort("User aborted (^c keyboard interupt)")
|
||||
except EOFError:
|
||||
raise tvdb_userabort("User aborted (EOF received)")
|
||||
|
||||
self.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 ans == "q":
|
||||
self.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 "# ? - this help"
|
||||
print "# q - abort tvnamer"
|
||||
else:
|
||||
self.log.debug('Unknown keypress %s' % (ans))
|
||||
else:
|
||||
self.log.debug('Trying to return ID: %d' % (selected_id))
|
||||
try:
|
||||
return allSeries[ selected_id ]
|
||||
except IndexError:
|
||||
self.log.debug('Invalid show number entered!')
|
||||
print "Invalid number (%s) selected!"
|
||||
self._displaySeries(allSeries)
|
||||
#end try
|
||||
#end while not valid_input
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
<?xml version="1.0"?>
|
||||
<window type="window" id="14000">
|
||||
<defaultcontrol always="true">13</defaultcontrol>
|
||||
<allowoverlay>yes</allowoverlay>
|
||||
<onload lang="python"><![CDATA[
|
||||
import mythboxee
|
||||
]]></onload>
|
||||
<controls>
|
||||
<control type="visualisation" id ="5001">
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<visible>Control.IsVisible(2000)</visible>
|
||||
</control>
|
||||
<control type="image">
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<texture>mb_bg.png</texture>
|
||||
<animation effect="fade" start="85" end="85" time="0" condition="true">Conditional</animation>
|
||||
</control>
|
||||
<control type="grouplist" id="1000">
|
||||
<posy>11</posy>
|
||||
<posx>216</posx>
|
||||
<itemgap>20</itemgap>
|
||||
<ondown>2000</ondown>
|
||||
<orientation>horizontal</orientation>
|
||||
</control>
|
||||
|
||||
<!-- SHOW LIST -->
|
||||
|
||||
<control type="panel" id="13">
|
||||
<posx>22</posx>
|
||||
<posy>82</posy>
|
||||
<width>1280</width>
|
||||
<height>592</height>
|
||||
<onleft>122</onleft>
|
||||
<onright>112</onright>
|
||||
<onup>-</onup>
|
||||
<ondown>-</ondown>
|
||||
<itemlayout width="420" height="120">
|
||||
<control type="image">
|
||||
<description>background</description>
|
||||
<posx>0</posx>
|
||||
<posy>0</posy>
|
||||
<width>400</width>
|
||||
<height>100</height>
|
||||
<texture border="5">mb_item_big.png</texture>
|
||||
</control>
|
||||
<control type="image">
|
||||
<description>thumbnail</description>
|
||||
<posx>10</posx>
|
||||
<posy>10</posy>
|
||||
<width>380</width>
|
||||
<height>62</height>
|
||||
<info>Listitem.Thumb</info>
|
||||
<aspectratio>scale</aspectratio>
|
||||
<bordersize>3</bordersize>
|
||||
<bordertexture>mb_thumb_bg.png</bordertexture>
|
||||
</control>
|
||||
<control type="label">
|
||||
<description>show title</description>
|
||||
<posx>10</posx>
|
||||
<posy>75</posy>
|
||||
<width>152</width>
|
||||
<height>60</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<info>ListItem.Label</info>
|
||||
<font>font16b</font>
|
||||
<textcolor>FFFEFEFE</textcolor>
|
||||
</control>
|
||||
<control type="label">
|
||||
<description>videos</description>
|
||||
<posx>275</posx>
|
||||
<posy>76</posy>
|
||||
<width>152</width>
|
||||
<height>60</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<label>Recordings: $INFO[ListItem.property(custom:videos)]</label>
|
||||
<font>font14</font>
|
||||
<textcolor>FFCCCCCC</textcolor>
|
||||
</control>
|
||||
</itemlayout>
|
||||
<focusedlayout width="420" height="120">
|
||||
<control type="togglebutton">
|
||||
<description>background</description>
|
||||
<posx>0</posx>
|
||||
<posy>0</posy>
|
||||
<width>400</width>
|
||||
<height>100</height>
|
||||
<texturefocus border="5">mb_item_big_hover.png</texturefocus>
|
||||
<texturenofocus border="5">mb_item_big_hover.png</texturenofocus>
|
||||
<alttexturefocus border="5">mb_item_big.png</alttexturefocus>
|
||||
<alttexturenofocus border="5">mb_item_big.png</alttexturenofocus>
|
||||
<usealttexture>!Control.HasFocus(13)</usealttexture>
|
||||
</control>
|
||||
<control type="image">
|
||||
<description>thumbnail</description>
|
||||
<posx>10</posx>
|
||||
<posy>10</posy>
|
||||
<width>106</width>
|
||||
<height>62</height>
|
||||
<info>Listitem.Thumb</info>
|
||||
<aspectratio>scale</aspectratio>
|
||||
<bordertexture>mb_thumb_bg.png</bordertexture>
|
||||
<bordersize>3</bordersize>
|
||||
</control>
|
||||
<control type="image">
|
||||
<description>thumbnail hover</description>
|
||||
<posx>10</posx>
|
||||
<posy>10</posy>
|
||||
<width>380</width>
|
||||
<height>62</height>
|
||||
<info>Listitem.Thumb</info>
|
||||
<aspectratio>scale</aspectratio>
|
||||
<bordertexture>mb_thumb_hover_bg.png</bordertexture>
|
||||
<bordersize>3</bordersize>
|
||||
<visible>Control.HasFocus(13)</visible>
|
||||
</control>
|
||||
<control type="label">
|
||||
<description>show title</description>
|
||||
<posx>10</posx>
|
||||
<posy>75</posy>
|
||||
<width>152</width>
|
||||
<height>60</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<info>ListItem.Label</info>
|
||||
<font>font16b</font>
|
||||
<textcolor>FFFEFEFE</textcolor>
|
||||
<scroll>true</scroll>
|
||||
</control>
|
||||
<control type="label">
|
||||
<description>videos</description>
|
||||
<posx>275</posx>
|
||||
<posy>76</posy>
|
||||
<width>152</width>
|
||||
<height>60</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<label>Recordings: $INFO[ListItem.property(custom:videos)]</label>
|
||||
<font>font14</font>
|
||||
<textcolor>FFCCCCCC</textcolor>
|
||||
<scroll>true</scroll>
|
||||
</control>
|
||||
</focusedlayout>
|
||||
<content type="action">
|
||||
<onclick lang="python"><![CDATA[mythboxee.LoadSingleShow()]]></onclick>
|
||||
</content>
|
||||
</control>
|
||||
</controls>
|
||||
</window>
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0"?>
|
||||
<window type="window" id="14005">
|
||||
<defaultcontrol always="true">9001</defaultcontrol>
|
||||
<allowoverlay>yes</allowoverlay>
|
||||
<controls>
|
||||
<control type="visualisation" id ="5001">
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<visible>Control.IsVisible(2000)</visible>
|
||||
</control>
|
||||
<control type="image">
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<texture>mb_bg_setup.png</texture>
|
||||
<animation effect="fade" start="85" end="85" time="0" condition="true">Conditional</animation>
|
||||
</control>
|
||||
<control type="grouplist" id="1000">
|
||||
<posy>11</posy>
|
||||
<posx>216</posx>
|
||||
<itemgap>20</itemgap>
|
||||
<ondown>2000</ondown>
|
||||
<orientation>horizontal</orientation>
|
||||
</control>
|
||||
<control type="image" id="5001">
|
||||
<description>logo</description>
|
||||
<posx>450</posx>
|
||||
<posy>250</posy>
|
||||
<width>402</width>
|
||||
<height>107</height>
|
||||
<texture flipY="true" flipX="false">logo.png</texture>
|
||||
<aspectratio>keep</aspectratio>
|
||||
</control>
|
||||
<control type="label" id="5002">
|
||||
<description>some label that shows text</description>
|
||||
<posx>425</posx>
|
||||
<posy>580</posy>
|
||||
<width>450</width>
|
||||
<height>40</height>
|
||||
<align>center</align>
|
||||
<aligny>center</aligny>
|
||||
<scroll>false</scroll>
|
||||
<label>Attempting to Locate MythTV Backend</label>
|
||||
<number></number>
|
||||
<haspath>false</haspath>
|
||||
<font>font18</font>
|
||||
<textcolor>white</textcolor>
|
||||
<wrapmultiline>false</wrapmultiline>
|
||||
</control>
|
||||
</controls>
|
||||
</window>
|
|
@ -0,0 +1,455 @@
|
|||
<?xml version="1.0"?>
|
||||
<window type="window" id="14001">
|
||||
<defaultcontrol always="true">2013</defaultcontrol>
|
||||
<allowoverlay>no</allowoverlay>
|
||||
<onload lang="python"><![CDATA[
|
||||
import mythboxee
|
||||
]]></onload>
|
||||
<controls>
|
||||
<control type="image">
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<texture>mb_bg.png</texture>
|
||||
<animation effect="fade" start="85" end="85" time="0" condition="true">Conditional</animation>
|
||||
</control>
|
||||
<control type="grouplist" id="2000">
|
||||
<posy>9</posy>
|
||||
<posx>1110</posx>
|
||||
<itemgap>20</itemgap>
|
||||
<ondown>2013</ondown>
|
||||
<orientation>horizontal</orientation>
|
||||
</control>
|
||||
|
||||
<control type="group" id="2012">
|
||||
<description>details box</description>
|
||||
<posx>28</posx>
|
||||
<posy>81</posy>
|
||||
<onleft>-</onleft>
|
||||
<onright>2013</onright>
|
||||
<onup>-</onup>
|
||||
<ondown>-</ondown>
|
||||
<control type="image">
|
||||
<description>details box's background</description>
|
||||
<posx>0</posx>
|
||||
<posy>0</posy>
|
||||
<width>283</width>
|
||||
<height>597</height>
|
||||
<texture>mb_show.png</texture>
|
||||
</control>
|
||||
<control type="image">
|
||||
<description>thumbnail</description>
|
||||
<posx>15</posx>
|
||||
<posy>10</posy>
|
||||
<width>253</width>
|
||||
<height>350</height>
|
||||
<texture>$INFO[Container(21).ListItem.Thumb]</texture>
|
||||
<aspectratio>scale</aspectratio>
|
||||
<fadetime>500</fadetime>
|
||||
</control>
|
||||
<control type="label">
|
||||
<description>show title</description>
|
||||
<posx>22</posx>
|
||||
<posy>370</posy>
|
||||
<width>228</width>
|
||||
<height>30</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<scroll>true</scroll>
|
||||
<label>$INFO[Container(21).ListItem.Label]</label>
|
||||
<font>font16b</font>
|
||||
<textcolor>FFFEFEFE</textcolor>
|
||||
<wrapmultiline>true</wrapmultiline>
|
||||
</control>
|
||||
<control type="label">
|
||||
<description>recordings</description>
|
||||
<visible>!stringcompare(Container(21).ListItem.property(custom:category),movie)</visible>
|
||||
<posx>22</posx>
|
||||
<posy>400</posy>
|
||||
<width>228</width>
|
||||
<height>20</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<scroll>true</scroll>
|
||||
<label>Recordings: $INFO[Container(21).ListItem.property(custom:videos)]</label>
|
||||
<font>font16b</font>
|
||||
<textcolor>FFCCCCCC</textcolor>
|
||||
<wrapmultiline>false</wrapmultiline>
|
||||
</control>
|
||||
<control type="textbox">
|
||||
<visible>!stringcompare(Container(21).ListItem.property(custom:category),movie)</visible>
|
||||
<description>description</description>
|
||||
<posx>22</posx>
|
||||
<posy>430</posy>
|
||||
<width>228</width>
|
||||
<height>160</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<scroll>true</scroll>
|
||||
<label>$INFO[Container(21).ListItem.property(description)]</label>
|
||||
<font>font16</font>
|
||||
<textcolor>FFCCCCCC</textcolor>
|
||||
<autoscroll delay="3000" time="2000" repeat="10000">Control.HasFocus(2013)</autoscroll>
|
||||
</control>
|
||||
<control type="label">
|
||||
<visible>stringcompare(Container(21).ListItem.property(custom:category),movie)</visible>
|
||||
<description>videos</description>
|
||||
<posx>22</posx>
|
||||
<posy>400</posy>
|
||||
<width>228</width>
|
||||
<height>20</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<scroll>true</scroll>
|
||||
<label>Recordings: $INFO[Container(21).ListItem.property(custom:videos)]</label>
|
||||
<font>font16b</font>
|
||||
<textcolor>FFCCCCCC</textcolor>
|
||||
<wrapmultiline>false</wrapmultiline>
|
||||
</control>
|
||||
<control type="textbox">
|
||||
<description>description</description>
|
||||
<visible>stringcompare(Container(21).ListItem.property(custom:category),movie)</visible>
|
||||
<posx>22</posx>
|
||||
<posy>430</posy>
|
||||
<width>228</width>
|
||||
<height>160</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<scroll>true</scroll>
|
||||
<label>$INFO[Container(21).ListItem.property(description)]</label>
|
||||
<font>font16</font>
|
||||
<textcolor>FFCCCCCC</textcolor>
|
||||
<autoscroll delay="3000" time="2000" repeat="10000">Control.HasFocus(2013)</autoscroll>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
<!-- end SHOW DETAILS -->
|
||||
|
||||
<!-- begin EPISODES LIST -->
|
||||
|
||||
<control type="list" id="2013">
|
||||
<posx>345</posx>
|
||||
<posy>82</posy>
|
||||
<width>700</width>
|
||||
<height>592</height>
|
||||
<onleft>2012</onleft>
|
||||
<onright>2014</onright>
|
||||
<onup>2000</onup>
|
||||
<ondown>-</ondown>
|
||||
<animation condition="Control.IsVisible(14)" type="Conditional" reversible="false">
|
||||
<effect end="100" start="0" time="200" type="fade" />
|
||||
</animation>
|
||||
<itemlayout width="700" height="85">
|
||||
<control type="image">
|
||||
<description>background</description>
|
||||
<posx>0</posx>
|
||||
<posy>0</posy>
|
||||
<width>700</width>
|
||||
<height>82</height>
|
||||
<texture border="5">mb_item_big.png</texture>
|
||||
</control>
|
||||
<control type="image">
|
||||
<description>thumbnail</description>
|
||||
<posx>10</posx>
|
||||
<posy>10</posy>
|
||||
<width>106</width>
|
||||
<height>62</height>
|
||||
<info>Listitem.Thumb</info>
|
||||
<aspectratio>scale</aspectratio>
|
||||
<bordersize>3</bordersize>
|
||||
<bordertexture>mb_thumb_bg.png</bordertexture>
|
||||
</control>
|
||||
<control type="label">
|
||||
<description>episode title</description>
|
||||
<posx>128</posx>
|
||||
<posy>6</posy>
|
||||
<width>440</width>
|
||||
<height>30</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<info>ListItem.Label</info>
|
||||
<font>font16b</font>
|
||||
<textcolor>FFFEFEFE</textcolor>
|
||||
<wrapmultiline>true</wrapmultiline>
|
||||
</control>
|
||||
<control type="label">
|
||||
<description>original air date</description>
|
||||
<posx>690</posx>
|
||||
<posy>5</posy>
|
||||
<width>240</width>
|
||||
<height>10</height>
|
||||
<align>right</align>
|
||||
<aligny>top</aligny>
|
||||
<label>Air Date: $INFO[ListItem.Date]</label>
|
||||
<font>font12</font>
|
||||
<textcolor>FFCCCCCC</textcolor>
|
||||
</control>
|
||||
<control type="label">
|
||||
<description>description</description>
|
||||
<posx>128</posx>
|
||||
<posy>31</posy>
|
||||
<width>560</width>
|
||||
<height>60</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<label>$INFO[ListItem.property(description)]</label>
|
||||
<font>font14</font>
|
||||
<textcolor>FFCCCCCC</textcolor>
|
||||
<wrapmultiline>true</wrapmultiline>
|
||||
</control>
|
||||
</itemlayout>
|
||||
<focusedlayout width="700" height="85">
|
||||
<control type="togglebutton">
|
||||
<description>background</description>
|
||||
<posx>0</posx>
|
||||
<posy>0</posy>
|
||||
<width>700</width>
|
||||
<height>82</height>
|
||||
<texturefocus border="5">mb_item_big_hover.png</texturefocus>
|
||||
<texturenofocus border="5">mb_item_big_hover.png</texturenofocus>
|
||||
<alttexturefocus border="5">mb_item_big.png</alttexturefocus>
|
||||
<alttexturenofocus border="5">mb_item_big.png</alttexturenofocus>
|
||||
<usealttexture>!Control.HasFocus(2013)</usealttexture>
|
||||
</control>
|
||||
<control type="image">
|
||||
<description>thumbnail</description>
|
||||
<posx>10</posx>
|
||||
<posy>10</posy>
|
||||
<width>106</width>
|
||||
<height>62</height>
|
||||
<info>Listitem.Thumb</info>
|
||||
<aspectratio>scale</aspectratio>
|
||||
<bordertexture>mb_thumb_bg.png</bordertexture>
|
||||
<bordersize>3</bordersize>
|
||||
</control>
|
||||
<control type="image">
|
||||
<description>thumbnail hover</description>
|
||||
<posx>10</posx>
|
||||
<posy>10</posy>
|
||||
<width>106</width>
|
||||
<height>62</height>
|
||||
<info>Listitem.Thumb</info>
|
||||
<aspectratio>scale</aspectratio>
|
||||
<bordertexture>mb_thumb_hover_bg.png</bordertexture>
|
||||
<bordersize>3</bordersize>
|
||||
<visible>Control.HasFocus(2013)</visible>
|
||||
</control>
|
||||
<control type="label">
|
||||
<description>episode title</description>
|
||||
<posx>128</posx>
|
||||
<posy>6</posy>
|
||||
<width>440</width>
|
||||
<height>30</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<info>ListItem.Label</info>
|
||||
<font>font16b</font>
|
||||
<textcolor>FFFEFEFE</textcolor>
|
||||
</control>
|
||||
<control type="label">
|
||||
<description>original air date</description>
|
||||
<posx>690</posx>
|
||||
<posy>5</posy>
|
||||
<width>240</width>
|
||||
<height>10</height>
|
||||
<align>right</align>
|
||||
<aligny>top</aligny>
|
||||
<label>Air Date: $INFO[ListItem.Date]</label>
|
||||
<font>font12</font>
|
||||
<textcolor>FFCCCCCC</textcolor>
|
||||
</control>
|
||||
<control type="label">
|
||||
<description>description</description>
|
||||
<posx>128</posx>
|
||||
<posy>31</posy>
|
||||
<width>560</width>
|
||||
<height>60</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<label>$INFO[ListItem.property(description)]</label>
|
||||
<font>font14</font>
|
||||
<textcolor>FFCCCCCC</textcolor>
|
||||
<wrapmultiline>true</wrapmultiline>
|
||||
</control>
|
||||
</focusedlayout>
|
||||
<content type="action">
|
||||
<onfocus lang="python"><![CDATA[mythboxee.ShowEpisodeDetails()]]></onfocus>
|
||||
</content>
|
||||
</control>
|
||||
|
||||
<!-- end EPISODES LIST -->
|
||||
|
||||
<!-- begin SORT LIST -->
|
||||
|
||||
<control type="label">
|
||||
<posx>1079</posx>
|
||||
<posy>75</posy>
|
||||
<label>SORT:</label>
|
||||
<font>font14b</font>
|
||||
<aligny>top</aligny>
|
||||
<textcolor>FF999999</textcolor>
|
||||
</control>
|
||||
|
||||
<control type="list" id="2014">
|
||||
<posx>1070</posx>
|
||||
<posy>100</posy>
|
||||
<width>164</width>
|
||||
<height>100</height>
|
||||
<onleft>2013</onleft>
|
||||
<onright>-</onright>
|
||||
<onup>-</onup>
|
||||
<ondown>2015</ondown>
|
||||
<itemlayout width="164" height="27">
|
||||
<control type="label">
|
||||
<description>title</description>
|
||||
<posx>9</posx>
|
||||
<posy>0</posy>
|
||||
<width>146</width>
|
||||
<height>25</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<info>ListItem.Label</info>
|
||||
<font>font16</font>
|
||||
<textcolor>FF999999</textcolor>
|
||||
<selectedcolor>FFFFFFFF</selectedcolor>
|
||||
</control>
|
||||
</itemlayout>
|
||||
<focusedlayout width="164" height="27">
|
||||
<control type="label">
|
||||
<description>title</description>
|
||||
<posx>9</posx>
|
||||
<posy>0</posy>
|
||||
<width>146</width>
|
||||
<height>25</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<info>ListItem.Label</info>
|
||||
<font>font16</font>
|
||||
<textcolor>FFFFFFFF</textcolor>
|
||||
<visible>Control.HasFocus(2014)</visible>
|
||||
<selectedcolor>FFFFFFFF</selectedcolor>
|
||||
</control>
|
||||
<control type="label">
|
||||
<visible>!Control.HasFocus(2014)</visible>
|
||||
<description>title</description>
|
||||
<posx>9</posx>
|
||||
<posy>0</posy>
|
||||
<width>146</width>
|
||||
<height>25</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<info>ListItem.Label</info>
|
||||
<font>font16</font>
|
||||
<textcolor>FF999999</textcolor>
|
||||
<selectedcolor>FFFFFFFF</selectedcolor>
|
||||
</control>
|
||||
</focusedlayout>
|
||||
<content type="action">
|
||||
<onclick lang="python"><![CDATA[mythboxee.SortBySeriesEpisodes()]]></onclick>
|
||||
</content>
|
||||
</control>
|
||||
|
||||
<!-- end SORT LIST -->
|
||||
|
||||
<control type="label">
|
||||
<posx>1079</posx>
|
||||
<posy>200</posy>
|
||||
<label>SORT DIRECTION:</label>
|
||||
<font>font14b</font>
|
||||
<aligny>top</aligny>
|
||||
<textcolor>FF999999</textcolor>
|
||||
</control>
|
||||
|
||||
<control type="list" id="2015">
|
||||
<posx>1070</posx>
|
||||
<posy>225</posy>
|
||||
<width>164</width>
|
||||
<height>100</height>
|
||||
<onleft>2013</onleft>
|
||||
<onright>-</onright>
|
||||
<onup>2014</onup>
|
||||
<ondown>-</ondown>
|
||||
<itemlayout width="164" height="27">
|
||||
<control type="label">
|
||||
<description>title</description>
|
||||
<posx>9</posx>
|
||||
<posy>0</posy>
|
||||
<width>146</width>
|
||||
<height>25</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<info>ListItem.Label</info>
|
||||
<font>font16</font>
|
||||
<textcolor>FF999999</textcolor>
|
||||
<selectedcolor>FFFFFFFF</selectedcolor>
|
||||
</control>
|
||||
</itemlayout>
|
||||
<focusedlayout width="164" height="27">
|
||||
<control type="label">
|
||||
<description>title</description>
|
||||
<posx>9</posx>
|
||||
<posy>0</posy>
|
||||
<width>146</width>
|
||||
<height>25</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<info>ListItem.Label</info>
|
||||
<font>font16</font>
|
||||
<textcolor>FFFFFFFF</textcolor>
|
||||
<visible>Control.HasFocus(2015)</visible>
|
||||
<selectedcolor>FFFFFFFF</selectedcolor>
|
||||
</control>
|
||||
<control type="label">
|
||||
<visible>!Control.HasFocus(2015)</visible>
|
||||
<description>title</description>
|
||||
<posx>9</posx>
|
||||
<posy>0</posy>
|
||||
<width>146</width>
|
||||
<height>25</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<info>ListItem.Label</info>
|
||||
<font>font16</font>
|
||||
<textcolor>FF999999</textcolor>
|
||||
<selectedcolor>FFFFFFFF</selectedcolor>
|
||||
</control>
|
||||
</focusedlayout>
|
||||
<content type="action">
|
||||
<onclick lang="python"><![CDATA[mythboxee.SortDirSeriesEpisodes()]]></onclick>
|
||||
</content>
|
||||
</control>
|
||||
|
||||
<!-- begin SHOW DETAILS CONTAINER -->
|
||||
|
||||
<control type="list" id="21">
|
||||
<posx>1</posx>
|
||||
<posy>1</posy>
|
||||
<width>1</width>
|
||||
<height>1</height>
|
||||
<itemlayout width="1" height="1">
|
||||
</itemlayout>
|
||||
<focusedlayout width="1" height="1">
|
||||
</focusedlayout>
|
||||
</control>
|
||||
|
||||
<!-- end SHOW DETAILS CONTAINER -->
|
||||
<!--
|
||||
<control type="multiimage" id="30">
|
||||
<description>loading control</description>
|
||||
<visible>Container(21).IsLoading | Container(13).IsLoading | Container(20).IsLoading</visible>
|
||||
<animation effect="fade" time="200">VisibleChange</animation>
|
||||
<posx>538</posx>
|
||||
<posy>306</posy>
|
||||
<width>200</width>
|
||||
<height>200</height>
|
||||
<imagepath>loading</imagepath>
|
||||
<timeperimage>80</timeperimage>
|
||||
<fadetime>10</fadetime>
|
||||
<pauseatend>0</pauseatend>
|
||||
<randomize>false</randomize>
|
||||
<loop>yes</loop>
|
||||
<aspectratio>keep</aspectratio>
|
||||
</control>
|
||||
-->
|
||||
|
||||
</controls>
|
||||
</window>
|
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 558 B |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 116 B |
After Width: | Height: | Size: 113 B |
|
@ -0,0 +1,51 @@
|
|||
"""Core XML support for Python.
|
||||
|
||||
This package contains four sub-packages:
|
||||
|
||||
dom -- The W3C Document Object Model. This supports DOM Level 1 +
|
||||
Namespaces.
|
||||
|
||||
parsers -- Python wrappers for XML parsers (currently only supports Expat).
|
||||
|
||||
sax -- The Simple API for XML, developed by XML-Dev, led by David
|
||||
Megginson and ported to Python by Lars Marius Garshol. This
|
||||
supports the SAX 2 API.
|
||||
|
||||
etree -- The ElementTree XML library. This is a subset of the full
|
||||
ElementTree XML release.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
__all__ = ["dom", "parsers", "sax", "etree"]
|
||||
|
||||
# When being checked-out without options, this has the form
|
||||
# "<dollar>Revision: x.y </dollar>"
|
||||
# When exported using -kv, it is "x.y".
|
||||
__version__ = "$Revision: 41660 $".split()[-2:][0]
|
||||
|
||||
|
||||
_MINIMUM_XMLPLUS_VERSION = (0, 8, 4)
|
||||
|
||||
|
||||
import os
|
||||
|
||||
# only prefer _xmlplus if the environment variable PY_USE_XMLPLUS is defined
|
||||
if 'PY_USE_XMLPLUS' in os.environ:
|
||||
try:
|
||||
import _xmlplus
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
v = _xmlplus.version_info
|
||||
except AttributeError:
|
||||
# _xmlplus is too old; ignore it
|
||||
pass
|
||||
else:
|
||||
if v >= _MINIMUM_XMLPLUS_VERSION:
|
||||
import sys
|
||||
_xmlplus.__path__.extend(__path__)
|
||||
sys.modules[__name__] = _xmlplus
|
||||
else:
|
||||
del v
|
|
@ -0,0 +1,27 @@
|
|||
# This is the Python mapping for interface NodeFilter from
|
||||
# DOM2-Traversal-Range. It contains only constants.
|
||||
|
||||
class NodeFilter:
|
||||
"""
|
||||
This is the DOM2 NodeFilter interface. It contains only constants.
|
||||
"""
|
||||
FILTER_ACCEPT = 1
|
||||
FILTER_REJECT = 2
|
||||
FILTER_SKIP = 3
|
||||
|
||||
SHOW_ALL = 0xFFFFFFFFL
|
||||
SHOW_ELEMENT = 0x00000001
|
||||
SHOW_ATTRIBUTE = 0x00000002
|
||||
SHOW_TEXT = 0x00000004
|
||||
SHOW_CDATA_SECTION = 0x00000008
|
||||
SHOW_ENTITY_REFERENCE = 0x00000010
|
||||
SHOW_ENTITY = 0x00000020
|
||||
SHOW_PROCESSING_INSTRUCTION = 0x00000040
|
||||
SHOW_COMMENT = 0x00000080
|
||||
SHOW_DOCUMENT = 0x00000100
|
||||
SHOW_DOCUMENT_TYPE = 0x00000200
|
||||
SHOW_DOCUMENT_FRAGMENT = 0x00000400
|
||||
SHOW_NOTATION = 0x00000800
|
||||
|
||||
def acceptNode(self, node):
|
||||
raise NotImplementedError
|
|
@ -0,0 +1,139 @@
|
|||
"""W3C Document Object Model implementation for Python.
|
||||
|
||||
The Python mapping of the Document Object Model is documented in the
|
||||
Python Library Reference in the section on the xml.dom package.
|
||||
|
||||
This package contains the following modules:
|
||||
|
||||
minidom -- A simple implementation of the Level 1 DOM with namespace
|
||||
support added (based on the Level 2 specification) and other
|
||||
minor Level 2 functionality.
|
||||
|
||||
pulldom -- DOM builder supporting on-demand tree-building for selected
|
||||
subtrees of the document.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class Node:
|
||||
"""Class giving the NodeType constants."""
|
||||
|
||||
# DOM implementations may use this as a base class for their own
|
||||
# Node implementations. If they don't, the constants defined here
|
||||
# should still be used as the canonical definitions as they match
|
||||
# the values given in the W3C recommendation. Client code can
|
||||
# safely refer to these values in all tests of Node.nodeType
|
||||
# values.
|
||||
|
||||
ELEMENT_NODE = 1
|
||||
ATTRIBUTE_NODE = 2
|
||||
TEXT_NODE = 3
|
||||
CDATA_SECTION_NODE = 4
|
||||
ENTITY_REFERENCE_NODE = 5
|
||||
ENTITY_NODE = 6
|
||||
PROCESSING_INSTRUCTION_NODE = 7
|
||||
COMMENT_NODE = 8
|
||||
DOCUMENT_NODE = 9
|
||||
DOCUMENT_TYPE_NODE = 10
|
||||
DOCUMENT_FRAGMENT_NODE = 11
|
||||
NOTATION_NODE = 12
|
||||
|
||||
|
||||
#ExceptionCode
|
||||
INDEX_SIZE_ERR = 1
|
||||
DOMSTRING_SIZE_ERR = 2
|
||||
HIERARCHY_REQUEST_ERR = 3
|
||||
WRONG_DOCUMENT_ERR = 4
|
||||
INVALID_CHARACTER_ERR = 5
|
||||
NO_DATA_ALLOWED_ERR = 6
|
||||
NO_MODIFICATION_ALLOWED_ERR = 7
|
||||
NOT_FOUND_ERR = 8
|
||||
NOT_SUPPORTED_ERR = 9
|
||||
INUSE_ATTRIBUTE_ERR = 10
|
||||
INVALID_STATE_ERR = 11
|
||||
SYNTAX_ERR = 12
|
||||
INVALID_MODIFICATION_ERR = 13
|
||||
NAMESPACE_ERR = 14
|
||||
INVALID_ACCESS_ERR = 15
|
||||
VALIDATION_ERR = 16
|
||||
|
||||
|
||||
class DOMException(Exception):
|
||||
"""Abstract base class for DOM exceptions.
|
||||
Exceptions with specific codes are specializations of this class."""
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
if self.__class__ is DOMException:
|
||||
raise RuntimeError(
|
||||
"DOMException should not be instantiated directly")
|
||||
Exception.__init__(self, *args, **kw)
|
||||
|
||||
def _get_code(self):
|
||||
return self.code
|
||||
|
||||
|
||||
class IndexSizeErr(DOMException):
|
||||
code = INDEX_SIZE_ERR
|
||||
|
||||
class DomstringSizeErr(DOMException):
|
||||
code = DOMSTRING_SIZE_ERR
|
||||
|
||||
class HierarchyRequestErr(DOMException):
|
||||
code = HIERARCHY_REQUEST_ERR
|
||||
|
||||
class WrongDocumentErr(DOMException):
|
||||
code = WRONG_DOCUMENT_ERR
|
||||
|
||||
class InvalidCharacterErr(DOMException):
|
||||
code = INVALID_CHARACTER_ERR
|
||||
|
||||
class NoDataAllowedErr(DOMException):
|
||||
code = NO_DATA_ALLOWED_ERR
|
||||
|
||||
class NoModificationAllowedErr(DOMException):
|
||||
code = NO_MODIFICATION_ALLOWED_ERR
|
||||
|
||||
class NotFoundErr(DOMException):
|
||||
code = NOT_FOUND_ERR
|
||||
|
||||
class NotSupportedErr(DOMException):
|
||||
code = NOT_SUPPORTED_ERR
|
||||
|
||||
class InuseAttributeErr(DOMException):
|
||||
code = INUSE_ATTRIBUTE_ERR
|
||||
|
||||
class InvalidStateErr(DOMException):
|
||||
code = INVALID_STATE_ERR
|
||||
|
||||
class SyntaxErr(DOMException):
|
||||
code = SYNTAX_ERR
|
||||
|
||||
class InvalidModificationErr(DOMException):
|
||||
code = INVALID_MODIFICATION_ERR
|
||||
|
||||
class NamespaceErr(DOMException):
|
||||
code = NAMESPACE_ERR
|
||||
|
||||
class InvalidAccessErr(DOMException):
|
||||
code = INVALID_ACCESS_ERR
|
||||
|
||||
class ValidationErr(DOMException):
|
||||
code = VALIDATION_ERR
|
||||
|
||||
class UserDataHandler:
|
||||
"""Class giving the operation constants for UserDataHandler.handle()."""
|
||||
|
||||
# Based on DOM Level 3 (WD 9 April 2002)
|
||||
|
||||
NODE_CLONED = 1
|
||||
NODE_IMPORTED = 2
|
||||
NODE_DELETED = 3
|
||||
NODE_RENAMED = 4
|
||||
|
||||
XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace"
|
||||
XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/"
|
||||
XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml"
|
||||
EMPTY_NAMESPACE = None
|
||||
EMPTY_PREFIX = None
|
||||
|
||||
from domreg import getDOMImplementation,registerDOMImplementation
|
|
@ -0,0 +1,99 @@
|
|||
"""Registration facilities for DOM. This module should not be used
|
||||
directly. Instead, the functions getDOMImplementation and
|
||||
registerDOMImplementation should be imported from xml.dom."""
|
||||
|
||||
from xml.dom.minicompat import * # isinstance, StringTypes
|
||||
|
||||
# This is a list of well-known implementations. Well-known names
|
||||
# should be published by posting to xml-sig@python.org, and are
|
||||
# subsequently recorded in this file.
|
||||
|
||||
well_known_implementations = {
|
||||
'minidom':'xml.dom.minidom',
|
||||
'4DOM': 'xml.dom.DOMImplementation',
|
||||
}
|
||||
|
||||
# DOM implementations not officially registered should register
|
||||
# themselves with their
|
||||
|
||||
registered = {}
|
||||
|
||||
def registerDOMImplementation(name, factory):
|
||||
"""registerDOMImplementation(name, factory)
|
||||
|
||||
Register the factory function with the name. The factory function
|
||||
should return an object which implements the DOMImplementation
|
||||
interface. The factory function can either return the same object,
|
||||
or a new one (e.g. if that implementation supports some
|
||||
customization)."""
|
||||
|
||||
registered[name] = factory
|
||||
|
||||
def _good_enough(dom, features):
|
||||
"_good_enough(dom, features) -> Return 1 if the dom offers the features"
|
||||
for f,v in features:
|
||||
if not dom.hasFeature(f,v):
|
||||
return 0
|
||||
return 1
|
||||
|
||||
def getDOMImplementation(name = None, features = ()):
|
||||
"""getDOMImplementation(name = None, features = ()) -> DOM implementation.
|
||||
|
||||
Return a suitable DOM implementation. The name is either
|
||||
well-known, the module name of a DOM implementation, or None. If
|
||||
it is not None, imports the corresponding module and returns
|
||||
DOMImplementation object if the import succeeds.
|
||||
|
||||
If name is not given, consider the available implementations to
|
||||
find one with the required feature set. If no implementation can
|
||||
be found, raise an ImportError. The features list must be a sequence
|
||||
of (feature, version) pairs which are passed to hasFeature."""
|
||||
|
||||
import os
|
||||
creator = None
|
||||
mod = well_known_implementations.get(name)
|
||||
if mod:
|
||||
mod = __import__(mod, {}, {}, ['getDOMImplementation'])
|
||||
return mod.getDOMImplementation()
|
||||
elif name:
|
||||
return registered[name]()
|
||||
elif os.environ.has_key("PYTHON_DOM"):
|
||||
return getDOMImplementation(name = os.environ["PYTHON_DOM"])
|
||||
|
||||
# User did not specify a name, try implementations in arbitrary
|
||||
# order, returning the one that has the required features
|
||||
if isinstance(features, StringTypes):
|
||||
features = _parse_feature_string(features)
|
||||
for creator in registered.values():
|
||||
dom = creator()
|
||||
if _good_enough(dom, features):
|
||||
return dom
|
||||
|
||||
for creator in well_known_implementations.keys():
|
||||
try:
|
||||
dom = getDOMImplementation(name = creator)
|
||||
except StandardError: # typically ImportError, or AttributeError
|
||||
continue
|
||||
if _good_enough(dom, features):
|
||||
return dom
|
||||
|
||||
raise ImportError,"no suitable DOM implementation found"
|
||||
|
||||
def _parse_feature_string(s):
|
||||
features = []
|
||||
parts = s.split()
|
||||
i = 0
|
||||
length = len(parts)
|
||||
while i < length:
|
||||
feature = parts[i]
|
||||
if feature[0] in "0123456789":
|
||||
raise ValueError, "bad feature name: %r" % (feature,)
|
||||
i = i + 1
|
||||
version = None
|
||||
if i < length:
|
||||
v = parts[i]
|
||||
if v[0] in "0123456789":
|
||||
i = i + 1
|
||||
version = v
|
||||
features.append((feature, version))
|
||||
return tuple(features)
|
|
@ -0,0 +1,983 @@
|
|||
"""Facility to use the Expat parser to load a minidom instance
|
||||
from a string or file.
|
||||
|
||||
This avoids all the overhead of SAX and pulldom to gain performance.
|
||||
"""
|
||||
|
||||
# Warning!
|
||||
#
|
||||
# This module is tightly bound to the implementation details of the
|
||||
# minidom DOM and can't be used with other DOM implementations. This
|
||||
# is due, in part, to a lack of appropriate methods in the DOM (there is
|
||||
# no way to create Entity and Notation nodes via the DOM Level 2
|
||||
# interface), and for performance. The later is the cause of some fairly
|
||||
# cryptic code.
|
||||
#
|
||||
# Performance hacks:
|
||||
#
|
||||
# - .character_data_handler() has an extra case in which continuing
|
||||
# data is appended to an existing Text node; this can be a
|
||||
# speedup since pyexpat can break up character data into multiple
|
||||
# callbacks even though we set the buffer_text attribute on the
|
||||
# parser. This also gives us the advantage that we don't need a
|
||||
# separate normalization pass.
|
||||
#
|
||||
# - Determining that a node exists is done using an identity comparison
|
||||
# with None rather than a truth test; this avoids searching for and
|
||||
# calling any methods on the node object if it exists. (A rather
|
||||
# nice speedup is achieved this way as well!)
|
||||
|
||||
from xml.dom import xmlbuilder, minidom, Node
|
||||
from xml.dom import EMPTY_NAMESPACE, EMPTY_PREFIX, XMLNS_NAMESPACE
|
||||
from xml.parsers import expat
|
||||
from xml.dom.minidom import _append_child, _set_attribute_node
|
||||
from xml.dom.NodeFilter import NodeFilter
|
||||
|
||||
from xml.dom.minicompat import *
|
||||
|
||||
TEXT_NODE = Node.TEXT_NODE
|
||||
CDATA_SECTION_NODE = Node.CDATA_SECTION_NODE
|
||||
DOCUMENT_NODE = Node.DOCUMENT_NODE
|
||||
|
||||
FILTER_ACCEPT = xmlbuilder.DOMBuilderFilter.FILTER_ACCEPT
|
||||
FILTER_REJECT = xmlbuilder.DOMBuilderFilter.FILTER_REJECT
|
||||
FILTER_SKIP = xmlbuilder.DOMBuilderFilter.FILTER_SKIP
|
||||
FILTER_INTERRUPT = xmlbuilder.DOMBuilderFilter.FILTER_INTERRUPT
|
||||
|
||||
theDOMImplementation = minidom.getDOMImplementation()
|
||||
|
||||
# Expat typename -> TypeInfo
|
||||
_typeinfo_map = {
|
||||
"CDATA": minidom.TypeInfo(None, "cdata"),
|
||||
"ENUM": minidom.TypeInfo(None, "enumeration"),
|
||||
"ENTITY": minidom.TypeInfo(None, "entity"),
|
||||
"ENTITIES": minidom.TypeInfo(None, "entities"),
|
||||
"ID": minidom.TypeInfo(None, "id"),
|
||||
"IDREF": minidom.TypeInfo(None, "idref"),
|
||||
"IDREFS": minidom.TypeInfo(None, "idrefs"),
|
||||
"NMTOKEN": minidom.TypeInfo(None, "nmtoken"),
|
||||
"NMTOKENS": minidom.TypeInfo(None, "nmtokens"),
|
||||
}
|
||||
|
||||
class ElementInfo(object):
|
||||
__slots__ = '_attr_info', '_model', 'tagName'
|
||||
|
||||
def __init__(self, tagName, model=None):
|
||||
self.tagName = tagName
|
||||
self._attr_info = []
|
||||
self._model = model
|
||||
|
||||
def __getstate__(self):
|
||||
return self._attr_info, self._model, self.tagName
|
||||
|
||||
def __setstate__(self, state):
|
||||
self._attr_info, self._model, self.tagName = state
|
||||
|
||||
def getAttributeType(self, aname):
|
||||
for info in self._attr_info:
|
||||
if info[1] == aname:
|
||||
t = info[-2]
|
||||
if t[0] == "(":
|
||||
return _typeinfo_map["ENUM"]
|
||||
else:
|
||||
return _typeinfo_map[info[-2]]
|
||||
return minidom._no_type
|
||||
|
||||
def getAttributeTypeNS(self, namespaceURI, localName):
|
||||
return minidom._no_type
|
||||
|
||||
def isElementContent(self):
|
||||
if self._model:
|
||||
type = self._model[0]
|
||||
return type not in (expat.model.XML_CTYPE_ANY,
|
||||
expat.model.XML_CTYPE_MIXED)
|
||||
else:
|
||||
return False
|
||||
|
||||
def isEmpty(self):
|
||||
if self._model:
|
||||
return self._model[0] == expat.model.XML_CTYPE_EMPTY
|
||||
else:
|
||||
return False
|
||||
|
||||
def isId(self, aname):
|
||||
for info in self._attr_info:
|
||||
if info[1] == aname:
|
||||
return info[-2] == "ID"
|
||||
return False
|
||||
|
||||
def isIdNS(self, euri, ename, auri, aname):
|
||||
# not sure this is meaningful
|
||||
return self.isId((auri, aname))
|
||||
|
||||
def _intern(builder, s):
|
||||
return builder._intern_setdefault(s, s)
|
||||
|
||||
def _parse_ns_name(builder, name):
|
||||
assert ' ' in name
|
||||
parts = name.split(' ')
|
||||
intern = builder._intern_setdefault
|
||||
if len(parts) == 3:
|
||||
uri, localname, prefix = parts
|
||||
prefix = intern(prefix, prefix)
|
||||
qname = "%s:%s" % (prefix, localname)
|
||||
qname = intern(qname, qname)
|
||||
localname = intern(localname, localname)
|
||||
else:
|
||||
uri, localname = parts
|
||||
prefix = EMPTY_PREFIX
|
||||
qname = localname = intern(localname, localname)
|
||||
return intern(uri, uri), localname, prefix, qname
|
||||
|
||||
|
||||
class ExpatBuilder:
|
||||
"""Document builder that uses Expat to build a ParsedXML.DOM document
|
||||
instance."""
|
||||
|
||||
def __init__(self, options=None):
|
||||
if options is None:
|
||||
options = xmlbuilder.Options()
|
||||
self._options = options
|
||||
if self._options.filter is not None:
|
||||
self._filter = FilterVisibilityController(self._options.filter)
|
||||
else:
|
||||
self._filter = None
|
||||
# This *really* doesn't do anything in this case, so
|
||||
# override it with something fast & minimal.
|
||||
self._finish_start_element = id
|
||||
self._parser = None
|
||||
self.reset()
|
||||
|
||||
def createParser(self):
|
||||
"""Create a new parser object."""
|
||||
return expat.ParserCreate()
|
||||
|
||||
def getParser(self):
|
||||
"""Return the parser object, creating a new one if needed."""
|
||||
if not self._parser:
|
||||
self._parser = self.createParser()
|
||||
self._intern_setdefault = self._parser.intern.setdefault
|
||||
self._parser.buffer_text = True
|
||||
self._parser.ordered_attributes = True
|
||||
self._parser.specified_attributes = True
|
||||
self.install(self._parser)
|
||||
return self._parser
|
||||
|
||||
def reset(self):
|
||||
"""Free all data structures used during DOM construction."""
|
||||
self.document = theDOMImplementation.createDocument(
|
||||
EMPTY_NAMESPACE, None, None)
|
||||
self.curNode = self.document
|
||||
self._elem_info = self.document._elem_info
|
||||
self._cdata = False
|
||||
|
||||
def install(self, parser):
|
||||
"""Install the callbacks needed to build the DOM into the parser."""
|
||||
# This creates circular references!
|
||||
parser.StartDoctypeDeclHandler = self.start_doctype_decl_handler
|
||||
parser.StartElementHandler = self.first_element_handler
|
||||
parser.EndElementHandler = self.end_element_handler
|
||||
parser.ProcessingInstructionHandler = self.pi_handler
|
||||
if self._options.entities:
|
||||
parser.EntityDeclHandler = self.entity_decl_handler
|
||||
parser.NotationDeclHandler = self.notation_decl_handler
|
||||
if self._options.comments:
|
||||
parser.CommentHandler = self.comment_handler
|
||||
if self._options.cdata_sections:
|
||||
parser.StartCdataSectionHandler = self.start_cdata_section_handler
|
||||
parser.EndCdataSectionHandler = self.end_cdata_section_handler
|
||||
parser.CharacterDataHandler = self.character_data_handler_cdata
|
||||
else:
|
||||
parser.CharacterDataHandler = self.character_data_handler
|
||||
parser.ExternalEntityRefHandler = self.external_entity_ref_handler
|
||||
parser.XmlDeclHandler = self.xml_decl_handler
|
||||
parser.ElementDeclHandler = self.element_decl_handler
|
||||
parser.AttlistDeclHandler = self.attlist_decl_handler
|
||||
|
||||
def parseFile(self, file):
|
||||
"""Parse a document from a file object, returning the document
|
||||
node."""
|
||||
parser = self.getParser()
|
||||
first_buffer = True
|
||||
try:
|
||||
while 1:
|
||||
buffer = file.read(16*1024)
|
||||
if not buffer:
|
||||
break
|
||||
parser.Parse(buffer, 0)
|
||||
if first_buffer and self.document.documentElement:
|
||||
self._setup_subset(buffer)
|
||||
first_buffer = False
|
||||
parser.Parse("", True)
|
||||
except ParseEscape:
|
||||
pass
|
||||
doc = self.document
|
||||
self.reset()
|
||||
self._parser = None
|
||||
return doc
|
||||
|
||||
def parseString(self, string):
|
||||
"""Parse a document from a string, returning the document node."""
|
||||
parser = self.getParser()
|
||||
try:
|
||||
parser.Parse(string, True)
|
||||
self._setup_subset(string)
|
||||
except ParseEscape:
|
||||
pass
|
||||
doc = self.document
|
||||
self.reset()
|
||||
self._parser = None
|
||||
return doc
|
||||
|
||||
def _setup_subset(self, buffer):
|
||||
"""Load the internal subset if there might be one."""
|
||||
if self.document.doctype:
|
||||
extractor = InternalSubsetExtractor()
|
||||
extractor.parseString(buffer)
|
||||
subset = extractor.getSubset()
|
||||
self.document.doctype.internalSubset = subset
|
||||
|
||||
def start_doctype_decl_handler(self, doctypeName, systemId, publicId,
|
||||
has_internal_subset):
|
||||
doctype = self.document.implementation.createDocumentType(
|
||||
doctypeName, publicId, systemId)
|
||||
doctype.ownerDocument = self.document
|
||||
self.document.childNodes.append(doctype)
|
||||
self.document.doctype = doctype
|
||||
if self._filter and self._filter.acceptNode(doctype) == FILTER_REJECT:
|
||||
self.document.doctype = None
|
||||
del self.document.childNodes[-1]
|
||||
doctype = None
|
||||
self._parser.EntityDeclHandler = None
|
||||
self._parser.NotationDeclHandler = None
|
||||
if has_internal_subset:
|
||||
if doctype is not None:
|
||||
doctype.entities._seq = []
|
||||
doctype.notations._seq = []
|
||||
self._parser.CommentHandler = None
|
||||
self._parser.ProcessingInstructionHandler = None
|
||||
self._parser.EndDoctypeDeclHandler = self.end_doctype_decl_handler
|
||||
|
||||
def end_doctype_decl_handler(self):
|
||||
if self._options.comments:
|
||||
self._parser.CommentHandler = self.comment_handler
|
||||
self._parser.ProcessingInstructionHandler = self.pi_handler
|
||||
if not (self._elem_info or self._filter):
|
||||
self._finish_end_element = id
|
||||
|
||||
def pi_handler(self, target, data):
|
||||
node = self.document.createProcessingInstruction(target, data)
|
||||
_append_child(self.curNode, node)
|
||||
if self._filter and self._filter.acceptNode(node) == FILTER_REJECT:
|
||||
self.curNode.removeChild(node)
|
||||
|
||||
def character_data_handler_cdata(self, data):
|
||||
childNodes = self.curNode.childNodes
|
||||
if self._cdata:
|
||||
if ( self._cdata_continue
|
||||
and childNodes[-1].nodeType == CDATA_SECTION_NODE):
|
||||
childNodes[-1].appendData(data)
|
||||
return
|
||||
node = self.document.createCDATASection(data)
|
||||
self._cdata_continue = True
|
||||
elif childNodes and childNodes[-1].nodeType == TEXT_NODE:
|
||||
node = childNodes[-1]
|
||||
value = node.data + data
|
||||
d = node.__dict__
|
||||
d['data'] = d['nodeValue'] = value
|
||||
return
|
||||
else:
|
||||
node = minidom.Text()
|
||||
d = node.__dict__
|
||||
d['data'] = d['nodeValue'] = data
|
||||
d['ownerDocument'] = self.document
|
||||
_append_child(self.curNode, node)
|
||||
|
||||
def character_data_handler(self, data):
|
||||
childNodes = self.curNode.childNodes
|
||||
if childNodes and childNodes[-1].nodeType == TEXT_NODE:
|
||||
node = childNodes[-1]
|
||||
d = node.__dict__
|
||||
d['data'] = d['nodeValue'] = node.data + data
|
||||
return
|
||||
node = minidom.Text()
|
||||
d = node.__dict__
|
||||
d['data'] = d['nodeValue'] = node.data + data
|
||||
d['ownerDocument'] = self.document
|
||||
_append_child(self.curNode, node)
|
||||
|
||||
def entity_decl_handler(self, entityName, is_parameter_entity, value,
|
||||
base, systemId, publicId, notationName):
|
||||
if is_parameter_entity:
|
||||
# we don't care about parameter entities for the DOM
|
||||
return
|
||||
if not self._options.entities:
|
||||
return
|
||||
node = self.document._create_entity(entityName, publicId,
|
||||
systemId, notationName)
|
||||
if value is not None:
|
||||
# internal entity
|
||||
# node *should* be readonly, but we'll cheat
|
||||
child = self.document.createTextNode(value)
|
||||
node.childNodes.append(child)
|
||||
self.document.doctype.entities._seq.append(node)
|
||||
if self._filter and self._filter.acceptNode(node) == FILTER_REJECT:
|
||||
del self.document.doctype.entities._seq[-1]
|
||||
|
||||
def notation_decl_handler(self, notationName, base, systemId, publicId):
|
||||
node = self.document._create_notation(notationName, publicId, systemId)
|
||||
self.document.doctype.notations._seq.append(node)
|
||||
if self._filter and self._filter.acceptNode(node) == FILTER_ACCEPT:
|
||||
del self.document.doctype.notations._seq[-1]
|
||||
|
||||
def comment_handler(self, data):
|
||||
node = self.document.createComment(data)
|
||||
_append_child(self.curNode, node)
|
||||
if self._filter and self._filter.acceptNode(node) == FILTER_REJECT:
|
||||
self.curNode.removeChild(node)
|
||||
|
||||
def start_cdata_section_handler(self):
|
||||
self._cdata = True
|
||||
self._cdata_continue = False
|
||||
|
||||
def end_cdata_section_handler(self):
|
||||
self._cdata = False
|
||||
self._cdata_continue = False
|
||||
|
||||
def external_entity_ref_handler(self, context, base, systemId, publicId):
|
||||
return 1
|
||||
|
||||
def first_element_handler(self, name, attributes):
|
||||
if self._filter is None and not self._elem_info:
|
||||
self._finish_end_element = id
|
||||
self.getParser().StartElementHandler = self.start_element_handler
|
||||
self.start_element_handler(name, attributes)
|
||||
|
||||
def start_element_handler(self, name, attributes):
|
||||
node = self.document.createElement(name)
|
||||
_append_child(self.curNode, node)
|
||||
self.curNode = node
|
||||
|
||||
if attributes:
|
||||
for i in range(0, len(attributes), 2):
|
||||
a = minidom.Attr(attributes[i], EMPTY_NAMESPACE,
|
||||
None, EMPTY_PREFIX)
|
||||
value = attributes[i+1]
|
||||
d = a.childNodes[0].__dict__
|
||||
d['data'] = d['nodeValue'] = value
|
||||
d = a.__dict__
|
||||
d['value'] = d['nodeValue'] = value
|
||||
d['ownerDocument'] = self.document
|
||||
_set_attribute_node(node, a)
|
||||
|
||||
if node is not self.document.documentElement:
|
||||
self._finish_start_element(node)
|
||||
|
||||
def _finish_start_element(self, node):
|
||||
if self._filter:
|
||||
# To be general, we'd have to call isSameNode(), but this
|
||||
# is sufficient for minidom:
|
||||
if node is self.document.documentElement:
|
||||
return
|
||||
filt = self._filter.startContainer(node)
|
||||
if filt == FILTER_REJECT:
|
||||
# ignore this node & all descendents
|
||||
Rejecter(self)
|
||||
elif filt == FILTER_SKIP:
|
||||
# ignore this node, but make it's children become
|
||||
# children of the parent node
|
||||
Skipper(self)
|
||||
else:
|
||||
return
|
||||
self.curNode = node.parentNode
|
||||
node.parentNode.removeChild(node)
|
||||
node.unlink()
|
||||
|
||||
# If this ever changes, Namespaces.end_element_handler() needs to
|
||||
# be changed to match.
|
||||
#
|
||||
def end_element_handler(self, name):
|
||||
curNode = self.curNode
|
||||
self.curNode = curNode.parentNode
|
||||
self._finish_end_element(curNode)
|
||||
|
||||
def _finish_end_element(self, curNode):
|
||||
info = self._elem_info.get(curNode.tagName)
|
||||
if info:
|
||||
self._handle_white_text_nodes(curNode, info)
|
||||
if self._filter:
|
||||
if curNode is self.document.documentElement:
|
||||
return
|
||||
if self._filter.acceptNode(curNode) == FILTER_REJECT:
|
||||
self.curNode.removeChild(curNode)
|
||||
curNode.unlink()
|
||||
|
||||
def _handle_white_text_nodes(self, node, info):
|
||||
if (self._options.whitespace_in_element_content
|
||||
or not info.isElementContent()):
|
||||
return
|
||||
|
||||
# We have element type information and should remove ignorable
|
||||
# whitespace; identify for text nodes which contain only
|
||||
# whitespace.
|
||||
L = []
|
||||
for child in node.childNodes:
|
||||
if child.nodeType == TEXT_NODE and not child.data.strip():
|
||||
L.append(child)
|
||||
|
||||
# Remove ignorable whitespace from the tree.
|
||||
for child in L:
|
||||
node.removeChild(child)
|
||||
|
||||
def element_decl_handler(self, name, model):
|
||||
info = self._elem_info.get(name)
|
||||
if info is None:
|
||||
self._elem_info[name] = ElementInfo(name, model)
|
||||
else:
|
||||
assert info._model is None
|
||||
info._model = model
|
||||
|
||||
def attlist_decl_handler(self, elem, name, type, default, required):
|
||||
info = self._elem_info.get(elem)
|
||||
if info is None:
|
||||
info = ElementInfo(elem)
|
||||
self._elem_info[elem] = info
|
||||
info._attr_info.append(
|
||||
[None, name, None, None, default, 0, type, required])
|
||||
|
||||
def xml_decl_handler(self, version, encoding, standalone):
|
||||
self.document.version = version
|
||||
self.document.encoding = encoding
|
||||
# This is still a little ugly, thanks to the pyexpat API. ;-(
|
||||
if standalone >= 0:
|
||||
if standalone:
|
||||
self.document.standalone = True
|
||||
else:
|
||||
self.document.standalone = False
|
||||
|
||||
|
||||
# Don't include FILTER_INTERRUPT, since that's checked separately
|
||||
# where allowed.
|
||||
_ALLOWED_FILTER_RETURNS = (FILTER_ACCEPT, FILTER_REJECT, FILTER_SKIP)
|
||||
|
||||
class FilterVisibilityController(object):
|
||||
"""Wrapper around a DOMBuilderFilter which implements the checks
|
||||
to make the whatToShow filter attribute work."""
|
||||
|
||||
__slots__ = 'filter',
|
||||
|
||||
def __init__(self, filter):
|
||||
self.filter = filter
|
||||
|
||||
def startContainer(self, node):
|
||||
mask = self._nodetype_mask[node.nodeType]
|
||||
if self.filter.whatToShow & mask:
|
||||
val = self.filter.startContainer(node)
|
||||
if val == FILTER_INTERRUPT:
|
||||
raise ParseEscape
|
||||
if val not in _ALLOWED_FILTER_RETURNS:
|
||||
raise ValueError, \
|
||||
"startContainer() returned illegal value: " + repr(val)
|
||||
return val
|
||||
else:
|
||||
return FILTER_ACCEPT
|
||||
|
||||
def acceptNode(self, node):
|
||||
mask = self._nodetype_mask[node.nodeType]
|
||||
if self.filter.whatToShow & mask:
|
||||
val = self.filter.acceptNode(node)
|
||||
if val == FILTER_INTERRUPT:
|
||||
raise ParseEscape
|
||||
if val == FILTER_SKIP:
|
||||
# move all child nodes to the parent, and remove this node
|
||||
parent = node.parentNode
|
||||
for child in node.childNodes[:]:
|
||||
parent.appendChild(child)
|
||||
# node is handled by the caller
|
||||
return FILTER_REJECT
|
||||
if val not in _ALLOWED_FILTER_RETURNS:
|
||||
raise ValueError, \
|
||||
"acceptNode() returned illegal value: " + repr(val)
|
||||
return val
|
||||
else:
|
||||
return FILTER_ACCEPT
|
||||
|
||||
_nodetype_mask = {
|
||||
Node.ELEMENT_NODE: NodeFilter.SHOW_ELEMENT,
|
||||
Node.ATTRIBUTE_NODE: NodeFilter.SHOW_ATTRIBUTE,
|
||||
Node.TEXT_NODE: NodeFilter.SHOW_TEXT,
|
||||
Node.CDATA_SECTION_NODE: NodeFilter.SHOW_CDATA_SECTION,
|
||||
Node.ENTITY_REFERENCE_NODE: NodeFilter.SHOW_ENTITY_REFERENCE,
|
||||
Node.ENTITY_NODE: NodeFilter.SHOW_ENTITY,
|
||||
Node.PROCESSING_INSTRUCTION_NODE: NodeFilter.SHOW_PROCESSING_INSTRUCTION,
|
||||
Node.COMMENT_NODE: NodeFilter.SHOW_COMMENT,
|
||||
Node.DOCUMENT_NODE: NodeFilter.SHOW_DOCUMENT,
|
||||
Node.DOCUMENT_TYPE_NODE: NodeFilter.SHOW_DOCUMENT_TYPE,
|
||||
Node.DOCUMENT_FRAGMENT_NODE: NodeFilter.SHOW_DOCUMENT_FRAGMENT,
|
||||
Node.NOTATION_NODE: NodeFilter.SHOW_NOTATION,
|
||||
}
|
||||
|
||||
|
||||
class FilterCrutch(object):
|
||||
__slots__ = '_builder', '_level', '_old_start', '_old_end'
|
||||
|
||||
def __init__(self, builder):
|
||||
self._level = 0
|
||||
self._builder = builder
|
||||
parser = builder._parser
|
||||
self._old_start = parser.StartElementHandler
|
||||
self._old_end = parser.EndElementHandler
|
||||
parser.StartElementHandler = self.start_element_handler
|
||||
parser.EndElementHandler = self.end_element_handler
|
||||
|
||||
class Rejecter(FilterCrutch):
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, builder):
|
||||
FilterCrutch.__init__(self, builder)
|
||||
parser = builder._parser
|
||||
for name in ("ProcessingInstructionHandler",
|
||||
"CommentHandler",
|
||||
"CharacterDataHandler",
|
||||
"StartCdataSectionHandler",
|
||||
"EndCdataSectionHandler",
|
||||
"ExternalEntityRefHandler",
|
||||
):
|
||||
setattr(parser, name, None)
|
||||
|
||||
def start_element_handler(self, *args):
|
||||
self._level = self._level + 1
|
||||
|
||||
def end_element_handler(self, *args):
|
||||
if self._level == 0:
|
||||
# restore the old handlers
|
||||
parser = self._builder._parser
|
||||
self._builder.install(parser)
|
||||
parser.StartElementHandler = self._old_start
|
||||
parser.EndElementHandler = self._old_end
|
||||
else:
|
||||
self._level = self._level - 1
|
||||
|
||||
class Skipper(FilterCrutch):
|
||||
__slots__ = ()
|
||||
|
||||
def start_element_handler(self, *args):
|
||||
node = self._builder.curNode
|
||||
self._old_start(*args)
|
||||
if self._builder.curNode is not node:
|
||||
self._level = self._level + 1
|
||||
|
||||
def end_element_handler(self, *args):
|
||||
if self._level == 0:
|
||||
# We're popping back out of the node we're skipping, so we
|
||||
# shouldn't need to do anything but reset the handlers.
|
||||
self._builder._parser.StartElementHandler = self._old_start
|
||||
self._builder._parser.EndElementHandler = self._old_end
|
||||
self._builder = None
|
||||
else:
|
||||
self._level = self._level - 1
|
||||
self._old_end(*args)
|
||||
|
||||
|
||||
# framework document used by the fragment builder.
|
||||
# Takes a string for the doctype, subset string, and namespace attrs string.
|
||||
|
||||
_FRAGMENT_BUILDER_INTERNAL_SYSTEM_ID = \
|
||||
"http://xml.python.org/entities/fragment-builder/internal"
|
||||
|
||||
_FRAGMENT_BUILDER_TEMPLATE = (
|
||||
'''\
|
||||
<!DOCTYPE wrapper
|
||||
%%s [
|
||||
<!ENTITY fragment-builder-internal
|
||||
SYSTEM "%s">
|
||||
%%s
|
||||
]>
|
||||
<wrapper %%s
|
||||
>&fragment-builder-internal;</wrapper>'''
|
||||
% _FRAGMENT_BUILDER_INTERNAL_SYSTEM_ID)
|
||||
|
||||
|
||||
class FragmentBuilder(ExpatBuilder):
|
||||
"""Builder which constructs document fragments given XML source
|
||||
text and a context node.
|
||||
|
||||
The context node is expected to provide information about the
|
||||
namespace declarations which are in scope at the start of the
|
||||
fragment.
|
||||
"""
|
||||
|
||||
def __init__(self, context, options=None):
|
||||
if context.nodeType == DOCUMENT_NODE:
|
||||
self.originalDocument = context
|
||||
self.context = context
|
||||
else:
|
||||
self.originalDocument = context.ownerDocument
|
||||
self.context = context
|
||||
ExpatBuilder.__init__(self, options)
|
||||
|
||||
def reset(self):
|
||||
ExpatBuilder.reset(self)
|
||||
self.fragment = None
|
||||
|
||||
def parseFile(self, file):
|
||||
"""Parse a document fragment from a file object, returning the
|
||||
fragment node."""
|
||||
return self.parseString(file.read())
|
||||
|
||||
def parseString(self, string):
|
||||
"""Parse a document fragment from a string, returning the
|
||||
fragment node."""
|
||||
self._source = string
|
||||
parser = self.getParser()
|
||||
doctype = self.originalDocument.doctype
|
||||
ident = ""
|
||||
if doctype:
|
||||
subset = doctype.internalSubset or self._getDeclarations()
|
||||
if doctype.publicId:
|
||||
ident = ('PUBLIC "%s" "%s"'
|
||||
% (doctype.publicId, doctype.systemId))
|
||||
elif doctype.systemId:
|
||||
ident = 'SYSTEM "%s"' % doctype.systemId
|
||||
else:
|
||||
subset = ""
|
||||
nsattrs = self._getNSattrs() # get ns decls from node's ancestors
|
||||
document = _FRAGMENT_BUILDER_TEMPLATE % (ident, subset, nsattrs)
|
||||
try:
|
||||
parser.Parse(document, 1)
|
||||
except:
|
||||
self.reset()
|
||||
raise
|
||||
fragment = self.fragment
|
||||
self.reset()
|
||||
## self._parser = None
|
||||
return fragment
|
||||
|
||||
def _getDeclarations(self):
|
||||
"""Re-create the internal subset from the DocumentType node.
|
||||
|
||||
This is only needed if we don't already have the
|
||||
internalSubset as a string.
|
||||
"""
|
||||
doctype = self.context.ownerDocument.doctype
|
||||
s = ""
|
||||
if doctype:
|
||||
for i in range(doctype.notations.length):
|
||||
notation = doctype.notations.item(i)
|
||||
if s:
|
||||
s = s + "\n "
|
||||
s = "%s<!NOTATION %s" % (s, notation.nodeName)
|
||||
if notation.publicId:
|
||||
s = '%s PUBLIC "%s"\n "%s">' \
|
||||
% (s, notation.publicId, notation.systemId)
|
||||
else:
|
||||
s = '%s SYSTEM "%s">' % (s, notation.systemId)
|
||||
for i in range(doctype.entities.length):
|
||||
entity = doctype.entities.item(i)
|
||||
if s:
|
||||
s = s + "\n "
|
||||
s = "%s<!ENTITY %s" % (s, entity.nodeName)
|
||||
if entity.publicId:
|
||||
s = '%s PUBLIC "%s"\n "%s"' \
|
||||
% (s, entity.publicId, entity.systemId)
|
||||
elif entity.systemId:
|
||||
s = '%s SYSTEM "%s"' % (s, entity.systemId)
|
||||
else:
|
||||
s = '%s "%s"' % (s, entity.firstChild.data)
|
||||
if entity.notationName:
|
||||
s = "%s NOTATION %s" % (s, entity.notationName)
|
||||
s = s + ">"
|
||||
return s
|
||||
|
||||
def _getNSattrs(self):
|
||||
return ""
|
||||
|
||||
def external_entity_ref_handler(self, context, base, systemId, publicId):
|
||||
if systemId == _FRAGMENT_BUILDER_INTERNAL_SYSTEM_ID:
|
||||
# this entref is the one that we made to put the subtree
|
||||
# in; all of our given input is parsed in here.
|
||||
old_document = self.document
|
||||
old_cur_node = self.curNode
|
||||
parser = self._parser.ExternalEntityParserCreate(context)
|
||||
# put the real document back, parse into the fragment to return
|
||||
self.document = self.originalDocument
|
||||
self.fragment = self.document.createDocumentFragment()
|
||||
self.curNode = self.fragment
|
||||
try:
|
||||
parser.Parse(self._source, 1)
|
||||
finally:
|
||||
self.curNode = old_cur_node
|
||||
self.document = old_document
|
||||
self._source = None
|
||||
return -1
|
||||
else:
|
||||
return ExpatBuilder.external_entity_ref_handler(
|
||||
self, context, base, systemId, publicId)
|
||||
|
||||
|
||||
class Namespaces:
|
||||
"""Mix-in class for builders; adds support for namespaces."""
|
||||
|
||||
def _initNamespaces(self):
|
||||
# list of (prefix, uri) ns declarations. Namespace attrs are
|
||||
# constructed from this and added to the element's attrs.
|
||||
self._ns_ordered_prefixes = []
|
||||
|
||||
def createParser(self):
|
||||
"""Create a new namespace-handling parser."""
|
||||
parser = expat.ParserCreate(namespace_separator=" ")
|
||||
parser.namespace_prefixes = True
|
||||
return parser
|
||||
|
||||
def install(self, parser):
|
||||
"""Insert the namespace-handlers onto the parser."""
|
||||
ExpatBuilder.install(self, parser)
|
||||
if self._options.namespace_declarations:
|
||||
parser.StartNamespaceDeclHandler = (
|
||||
self.start_namespace_decl_handler)
|
||||
|
||||
def start_namespace_decl_handler(self, prefix, uri):
|
||||
"""Push this namespace declaration on our storage."""
|
||||
self._ns_ordered_prefixes.append((prefix, uri))
|
||||
|
||||
def start_element_handler(self, name, attributes):
|
||||
if ' ' in name:
|
||||
uri, localname, prefix, qname = _parse_ns_name(self, name)
|
||||
else:
|
||||
uri = EMPTY_NAMESPACE
|
||||
qname = name
|
||||
localname = None
|
||||
prefix = EMPTY_PREFIX
|
||||
node = minidom.Element(qname, uri, prefix, localname)
|
||||
node.ownerDocument = self.document
|
||||
_append_child(self.curNode, node)
|
||||
self.curNode = node
|
||||
|
||||
if self._ns_ordered_prefixes:
|
||||
for prefix, uri in self._ns_ordered_prefixes:
|
||||
if prefix:
|
||||
a = minidom.Attr(_intern(self, 'xmlns:' + prefix),
|
||||
XMLNS_NAMESPACE, prefix, "xmlns")
|
||||
else:
|
||||
a = minidom.Attr("xmlns", XMLNS_NAMESPACE,
|
||||
"xmlns", EMPTY_PREFIX)
|
||||
d = a.childNodes[0].__dict__
|
||||
d['data'] = d['nodeValue'] = uri
|
||||
d = a.__dict__
|
||||
d['value'] = d['nodeValue'] = uri
|
||||
d['ownerDocument'] = self.document
|
||||
_set_attribute_node(node, a)
|
||||
del self._ns_ordered_prefixes[:]
|
||||
|
||||
if attributes:
|
||||
_attrs = node._attrs
|
||||
_attrsNS = node._attrsNS
|
||||
for i in range(0, len(attributes), 2):
|
||||
aname = attributes[i]
|
||||
value = attributes[i+1]
|
||||
if ' ' in aname:
|
||||
uri, localname, prefix, qname = _parse_ns_name(self, aname)
|
||||
a = minidom.Attr(qname, uri, localname, prefix)
|
||||
_attrs[qname] = a
|
||||
_attrsNS[(uri, localname)] = a
|
||||
else:
|
||||
a = minidom.Attr(aname, EMPTY_NAMESPACE,
|
||||
aname, EMPTY_PREFIX)
|
||||
_attrs[aname] = a
|
||||
_attrsNS[(EMPTY_NAMESPACE, aname)] = a
|
||||
d = a.childNodes[0].__dict__
|
||||
d['data'] = d['nodeValue'] = value
|
||||
d = a.__dict__
|
||||
d['ownerDocument'] = self.document
|
||||
d['value'] = d['nodeValue'] = value
|
||||
d['ownerElement'] = node
|
||||
|
||||
if __debug__:
|
||||
# This only adds some asserts to the original
|
||||
# end_element_handler(), so we only define this when -O is not
|
||||
# used. If changing one, be sure to check the other to see if
|
||||
# it needs to be changed as well.
|
||||
#
|
||||
def end_element_handler(self, name):
|
||||
curNode = self.curNode
|
||||
if ' ' in name:
|
||||
uri, localname, prefix, qname = _parse_ns_name(self, name)
|
||||
assert (curNode.namespaceURI == uri
|
||||
and curNode.localName == localname
|
||||
and curNode.prefix == prefix), \
|
||||
"element stack messed up! (namespace)"
|
||||
else:
|
||||
assert curNode.nodeName == name, \
|
||||
"element stack messed up - bad nodeName"
|
||||
assert curNode.namespaceURI == EMPTY_NAMESPACE, \
|
||||
"element stack messed up - bad namespaceURI"
|
||||
self.curNode = curNode.parentNode
|
||||
self._finish_end_element(curNode)
|
||||
|
||||
|
||||
class ExpatBuilderNS(Namespaces, ExpatBuilder):
|
||||
"""Document builder that supports namespaces."""
|
||||
|
||||
def reset(self):
|
||||
ExpatBuilder.reset(self)
|
||||
self._initNamespaces()
|
||||
|
||||
|
||||
class FragmentBuilderNS(Namespaces, FragmentBuilder):
|
||||
"""Fragment builder that supports namespaces."""
|
||||
|
||||
def reset(self):
|
||||
FragmentBuilder.reset(self)
|
||||
self._initNamespaces()
|
||||
|
||||
def _getNSattrs(self):
|
||||
"""Return string of namespace attributes from this element and
|
||||
ancestors."""
|
||||
# XXX This needs to be re-written to walk the ancestors of the
|
||||
# context to build up the namespace information from
|
||||
# declarations, elements, and attributes found in context.
|
||||
# Otherwise we have to store a bunch more data on the DOM
|
||||
# (though that *might* be more reliable -- not clear).
|
||||
attrs = ""
|
||||
context = self.context
|
||||
L = []
|
||||
while context:
|
||||
if hasattr(context, '_ns_prefix_uri'):
|
||||
for prefix, uri in context._ns_prefix_uri.items():
|
||||
# add every new NS decl from context to L and attrs string
|
||||
if prefix in L:
|
||||
continue
|
||||
L.append(prefix)
|
||||
if prefix:
|
||||
declname = "xmlns:" + prefix
|
||||
else:
|
||||
declname = "xmlns"
|
||||
if attrs:
|
||||
attrs = "%s\n %s='%s'" % (attrs, declname, uri)
|
||||
else:
|
||||
attrs = " %s='%s'" % (declname, uri)
|
||||
context = context.parentNode
|
||||
return attrs
|
||||
|
||||
|
||||
class ParseEscape(Exception):
|
||||
"""Exception raised to short-circuit parsing in InternalSubsetExtractor."""
|
||||
pass
|
||||
|
||||
class InternalSubsetExtractor(ExpatBuilder):
|
||||
"""XML processor which can rip out the internal document type subset."""
|
||||
|
||||
subset = None
|
||||
|
||||
def getSubset(self):
|
||||
"""Return the internal subset as a string."""
|
||||
return self.subset
|
||||
|
||||
def parseFile(self, file):
|
||||
try:
|
||||
ExpatBuilder.parseFile(self, file)
|
||||
except ParseEscape:
|
||||
pass
|
||||
|
||||
def parseString(self, string):
|
||||
try:
|
||||
ExpatBuilder.parseString(self, string)
|
||||
except ParseEscape:
|
||||
pass
|
||||
|
||||
def install(self, parser):
|
||||
parser.StartDoctypeDeclHandler = self.start_doctype_decl_handler
|
||||
parser.StartElementHandler = self.start_element_handler
|
||||
|
||||
def start_doctype_decl_handler(self, name, publicId, systemId,
|
||||
has_internal_subset):
|
||||
if has_internal_subset:
|
||||
parser = self.getParser()
|
||||
self.subset = []
|
||||
parser.DefaultHandler = self.subset.append
|
||||
parser.EndDoctypeDeclHandler = self.end_doctype_decl_handler
|
||||
else:
|
||||
raise ParseEscape()
|
||||
|
||||
def end_doctype_decl_handler(self):
|
||||
s = ''.join(self.subset).replace('\r\n', '\n').replace('\r', '\n')
|
||||
self.subset = s
|
||||
raise ParseEscape()
|
||||
|
||||
def start_element_handler(self, name, attrs):
|
||||
raise ParseEscape()
|
||||
|
||||
|
||||
def parse(file, namespaces=True):
|
||||
"""Parse a document, returning the resulting Document node.
|
||||
|
||||
'file' may be either a file name or an open file object.
|
||||
"""
|
||||
if namespaces:
|
||||
builder = ExpatBuilderNS()
|
||||
else:
|
||||
builder = ExpatBuilder()
|
||||
|
||||
if isinstance(file, StringTypes):
|
||||
fp = open(file, 'rb')
|
||||
try:
|
||||
result = builder.parseFile(fp)
|
||||
finally:
|
||||
fp.close()
|
||||
else:
|
||||
result = builder.parseFile(file)
|
||||
return result
|
||||
|
||||
|
||||
def parseString(string, namespaces=True):
|
||||
"""Parse a document from a string, returning the resulting
|
||||
Document node.
|
||||
"""
|
||||
if namespaces:
|
||||
builder = ExpatBuilderNS()
|
||||
else:
|
||||
builder = ExpatBuilder()
|
||||
return builder.parseString(string)
|
||||
|
||||
|
||||
def parseFragment(file, context, namespaces=True):
|
||||
"""Parse a fragment of a document, given the context from which it
|
||||
was originally extracted. context should be the parent of the
|
||||
node(s) which are in the fragment.
|
||||
|
||||
'file' may be either a file name or an open file object.
|
||||
"""
|
||||
if namespaces:
|
||||
builder = FragmentBuilderNS(context)
|
||||
else:
|
||||
builder = FragmentBuilder(context)
|
||||
|
||||
if isinstance(file, StringTypes):
|
||||
fp = open(file, 'rb')
|
||||
try:
|
||||
result = builder.parseFile(fp)
|
||||
finally:
|
||||
fp.close()
|
||||
else:
|
||||
result = builder.parseFile(file)
|
||||
return result
|
||||
|
||||
|
||||
def parseFragmentString(string, context, namespaces=True):
|
||||
"""Parse a fragment of a document from a string, given the context
|
||||
from which it was originally extracted. context should be the
|
||||
parent of the node(s) which are in the fragment.
|
||||
"""
|
||||
if namespaces:
|
||||
builder = FragmentBuilderNS(context)
|
||||
else:
|
||||
builder = FragmentBuilder(context)
|
||||
return builder.parseString(string)
|
||||
|
||||
|
||||
def makeBuilder(options):
|
||||
"""Create a builder based on an Options object."""
|
||||
if options.namespaces:
|
||||
return ExpatBuilderNS(options)
|
||||
else:
|
||||
return ExpatBuilder(options)
|
|
@ -0,0 +1,110 @@
|
|||
"""Python version compatibility support for minidom."""
|
||||
|
||||
# This module should only be imported using "import *".
|
||||
#
|
||||
# The following names are defined:
|
||||
#
|
||||
# NodeList -- lightest possible NodeList implementation
|
||||
#
|
||||
# EmptyNodeList -- lightest possible NodeList that is guarateed to
|
||||
# remain empty (immutable)
|
||||
#
|
||||
# StringTypes -- tuple of defined string types
|
||||
#
|
||||
# defproperty -- function used in conjunction with GetattrMagic;
|
||||
# using these together is needed to make them work
|
||||
# as efficiently as possible in both Python 2.2+
|
||||
# and older versions. For example:
|
||||
#
|
||||
# class MyClass(GetattrMagic):
|
||||
# def _get_myattr(self):
|
||||
# return something
|
||||
#
|
||||
# defproperty(MyClass, "myattr",
|
||||
# "return some value")
|
||||
#
|
||||
# For Python 2.2 and newer, this will construct a
|
||||
# property object on the class, which avoids
|
||||
# needing to override __getattr__(). It will only
|
||||
# work for read-only attributes.
|
||||
#
|
||||
# For older versions of Python, inheriting from
|
||||
# GetattrMagic will use the traditional
|
||||
# __getattr__() hackery to achieve the same effect,
|
||||
# but less efficiently.
|
||||
#
|
||||
# defproperty() should be used for each version of
|
||||
# the relevant _get_<property>() function.
|
||||
|
||||
__all__ = ["NodeList", "EmptyNodeList", "StringTypes", "defproperty"]
|
||||
|
||||
import xml.dom
|
||||
|
||||
try:
|
||||
unicode
|
||||
except NameError:
|
||||
StringTypes = type(''),
|
||||
else:
|
||||
StringTypes = type(''), type(unicode(''))
|
||||
|
||||
|
||||
class NodeList(list):
|
||||
__slots__ = ()
|
||||
|
||||
def item(self, index):
|
||||
if 0 <= index < len(self):
|
||||
return self[index]
|
||||
|
||||
def _get_length(self):
|
||||
return len(self)
|
||||
|
||||
def _set_length(self, value):
|
||||
raise xml.dom.NoModificationAllowedErr(
|
||||
"attempt to modify read-only attribute 'length'")
|
||||
|
||||
length = property(_get_length, _set_length,
|
||||
doc="The number of nodes in the NodeList.")
|
||||
|
||||
def __getstate__(self):
|
||||
return list(self)
|
||||
|
||||
def __setstate__(self, state):
|
||||
self[:] = state
|
||||
|
||||
|
||||
class EmptyNodeList(tuple):
|
||||
__slots__ = ()
|
||||
|
||||
def __add__(self, other):
|
||||
NL = NodeList()
|
||||
NL.extend(other)
|
||||
return NL
|
||||
|
||||
def __radd__(self, other):
|
||||
NL = NodeList()
|
||||
NL.extend(other)
|
||||
return NL
|
||||
|
||||
def item(self, index):
|
||||
return None
|
||||
|
||||
def _get_length(self):
|
||||
return 0
|
||||
|
||||
def _set_length(self, value):
|
||||
raise xml.dom.NoModificationAllowedErr(
|
||||
"attempt to modify read-only attribute 'length'")
|
||||
|
||||
length = property(_get_length, _set_length,
|
||||
doc="The number of nodes in the NodeList.")
|
||||
|
||||
|
||||
def defproperty(klass, name, doc):
|
||||
get = getattr(klass, ("_get_" + name)).im_func
|
||||
def set(self, value, name=name):
|
||||
raise xml.dom.NoModificationAllowedErr(
|
||||
"attempt to modify read-only attribute " + repr(name))
|
||||
assert not hasattr(klass, "_set_" + name), \
|
||||
"expected not to find _set_" + name
|
||||
prop = property(get, set, doc=doc)
|
||||
setattr(klass, name, prop)
|
|
@ -0,0 +1,351 @@
|
|||
import xml.sax
|
||||
import xml.sax.handler
|
||||
import types
|
||||
|
||||
try:
|
||||
_StringTypes = [types.StringType, types.UnicodeType]
|
||||
except AttributeError:
|
||||
_StringTypes = [types.StringType]
|
||||
|
||||
START_ELEMENT = "START_ELEMENT"
|
||||
END_ELEMENT = "END_ELEMENT"
|
||||
COMMENT = "COMMENT"
|
||||
START_DOCUMENT = "START_DOCUMENT"
|
||||
END_DOCUMENT = "END_DOCUMENT"
|
||||
PROCESSING_INSTRUCTION = "PROCESSING_INSTRUCTION"
|
||||
IGNORABLE_WHITESPACE = "IGNORABLE_WHITESPACE"
|
||||
CHARACTERS = "CHARACTERS"
|
||||
|
||||
class PullDOM(xml.sax.ContentHandler):
|
||||
_locator = None
|
||||
document = None
|
||||
|
||||
def __init__(self, documentFactory=None):
|
||||
from xml.dom import XML_NAMESPACE
|
||||
self.documentFactory = documentFactory
|
||||
self.firstEvent = [None, None]
|
||||
self.lastEvent = self.firstEvent
|
||||
self.elementStack = []
|
||||
self.push = self.elementStack.append
|
||||
try:
|
||||
self.pop = self.elementStack.pop
|
||||
except AttributeError:
|
||||
# use class' pop instead
|
||||
pass
|
||||
self._ns_contexts = [{XML_NAMESPACE:'xml'}] # contains uri -> prefix dicts
|
||||
self._current_context = self._ns_contexts[-1]
|
||||
self.pending_events = []
|
||||
|
||||
def pop(self):
|
||||
result = self.elementStack[-1]
|
||||
del self.elementStack[-1]
|
||||
return result
|
||||
|
||||
def setDocumentLocator(self, locator):
|
||||
self._locator = locator
|
||||
|
||||
def startPrefixMapping(self, prefix, uri):
|
||||
if not hasattr(self, '_xmlns_attrs'):
|
||||
self._xmlns_attrs = []
|
||||
self._xmlns_attrs.append((prefix or 'xmlns', uri))
|
||||
self._ns_contexts.append(self._current_context.copy())
|
||||
self._current_context[uri] = prefix or None
|
||||
|
||||
def endPrefixMapping(self, prefix):
|
||||
self._current_context = self._ns_contexts.pop()
|
||||
|
||||
def startElementNS(self, name, tagName , attrs):
|
||||
# Retrieve xml namespace declaration attributes.
|
||||
xmlns_uri = 'http://www.w3.org/2000/xmlns/'
|
||||
xmlns_attrs = getattr(self, '_xmlns_attrs', None)
|
||||
if xmlns_attrs is not None:
|
||||
for aname, value in xmlns_attrs:
|
||||
attrs._attrs[(xmlns_uri, aname)] = value
|
||||
self._xmlns_attrs = []
|
||||
uri, localname = name
|
||||
if uri:
|
||||
# When using namespaces, the reader may or may not
|
||||
# provide us with the original name. If not, create
|
||||
# *a* valid tagName from the current context.
|
||||
if tagName is None:
|
||||
prefix = self._current_context[uri]
|
||||
if prefix:
|
||||
tagName = prefix + ":" + localname
|
||||
else:
|
||||
tagName = localname
|
||||
if self.document:
|
||||
node = self.document.createElementNS(uri, tagName)
|
||||
else:
|
||||
node = self.buildDocument(uri, tagName)
|
||||
else:
|
||||
# When the tagname is not prefixed, it just appears as
|
||||
# localname
|
||||
if self.document:
|
||||
node = self.document.createElement(localname)
|
||||
else:
|
||||
node = self.buildDocument(None, localname)
|
||||
|
||||
for aname,value in attrs.items():
|
||||
a_uri, a_localname = aname
|
||||
if a_uri == xmlns_uri:
|
||||
if a_localname == 'xmlns':
|
||||
qname = a_localname
|
||||
else:
|
||||
qname = 'xmlns:' + a_localname
|
||||
attr = self.document.createAttributeNS(a_uri, qname)
|
||||
node.setAttributeNodeNS(attr)
|
||||
elif a_uri:
|
||||
prefix = self._current_context[a_uri]
|
||||
if prefix:
|
||||
qname = prefix + ":" + a_localname
|
||||
else:
|
||||
qname = a_localname
|
||||
attr = self.document.createAttributeNS(a_uri, qname)
|
||||
node.setAttributeNodeNS(attr)
|
||||
else:
|
||||
attr = self.document.createAttribute(a_localname)
|
||||
node.setAttributeNode(attr)
|
||||
attr.value = value
|
||||
|
||||
self.lastEvent[1] = [(START_ELEMENT, node), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
self.push(node)
|
||||
|
||||
def endElementNS(self, name, tagName):
|
||||
self.lastEvent[1] = [(END_ELEMENT, self.pop()), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
|
||||
def startElement(self, name, attrs):
|
||||
if self.document:
|
||||
node = self.document.createElement(name)
|
||||
else:
|
||||
node = self.buildDocument(None, name)
|
||||
|
||||
for aname,value in attrs.items():
|
||||
attr = self.document.createAttribute(aname)
|
||||
attr.value = value
|
||||
node.setAttributeNode(attr)
|
||||
|
||||
self.lastEvent[1] = [(START_ELEMENT, node), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
self.push(node)
|
||||
|
||||
def endElement(self, name):
|
||||
self.lastEvent[1] = [(END_ELEMENT, self.pop()), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
|
||||
def comment(self, s):
|
||||
if self.document:
|
||||
node = self.document.createComment(s)
|
||||
self.lastEvent[1] = [(COMMENT, node), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
else:
|
||||
event = [(COMMENT, s), None]
|
||||
self.pending_events.append(event)
|
||||
|
||||
def processingInstruction(self, target, data):
|
||||
if self.document:
|
||||
node = self.document.createProcessingInstruction(target, data)
|
||||
self.lastEvent[1] = [(PROCESSING_INSTRUCTION, node), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
else:
|
||||
event = [(PROCESSING_INSTRUCTION, target, data), None]
|
||||
self.pending_events.append(event)
|
||||
|
||||
def ignorableWhitespace(self, chars):
|
||||
node = self.document.createTextNode(chars)
|
||||
self.lastEvent[1] = [(IGNORABLE_WHITESPACE, node), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
|
||||
def characters(self, chars):
|
||||
node = self.document.createTextNode(chars)
|
||||
self.lastEvent[1] = [(CHARACTERS, node), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
|
||||
def startDocument(self):
|
||||
if self.documentFactory is None:
|
||||
import xml.dom.minidom
|
||||
self.documentFactory = xml.dom.minidom.Document.implementation
|
||||
|
||||
def buildDocument(self, uri, tagname):
|
||||
# Can't do that in startDocument, since we need the tagname
|
||||
# XXX: obtain DocumentType
|
||||
node = self.documentFactory.createDocument(uri, tagname, None)
|
||||
self.document = node
|
||||
self.lastEvent[1] = [(START_DOCUMENT, node), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
self.push(node)
|
||||
# Put everything we have seen so far into the document
|
||||
for e in self.pending_events:
|
||||
if e[0][0] == PROCESSING_INSTRUCTION:
|
||||
_,target,data = e[0]
|
||||
n = self.document.createProcessingInstruction(target, data)
|
||||
e[0] = (PROCESSING_INSTRUCTION, n)
|
||||
elif e[0][0] == COMMENT:
|
||||
n = self.document.createComment(e[0][1])
|
||||
e[0] = (COMMENT, n)
|
||||
else:
|
||||
raise AssertionError("Unknown pending event ",e[0][0])
|
||||
self.lastEvent[1] = e
|
||||
self.lastEvent = e
|
||||
self.pending_events = None
|
||||
return node.firstChild
|
||||
|
||||
def endDocument(self):
|
||||
self.lastEvent[1] = [(END_DOCUMENT, self.document), None]
|
||||
self.pop()
|
||||
|
||||
def clear(self):
|
||||
"clear(): Explicitly release parsing structures"
|
||||
self.document = None
|
||||
|
||||
class ErrorHandler:
|
||||
def warning(self, exception):
|
||||
print exception
|
||||
def error(self, exception):
|
||||
raise exception
|
||||
def fatalError(self, exception):
|
||||
raise exception
|
||||
|
||||
class DOMEventStream:
|
||||
def __init__(self, stream, parser, bufsize):
|
||||
self.stream = stream
|
||||
self.parser = parser
|
||||
self.bufsize = bufsize
|
||||
if not hasattr(self.parser, 'feed'):
|
||||
self.getEvent = self._slurp
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.pulldom = PullDOM()
|
||||
# This content handler relies on namespace support
|
||||
self.parser.setFeature(xml.sax.handler.feature_namespaces, 1)
|
||||
self.parser.setContentHandler(self.pulldom)
|
||||
|
||||
def __getitem__(self, pos):
|
||||
rc = self.getEvent()
|
||||
if rc:
|
||||
return rc
|
||||
raise IndexError
|
||||
|
||||
def next(self):
|
||||
rc = self.getEvent()
|
||||
if rc:
|
||||
return rc
|
||||
raise StopIteration
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def expandNode(self, node):
|
||||
event = self.getEvent()
|
||||
parents = [node]
|
||||
while event:
|
||||
token, cur_node = event
|
||||
if cur_node is node:
|
||||
return
|
||||
if token != END_ELEMENT:
|
||||
parents[-1].appendChild(cur_node)
|
||||
if token == START_ELEMENT:
|
||||
parents.append(cur_node)
|
||||
elif token == END_ELEMENT:
|
||||
del parents[-1]
|
||||
event = self.getEvent()
|
||||
|
||||
def getEvent(self):
|
||||
# use IncrementalParser interface, so we get the desired
|
||||
# pull effect
|
||||
if not self.pulldom.firstEvent[1]:
|
||||
self.pulldom.lastEvent = self.pulldom.firstEvent
|
||||
while not self.pulldom.firstEvent[1]:
|
||||
buf = self.stream.read(self.bufsize)
|
||||
if not buf:
|
||||
self.parser.close()
|
||||
return None
|
||||
self.parser.feed(buf)
|
||||
rc = self.pulldom.firstEvent[1][0]
|
||||
self.pulldom.firstEvent[1] = self.pulldom.firstEvent[1][1]
|
||||
return rc
|
||||
|
||||
def _slurp(self):
|
||||
""" Fallback replacement for getEvent() using the
|
||||
standard SAX2 interface, which means we slurp the
|
||||
SAX events into memory (no performance gain, but
|
||||
we are compatible to all SAX parsers).
|
||||
"""
|
||||
self.parser.parse(self.stream)
|
||||
self.getEvent = self._emit
|
||||
return self._emit()
|
||||
|
||||
def _emit(self):
|
||||
""" Fallback replacement for getEvent() that emits
|
||||
the events that _slurp() read previously.
|
||||
"""
|
||||
rc = self.pulldom.firstEvent[1][0]
|
||||
self.pulldom.firstEvent[1] = self.pulldom.firstEvent[1][1]
|
||||
return rc
|
||||
|
||||
def clear(self):
|
||||
"""clear(): Explicitly release parsing objects"""
|
||||
self.pulldom.clear()
|
||||
del self.pulldom
|
||||
self.parser = None
|
||||
self.stream = None
|
||||
|
||||
class SAX2DOM(PullDOM):
|
||||
|
||||
def startElementNS(self, name, tagName , attrs):
|
||||
PullDOM.startElementNS(self, name, tagName, attrs)
|
||||
curNode = self.elementStack[-1]
|
||||
parentNode = self.elementStack[-2]
|
||||
parentNode.appendChild(curNode)
|
||||
|
||||
def startElement(self, name, attrs):
|
||||
PullDOM.startElement(self, name, attrs)
|
||||
curNode = self.elementStack[-1]
|
||||
parentNode = self.elementStack[-2]
|
||||
parentNode.appendChild(curNode)
|
||||
|
||||
def processingInstruction(self, target, data):
|
||||
PullDOM.processingInstruction(self, target, data)
|
||||
node = self.lastEvent[0][1]
|
||||
parentNode = self.elementStack[-1]
|
||||
parentNode.appendChild(node)
|
||||
|
||||
def ignorableWhitespace(self, chars):
|
||||
PullDOM.ignorableWhitespace(self, chars)
|
||||
node = self.lastEvent[0][1]
|
||||
parentNode = self.elementStack[-1]
|
||||
parentNode.appendChild(node)
|
||||
|
||||
def characters(self, chars):
|
||||
PullDOM.characters(self, chars)
|
||||
node = self.lastEvent[0][1]
|
||||
parentNode = self.elementStack[-1]
|
||||
parentNode.appendChild(node)
|
||||
|
||||
|
||||
default_bufsize = (2 ** 14) - 20
|
||||
|
||||
def parse(stream_or_string, parser=None, bufsize=None):
|
||||
if bufsize is None:
|
||||
bufsize = default_bufsize
|
||||
if type(stream_or_string) in _StringTypes:
|
||||
stream = open(stream_or_string)
|
||||
else:
|
||||
stream = stream_or_string
|
||||
if not parser:
|
||||
parser = xml.sax.make_parser()
|
||||
return DOMEventStream(stream, parser, bufsize)
|
||||
|
||||
def parseString(string, parser=None):
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
bufsize = len(string)
|
||||
buf = StringIO(string)
|
||||
if not parser:
|
||||
parser = xml.sax.make_parser()
|
||||
return DOMEventStream(buf, parser, bufsize)
|
|
@ -0,0 +1,386 @@
|
|||
"""Implementation of the DOM Level 3 'LS-Load' feature."""
|
||||
|
||||
import copy
|
||||
import xml.dom
|
||||
|
||||
from xml.dom.NodeFilter import NodeFilter
|
||||
|
||||
|
||||
__all__ = ["DOMBuilder", "DOMEntityResolver", "DOMInputSource"]
|
||||
|
||||
|
||||
class Options:
|
||||
"""Features object that has variables set for each DOMBuilder feature.
|
||||
|
||||
The DOMBuilder class uses an instance of this class to pass settings to
|
||||
the ExpatBuilder class.
|
||||
"""
|
||||
|
||||
# Note that the DOMBuilder class in LoadSave constrains which of these
|
||||
# values can be set using the DOM Level 3 LoadSave feature.
|
||||
|
||||
namespaces = 1
|
||||
namespace_declarations = True
|
||||
validation = False
|
||||
external_parameter_entities = True
|
||||
external_general_entities = True
|
||||
external_dtd_subset = True
|
||||
validate_if_schema = False
|
||||
validate = False
|
||||
datatype_normalization = False
|
||||
create_entity_ref_nodes = True
|
||||
entities = True
|
||||
whitespace_in_element_content = True
|
||||
cdata_sections = True
|
||||
comments = True
|
||||
charset_overrides_xml_encoding = True
|
||||
infoset = False
|
||||
supported_mediatypes_only = False
|
||||
|
||||
errorHandler = None
|
||||
filter = None
|
||||
|
||||
|
||||
class DOMBuilder:
|
||||
entityResolver = None
|
||||
errorHandler = None
|
||||
filter = None
|
||||
|
||||
ACTION_REPLACE = 1
|
||||
ACTION_APPEND_AS_CHILDREN = 2
|
||||
ACTION_INSERT_AFTER = 3
|
||||
ACTION_INSERT_BEFORE = 4
|
||||
|
||||
_legal_actions = (ACTION_REPLACE, ACTION_APPEND_AS_CHILDREN,
|
||||
ACTION_INSERT_AFTER, ACTION_INSERT_BEFORE)
|
||||
|
||||
def __init__(self):
|
||||
self._options = Options()
|
||||
|
||||
def _get_entityResolver(self):
|
||||
return self.entityResolver
|
||||
def _set_entityResolver(self, entityResolver):
|
||||
self.entityResolver = entityResolver
|
||||
|
||||
def _get_errorHandler(self):
|
||||
return self.errorHandler
|
||||
def _set_errorHandler(self, errorHandler):
|
||||
self.errorHandler = errorHandler
|
||||
|
||||
def _get_filter(self):
|
||||
return self.filter
|
||||
def _set_filter(self, filter):
|
||||
self.filter = filter
|
||||
|
||||
def setFeature(self, name, state):
|
||||
if self.supportsFeature(name):
|
||||
state = state and 1 or 0
|
||||
try:
|
||||
settings = self._settings[(_name_xform(name), state)]
|
||||
except KeyError:
|
||||
raise xml.dom.NotSupportedErr(
|
||||
"unsupported feature: %r" % (name,))
|
||||
else:
|
||||
for name, value in settings:
|
||||
setattr(self._options, name, value)
|
||||
else:
|
||||
raise xml.dom.NotFoundErr("unknown feature: " + repr(name))
|
||||
|
||||
def supportsFeature(self, name):
|
||||
return hasattr(self._options, _name_xform(name))
|
||||
|
||||
def canSetFeature(self, name, state):
|
||||
key = (_name_xform(name), state and 1 or 0)
|
||||
return self._settings.has_key(key)
|
||||
|
||||
# This dictionary maps from (feature,value) to a list of
|
||||
# (option,value) pairs that should be set on the Options object.
|
||||
# If a (feature,value) setting is not in this dictionary, it is
|
||||
# not supported by the DOMBuilder.
|
||||
#
|
||||
_settings = {
|
||||
("namespace_declarations", 0): [
|
||||
("namespace_declarations", 0)],
|
||||
("namespace_declarations", 1): [
|
||||
("namespace_declarations", 1)],
|
||||
("validation", 0): [
|
||||
("validation", 0)],
|
||||
("external_general_entities", 0): [
|
||||
("external_general_entities", 0)],
|
||||
("external_general_entities", 1): [
|
||||
("external_general_entities", 1)],
|
||||
("external_parameter_entities", 0): [
|
||||
("external_parameter_entities", 0)],
|
||||
("external_parameter_entities", 1): [
|
||||
("external_parameter_entities", 1)],
|
||||
("validate_if_schema", 0): [
|
||||
("validate_if_schema", 0)],
|
||||
("create_entity_ref_nodes", 0): [
|
||||
("create_entity_ref_nodes", 0)],
|
||||
("create_entity_ref_nodes", 1): [
|
||||
("create_entity_ref_nodes", 1)],
|
||||
("entities", 0): [
|
||||
("create_entity_ref_nodes", 0),
|
||||
("entities", 0)],
|
||||
("entities", 1): [
|
||||
("entities", 1)],
|
||||
("whitespace_in_element_content", 0): [
|
||||
("whitespace_in_element_content", 0)],
|
||||
("whitespace_in_element_content", 1): [
|
||||
("whitespace_in_element_content", 1)],
|
||||
("cdata_sections", 0): [
|
||||
("cdata_sections", 0)],
|
||||
("cdata_sections", 1): [
|
||||
("cdata_sections", 1)],
|
||||
("comments", 0): [
|
||||
("comments", 0)],
|
||||
("comments", 1): [
|
||||
("comments", 1)],
|
||||
("charset_overrides_xml_encoding", 0): [
|
||||
("charset_overrides_xml_encoding", 0)],
|
||||
("charset_overrides_xml_encoding", 1): [
|
||||
("charset_overrides_xml_encoding", 1)],
|
||||
("infoset", 0): [],
|
||||
("infoset", 1): [
|
||||
("namespace_declarations", 0),
|
||||
("validate_if_schema", 0),
|
||||
("create_entity_ref_nodes", 0),
|
||||
("entities", 0),
|
||||
("cdata_sections", 0),
|
||||
("datatype_normalization", 1),
|
||||
("whitespace_in_element_content", 1),
|
||||
("comments", 1),
|
||||
("charset_overrides_xml_encoding", 1)],
|
||||
("supported_mediatypes_only", 0): [
|
||||
("supported_mediatypes_only", 0)],
|
||||
("namespaces", 0): [
|
||||
("namespaces", 0)],
|
||||
("namespaces", 1): [
|
||||
("namespaces", 1)],
|
||||
}
|
||||
|
||||
def getFeature(self, name):
|
||||
xname = _name_xform(name)
|
||||
try:
|
||||
return getattr(self._options, xname)
|
||||
except AttributeError:
|
||||
if name == "infoset":
|
||||
options = self._options
|
||||
return (options.datatype_normalization
|
||||
and options.whitespace_in_element_content
|
||||
and options.comments
|
||||
and options.charset_overrides_xml_encoding
|
||||
and not (options.namespace_declarations
|
||||
or options.validate_if_schema
|
||||
or options.create_entity_ref_nodes
|
||||
or options.entities
|
||||
or options.cdata_sections))
|
||||
raise xml.dom.NotFoundErr("feature %s not known" % repr(name))
|
||||
|
||||
def parseURI(self, uri):
|
||||
if self.entityResolver:
|
||||
input = self.entityResolver.resolveEntity(None, uri)
|
||||
else:
|
||||
input = DOMEntityResolver().resolveEntity(None, uri)
|
||||
return self.parse(input)
|
||||
|
||||
def parse(self, input):
|
||||
options = copy.copy(self._options)
|
||||
options.filter = self.filter
|
||||
options.errorHandler = self.errorHandler
|
||||
fp = input.byteStream
|
||||
if fp is None and options.systemId:
|
||||
import urllib2
|
||||
fp = urllib2.urlopen(input.systemId)
|
||||
return self._parse_bytestream(fp, options)
|
||||
|
||||
def parseWithContext(self, input, cnode, action):
|
||||
if action not in self._legal_actions:
|
||||
raise ValueError("not a legal action")
|
||||
raise NotImplementedError("Haven't written this yet...")
|
||||
|
||||
def _parse_bytestream(self, stream, options):
|
||||
import xml.dom.expatbuilder
|
||||
builder = xml.dom.expatbuilder.makeBuilder(options)
|
||||
return builder.parseFile(stream)
|
||||
|
||||
|
||||
def _name_xform(name):
|
||||
return name.lower().replace('-', '_')
|
||||
|
||||
|
||||
class DOMEntityResolver(object):
|
||||
__slots__ = '_opener',
|
||||
|
||||
def resolveEntity(self, publicId, systemId):
|
||||
assert systemId is not None
|
||||
source = DOMInputSource()
|
||||
source.publicId = publicId
|
||||
source.systemId = systemId
|
||||
source.byteStream = self._get_opener().open(systemId)
|
||||
|
||||
# determine the encoding if the transport provided it
|
||||
source.encoding = self._guess_media_encoding(source)
|
||||
|
||||
# determine the base URI is we can
|
||||
import posixpath, urlparse
|
||||
parts = urlparse.urlparse(systemId)
|
||||
scheme, netloc, path, params, query, fragment = parts
|
||||
# XXX should we check the scheme here as well?
|
||||
if path and not path.endswith("/"):
|
||||
path = posixpath.dirname(path) + "/"
|
||||
parts = scheme, netloc, path, params, query, fragment
|
||||
source.baseURI = urlparse.urlunparse(parts)
|
||||
|
||||
return source
|
||||
|
||||
def _get_opener(self):
|
||||
try:
|
||||
return self._opener
|
||||
except AttributeError:
|
||||
self._opener = self._create_opener()
|
||||
return self._opener
|
||||
|
||||
def _create_opener(self):
|
||||
import urllib2
|
||||
return urllib2.build_opener()
|
||||
|
||||
def _guess_media_encoding(self, source):
|
||||
info = source.byteStream.info()
|
||||
if info.has_key("Content-Type"):
|
||||
for param in info.getplist():
|
||||
if param.startswith("charset="):
|
||||
return param.split("=", 1)[1].lower()
|
||||
|
||||
|
||||
class DOMInputSource(object):
|
||||
__slots__ = ('byteStream', 'characterStream', 'stringData',
|
||||
'encoding', 'publicId', 'systemId', 'baseURI')
|
||||
|
||||
def __init__(self):
|
||||
self.byteStream = None
|
||||
self.characterStream = None
|
||||
self.stringData = None
|
||||
self.encoding = None
|
||||
self.publicId = None
|
||||
self.systemId = None
|
||||
self.baseURI = None
|
||||
|
||||
def _get_byteStream(self):
|
||||
return self.byteStream
|
||||
def _set_byteStream(self, byteStream):
|
||||
self.byteStream = byteStream
|
||||
|
||||
def _get_characterStream(self):
|
||||
return self.characterStream
|
||||
def _set_characterStream(self, characterStream):
|
||||
self.characterStream = characterStream
|
||||
|
||||
def _get_stringData(self):
|
||||
return self.stringData
|
||||
def _set_stringData(self, data):
|
||||
self.stringData = data
|
||||
|
||||
def _get_encoding(self):
|
||||
return self.encoding
|
||||
def _set_encoding(self, encoding):
|
||||
self.encoding = encoding
|
||||
|
||||
def _get_publicId(self):
|
||||
return self.publicId
|
||||
def _set_publicId(self, publicId):
|
||||
self.publicId = publicId
|
||||
|
||||
def _get_systemId(self):
|
||||
return self.systemId
|
||||
def _set_systemId(self, systemId):
|
||||
self.systemId = systemId
|
||||
|
||||
def _get_baseURI(self):
|
||||
return self.baseURI
|
||||
def _set_baseURI(self, uri):
|
||||
self.baseURI = uri
|
||||
|
||||
|
||||
class DOMBuilderFilter:
|
||||
"""Element filter which can be used to tailor construction of
|
||||
a DOM instance.
|
||||
"""
|
||||
|
||||
# There's really no need for this class; concrete implementations
|
||||
# should just implement the endElement() and startElement()
|
||||
# methods as appropriate. Using this makes it easy to only
|
||||
# implement one of them.
|
||||
|
||||
FILTER_ACCEPT = 1
|
||||
FILTER_REJECT = 2
|
||||
FILTER_SKIP = 3
|
||||
FILTER_INTERRUPT = 4
|
||||
|
||||
whatToShow = NodeFilter.SHOW_ALL
|
||||
|
||||
def _get_whatToShow(self):
|
||||
return self.whatToShow
|
||||
|
||||
def acceptNode(self, element):
|
||||
return self.FILTER_ACCEPT
|
||||
|
||||
def startContainer(self, element):
|
||||
return self.FILTER_ACCEPT
|
||||
|
||||
del NodeFilter
|
||||
|
||||
|
||||
class DocumentLS:
|
||||
"""Mixin to create documents that conform to the load/save spec."""
|
||||
|
||||
async = False
|
||||
|
||||
def _get_async(self):
|
||||
return False
|
||||
def _set_async(self, async):
|
||||
if async:
|
||||
raise xml.dom.NotSupportedErr(
|
||||
"asynchronous document loading is not supported")
|
||||
|
||||
def abort(self):
|
||||
# What does it mean to "clear" a document? Does the
|
||||
# documentElement disappear?
|
||||
raise NotImplementedError(
|
||||
"haven't figured out what this means yet")
|
||||
|
||||
def load(self, uri):
|
||||
raise NotImplementedError("haven't written this yet")
|
||||
|
||||
def loadXML(self, source):
|
||||
raise NotImplementedError("haven't written this yet")
|
||||
|
||||
def saveXML(self, snode):
|
||||
if snode is None:
|
||||
snode = self
|
||||
elif snode.ownerDocument is not self:
|
||||
raise xml.dom.WrongDocumentErr()
|
||||
return snode.toxml()
|
||||
|
||||
|
||||
class DOMImplementationLS:
|
||||
MODE_SYNCHRONOUS = 1
|
||||
MODE_ASYNCHRONOUS = 2
|
||||
|
||||
def createDOMBuilder(self, mode, schemaType):
|
||||
if schemaType is not None:
|
||||
raise xml.dom.NotSupportedErr(
|
||||
"schemaType not yet supported")
|
||||
if mode == self.MODE_SYNCHRONOUS:
|
||||
return DOMBuilder()
|
||||
if mode == self.MODE_ASYNCHRONOUS:
|
||||
raise xml.dom.NotSupportedErr(
|
||||
"asynchronous builders are not supported")
|
||||
raise ValueError("unknown value for mode")
|
||||
|
||||
def createDOMWriter(self):
|
||||
raise NotImplementedError(
|
||||
"the writer interface hasn't been written yet!")
|
||||
|
||||
def createDOMInputSource(self):
|
||||
return DOMInputSource()
|
|
@ -0,0 +1,143 @@
|
|||
#
|
||||
# ElementTree
|
||||
# $Id: ElementInclude.py 1862 2004-06-18 07:31:02Z Fredrik $
|
||||
#
|
||||
# limited xinclude support for element trees
|
||||
#
|
||||
# history:
|
||||
# 2003-08-15 fl created
|
||||
# 2003-11-14 fl fixed default loader
|
||||
#
|
||||
# Copyright (c) 2003-2004 by Fredrik Lundh. All rights reserved.
|
||||
#
|
||||
# fredrik@pythonware.com
|
||||
# http://www.pythonware.com
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# The ElementTree toolkit is
|
||||
#
|
||||
# Copyright (c) 1999-2004 by Fredrik Lundh
|
||||
#
|
||||
# By obtaining, using, and/or copying this software and/or its
|
||||
# associated documentation, you agree that you have read, understood,
|
||||
# and will comply with the following terms and conditions:
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its associated documentation for any purpose and without fee is
|
||||
# hereby granted, provided that the above copyright notice appears in
|
||||
# all copies, and that both that copyright notice and this permission
|
||||
# notice appear in supporting documentation, and that the name of
|
||||
# Secret Labs AB or the author not be used in advertising or publicity
|
||||
# pertaining to distribution of the software without specific, written
|
||||
# prior permission.
|
||||
#
|
||||
# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
|
||||
# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
|
||||
# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
|
||||
# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
|
||||
# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
|
||||
# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
|
||||
# OF THIS SOFTWARE.
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See http://www.python.org/2.4/license for licensing details.
|
||||
|
||||
##
|
||||
# Limited XInclude support for the ElementTree package.
|
||||
##
|
||||
|
||||
import copy
|
||||
import ElementTree
|
||||
|
||||
XINCLUDE = "{http://www.w3.org/2001/XInclude}"
|
||||
|
||||
XINCLUDE_INCLUDE = XINCLUDE + "include"
|
||||
XINCLUDE_FALLBACK = XINCLUDE + "fallback"
|
||||
|
||||
##
|
||||
# Fatal include error.
|
||||
|
||||
class FatalIncludeError(SyntaxError):
|
||||
pass
|
||||
|
||||
##
|
||||
# Default loader. This loader reads an included resource from disk.
|
||||
#
|
||||
# @param href Resource reference.
|
||||
# @param parse Parse mode. Either "xml" or "text".
|
||||
# @param encoding Optional text encoding.
|
||||
# @return The expanded resource. If the parse mode is "xml", this
|
||||
# is an ElementTree instance. If the parse mode is "text", this
|
||||
# is a Unicode string. If the loader fails, it can return None
|
||||
# or raise an IOError exception.
|
||||
# @throws IOError If the loader fails to load the resource.
|
||||
|
||||
def default_loader(href, parse, encoding=None):
|
||||
file = open(href)
|
||||
if parse == "xml":
|
||||
data = ElementTree.parse(file).getroot()
|
||||
else:
|
||||
data = file.read()
|
||||
if encoding:
|
||||
data = data.decode(encoding)
|
||||
file.close()
|
||||
return data
|
||||
|
||||
##
|
||||
# Expand XInclude directives.
|
||||
#
|
||||
# @param elem Root element.
|
||||
# @param loader Optional resource loader. If omitted, it defaults
|
||||
# to {@link default_loader}. If given, it should be a callable
|
||||
# that implements the same interface as <b>default_loader</b>.
|
||||
# @throws FatalIncludeError If the function fails to include a given
|
||||
# resource, or if the tree contains malformed XInclude elements.
|
||||
# @throws IOError If the function fails to load a given resource.
|
||||
|
||||
def include(elem, loader=None):
|
||||
if loader is None:
|
||||
loader = default_loader
|
||||
# look for xinclude elements
|
||||
i = 0
|
||||
while i < len(elem):
|
||||
e = elem[i]
|
||||
if e.tag == XINCLUDE_INCLUDE:
|
||||
# process xinclude directive
|
||||
href = e.get("href")
|
||||
parse = e.get("parse", "xml")
|
||||
if parse == "xml":
|
||||
node = loader(href, parse)
|
||||
if node is None:
|
||||
raise FatalIncludeError(
|
||||
"cannot load %r as %r" % (href, parse)
|
||||
)
|
||||
node = copy.copy(node)
|
||||
if e.tail:
|
||||
node.tail = (node.tail or "") + e.tail
|
||||
elem[i] = node
|
||||
elif parse == "text":
|
||||
text = loader(href, parse, e.get("encoding"))
|
||||
if text is None:
|
||||
raise FatalIncludeError(
|
||||
"cannot load %r as %r" % (href, parse)
|
||||
)
|
||||
if i:
|
||||
node = elem[i-1]
|
||||
node.tail = (node.tail or "") + text
|
||||
else:
|
||||
elem.text = (elem.text or "") + text + (e.tail or "")
|
||||
del elem[i]
|
||||
continue
|
||||
else:
|
||||
raise FatalIncludeError(
|
||||
"unknown parse type in xi:include tag (%r)" % parse
|
||||
)
|
||||
elif e.tag == XINCLUDE_FALLBACK:
|
||||
raise FatalIncludeError(
|
||||
"xi:fallback tag must be child of xi:include (%r)" % e.tag
|
||||
)
|
||||
else:
|
||||
include(e, loader)
|
||||
i = i + 1
|
|
@ -0,0 +1,198 @@
|
|||
#
|
||||
# ElementTree
|
||||
# $Id: ElementPath.py 1858 2004-06-17 21:31:41Z Fredrik $
|
||||
#
|
||||
# limited xpath support for element trees
|
||||
#
|
||||
# history:
|
||||
# 2003-05-23 fl created
|
||||
# 2003-05-28 fl added support for // etc
|
||||
# 2003-08-27 fl fixed parsing of periods in element names
|
||||
#
|
||||
# Copyright (c) 2003-2004 by Fredrik Lundh. All rights reserved.
|
||||
#
|
||||
# fredrik@pythonware.com
|
||||
# http://www.pythonware.com
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# The ElementTree toolkit is
|
||||
#
|
||||
# Copyright (c) 1999-2004 by Fredrik Lundh
|
||||
#
|
||||
# By obtaining, using, and/or copying this software and/or its
|
||||
# associated documentation, you agree that you have read, understood,
|
||||
# and will comply with the following terms and conditions:
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its associated documentation for any purpose and without fee is
|
||||
# hereby granted, provided that the above copyright notice appears in
|
||||
# all copies, and that both that copyright notice and this permission
|
||||
# notice appear in supporting documentation, and that the name of
|
||||
# Secret Labs AB or the author not be used in advertising or publicity
|
||||
# pertaining to distribution of the software without specific, written
|
||||
# prior permission.
|
||||
#
|
||||
# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
|
||||
# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
|
||||
# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
|
||||
# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
|
||||
# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
|
||||
# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
|
||||
# OF THIS SOFTWARE.
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See http://www.python.org/2.4/license for licensing details.
|
||||
|
||||
##
|
||||
# Implementation module for XPath support. There's usually no reason
|
||||
# to import this module directly; the <b>ElementTree</b> does this for
|
||||
# you, if needed.
|
||||
##
|
||||
|
||||
import re
|
||||
|
||||
xpath_tokenizer = re.compile(
|
||||
"(::|\.\.|\(\)|[/.*:\[\]\(\)@=])|((?:\{[^}]+\})?[^/:\[\]\(\)@=\s]+)|\s+"
|
||||
).findall
|
||||
|
||||
class xpath_descendant_or_self:
|
||||
pass
|
||||
|
||||
##
|
||||
# Wrapper for a compiled XPath.
|
||||
|
||||
class Path:
|
||||
|
||||
##
|
||||
# Create an Path instance from an XPath expression.
|
||||
|
||||
def __init__(self, path):
|
||||
tokens = xpath_tokenizer(path)
|
||||
# the current version supports 'path/path'-style expressions only
|
||||
self.path = []
|
||||
self.tag = None
|
||||
if tokens and tokens[0][0] == "/":
|
||||
raise SyntaxError("cannot use absolute path on element")
|
||||
while tokens:
|
||||
op, tag = tokens.pop(0)
|
||||
if tag or op == "*":
|
||||
self.path.append(tag or op)
|
||||
elif op == ".":
|
||||
pass
|
||||
elif op == "/":
|
||||
self.path.append(xpath_descendant_or_self())
|
||||
continue
|
||||
else:
|
||||
raise SyntaxError("unsupported path syntax (%s)" % op)
|
||||
if tokens:
|
||||
op, tag = tokens.pop(0)
|
||||
if op != "/":
|
||||
raise SyntaxError(
|
||||
"expected path separator (%s)" % (op or tag)
|
||||
)
|
||||
if self.path and isinstance(self.path[-1], xpath_descendant_or_self):
|
||||
raise SyntaxError("path cannot end with //")
|
||||
if len(self.path) == 1 and isinstance(self.path[0], type("")):
|
||||
self.tag = self.path[0]
|
||||
|
||||
##
|
||||
# Find first matching object.
|
||||
|
||||
def find(self, element):
|
||||
tag = self.tag
|
||||
if tag is None:
|
||||
nodeset = self.findall(element)
|
||||
if not nodeset:
|
||||
return None
|
||||
return nodeset[0]
|
||||
for elem in element:
|
||||
if elem.tag == tag:
|
||||
return elem
|
||||
return None
|
||||
|
||||
##
|
||||
# Find text for first matching object.
|
||||
|
||||
def findtext(self, element, default=None):
|
||||
tag = self.tag
|
||||
if tag is None:
|
||||
nodeset = self.findall(element)
|
||||
if not nodeset:
|
||||
return default
|
||||
return nodeset[0].text or ""
|
||||
for elem in element:
|
||||
if elem.tag == tag:
|
||||
return elem.text or ""
|
||||
return default
|
||||
|
||||
##
|
||||
# Find all matching objects.
|
||||
|
||||
def findall(self, element):
|
||||
nodeset = [element]
|
||||
index = 0
|
||||
while 1:
|
||||
try:
|
||||
path = self.path[index]
|
||||
index = index + 1
|
||||
except IndexError:
|
||||
return nodeset
|
||||
set = []
|
||||
if isinstance(path, xpath_descendant_or_self):
|
||||
try:
|
||||
tag = self.path[index]
|
||||
if not isinstance(tag, type("")):
|
||||
tag = None
|
||||
else:
|
||||
index = index + 1
|
||||
except IndexError:
|
||||
tag = None # invalid path
|
||||
for node in nodeset:
|
||||
new = list(node.getiterator(tag))
|
||||
if new and new[0] is node:
|
||||
set.extend(new[1:])
|
||||
else:
|
||||
set.extend(new)
|
||||
else:
|
||||
for node in nodeset:
|
||||
for node in node:
|
||||
if path == "*" or node.tag == path:
|
||||
set.append(node)
|
||||
if not set:
|
||||
return []
|
||||
nodeset = set
|
||||
|
||||
_cache = {}
|
||||
|
||||
##
|
||||
# (Internal) Compile path.
|
||||
|
||||
def _compile(path):
|
||||
p = _cache.get(path)
|
||||
if p is not None:
|
||||
return p
|
||||
p = Path(path)
|
||||
if len(_cache) >= 100:
|
||||
_cache.clear()
|
||||
_cache[path] = p
|
||||
return p
|
||||
|
||||
##
|
||||
# Find first matching object.
|
||||
|
||||
def find(element, path):
|
||||
return _compile(path).find(element)
|
||||
|
||||
##
|
||||
# Find text for first matching object.
|
||||
|
||||
def findtext(element, path, default=None):
|
||||
return _compile(path).findtext(element, default)
|
||||
|
||||
##
|
||||
# Find all matching objects.
|
||||
|
||||
def findall(element, path):
|
||||
return _compile(path).findall(element)
|
|
@ -0,0 +1,33 @@
|
|||
# $Id: __init__.py 1821 2004-06-03 16:57:49Z fredrik $
|
||||
# elementtree package
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# The ElementTree toolkit is
|
||||
#
|
||||
# Copyright (c) 1999-2004 by Fredrik Lundh
|
||||
#
|
||||
# By obtaining, using, and/or copying this software and/or its
|
||||
# associated documentation, you agree that you have read, understood,
|
||||
# and will comply with the following terms and conditions:
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its associated documentation for any purpose and without fee is
|
||||
# hereby granted, provided that the above copyright notice appears in
|
||||
# all copies, and that both that copyright notice and this permission
|
||||
# notice appear in supporting documentation, and that the name of
|
||||
# Secret Labs AB or the author not be used in advertising or publicity
|
||||
# pertaining to distribution of the software without specific, written
|
||||
# prior permission.
|
||||
#
|
||||
# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
|
||||
# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
|
||||
# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
|
||||
# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
|
||||
# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
|
||||
# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
|
||||
# OF THIS SOFTWARE.
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See http://www.python.org/2.4/license for licensing details.
|
|
@ -0,0 +1,3 @@
|
|||
# Wrapper module for _elementtree
|
||||
|
||||
from ElementTree import *
|