initial commit

This commit is contained in:
Erik Kristensen 2010-07-30 22:21:27 -04:00
commit 5fbcc143bc
117 changed files with 27135 additions and 0 deletions

15
descriptor.xml Normal file
View File

@ -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>

39
launch.py Normal file
View File

@ -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
mysql/__init__.py Normal file
View File

View File

@ -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',
]

View File

@ -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', '')

View File

@ -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)

View File

@ -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)

View File

@ -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)

542
mysql/connector/cursor.py Normal file
View File

@ -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

64
mysql/connector/dbapi.py Normal file
View File

@ -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()

87
mysql/connector/errors.py Normal file
View File

@ -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

414
mysql/connector/mysql.py Normal file
View File

@ -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")

863
mysql/connector/protocol.py Normal file
View File

@ -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)

392
mysql/connector/utils.py Normal file
View File

@ -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

212
mythboxee.py Normal file
View File

@ -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

35
mythtv/.svn/all-wcprops Normal file
View File

@ -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

204
mythtv/.svn/entries Normal file
View File

@ -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

View File

@ -0,0 +1,5 @@
K 14
svn:executable
V 1
*
END

View File

@ -0,0 +1,5 @@
K 14
svn:executable
V 1
*
END

View File

@ -0,0 +1,5 @@
K 14
svn:executable
V 1
*
END

View File

@ -0,0 +1,5 @@
K 14
svn:executable
V 1
*
END

View File

@ -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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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 = '[]:[]'

View File

@ -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)

1872
mythtv/MythBase.py Executable file

File diff suppressed because it is too large Load Diff

BIN
mythtv/MythBase.pyc Executable file

Binary file not shown.

1502
mythtv/MythData.py Executable file

File diff suppressed because it is too large Load Diff

BIN
mythtv/MythData.pyc Executable file

Binary file not shown.

1167
mythtv/MythFunc.py Executable file

File diff suppressed because it is too large Load Diff

BIN
mythtv/MythFunc.pyc Executable file

Binary file not shown.

12
mythtv/MythStatic.py Executable file
View File

@ -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 = '[]:[]'

BIN
mythtv/MythStatic.pyc Executable file

Binary file not shown.

51
mythtv/__init__.py Executable file
View File

@ -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)

BIN
mythtv/__init__.pyc Executable file

Binary file not shown.

View File

@ -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

164
mythtv/tmdb/.svn/entries Normal file
View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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__

View File

@ -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
mythtv/tmdb/__init__.py Normal file
View File

1005
mythtv/tmdb/tmdb_api.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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__

266
mythtv/tmdb/tmdb_ui.py Normal file
View File

@ -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()

View File

@ -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

232
mythtv/ttvdb/.svn/entries Normal file
View File

@ -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

View File

@ -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()

View File

@ -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------------------------------------------------------------------------------

View File

@ -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 &amp; with &
- Trailing whitespace
"""
data = data.replace(u"&amp;", 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()

View File

@ -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

View File

@ -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
mythtv/ttvdb/__init__.py Normal file
View File

232
mythtv/ttvdb/cache.py Normal file
View File

@ -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()

View File

@ -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------------------------------------------------------------------------------

805
mythtv/ttvdb/tvdb_api.py Normal file
View File

@ -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 &amp; with &
- Trailing whitespace
"""
data = data.replace(u"&amp;", 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()

View File

@ -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

124
mythtv/ttvdb/tvdb_ui.py Normal file
View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

51
xml/__init__.py Normal file
View File

@ -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

BIN
xml/__init__.pyc Normal file

Binary file not shown.

27
xml/dom/NodeFilter.py Normal file
View File

@ -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

BIN
xml/dom/NodeFilter.pyc Normal file

Binary file not shown.

139
xml/dom/__init__.py Normal file
View File

@ -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

BIN
xml/dom/__init__.pyc Normal file

Binary file not shown.

99
xml/dom/domreg.py Normal file
View File

@ -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)

BIN
xml/dom/domreg.pyc Normal file

Binary file not shown.

983
xml/dom/expatbuilder.py Normal file
View File

@ -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)

BIN
xml/dom/expatbuilder.pyc Normal file

Binary file not shown.

110
xml/dom/minicompat.py Normal file
View File

@ -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)

BIN
xml/dom/minicompat.pyc Normal file

Binary file not shown.

1941
xml/dom/minidom.py Normal file

File diff suppressed because it is too large Load Diff

BIN
xml/dom/minidom.pyc Normal file

Binary file not shown.

351
xml/dom/pulldom.py Normal file
View File

@ -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)

BIN
xml/dom/pulldom.pyc Normal file

Binary file not shown.

386
xml/dom/xmlbuilder.py Normal file
View File

@ -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()

BIN
xml/dom/xmlbuilder.pyc Normal file

Binary file not shown.

143
xml/etree/ElementInclude.py Normal file
View File

@ -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

Binary file not shown.

198
xml/etree/ElementPath.py Normal file
View File

@ -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)

BIN
xml/etree/ElementPath.pyc Normal file

Binary file not shown.

1260
xml/etree/ElementTree.py Normal file

File diff suppressed because it is too large Load Diff

BIN
xml/etree/ElementTree.pyc Normal file

Binary file not shown.

33
xml/etree/__init__.py Normal file
View File

@ -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.

BIN
xml/etree/__init__.pyc Normal file

Binary file not shown.

View File

@ -0,0 +1,3 @@
# Wrapper module for _elementtree
from ElementTree import *

Some files were not shown because too many files have changed in this diff Show More