Merge branch 'mythboxee4'
|
@ -0,0 +1,15 @@
|
|||
<app>
|
||||
<id>mythboxy</id>
|
||||
<name>MythBoxy</name>
|
||||
<version>4.0.beta</version>
|
||||
<description>Watch all your MythTV recordings from Boxee.</description>
|
||||
<thumb>http://erikkristensen.com/boxee/logo4.jpg</thumb>
|
||||
<media>video</media>
|
||||
<copyright>Erik Kristensen</copyright>
|
||||
<email>erik@erikkristensen.com</email>
|
||||
<type>skin</type>
|
||||
<startWindow>launch</startWindow>
|
||||
<platform>all</platform>
|
||||
<minversion>0.9.20</minversion>
|
||||
<test-app>true</test-app>
|
||||
</app>
|
|
@ -0,0 +1,8 @@
|
|||
import mc
|
||||
from mythboxee import MythBoxee
|
||||
|
||||
# DEBUG #
|
||||
#mc.GetApp().GetLocalConfig().ResetAll()
|
||||
# DEBUG #
|
||||
|
||||
mc.ActivateWindow(14001)
|
|
@ -0,0 +1,74 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""
|
||||
MySQL Connector/Python - MySQL drive written in Python
|
||||
"""
|
||||
|
||||
import sys
|
||||
_name = 'MySQL Connector/Python'
|
||||
if not hasattr(sys, "version_info") or sys.version_info < (2,4):
|
||||
raise RuntimeError("%s requires Python 2.4 or higher." % (_name))
|
||||
elif sys.version_info >= (3,0):
|
||||
raise RuntimeError("%s does not yet support Python v3." % (_name))
|
||||
del _name
|
||||
del sys
|
||||
|
||||
# Python Db API v2
|
||||
apilevel = '2.0'
|
||||
threadsafety = 1
|
||||
paramstyle = 'pyformat'
|
||||
|
||||
# Read the version from an generated file
|
||||
import _version
|
||||
__version__ = _version.version
|
||||
|
||||
from mysql import MySQL
|
||||
from errors import *
|
||||
from constants import FieldFlag, FieldType, CharacterSet, RefreshOption
|
||||
from dbapi import *
|
||||
|
||||
def Connect(*args, **kwargs):
|
||||
"""Shortcut for creating a mysql.MySQL object."""
|
||||
return MySQL(*args, **kwargs)
|
||||
connect = Connect
|
||||
|
||||
__all__ = [
|
||||
'MySQL', 'Connect',
|
||||
|
||||
# Some useful constants
|
||||
'FieldType','FieldFlag','CharacterSet','RefreshOption',
|
||||
|
||||
# Error handling
|
||||
'Error','Warning',
|
||||
'InterfaceError','DatabaseError',
|
||||
'NotSupportedError','DataError','IntegrityError','ProgrammingError',
|
||||
'OperationalError','InternalError',
|
||||
|
||||
# DBAPI PEP 249 required exports
|
||||
'connect','apilevel','threadsafety','paramstyle',
|
||||
'Date', 'Time', 'Timestamp', 'Binary',
|
||||
'DateFromTicks', 'DateFromTicks', 'TimestampFromTicks',
|
||||
'STRING', 'BINARY', 'NUMBER',
|
||||
'DATETIME', 'ROWID',
|
||||
]
|
|
@ -0,0 +1,28 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""Holds version of MySQL Connector/Python
|
||||
"""
|
||||
|
||||
# Next line is generated
|
||||
version = (0, 1, 0, 'devel', '')
|
|
@ -0,0 +1,148 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""Implementing communication to MySQL servers
|
||||
"""
|
||||
|
||||
import socket
|
||||
import os
|
||||
|
||||
import protocol
|
||||
import errors
|
||||
from constants import CharacterSet
|
||||
|
||||
class MySQLBaseConnection(object):
|
||||
"""Base class for MySQL Connections subclasses.
|
||||
|
||||
Should not be used directly but overloaded, changing the
|
||||
open_connection part. Examples over subclasses are
|
||||
MySQLTCPConnection
|
||||
MySQLUNIXConnection
|
||||
"""
|
||||
def __init__(self, prtcls=None):
|
||||
self.sock = None # holds the socket connection
|
||||
self.connection_timeout = None
|
||||
self.protocol = None
|
||||
self.socket_flags = 0
|
||||
try:
|
||||
self.protocol = prtcls(self)
|
||||
except:
|
||||
self.protocol = protocol.MySQLProtocol(self)
|
||||
self._set_socket_flags()
|
||||
|
||||
def open_connection(self):
|
||||
pass
|
||||
|
||||
def close_connection(self):
|
||||
try:
|
||||
self.sock.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
def send(self, buf):
|
||||
"""
|
||||
Send packets using the socket to the server.
|
||||
"""
|
||||
pktlen = len(buf)
|
||||
try:
|
||||
while pktlen:
|
||||
pktlen -= self.sock.send(buf)
|
||||
except Exception, e:
|
||||
raise errors.OperationalError('%s' % e)
|
||||
|
||||
def recv(self):
|
||||
"""
|
||||
Receive packets using the socket from the server.
|
||||
"""
|
||||
try:
|
||||
header = self.sock.recv(4, self.socket_flags)
|
||||
(pktsize, pktnr) = self.protocol.handle_header(header)
|
||||
buf = header + self.sock.recv(pktsize, self.socket_flags)
|
||||
self.protocol.is_error(buf)
|
||||
except:
|
||||
raise
|
||||
|
||||
return (buf, pktsize, pktnr)
|
||||
|
||||
def set_protocol(self, prtcls):
|
||||
try:
|
||||
self.protocol = prtcls(self, self.protocol.handshake)
|
||||
except:
|
||||
self.protocol = protocol.MySQLProtocol(self)
|
||||
|
||||
def set_connection_timeout(self, timeout):
|
||||
self.connection_timeout = timeout
|
||||
|
||||
def _set_socket_flags(self, flags=None):
|
||||
self.socket_flags = 0
|
||||
if flags is None:
|
||||
if os.name == 'nt':
|
||||
flags = 0
|
||||
else:
|
||||
flags = socket.MSG_WAITALL
|
||||
|
||||
if flags is not None:
|
||||
self.socket_flags = flags
|
||||
|
||||
|
||||
class MySQLUnixConnection(MySQLBaseConnection):
|
||||
"""Opens a connection through the UNIX socket of the MySQL Server."""
|
||||
|
||||
def __init__(self, prtcls=None,unix_socket='/tmp/mysql.sock'):
|
||||
MySQLBaseConnection.__init__(self, prtcls=prtcls)
|
||||
self.unix_socket = unix_socket
|
||||
self.socket_flags = socket.MSG_WAITALL
|
||||
|
||||
def open_connection(self):
|
||||
"""Opens a UNIX socket and checks the MySQL handshake."""
|
||||
try:
|
||||
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.sock.settimeout(self.connection_timeout)
|
||||
self.sock.connect(self.unix_socket)
|
||||
except StandardError, e:
|
||||
raise errors.OperationalError('%s' % e)
|
||||
|
||||
buf = self.recv()[0]
|
||||
self.protocol.handle_handshake(buf)
|
||||
|
||||
class MySQLTCPConnection(MySQLBaseConnection):
|
||||
"""Opens a TCP connection to the MySQL Server."""
|
||||
|
||||
def __init__(self, prtcls=None, host='127.0.0.1', port=3306):
|
||||
MySQLBaseConnection.__init__(self, prtcls=prtcls)
|
||||
self.server_host = host
|
||||
self.server_port = port
|
||||
|
||||
def open_connection(self):
|
||||
"""Opens a TCP Connection and checks the MySQL handshake."""
|
||||
try:
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.settimeout(self.connection_timeout)
|
||||
self.sock.connect( (self.server_host, self.server_port) )
|
||||
except StandardError, e:
|
||||
raise errors.OperationalError('%s' % e)
|
||||
|
||||
buf = self.recv()[0]
|
||||
self.protocol.handle_handshake(buf)
|
||||
|
||||
|
|
@ -0,0 +1,571 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""Various MySQL constants and character sets
|
||||
"""
|
||||
|
||||
from errors import ProgrammingError
|
||||
|
||||
class _constants(object):
|
||||
|
||||
prefix = ''
|
||||
desc = {}
|
||||
|
||||
def __new__(cls):
|
||||
raise TypeError, "Can not instanciate from %s" % cls.__name__
|
||||
|
||||
@classmethod
|
||||
def get_desc(cls,name):
|
||||
res = ''
|
||||
try:
|
||||
res = cls.desc[name][1]
|
||||
except KeyError, e:
|
||||
raise KeyError, e
|
||||
else:
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def get_info(cls,n):
|
||||
res = ()
|
||||
for k,v in cls.desc.items():
|
||||
if v[0] == n:
|
||||
return v[1]
|
||||
raise KeyError, e
|
||||
|
||||
@classmethod
|
||||
def get_full_info(cls):
|
||||
res = ()
|
||||
try:
|
||||
res = ["%s : %s" % (k,v[1]) for k,v in cls.desc.items()]
|
||||
except StandardError, e:
|
||||
res = ('No information found in constant class.%s' % e)
|
||||
|
||||
return res
|
||||
|
||||
class FieldType(_constants):
|
||||
|
||||
prefix = 'FIELD_TYPE_'
|
||||
DECIMAL = 0x00
|
||||
TINY = 0x01
|
||||
SHORT = 0x02
|
||||
LONG = 0x03
|
||||
FLOAT = 0x04
|
||||
DOUBLE = 0x05
|
||||
NULL = 0x06
|
||||
TIMESTAMP = 0x07
|
||||
LONGLONG = 0x08
|
||||
INT24 = 0x09
|
||||
DATE = 0x0a
|
||||
TIME = 0x0b
|
||||
DATETIME = 0x0c
|
||||
YEAR = 0x0d
|
||||
NEWDATE = 0x0e
|
||||
VARCHAR = 0x0f
|
||||
BIT = 0x10
|
||||
NEWDECIMAL = 0xf6
|
||||
ENUM = 0xf7
|
||||
SET = 0xf8
|
||||
TINY_BLOB = 0xf9
|
||||
MEDIUM_BLOB = 0xfa
|
||||
LONG_BLOB = 0xfb
|
||||
BLOB = 0xfc
|
||||
VAR_STRING = 0xfd
|
||||
STRING = 0xfe
|
||||
GEOMETRY = 0xff
|
||||
|
||||
desc = {
|
||||
'DECIMAL': (0x00, 'DECIMAL'),
|
||||
'TINY': (0x01, 'TINY'),
|
||||
'SHORT': (0x02, 'SHORT'),
|
||||
'LONG': (0x03, 'LONG'),
|
||||
'FLOAT': (0x04, 'FLOAT'),
|
||||
'DOUBLE': (0x05, 'DOUBLE'),
|
||||
'NULL': (0x06, 'NULL'),
|
||||
'TIMESTAMP': (0x07, 'TIMESTAMP'),
|
||||
'LONGLONG': (0x08, 'LONGLONG'),
|
||||
'INT24': (0x09, 'INT24'),
|
||||
'DATE': (0x0a, 'DATE'),
|
||||
'TIME': (0x0b, 'TIME'),
|
||||
'DATETIME': (0x0c, 'DATETIME'),
|
||||
'YEAR': (0x0d, 'YEAR'),
|
||||
'NEWDATE': (0x0e, 'NEWDATE'),
|
||||
'VARCHAR': (0x0f, 'VARCHAR'),
|
||||
'BIT': (0x10, 'BIT'),
|
||||
'NEWDECIMAL': (0xf6, 'NEWDECIMAL'),
|
||||
'ENUM': (0xf7, 'ENUM'),
|
||||
'SET': (0xf8, 'SET'),
|
||||
'TINY_BLOB': (0xf9, 'TINY_BLOB'),
|
||||
'MEDIUM_BLOB': (0xfa, 'MEDIUM_BLOB'),
|
||||
'LONG_BLOB': (0xfb, 'LONG_BLOB'),
|
||||
'BLOB': (0xfc, 'BLOB'),
|
||||
'VAR_STRING': (0xfd, 'VAR_STRING'),
|
||||
'STRING': (0xfe, 'STRING'),
|
||||
'GEOMETRY': (0xff, 'GEOMETRY'),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_string_types(cls):
|
||||
return [
|
||||
cls.VARCHAR,
|
||||
cls.ENUM,
|
||||
cls.VAR_STRING, cls.STRING,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_binary_types(cls):
|
||||
return [
|
||||
cls.TINY_BLOB, cls.MEDIUM_BLOB,
|
||||
cls.LONG_BLOB, cls.BLOB,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_number_types(cls):
|
||||
return [
|
||||
cls.DECIMAL, cls.NEWDECIMAL,
|
||||
cls.TINY, cls.SHORT, cls.LONG,
|
||||
cls.FLOAT, cls.DOUBLE,
|
||||
cls.LONGLONG, cls.INT24,
|
||||
cls.BIT,
|
||||
cls.YEAR,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_timestamp_types(cls):
|
||||
return [
|
||||
cls.DATETIME, cls.TIMESTAMP,
|
||||
]
|
||||
|
||||
class FieldFlag(_constants):
|
||||
"""
|
||||
Field flags as found in MySQL sources mysql-src/include/mysql_com.h
|
||||
"""
|
||||
_prefix = ''
|
||||
NOT_NULL = 1 << 0
|
||||
PRI_KEY = 1 << 1
|
||||
UNIQUE_KEY = 1 << 2
|
||||
MULTIPLE_KEY = 1 << 3
|
||||
BLOB = 1 << 4
|
||||
UNSIGNED = 1 << 5
|
||||
ZEROFILL = 1 << 6
|
||||
BINARY = 1 << 7
|
||||
|
||||
ENUM = 1 << 8
|
||||
AUTO_INCREMENT = 1 << 9
|
||||
TIMESTAMP = 1 << 10
|
||||
SET = 1 << 11
|
||||
|
||||
NO_DEFAULT_VALUE = 1 << 12
|
||||
ON_UPDATE_NOW = 1 << 13
|
||||
NUM = 1 << 14
|
||||
PART_KEY = 1 << 15
|
||||
GROUP = 1 << 14 # SAME AS NUM !!!!!!!????
|
||||
UNIQUE = 1 << 16
|
||||
BINCMP = 1 << 17
|
||||
|
||||
GET_FIXED_FIELDS = 1 << 18
|
||||
FIELD_IN_PART_FUNC = 1 << 19
|
||||
FIELD_IN_ADD_INDEX = 1 << 20
|
||||
FIELD_IS_RENAMED = 1 << 21
|
||||
|
||||
desc = {
|
||||
'NOT_NULL': (1 << 0, "Field can't be NULL"),
|
||||
'PRI_KEY': (1 << 1, "Field is part of a primary key"),
|
||||
'UNIQUE_KEY': (1 << 2, "Field is part of a unique key"),
|
||||
'MULTIPLE_KEY': (1 << 3, "Field is part of a key"),
|
||||
'BLOB': (1 << 4, "Field is a blob"),
|
||||
'UNSIGNED': (1 << 5, "Field is unsigned"),
|
||||
'ZEROFILL': (1 << 6, "Field is zerofill"),
|
||||
'BINARY': (1 << 7, "Field is binary "),
|
||||
'ENUM': (1 << 8, "field is an enum"),
|
||||
'AUTO_INCREMENT': (1 << 9, "field is a autoincrement field"),
|
||||
'TIMESTAMP': (1 << 10, "Field is a timestamp"),
|
||||
'SET': (1 << 11, "field is a set"),
|
||||
'NO_DEFAULT_VALUE': (1 << 12, "Field doesn't have default value"),
|
||||
'ON_UPDATE_NOW': (1 << 13, "Field is set to NOW on UPDATE"),
|
||||
'NUM': (1 << 14, "Field is num (for clients)"),
|
||||
|
||||
'PART_KEY': (1 << 15, "Intern; Part of some key"),
|
||||
'GROUP': (1 << 14, "Intern: Group field"), # Same as NUM
|
||||
'UNIQUE': (1 << 16, "Intern: Used by sql_yacc"),
|
||||
'BINCMP': (1 << 17, "Intern: Used by sql_yacc"),
|
||||
'GET_FIXED_FIELDS': (1 << 18, "Used to get fields in item tree"),
|
||||
'FIELD_IN_PART_FUNC': (1 << 19, "Field part of partition func"),
|
||||
'FIELD_IN_ADD_INDEX': (1 << 20, "Intern: Field used in ADD INDEX"),
|
||||
'FIELD_IS_RENAMED': (1 << 21, "Intern: Field is being renamed"),
|
||||
}
|
||||
|
||||
|
||||
class ServerCmd(_constants):
|
||||
_prefix = 'COM_'
|
||||
SLEEP = 0
|
||||
QUIT = 1
|
||||
INIT_DB = 2
|
||||
QUERY = 3
|
||||
FIELD_LIST = 4
|
||||
CREATE_DB = 5
|
||||
DROP_DB = 6
|
||||
REFRESH = 7
|
||||
SHUTDOWN = 8
|
||||
STATISTICS = 9
|
||||
PROCESS_INFO = 10
|
||||
CONNECT = 11
|
||||
PROCESS_KILL = 12
|
||||
DEBUG = 13
|
||||
PING = 14
|
||||
TIME = 15
|
||||
DELAYED_INSERT = 16
|
||||
CHANGE_USER = 17
|
||||
BINLOG_DUMP = 18
|
||||
TABLE_DUMP = 19
|
||||
CONNECT_OUT = 20
|
||||
REGISTER_SLAVE = 21
|
||||
STMT_PREPARE = 22
|
||||
STMT_EXECUTE = 23
|
||||
STMT_SEND_LONG_DATA = 24
|
||||
STMT_CLOSE = 25
|
||||
STMT_RESET = 26
|
||||
SET_OPTION = 27
|
||||
STMT_FETCH = 28
|
||||
DAEMON = 29
|
||||
|
||||
class ClientFlag(_constants):
|
||||
"""
|
||||
Client Options as found in the MySQL sources mysql-src/include/mysql_com.h
|
||||
"""
|
||||
LONG_PASSWD = 1 << 0
|
||||
FOUND_ROWS = 1 << 1
|
||||
LONG_FLAG = 1 << 2
|
||||
CONNECT_WITH_DB = 1 << 3
|
||||
NO_SCHEMA = 1 << 4
|
||||
COMPRESS = 1 << 5
|
||||
ODBC = 1 << 6
|
||||
LOCAL_FILES = 1 << 7
|
||||
IGNORE_SPACE = 1 << 8
|
||||
PROTOCOL_41 = 1 << 9
|
||||
INTERACTIVE = 1 << 10
|
||||
SSL = 1 << 11
|
||||
IGNORE_SIGPIPE = 1 << 12
|
||||
TRANSACTIONS = 1 << 13
|
||||
RESERVED = 1 << 14
|
||||
SECURE_CONNECTION = 1 << 15
|
||||
MULTI_STATEMENTS = 1 << 16
|
||||
MULTI_RESULTS = 1 << 17
|
||||
SSL_VERIFY_SERVER_CERT = 1 << 30
|
||||
REMEMBER_OPTIONS = 1 << 31
|
||||
|
||||
desc = {
|
||||
'LONG_PASSWD': (1 << 0, 'New more secure passwords'),
|
||||
'FOUND_ROWS': (1 << 1, 'Found instead of affected rows'),
|
||||
'LONG_FLAG': (1 << 2, 'Get all column flags'),
|
||||
'CONNECT_WITH_DB': (1 << 3, 'One can specify db on connect'),
|
||||
'NO_SCHEMA': (1 << 4, "Don't allow database.table.column"),
|
||||
'COMPRESS': (1 << 5, 'Can use compression protocol'),
|
||||
'ODBC': (1 << 6, 'ODBC client'),
|
||||
'LOCAL_FILES': (1 << 7, 'Can use LOAD DATA LOCAL'),
|
||||
'IGNORE_SPACE': (1 << 8, "Ignore spaces before ''"),
|
||||
'PROTOCOL_41': (1 << 9, 'New 4.1 protocol'),
|
||||
'INTERACTIVE': (1 << 10, 'This is an interactive client'),
|
||||
'SSL': (1 << 11, 'Switch to SSL after handshake'),
|
||||
'IGNORE_SIGPIPE': (1 << 12, 'IGNORE sigpipes'),
|
||||
'TRANSACTIONS': (1 << 13, 'Client knows about transactions'),
|
||||
'RESERVED': (1 << 14, 'Old flag for 4.1 protocol'),
|
||||
'SECURE_CONNECTION': (1 << 15, 'New 4.1 authentication'),
|
||||
'MULTI_STATEMENTS': (1 << 16, 'Enable/disable multi-stmt support'),
|
||||
'MULTI_RESULTS': (1 << 17, 'Enable/disable multi-results'),
|
||||
'SSL_VERIFY_SERVER_CERT': (1 << 30, ''),
|
||||
'REMEMBER_OPTIONS': (1 << 31, ''),
|
||||
}
|
||||
|
||||
default = [
|
||||
LONG_PASSWD,
|
||||
LONG_FLAG,
|
||||
CONNECT_WITH_DB,
|
||||
PROTOCOL_41,
|
||||
TRANSACTIONS,
|
||||
SECURE_CONNECTION,
|
||||
MULTI_STATEMENTS,
|
||||
MULTI_RESULTS,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_default(cls):
|
||||
flags = 0
|
||||
for f in cls.default:
|
||||
flags |= f
|
||||
return flags
|
||||
|
||||
class ServerFlag(_constants):
|
||||
"""
|
||||
Server flags as found in the MySQL sources mysql-src/include/mysql_com.h
|
||||
"""
|
||||
_prefix = 'SERVER_'
|
||||
STATUS_IN_TRANS = 1 << 0
|
||||
STATUS_AUTOCOMMIT = 1 << 1
|
||||
MORE_RESULTS_EXISTS = 1 << 3
|
||||
QUERY_NO_GOOD_INDEX_USED = 1 << 4
|
||||
QUERY_NO_INDEX_USED = 1 << 5
|
||||
STATUS_CURSOR_EXISTS = 1 << 6
|
||||
STATUS_LAST_ROW_SENT = 1 << 7
|
||||
STATUS_DB_DROPPED = 1 << 8
|
||||
STATUS_NO_BACKSLASH_ESCAPES = 1 << 9
|
||||
|
||||
desc = {
|
||||
'SERVER_STATUS_IN_TRANS': (1 << 0, 'Transaction has started'),
|
||||
'SERVER_STATUS_AUTOCOMMIT': (1 << 1, 'Server in auto_commit mode'),
|
||||
'SERVER_MORE_RESULTS_EXISTS': (1 << 3, 'Multi query - next query exists'),
|
||||
'SERVER_QUERY_NO_GOOD_INDEX_USED': (1 << 4, ''),
|
||||
'SERVER_QUERY_NO_INDEX_USED': (1 << 5, ''),
|
||||
'SERVER_STATUS_CURSOR_EXISTS': (1 << 6, ''),
|
||||
'SERVER_STATUS_LAST_ROW_SENT': (1 << 7, ''),
|
||||
'SERVER_STATUS_DB_DROPPED': (1 << 8, 'A database was dropped'),
|
||||
'SERVER_STATUS_NO_BACKSLASH_ESCAPES': (1 << 9, ''),
|
||||
}
|
||||
|
||||
class RefreshOption(_constants):
|
||||
"""Options used when sending the COM_REFRESH server command."""
|
||||
|
||||
_prefix = 'REFRESH_'
|
||||
GRANT = 1 << 0
|
||||
LOG = 1 << 1
|
||||
TABLES = 1 << 2
|
||||
HOST = 1 << 3
|
||||
STATUS = 1 << 4
|
||||
THREADS = 1 << 5
|
||||
SLAVE = 1 << 6
|
||||
|
||||
desc = {
|
||||
'GRANT': (1 << 0, 'Refresh grant tables'),
|
||||
'LOG': (1 << 1, 'Start on new log file'),
|
||||
'TABLES': (1 << 2, 'close all tables'),
|
||||
'HOSTS': (1 << 3, 'Flush host cache'),
|
||||
'STATUS': (1 << 4, 'Flush status variables'),
|
||||
'THREADS': (1 << 5, 'Flush thread cache'),
|
||||
'SLAVE': (1 << 6, 'Reset master info and restart slave thread'),
|
||||
}
|
||||
|
||||
class CharacterSet(_constants):
|
||||
"""
|
||||
List of supported character sets with their collations. This maps to the
|
||||
character set we get from the server within the handshake packet.
|
||||
|
||||
To update this list, use the following query:
|
||||
SELECT ID,CHARACTER_SET_NAME, COLLATION_NAME
|
||||
FROM INFORMATION_SCHEMA.COLLATIONS
|
||||
ORDER BY ID
|
||||
|
||||
This list is hardcoded because we want to avoid doing each time the above
|
||||
query to get the name of the character set used.
|
||||
"""
|
||||
|
||||
_max_id = 211 # SELECT MAX(ID)+1 FROM INFORMATION_SCHEMA.COLLATIONS
|
||||
|
||||
@classmethod
|
||||
def _init_desc(cls):
|
||||
if not cls.__dict__.has_key('desc'):
|
||||
|
||||
# Do not forget to update the tests in test_constants!
|
||||
cls.desc = [ None for i in range(cls._max_id)]
|
||||
cls.desc[1] = ('big5','big5_chinese_ci')
|
||||
cls.desc[2] = ('latin2','latin2_czech_cs')
|
||||
cls.desc[3] = ('dec8','dec8_swedish_ci')
|
||||
cls.desc[4] = ('cp850','cp850_general_ci')
|
||||
cls.desc[5] = ('latin1','latin1_german1_ci')
|
||||
cls.desc[6] = ('hp8','hp8_english_ci')
|
||||
cls.desc[7] = ('koi8r','koi8r_general_ci')
|
||||
cls.desc[8] = ('latin1','latin1_swedish_ci')
|
||||
cls.desc[9] = ('latin2','latin2_general_ci')
|
||||
cls.desc[10] = ('swe7','swe7_swedish_ci')
|
||||
cls.desc[11] = ('ascii','ascii_general_ci')
|
||||
cls.desc[12] = ('ujis','ujis_japanese_ci')
|
||||
cls.desc[13] = ('sjis','sjis_japanese_ci')
|
||||
cls.desc[14] = ('cp1251','cp1251_bulgarian_ci')
|
||||
cls.desc[15] = ('latin1','latin1_danish_ci')
|
||||
cls.desc[16] = ('hebrew','hebrew_general_ci')
|
||||
cls.desc[18] = ('tis620','tis620_thai_ci')
|
||||
cls.desc[19] = ('euckr','euckr_korean_ci')
|
||||
cls.desc[20] = ('latin7','latin7_estonian_cs')
|
||||
cls.desc[21] = ('latin2','latin2_hungarian_ci')
|
||||
cls.desc[22] = ('koi8u','koi8u_general_ci')
|
||||
cls.desc[23] = ('cp1251','cp1251_ukrainian_ci')
|
||||
cls.desc[24] = ('gb2312','gb2312_chinese_ci')
|
||||
cls.desc[25] = ('greek','greek_general_ci')
|
||||
cls.desc[26] = ('cp1250','cp1250_general_ci')
|
||||
cls.desc[27] = ('latin2','latin2_croatian_ci')
|
||||
cls.desc[28] = ('gbk','gbk_chinese_ci')
|
||||
cls.desc[29] = ('cp1257','cp1257_lithuanian_ci')
|
||||
cls.desc[30] = ('latin5','latin5_turkish_ci')
|
||||
cls.desc[31] = ('latin1','latin1_german2_ci')
|
||||
cls.desc[32] = ('armscii8','armscii8_general_ci')
|
||||
cls.desc[33] = ('utf8','utf8_general_ci')
|
||||
cls.desc[34] = ('cp1250','cp1250_czech_cs')
|
||||
cls.desc[35] = ('ucs2','ucs2_general_ci')
|
||||
cls.desc[36] = ('cp866','cp866_general_ci')
|
||||
cls.desc[37] = ('keybcs2','keybcs2_general_ci')
|
||||
cls.desc[38] = ('macce','macce_general_ci')
|
||||
cls.desc[39] = ('macroman','macroman_general_ci')
|
||||
cls.desc[40] = ('cp852','cp852_general_ci')
|
||||
cls.desc[41] = ('latin7','latin7_general_ci')
|
||||
cls.desc[42] = ('latin7','latin7_general_cs')
|
||||
cls.desc[43] = ('macce','macce_bin')
|
||||
cls.desc[44] = ('cp1250','cp1250_croatian_ci')
|
||||
cls.desc[47] = ('latin1','latin1_bin')
|
||||
cls.desc[48] = ('latin1','latin1_general_ci')
|
||||
cls.desc[49] = ('latin1','latin1_general_cs')
|
||||
cls.desc[50] = ('cp1251','cp1251_bin')
|
||||
cls.desc[51] = ('cp1251','cp1251_general_ci')
|
||||
cls.desc[52] = ('cp1251','cp1251_general_cs')
|
||||
cls.desc[53] = ('macroman','macroman_bin')
|
||||
cls.desc[57] = ('cp1256','cp1256_general_ci')
|
||||
cls.desc[58] = ('cp1257','cp1257_bin')
|
||||
cls.desc[59] = ('cp1257','cp1257_general_ci')
|
||||
cls.desc[63] = ('binary','binary')
|
||||
cls.desc[64] = ('armscii8','armscii8_bin')
|
||||
cls.desc[65] = ('ascii','ascii_bin')
|
||||
cls.desc[66] = ('cp1250','cp1250_bin')
|
||||
cls.desc[67] = ('cp1256','cp1256_bin')
|
||||
cls.desc[68] = ('cp866','cp866_bin')
|
||||
cls.desc[69] = ('dec8','dec8_bin')
|
||||
cls.desc[70] = ('greek','greek_bin')
|
||||
cls.desc[71] = ('hebrew','hebrew_bin')
|
||||
cls.desc[72] = ('hp8','hp8_bin')
|
||||
cls.desc[73] = ('keybcs2','keybcs2_bin')
|
||||
cls.desc[74] = ('koi8r','koi8r_bin')
|
||||
cls.desc[75] = ('koi8u','koi8u_bin')
|
||||
cls.desc[77] = ('latin2','latin2_bin')
|
||||
cls.desc[78] = ('latin5','latin5_bin')
|
||||
cls.desc[79] = ('latin7','latin7_bin')
|
||||
cls.desc[80] = ('cp850','cp850_bin')
|
||||
cls.desc[81] = ('cp852','cp852_bin')
|
||||
cls.desc[82] = ('swe7','swe7_bin')
|
||||
cls.desc[83] = ('utf8','utf8_bin')
|
||||
cls.desc[84] = ('big5','big5_bin')
|
||||
cls.desc[85] = ('euckr','euckr_bin')
|
||||
cls.desc[86] = ('gb2312','gb2312_bin')
|
||||
cls.desc[87] = ('gbk','gbk_bin')
|
||||
cls.desc[88] = ('sjis','sjis_bin')
|
||||
cls.desc[89] = ('tis620','tis620_bin')
|
||||
cls.desc[90] = ('ucs2','ucs2_bin')
|
||||
cls.desc[91] = ('ujis','ujis_bin')
|
||||
cls.desc[92] = ('geostd8','geostd8_general_ci')
|
||||
cls.desc[93] = ('geostd8','geostd8_bin')
|
||||
cls.desc[94] = ('latin1','latin1_spanish_ci')
|
||||
cls.desc[95] = ('cp932','cp932_japanese_ci')
|
||||
cls.desc[96] = ('cp932','cp932_bin')
|
||||
cls.desc[97] = ('eucjpms','eucjpms_japanese_ci')
|
||||
cls.desc[98] = ('eucjpms','eucjpms_bin')
|
||||
cls.desc[128] = ('ucs2','ucs2_unicode_ci')
|
||||
cls.desc[129] = ('ucs2','ucs2_icelandic_ci')
|
||||
cls.desc[130] = ('ucs2','ucs2_latvian_ci')
|
||||
cls.desc[131] = ('ucs2','ucs2_romanian_ci')
|
||||
cls.desc[132] = ('ucs2','ucs2_slovenian_ci')
|
||||
cls.desc[133] = ('ucs2','ucs2_polish_ci')
|
||||
cls.desc[134] = ('ucs2','ucs2_estonian_ci')
|
||||
cls.desc[135] = ('ucs2','ucs2_spanish_ci')
|
||||
cls.desc[136] = ('ucs2','ucs2_swedish_ci')
|
||||
cls.desc[137] = ('ucs2','ucs2_turkish_ci')
|
||||
cls.desc[138] = ('ucs2','ucs2_czech_ci')
|
||||
cls.desc[139] = ('ucs2','ucs2_danish_ci')
|
||||
cls.desc[140] = ('ucs2','ucs2_lithuanian_ci')
|
||||
cls.desc[141] = ('ucs2','ucs2_slovak_ci')
|
||||
cls.desc[142] = ('ucs2','ucs2_spanish2_ci')
|
||||
cls.desc[143] = ('ucs2','ucs2_roman_ci')
|
||||
cls.desc[144] = ('ucs2','ucs2_persian_ci')
|
||||
cls.desc[145] = ('ucs2','ucs2_esperanto_ci')
|
||||
cls.desc[146] = ('ucs2','ucs2_hungarian_ci')
|
||||
cls.desc[192] = ('utf8','utf8_unicode_ci')
|
||||
cls.desc[193] = ('utf8','utf8_icelandic_ci')
|
||||
cls.desc[194] = ('utf8','utf8_latvian_ci')
|
||||
cls.desc[195] = ('utf8','utf8_romanian_ci')
|
||||
cls.desc[196] = ('utf8','utf8_slovenian_ci')
|
||||
cls.desc[197] = ('utf8','utf8_polish_ci')
|
||||
cls.desc[198] = ('utf8','utf8_estonian_ci')
|
||||
cls.desc[199] = ('utf8','utf8_spanish_ci')
|
||||
cls.desc[200] = ('utf8','utf8_swedish_ci')
|
||||
cls.desc[201] = ('utf8','utf8_turkish_ci')
|
||||
cls.desc[202] = ('utf8','utf8_czech_ci')
|
||||
cls.desc[203] = ('utf8','utf8_danish_ci')
|
||||
cls.desc[204] = ('utf8','utf8_lithuanian_ci')
|
||||
cls.desc[205] = ('utf8','utf8_slovak_ci')
|
||||
cls.desc[206] = ('utf8','utf8_spanish2_ci')
|
||||
cls.desc[207] = ('utf8','utf8_roman_ci')
|
||||
cls.desc[208] = ('utf8','utf8_persian_ci')
|
||||
cls.desc[209] = ('utf8','utf8_esperanto_ci')
|
||||
cls.desc[210] = ('utf8','utf8_hungarian_ci')
|
||||
|
||||
@classmethod
|
||||
def get_info(cls,setid):
|
||||
"""Returns information about the charset for given MySQL ID."""
|
||||
cls._init_desc()
|
||||
res = ()
|
||||
errmsg = "Character set with id '%d' unsupported." % (setid)
|
||||
try:
|
||||
res = cls.desc[setid]
|
||||
except:
|
||||
raise ProgrammingError, errmsg
|
||||
|
||||
if res is None:
|
||||
raise ProgrammingError, errmsg
|
||||
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def get_desc(cls,setid):
|
||||
"""Returns info string about the charset for given MySQL ID."""
|
||||
res = ()
|
||||
try:
|
||||
res = "%s/%s" % self.get_info(setid)
|
||||
except ProgrammingError, e:
|
||||
raise
|
||||
else:
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def get_charset_info(cls, name, collation=None):
|
||||
"""Returns information about the charset and optional collation."""
|
||||
cls._init_desc()
|
||||
l = len(cls.desc)
|
||||
errmsg = "Character set '%s' unsupported." % (name)
|
||||
|
||||
if collation is None:
|
||||
collation = '%s_general_ci' % (name)
|
||||
|
||||
# Search the list and return when found
|
||||
idx = 0
|
||||
for info in cls.desc:
|
||||
if info and info[0] == name and info[1] == collation:
|
||||
return (idx,info[0],info[1])
|
||||
idx += 1
|
||||
|
||||
# If we got here, we didn't find the charset
|
||||
raise ProgrammingError, errmsg
|
||||
|
||||
@classmethod
|
||||
def get_supported(cls):
|
||||
"""Returns a list with names of all supproted character sets."""
|
||||
res = []
|
||||
for info in cls.desc:
|
||||
if info and info[0] not in res:
|
||||
res.append(info[0])
|
||||
return tuple(res)
|
||||
|
||||
|
|
@ -0,0 +1,401 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""Converting MySQL and Python types
|
||||
"""
|
||||
|
||||
from types import NoneType
|
||||
import re
|
||||
import datetime
|
||||
import time
|
||||
from decimal import Decimal
|
||||
|
||||
import errors
|
||||
from constants import FieldType, FieldFlag
|
||||
|
||||
class ConverterBase(object):
|
||||
|
||||
def __init__(self, charset='utf8', use_unicode=True):
|
||||
self.python_types = None
|
||||
self.mysql_types = None
|
||||
self.set_charset(charset)
|
||||
self.set_unicode(use_unicode)
|
||||
|
||||
def set_charset(self, charset):
|
||||
if charset is not None:
|
||||
self.charset = charset
|
||||
else:
|
||||
# default to utf8
|
||||
self.charset = 'utf8'
|
||||
|
||||
def set_unicode(self, value=True):
|
||||
self.use_unicode = value
|
||||
|
||||
def to_mysql(self, value):
|
||||
return value
|
||||
|
||||
def to_python(self, vtype, value):
|
||||
return value
|
||||
|
||||
def escape(self, buf):
|
||||
return buf
|
||||
|
||||
def quote(self, buf):
|
||||
return str(buf)
|
||||
|
||||
class MySQLConverter(ConverterBase):
|
||||
"""
|
||||
A converted class grouping:
|
||||
o escape method: for escpaing values send to MySQL
|
||||
o quoting method: for quoting values send to MySQL in statements
|
||||
o conversion mapping: maps Python and MySQL data types to
|
||||
function for converting them.
|
||||
|
||||
This class should be overloaded whenever one needs differences
|
||||
in how values are to be converted. Each MySQLConnection object
|
||||
has a default_converter property, which can be set like
|
||||
MySQL.converter(CustomMySQLConverter)
|
||||
|
||||
"""
|
||||
def __init__(self, charset=None, use_unicode=True):
|
||||
ConverterBase.__init__(self, charset, use_unicode)
|
||||
|
||||
# Python types
|
||||
self.python_types = {
|
||||
int : int,
|
||||
str : self._str_to_mysql,
|
||||
long : long,
|
||||
float : float,
|
||||
unicode : self._unicode_to_mysql,
|
||||
bool : self._bool_to_mysql,
|
||||
NoneType : self._none_to_mysql,
|
||||
datetime.datetime : self._datetime_to_mysql,
|
||||
datetime.date : self._date_to_mysql,
|
||||
datetime.time : self._time_to_mysql,
|
||||
time.struct_time : self._struct_time_to_mysql,
|
||||
datetime.timedelta : self._timedelta_to_mysql,
|
||||
Decimal : self._decimal_to_mysql,
|
||||
}
|
||||
|
||||
# MySQL types
|
||||
self.mysql_types = {
|
||||
FieldType.TINY : self._int,
|
||||
FieldType.SHORT : self._int,
|
||||
FieldType.INT24 : self._int,
|
||||
FieldType.LONG : self._long,
|
||||
FieldType.LONGLONG : self._long,
|
||||
FieldType.FLOAT : self._float,
|
||||
FieldType.DOUBLE : self._float,
|
||||
FieldType.DECIMAL : self._decimal,
|
||||
FieldType.NEWDECIMAL : self._decimal,
|
||||
FieldType.VAR_STRING : self._STRING_to_python,
|
||||
FieldType.STRING : self._STRING_to_python,
|
||||
FieldType.SET : self._SET_to_python,
|
||||
FieldType.TIME : self._TIME_to_python,
|
||||
FieldType.DATE : self._DATE_to_python,
|
||||
FieldType.NEWDATE : self._DATE_to_python,
|
||||
FieldType.DATETIME : self._DATETIME_to_python,
|
||||
FieldType.TIMESTAMP : self._DATETIME_to_python,
|
||||
FieldType.BLOB : self._STRING_to_python,
|
||||
}
|
||||
|
||||
def escape(self, value):
|
||||
"""
|
||||
Escapes special characters as they are expected to by when MySQL
|
||||
receives them.
|
||||
As found in MySQL source mysys/charset.c
|
||||
|
||||
Returns the value if not a string, or the escaped string.
|
||||
"""
|
||||
if value is None:
|
||||
return value
|
||||
elif isinstance(value, (int,float,long,Decimal)):
|
||||
return value
|
||||
backslash = re.compile(r'\134')
|
||||
res = value
|
||||
res = backslash.sub(r'\\\\', res)
|
||||
res = res.replace('\n','\\n')
|
||||
res = res.replace('\r','\\r')
|
||||
res = res.replace('\047','\134\047') # single quotes
|
||||
res = res.replace('\042','\134\042') # double quotes
|
||||
res = res.replace('\032','\134\032') # for Win32
|
||||
return res
|
||||
|
||||
def quote(self, buf):
|
||||
"""
|
||||
Quote the parameters for commands. General rules:
|
||||
o numbers are returns as str type (because operation expect it)
|
||||
o None is returned as str('NULL')
|
||||
o String are quoted with single quotes '<string>'
|
||||
|
||||
Returns a string.
|
||||
"""
|
||||
if isinstance(buf, (int,float,long,Decimal)):
|
||||
return str(buf)
|
||||
elif isinstance(buf, NoneType):
|
||||
return "NULL"
|
||||
else:
|
||||
# Anything else would be a string
|
||||
return "'%s'" % buf
|
||||
|
||||
def to_mysql(self, value):
|
||||
vtype = type(value)
|
||||
return self.python_types[vtype](value)
|
||||
|
||||
def _str_to_mysql(self, value):
|
||||
return str(value)
|
||||
|
||||
def _unicode_to_mysql(self, value):
|
||||
"""
|
||||
Encodes value, a Python unicode string, to whatever the
|
||||
character set for this converter is set too.
|
||||
"""
|
||||
return value.encode(self.charset)
|
||||
|
||||
def _bool_to_mysql(self, value):
|
||||
if value:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def _none_to_mysql(self, value):
|
||||
"""
|
||||
This would return what None would be in MySQL, but instead we
|
||||
leave it None and return it right away. The actual convertion
|
||||
from None to NULL happens in the quoting functionality.
|
||||
|
||||
Return None.
|
||||
"""
|
||||
return None
|
||||
|
||||
def _datetime_to_mysql(self, value):
|
||||
"""
|
||||
Converts a datetime instance to a string suitable for MySQL.
|
||||
The returned string has format: %Y-%m-%d %H:%M:%S
|
||||
|
||||
If the instance isn't a datetime.datetime type, it return None.
|
||||
|
||||
Returns a string or None when not valid.
|
||||
"""
|
||||
if isinstance(value, datetime.datetime):
|
||||
return value.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
return None
|
||||
|
||||
def _date_to_mysql(self, value):
|
||||
"""
|
||||
Converts a date instance to a string suitable for MySQL.
|
||||
The returned string has format: %Y-%m-%d
|
||||
|
||||
If the instance isn't a datetime.date type, it return None.
|
||||
|
||||
Returns a string or None when not valid.
|
||||
"""
|
||||
if isinstance(value, datetime.date):
|
||||
return value.strftime('%Y-%m-%d')
|
||||
|
||||
|
||||
return None
|
||||
|
||||
def _time_to_mysql(self, value):
|
||||
"""
|
||||
Converts a time instance to a string suitable for MySQL.
|
||||
The returned string has format: %H:%M:%S
|
||||
|
||||
If the instance isn't a datetime.time type, it return None.
|
||||
|
||||
Returns a string or None when not valid.
|
||||
"""
|
||||
if isinstance(value, datetime.time):
|
||||
return value.strftime('%H:%M:%S')
|
||||
|
||||
return None
|
||||
|
||||
def _struct_time_to_mysql(self, value):
|
||||
"""
|
||||
Converts a time.struct_time sequence to a string suitable
|
||||
for MySQL.
|
||||
The returned string has format: %Y-%m-%d %H:%M:%S
|
||||
|
||||
Returns a string or None when not valid.
|
||||
"""
|
||||
if isinstance(value, time.struct_time):
|
||||
return time.strftime('%Y-%m-%d %H:%M:%S',value)
|
||||
return None
|
||||
|
||||
def _timedelta_to_mysql(self, value):
|
||||
"""
|
||||
Converts a timedelta instance to a string suitable for MySQL.
|
||||
The returned string has format: %H:%M:%S
|
||||
|
||||
Returns a string or None when not valid.
|
||||
"""
|
||||
if isinstance(value, datetime.timedelta):
|
||||
secs = value.seconds%60
|
||||
mins = value.seconds%3600/60
|
||||
hours = value.seconds/3600+(value.days*24)
|
||||
return '%d:%02d:%02d' % (hours,mins,secs)
|
||||
|
||||
return None
|
||||
|
||||
def _decimal_to_mysql(self, value):
|
||||
"""
|
||||
Converts a decimal.Decimal instance to a string suitable for
|
||||
MySQL.
|
||||
|
||||
Returns a string or None when not valid.
|
||||
"""
|
||||
if isinstance(value, Decimal):
|
||||
return str(value)
|
||||
|
||||
return None
|
||||
|
||||
def to_python(self, flddsc, value):
|
||||
"""
|
||||
Converts a given value coming from MySQL to a certain type in Python.
|
||||
The flddsc contains additional information for the field in the
|
||||
table. It's an element from MySQLCursor.description.
|
||||
|
||||
Returns a mixed value.
|
||||
"""
|
||||
res = value
|
||||
|
||||
if value == '\x00':
|
||||
# Don't go further when we hit a NULL value
|
||||
return None
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
res = self.mysql_types[flddsc[1]](value, flddsc)
|
||||
except KeyError:
|
||||
# If one type is not defined, we just return the value as str
|
||||
return str(value)
|
||||
except ValueError, e:
|
||||
raise ValueError, "%s (field %s)" % (e, flddsc[0])
|
||||
except TypeError, e:
|
||||
raise TypeError, "%s (field %s)" % (e, flddsc[0])
|
||||
except:
|
||||
raise
|
||||
|
||||
return res
|
||||
|
||||
def _float(self, v, desc=None):
|
||||
"""
|
||||
Returns v as float type.
|
||||
"""
|
||||
return float(v)
|
||||
|
||||
def _int(self, v, desc=None):
|
||||
"""
|
||||
Returns v as int type.
|
||||
"""
|
||||
return int(v)
|
||||
|
||||
def _long(self, v, desc=None):
|
||||
"""
|
||||
Returns v as long type.
|
||||
"""
|
||||
return long(v)
|
||||
|
||||
def _decimal(self, v, desc=None):
|
||||
"""
|
||||
Returns v as a decimal.Decimal.
|
||||
"""
|
||||
return Decimal(v)
|
||||
|
||||
def _str(self, v, desc=None):
|
||||
"""
|
||||
Returns v as str type.
|
||||
"""
|
||||
return str(v)
|
||||
|
||||
def _DATE_to_python(self, v, dsc=None):
|
||||
"""
|
||||
Returns DATE column type as datetime.date type.
|
||||
"""
|
||||
pv = None
|
||||
try:
|
||||
pv = datetime.date(*[ int(s) for s in v.split('-')])
|
||||
except ValueError:
|
||||
return None
|
||||
else:
|
||||
return pv
|
||||
|
||||
def _TIME_to_python(self, v, dsc=None):
|
||||
"""
|
||||
Returns TIME column type as datetime.time type.
|
||||
"""
|
||||
pv = None
|
||||
try:
|
||||
(h, m, s) = [ int(s) for s in v.split(':')]
|
||||
pv = datetime.timedelta(hours=h,minutes=m,seconds=s)
|
||||
except ValueError:
|
||||
raise ValueError, "Could not convert %s to python datetime.timedelta" % v
|
||||
else:
|
||||
return pv
|
||||
|
||||
def _DATETIME_to_python(self, v, dsc=None):
|
||||
"""
|
||||
Returns DATETIME column type as datetime.datetime type.
|
||||
"""
|
||||
pv = None
|
||||
try:
|
||||
pv = datetime.datetime(*time.strptime(v, "%Y-%m-%d %H:%M:%S")[0:6])
|
||||
except ValueError:
|
||||
pv = None
|
||||
|
||||
return pv
|
||||
|
||||
def _SET_to_python(self, v, dsc=None):
|
||||
"""
|
||||
Actually, MySQL protocol sees a SET as a string type field. So this
|
||||
code isn't called directly, but used by STRING_to_python() method.
|
||||
|
||||
Returns SET column type as string splitted using a comma.
|
||||
"""
|
||||
pv = None
|
||||
try:
|
||||
pv = v.split(',')
|
||||
except ValueError:
|
||||
raise ValueError, "Could not convert set %s to a sequence." % v
|
||||
return pv
|
||||
|
||||
def _STRING_to_python(self, v, dsc=None):
|
||||
"""
|
||||
Note that a SET is a string too, but using the FieldFlag we can see
|
||||
whether we have to split it.
|
||||
|
||||
Returns string typed columns as string type.
|
||||
"""
|
||||
if dsc is not None:
|
||||
# Check if we deal with a SET
|
||||
if dsc[7] & FieldFlag.SET:
|
||||
return self._SET_to_python(v, dsc)
|
||||
|
||||
if self.use_unicode:
|
||||
try:
|
||||
return unicode(v, self.charset)
|
||||
except:
|
||||
raise
|
||||
return str(v)
|
|
@ -0,0 +1,542 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""Cursor classes
|
||||
"""
|
||||
|
||||
import exceptions
|
||||
|
||||
import mysql
|
||||
import connection
|
||||
import protocol
|
||||
import errors
|
||||
import utils
|
||||
|
||||
class CursorBase(object):
|
||||
"""
|
||||
Base for defining MySQLCursor. This class is a skeleton and defines
|
||||
methods and members as required for the Python Database API
|
||||
Specification v2.0.
|
||||
|
||||
It's better to inherite from MySQLCursor.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.description = None
|
||||
self.rowcount = -1
|
||||
self.arraysize = 1
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def callproc(self, procname, args=()):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def execute(self, operation, params=()):
|
||||
pass
|
||||
|
||||
def executemany(self, operation, seqparams):
|
||||
pass
|
||||
|
||||
def fetchone(self):
|
||||
pass
|
||||
|
||||
def fetchmany(self, size=1):
|
||||
pass
|
||||
|
||||
def fetchall(self):
|
||||
pass
|
||||
|
||||
def nextset(self):
|
||||
pass
|
||||
|
||||
def setinputsizes(self, sizes):
|
||||
pass
|
||||
|
||||
def setoutputsize(self, size, column=None):
|
||||
pass
|
||||
|
||||
class MySQLCursor(CursorBase):
|
||||
"""
|
||||
Default cursor which fetches all rows and stores it for later
|
||||
usage. It uses the converter set for the MySQLConnection to map
|
||||
MySQL types to Python types automatically.
|
||||
|
||||
This class should be inherited whenever other functionallity is
|
||||
required. An example would to change the fetch* member functions
|
||||
to return dictionaries instead of lists of values.
|
||||
|
||||
Implements the Python Database API Specification v2.0.
|
||||
|
||||
Possible parameters are:
|
||||
|
||||
db
|
||||
A MySQLConnection instance.
|
||||
"""
|
||||
|
||||
def __init__(self, db=None):
|
||||
CursorBase.__init__(self)
|
||||
self.db = None
|
||||
self.fields = ()
|
||||
self.nrflds = 0
|
||||
self._result = []
|
||||
self._nextrow = (None, None)
|
||||
self.lastrowid = None
|
||||
self._warnings = None
|
||||
self._warning_count = 0
|
||||
self._executed = None
|
||||
self._have_result = False
|
||||
self._get_warnings = False
|
||||
|
||||
if db is not None:
|
||||
self.set_connection(db)
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Iteration over the result set which calls self.fetchone()
|
||||
and returns the next row.
|
||||
"""
|
||||
return iter(self.fetchone, None)
|
||||
|
||||
def _valid_protocol(self,db):
|
||||
if not hasattr(db,'conn'):
|
||||
raise errors.InterfaceError(
|
||||
"MySQL connection object connection not valid.")
|
||||
|
||||
try:
|
||||
if not isinstance(db.conn.protocol,protocol.MySQLProtocol):
|
||||
raise errors.InterfaceError(
|
||||
"MySQL connection has no protocol set.")
|
||||
except AttributeError:
|
||||
raise errors.InterfaceError(
|
||||
"MySQL connection object connection not valid.")
|
||||
|
||||
return True
|
||||
|
||||
def set_connection(self, db):
|
||||
if isinstance(db,mysql.MySQLBase):
|
||||
if self._valid_protocol(db):
|
||||
self.db = db
|
||||
self.protocol = db.conn.protocol
|
||||
self.db.register_cursor(self)
|
||||
self._get_warnings = self.db.get_warnings
|
||||
else:
|
||||
raise errors.InterfaceError(
|
||||
"MySQLCursor db-argument must subclass of mysql.MySQLBase")
|
||||
|
||||
def _reset_result(self):
|
||||
del self._result[:]
|
||||
self.rowcount = -1
|
||||
self._nextrow = (None, None)
|
||||
self._have_result = False
|
||||
self._warnings = None
|
||||
self._warning_count = 0
|
||||
self._fields = ()
|
||||
|
||||
def next(self):
|
||||
"""
|
||||
Used for iterating over the result set. Calles self.fetchone()
|
||||
to get the next row.
|
||||
"""
|
||||
try:
|
||||
row = self.fetchone()
|
||||
except errors.InterfaceError:
|
||||
raise StopIteration
|
||||
if not row:
|
||||
raise StopIteration
|
||||
return row
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close the cursor, disconnecting it from the MySQL object.
|
||||
|
||||
Returns True when succesful, otherwise False.
|
||||
"""
|
||||
if self.db is None:
|
||||
return False
|
||||
try:
|
||||
self.db.remove_cursor(self)
|
||||
self.db = None
|
||||
except:
|
||||
return False
|
||||
|
||||
del self._result[:]
|
||||
return True
|
||||
|
||||
def _process_params_dict(self, params):
|
||||
try:
|
||||
to_mysql = self.db.converter.to_mysql
|
||||
escape = self.db.converter.escape
|
||||
quote = self.db.converter.quote
|
||||
res = {}
|
||||
for k,v in params.items():
|
||||
c = v
|
||||
c = to_mysql(c)
|
||||
c = escape(c)
|
||||
c = quote(c)
|
||||
res[k] = c
|
||||
except StandardError, e:
|
||||
raise errors.ProgrammingError(
|
||||
"Failed processing pyformat-parameters; %s" % e)
|
||||
else:
|
||||
return res
|
||||
|
||||
return None
|
||||
|
||||
def _process_params(self, params):
|
||||
"""
|
||||
Process the parameters which were given when self.execute() was
|
||||
called. It does following using the MySQLConnection converter:
|
||||
* Convert Python types to MySQL types
|
||||
* Escapes characters required for MySQL.
|
||||
* Quote values when needed.
|
||||
|
||||
Returns a list.
|
||||
"""
|
||||
if isinstance(params,dict):
|
||||
return self._process_params_dict(params)
|
||||
|
||||
try:
|
||||
res = params
|
||||
|
||||
to_mysql = self.db.converter.to_mysql
|
||||
escape = self.db.converter.escape
|
||||
quote = self.db.converter.quote
|
||||
|
||||
res = map(to_mysql,res)
|
||||
res = map(escape,res)
|
||||
res = map(quote,res)
|
||||
except StandardError, e:
|
||||
raise errors.ProgrammingError(
|
||||
"Failed processing format-parameters; %s" % e)
|
||||
else:
|
||||
return tuple(res)
|
||||
return None
|
||||
|
||||
def _get_description(self, res=None):
|
||||
"""
|
||||
Gets the description of the fields out of a result we got from
|
||||
the MySQL Server. If res is None then self.description is
|
||||
returned (which can be None).
|
||||
|
||||
Returns a list or None when no descriptions are available.
|
||||
"""
|
||||
if not res:
|
||||
return self.description
|
||||
|
||||
desc = []
|
||||
try:
|
||||
for fld in res[1]:
|
||||
if not isinstance(fld, protocol.FieldPacket):
|
||||
raise errors.ProgrammingError(
|
||||
"Can only get description from protocol.FieldPacket")
|
||||
desc.append(fld.get_description())
|
||||
except TypeError:
|
||||
raise errors.ProgrammingError(
|
||||
"_get_description needs a list as argument."
|
||||
)
|
||||
return desc
|
||||
|
||||
def _row_to_python(self, rowdata, desc=None):
|
||||
res = ()
|
||||
try:
|
||||
to_python = self.db.converter.to_python
|
||||
if not desc:
|
||||
desc = self.description
|
||||
for idx,v in enumerate(rowdata):
|
||||
flddsc = desc[idx]
|
||||
res += (to_python(flddsc, v),)
|
||||
except StandardError, e:
|
||||
raise errors.InterfaceError(
|
||||
"Failed converting row to Python types; %s" % e)
|
||||
else:
|
||||
return res
|
||||
|
||||
return None
|
||||
|
||||
def _handle_noresultset(self, res):
|
||||
"""Handles result of execute() when there is no result set."""
|
||||
try:
|
||||
self.rowcount = res.affected_rows
|
||||
self.lastrowid = res.insert_id
|
||||
self._warning_count = res.warning_count
|
||||
if self._get_warnings is True and self._warning_count:
|
||||
self._warnings = self._fetch_warnings()
|
||||
except StandardError, e:
|
||||
raise errors.ProgrammingError(
|
||||
"Failed handling non-resultset; %s" % e)
|
||||
|
||||
def _handle_resultset(self):
|
||||
pass
|
||||
|
||||
def execute(self, operation, params=None):
|
||||
"""
|
||||
Executes the given operation. The parameters given through params
|
||||
are used to substitute %%s in the operation string.
|
||||
For example, getting all rows where id is 5:
|
||||
cursor.execute("SELECT * FROM t1 WHERE id = %s", (5,))
|
||||
|
||||
If warnings where generated, and db.get_warnings is True, then
|
||||
self._warnings will be a list containing these warnings.
|
||||
|
||||
Raises exceptions when any error happens.
|
||||
"""
|
||||
if not operation:
|
||||
return 0
|
||||
self._reset_result()
|
||||
stmt = ''
|
||||
|
||||
# Make sure we send the query in correct character set
|
||||
try:
|
||||
if isinstance(operation, unicode):
|
||||
operation.encode(self.db.charset_name)
|
||||
if params is not None:
|
||||
stmt = operation % self._process_params(params)
|
||||
else:
|
||||
stmt = operation
|
||||
res = self.protocol.cmd_query(stmt)
|
||||
if isinstance(res, protocol.OKResultPacket):
|
||||
self._have_result = False
|
||||
self._handle_noresultset(res)
|
||||
else:
|
||||
self.description = self._get_description(res)
|
||||
self._have_result = True
|
||||
self._handle_resultset()
|
||||
except errors.ProgrammingError:
|
||||
raise
|
||||
except errors.OperationalError:
|
||||
raise
|
||||
except StandardError, e:
|
||||
raise errors.InterfaceError(
|
||||
"Failed executing the operation; %s" % e)
|
||||
else:
|
||||
self._executed = stmt
|
||||
return self.rowcount
|
||||
|
||||
return 0
|
||||
|
||||
def executemany(self, operation, seq_params):
|
||||
"""Loops over seq_params and calls excute()"""
|
||||
if not operation:
|
||||
return 0
|
||||
|
||||
rowcnt = 0
|
||||
try:
|
||||
for params in seq_params:
|
||||
self.execute(operation, params)
|
||||
if self._have_result:
|
||||
self.fetchall()
|
||||
rowcnt += self.rowcount
|
||||
except (ValueError,TypeError), e:
|
||||
raise errors.InterfaceError(
|
||||
"Failed executing the operation; %s" % e)
|
||||
except:
|
||||
# Raise whatever execute() raises
|
||||
raise
|
||||
|
||||
return rowcnt
|
||||
|
||||
def callproc(self, procname, args=()):
|
||||
"""Calls a stored procedue with the given arguments
|
||||
|
||||
The arguments will be set during this session, meaning
|
||||
they will be called like _<procname>__arg<nr> where
|
||||
<nr> is an enumeration (+1) of the arguments.
|
||||
|
||||
Coding Example:
|
||||
1) Definining the Stored Routine in MySQL:
|
||||
CREATE PROCEDURE multiply(IN pFac1 INT, IN pFac2 INT, OUT pProd INT)
|
||||
BEGIN
|
||||
SET pProd := pFac1 * pFac2;
|
||||
END
|
||||
|
||||
2) Executing in Python:
|
||||
args = (5,5,0) # 0 is to hold pprod
|
||||
cursor.callproc(multiply, args)
|
||||
print cursor.fetchone()
|
||||
|
||||
The last print should output ('5', '5', 25L)
|
||||
|
||||
Does not return a value, but a result set will be
|
||||
available when the CALL-statement execute succesfully.
|
||||
Raises exceptions when something is wrong.
|
||||
"""
|
||||
argfmt = "@_%s_arg%d"
|
||||
|
||||
try:
|
||||
procargs = self._process_params(args)
|
||||
argnames = []
|
||||
|
||||
for idx,arg in enumerate(procargs):
|
||||
argname = argfmt % (procname, idx+1)
|
||||
argnames.append(argname)
|
||||
setquery = "SET %s=%%s" % argname
|
||||
self.execute(setquery, (arg,))
|
||||
|
||||
call = "CALL %s(%s)" % (procname,','.join(argnames))
|
||||
res = self.protocol.cmd_query(call)
|
||||
|
||||
select = "SELECT %s" % ','.join(argnames)
|
||||
self.execute(select)
|
||||
|
||||
except errors.ProgrammingError:
|
||||
raise
|
||||
except StandardError, e:
|
||||
raise errors.InterfaceError(
|
||||
"Failed calling stored routine; %s" % e)
|
||||
|
||||
def getlastrowid(self):
|
||||
return self.lastrowid
|
||||
|
||||
def _fetch_warnings(self):
|
||||
"""
|
||||
Fetch warnings doing a SHOW WARNINGS. Can be called after getting
|
||||
the result.
|
||||
|
||||
Returns a result set or None when there were no warnings.
|
||||
"""
|
||||
res = []
|
||||
try:
|
||||
c = self.db.cursor()
|
||||
cnt = c.execute("SHOW WARNINGS")
|
||||
res = c.fetchall()
|
||||
c.close()
|
||||
except StandardError, e:
|
||||
raise errors.ProgrammingError(
|
||||
"Failed getting warnings; %s" % e)
|
||||
else:
|
||||
if len(res):
|
||||
return res
|
||||
|
||||
return None
|
||||
|
||||
def _handle_eof(self, eof):
|
||||
self._have_result = False
|
||||
self._nextrow = (None, None)
|
||||
self._warning_count = eof.warning_count
|
||||
if self.db.get_warnings is True and eof.warning_count:
|
||||
self._warnings = self._fetch_warnings()
|
||||
|
||||
def _fetch_row(self):
|
||||
if self._have_result is False:
|
||||
return None
|
||||
row = None
|
||||
try:
|
||||
if self._nextrow == (None, None):
|
||||
(row, eof) = self.protocol.result_get_row()
|
||||
else:
|
||||
(row, eof) = self._nextrow
|
||||
if row:
|
||||
(foo, eof) = self._nextrow = self.protocol.result_get_row()
|
||||
if eof is not None:
|
||||
self._handle_eof(eof)
|
||||
if self.rowcount == -1:
|
||||
self.rowcount = 1
|
||||
else:
|
||||
self.rowcount += 1
|
||||
if eof:
|
||||
self._handle_eof(eof)
|
||||
except:
|
||||
raise
|
||||
else:
|
||||
return row
|
||||
|
||||
return None
|
||||
|
||||
def fetchwarnings(self):
|
||||
return self._warnings
|
||||
|
||||
def fetchone(self):
|
||||
row = self._fetch_row()
|
||||
if row:
|
||||
return self._row_to_python(row)
|
||||
return None
|
||||
|
||||
def fetchmany(self,size=None):
|
||||
res = []
|
||||
cnt = (size or self.arraysize)
|
||||
while cnt > 0 and self._have_result:
|
||||
cnt -= 1
|
||||
row = self.fetchone()
|
||||
if row:
|
||||
res.append(row)
|
||||
|
||||
return res
|
||||
|
||||
def fetchall(self):
|
||||
if self._have_result is False:
|
||||
raise errors.InterfaceError("No result set to fetch from.")
|
||||
res = []
|
||||
row = None
|
||||
while self._have_result:
|
||||
row = self.fetchone()
|
||||
if row:
|
||||
res.append(row)
|
||||
return res
|
||||
|
||||
def __unicode__(self):
|
||||
fmt = "MySQLCursor: %s"
|
||||
if self._executed:
|
||||
if len(self._executed) > 30:
|
||||
res = fmt % (self._executed[:30] + '..')
|
||||
else:
|
||||
res = fmt % (self._executed)
|
||||
else:
|
||||
res = fmt % '(Nothing executed yet)'
|
||||
return res
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.__unicode__())
|
||||
|
||||
class MySQLCursorBuffered(MySQLCursor):
|
||||
"""Cursor which fetches rows within execute()"""
|
||||
|
||||
def __init__(self, db=None):
|
||||
MySQLCursor.__init__(self, db)
|
||||
self._rows = []
|
||||
self._next_row = 0
|
||||
|
||||
def _handle_resultset(self):
|
||||
self._get_all_rows()
|
||||
|
||||
def _get_all_rows(self):
|
||||
(self._rows, eof) = self.protocol.result_get_rows()
|
||||
self.rowcount = len(self._rows)
|
||||
self._handle_eof(eof)
|
||||
self._next_row = 0
|
||||
self._have_result = True
|
||||
|
||||
def _fetch_row(self):
|
||||
row = None
|
||||
try:
|
||||
row = self._rows[self._next_row]
|
||||
except:
|
||||
self._have_result = False
|
||||
return None
|
||||
else:
|
||||
self._next_row += 1
|
||||
return row
|
||||
return None
|
|
@ -0,0 +1,64 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""DB API v2.0 required
|
||||
"""
|
||||
|
||||
import time
|
||||
import datetime
|
||||
|
||||
import constants
|
||||
|
||||
class _DBAPITypeObject:
|
||||
|
||||
def __init__(self,*values):
|
||||
self.values = values
|
||||
|
||||
def __cmp__(self,other):
|
||||
if other in self.values:
|
||||
return 0
|
||||
if other < self.values:
|
||||
return 1
|
||||
else:
|
||||
return -1
|
||||
|
||||
Date = datetime.date
|
||||
Time = datetime.time
|
||||
Timestamp = datetime.datetime
|
||||
|
||||
def DateFromTicks(ticks):
|
||||
return Date(*time.localtime(ticks)[:3])
|
||||
|
||||
def TimeFromTicks(ticks):
|
||||
return Time(*time.localtime(ticks)[3:6])
|
||||
|
||||
def TimestampFromTicks(ticks):
|
||||
return Timestamp(*time.localtime(ticks)[:6])
|
||||
|
||||
Binary = str
|
||||
|
||||
STRING = _DBAPITypeObject(constants.FieldType.get_string_types())
|
||||
BINARY = _DBAPITypeObject(constants.FieldType.get_binary_types())
|
||||
NUMBER = _DBAPITypeObject(constants.FieldType.get_number_types())
|
||||
DATETIME = _DBAPITypeObject(constants.FieldType.get_timestamp_types())
|
||||
ROWID = _DBAPITypeObject()
|
|
@ -0,0 +1,87 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""Python exceptions
|
||||
"""
|
||||
|
||||
import exceptions
|
||||
import protocol
|
||||
|
||||
class Error(StandardError):
|
||||
|
||||
def __init__(self, m):
|
||||
if isinstance(m,protocol.ErrorResultPacket):
|
||||
# process MySQL error packet
|
||||
self._process_packet(m)
|
||||
else:
|
||||
# else the message should be a string
|
||||
self.errno = -1
|
||||
self.errmsg = str(m)
|
||||
self.sqlstate = -1
|
||||
self.msg = str(m)
|
||||
|
||||
def _process_packet(self, packet):
|
||||
self.errno = packet.errno
|
||||
self.errmsg = packet.errmsg
|
||||
self.sqlstate = packet.sqlstate
|
||||
if self.sqlstate:
|
||||
m = '%d (%s): %s' % (self.errno, self.sqlstate, self.errmsg)
|
||||
else:
|
||||
m = '%d: %s' % (self.errno, self.errmsg)
|
||||
self.errmsglong = m
|
||||
self.msg = m
|
||||
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
|
||||
def __unicode__(self):
|
||||
return self.msg
|
||||
|
||||
class Warning(StandardError):
|
||||
pass
|
||||
|
||||
class InterfaceError(Error):
|
||||
def __init__(self, msg):
|
||||
Error.__init__(self, msg)
|
||||
|
||||
class DatabaseError(Error):
|
||||
def __init__(self, msg):
|
||||
Error.__init__(self, msg)
|
||||
|
||||
class InternalError(DatabaseError):
|
||||
pass
|
||||
|
||||
class OperationalError(DatabaseError):
|
||||
pass
|
||||
|
||||
class ProgrammingError(DatabaseError):
|
||||
pass
|
||||
|
||||
class IntegrityError(DatabaseError):
|
||||
pass
|
||||
|
||||
class DataError(DatabaseError):
|
||||
pass
|
||||
|
||||
class NotSupportedError(DatabaseError):
|
||||
pass
|
|
@ -0,0 +1,414 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""Main classes for interacting with MySQL
|
||||
"""
|
||||
|
||||
import socket, string, os
|
||||
|
||||
from connection import *
|
||||
import constants
|
||||
import conversion
|
||||
import protocol
|
||||
import errors
|
||||
import utils
|
||||
import cursor
|
||||
|
||||
|
||||
class MySQLBase(object):
|
||||
"""MySQLBase"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initializing"""
|
||||
self.conn = None # Holding the connection
|
||||
self.converter = None
|
||||
|
||||
self.client_flags = constants.ClientFlag.get_default()
|
||||
(self.charset,
|
||||
self.charset_name,
|
||||
self.collation_name) = constants.CharacterSet.get_charset_info('utf8')
|
||||
|
||||
self.username = ''
|
||||
self.password = ''
|
||||
self.database = ''
|
||||
self.client_host = ''
|
||||
self.client_port = 0
|
||||
|
||||
self.affected_rows = 0
|
||||
self.server_status = 0
|
||||
self.warning_count = 0
|
||||
self.field_count = 0
|
||||
self.insert_id = 0
|
||||
self.info_msg = ''
|
||||
self.use_unicode = True
|
||||
self.get_warnings = False
|
||||
self.autocommit = False
|
||||
self.connection_timeout = None
|
||||
self.buffered = False
|
||||
|
||||
def connect(self):
|
||||
"""To be implemented while subclassing MySQLBase."""
|
||||
pass
|
||||
|
||||
def _set_connection(self, prtcls=None):
|
||||
"""Automatically chooses based on configuration which connection type to setup."""
|
||||
if self.unix_socket and os.name != 'nt':
|
||||
self.conn = MySQLUnixConnection(prtcls=prtcls,
|
||||
unix_socket=self.unix_socket)
|
||||
else:
|
||||
self.conn = MySQLTCPConnection(prtcls=prtcls,
|
||||
host=self.server_host, port=self.server_port)
|
||||
self.conn.set_connection_timeout(self.connection_timeout)
|
||||
|
||||
def _open_connection(self):
|
||||
"""Opens the connection and sets the appropriated protocol."""
|
||||
# We don't know yet the MySQL version we connect too
|
||||
self._set_connection()
|
||||
try:
|
||||
self.conn.open_connection()
|
||||
version = self.conn.protocol.server_version
|
||||
if version < (4,1):
|
||||
raise InterfaceError("MySQL Version %s is not supported." % version)
|
||||
else:
|
||||
self.conn.set_protocol(protocol.MySQLProtocol)
|
||||
self.protocol = self.conn.protocol
|
||||
self.protocol.do_auth(username=self.username, password=self.password,
|
||||
database=self.database)
|
||||
except:
|
||||
raise
|
||||
|
||||
def _post_connection(self):
|
||||
"""Should be called after a connection was established"""
|
||||
self.get_characterset_info()
|
||||
self.set_converter_class(conversion.MySQLConverter)
|
||||
|
||||
try:
|
||||
self.set_charset(self.charset_name)
|
||||
self.set_autocommit(self.autocommit)
|
||||
except:
|
||||
raise
|
||||
|
||||
def is_connected(self):
|
||||
"""
|
||||
Check whether we are connected to the MySQL server.
|
||||
"""
|
||||
return self.protocol.cmd_ping()
|
||||
ping = is_connected
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
Disconnect from the MySQL server.
|
||||
"""
|
||||
if not self.conn:
|
||||
return
|
||||
|
||||
if self.conn.sock is not None:
|
||||
self.protocol.cmd_quit()
|
||||
try:
|
||||
self.conn.close_connection()
|
||||
except:
|
||||
pass
|
||||
self.protocol = None
|
||||
self.conn = None
|
||||
|
||||
def set_converter_class(self, convclass):
|
||||
"""
|
||||
Set the converter class to be used. This should be a class overloading
|
||||
methods and members of conversion.MySQLConverter.
|
||||
"""
|
||||
self.converter_class = convclass
|
||||
self.converter = self.converter_class(self.charset_name, self.use_unicode)
|
||||
|
||||
def get_characterset_info(self):
|
||||
try:
|
||||
(self.charset_name, self.collation_name) = constants.CharacterSet.get_info(self.charset)
|
||||
except:
|
||||
raise ProgrammingError, "Illegal character set information (id=%d)" % self.charset
|
||||
return (self.charset_name, self.collation_name)
|
||||
|
||||
def get_server_version(self):
|
||||
"""Returns the server version as a tuple"""
|
||||
try:
|
||||
return self.protocol.server_version
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def get_server_info(self):
|
||||
"""Returns the server version as a string"""
|
||||
return self.protocol.server_version_original
|
||||
|
||||
def get_server_threadid(self):
|
||||
"""Returns the MySQL threadid of the connection."""
|
||||
threadid = None
|
||||
try:
|
||||
threadid = self.protocol.server_threadid
|
||||
except:
|
||||
pass
|
||||
|
||||
return threadid
|
||||
|
||||
def set_host(self, host):
|
||||
"""
|
||||
Set the host for connection to the MySQL server.
|
||||
"""
|
||||
self.server_host = host
|
||||
|
||||
def set_port(self, port):
|
||||
"""
|
||||
Set the TCP port to be used when connecting to the server, usually 3306.
|
||||
"""
|
||||
self.server_port = port
|
||||
|
||||
def set_login(self, username=None, password=None):
|
||||
"""
|
||||
Set the username and/or password for the user connecting to the MySQL Server.
|
||||
"""
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def set_unicode(self, value=True):
|
||||
"""
|
||||
Set whether we return string fields as unicode or not.
|
||||
Default is True.
|
||||
"""
|
||||
self.use_unicode = value
|
||||
if self.converter:
|
||||
self.converter.set_unicode(value)
|
||||
|
||||
def set_database(self, database):
|
||||
"""
|
||||
Set the database to be used after connection succeeded.
|
||||
"""
|
||||
self.database = database
|
||||
|
||||
def set_charset(self, name):
|
||||
"""
|
||||
Set the character set used for the connection. This is the recommended
|
||||
way of change it per connection basis. It does execute SET NAMES
|
||||
internally, but it's good not to use this command directly, since we
|
||||
are setting some other members accordingly.
|
||||
"""
|
||||
if name not in constants.CharacterSet.get_supported():
|
||||
raise errors.ProgrammingError, "Character set '%s' not supported." % name
|
||||
return
|
||||
try:
|
||||
info = constants.CharacterSet.get_charset_info(name)
|
||||
except errors.ProgrammingError, e:
|
||||
raise
|
||||
|
||||
try:
|
||||
self.protocol.cmd_query("SET NAMES '%s'" % name)
|
||||
except:
|
||||
raise
|
||||
else:
|
||||
(self.charset, self.charset_name, self.collation_name) = info
|
||||
self.converter.set_charset(self.charset_name)
|
||||
|
||||
def set_getwarnings(self, bool):
|
||||
"""
|
||||
Set wheter we should get warnings whenever an operation produced some.
|
||||
"""
|
||||
self.get_warnings = bool
|
||||
|
||||
def set_autocommit(self, switch):
|
||||
"""
|
||||
Set auto commit on or off. The argument 'switch' must be a boolean type.
|
||||
"""
|
||||
if not isinstance(switch, bool):
|
||||
raise ValueError, "The switch argument must be boolean."
|
||||
|
||||
s = 'OFF'
|
||||
if switch:
|
||||
s = 'ON'
|
||||
|
||||
try:
|
||||
self.protocol.cmd_query("SET AUTOCOMMIT = %s" % s)
|
||||
except:
|
||||
raise
|
||||
else:
|
||||
self.autocommit = switch
|
||||
|
||||
def set_unixsocket(self, loc):
|
||||
"""Set the UNIX Socket location. Does not check if it exists."""
|
||||
self.unix_socket = loc
|
||||
|
||||
def set_connection_timeout(self, timeout):
|
||||
self.connection_timeout = timeout
|
||||
|
||||
def set_client_flags(self, flags):
|
||||
self.client_flags = flags
|
||||
|
||||
def set_buffered(self, val=False):
|
||||
"""Sets whether cursor .execute() fetches rows"""
|
||||
self.buffered = val
|
||||
|
||||
class MySQL(MySQLBase):
|
||||
"""
|
||||
Class implementing Python DB API v2.0.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Initializes the MySQL object. Calls connect() to open the connection
|
||||
when an instance is created.
|
||||
"""
|
||||
MySQLBase.__init__(self)
|
||||
self.cursors = []
|
||||
self.affected_rows = 0
|
||||
self.server_status = 0
|
||||
self.warning_count = 0
|
||||
self.field_count = 0
|
||||
self.insert_id = 0
|
||||
self.info_msg = ''
|
||||
|
||||
self.connect(*args, **kwargs)
|
||||
|
||||
def connect(self, dsn='', user='', password='', host='127.0.0.1',
|
||||
port=3306, db=None, database=None, use_unicode=True, charset='utf8', get_warnings=False,
|
||||
autocommit=False, unix_socket=None,
|
||||
connection_timeout=None, client_flags=None, buffered=False):
|
||||
"""
|
||||
Establishes a connection to the MySQL Server. Called also when instansiating
|
||||
a new MySQLConnection object through the __init__ method.
|
||||
|
||||
Possible parameters are:
|
||||
|
||||
dsn
|
||||
(not used)
|
||||
user
|
||||
The username used to authenticate with the MySQL Server.
|
||||
|
||||
password
|
||||
The password to authenticate the user with the MySQL Server.
|
||||
|
||||
host
|
||||
The hostname or the IP address of the MySQL Server we are connecting with.
|
||||
(default 127.0.0.1)
|
||||
|
||||
port
|
||||
TCP port to use for connecting to the MySQL Server.
|
||||
(default 3306)
|
||||
|
||||
database
|
||||
db
|
||||
Initial database to use once we are connected with the MySQL Server.
|
||||
The db argument is synonym, but database takes precedence.
|
||||
|
||||
use_unicode
|
||||
If set to true, string values received from MySQL will be returned
|
||||
as Unicode strings.
|
||||
Default: True
|
||||
|
||||
charset
|
||||
Which character shall we use for sending data to MySQL. One can still
|
||||
override this by using the SET NAMES command directly, but this is
|
||||
discouraged. Instead, use the set_charset() method if you
|
||||
want to change it.
|
||||
Default: Whatever the MySQL server has default.
|
||||
|
||||
get_warnings
|
||||
If set to true, whenever a query gives a warning, a SHOW WARNINGS will
|
||||
be done to fetch them. They will be available as MySQLCursor.warnings.
|
||||
The default is to ignore these warnings, for debugging it's good to
|
||||
enable it though, or use strict mode in MySQL to make most of these
|
||||
warnings errors.
|
||||
Default: False
|
||||
|
||||
autocommit
|
||||
Auto commit is OFF by default, which is required by the Python Db API
|
||||
2.0 specification.
|
||||
Default: False
|
||||
|
||||
unix_socket
|
||||
Full path to the MySQL Server UNIX socket. By default TCP connection will
|
||||
be used using the address specified by the host argument.
|
||||
|
||||
connection_timeout
|
||||
Timeout for the TCP and UNIX socket connection.
|
||||
|
||||
client_flags
|
||||
Allows to set flags for the connection. Check following for possible flags:
|
||||
>>> from mysql.connector.constants import ClientFlag
|
||||
>>> print '\n'.join(ClientFlag.get_full_info())
|
||||
|
||||
buffered
|
||||
When set to True .execute() will fetch the rows immediatly.
|
||||
|
||||
"""
|
||||
# db is not part of Db API v2.0, but MySQLdb supports it.
|
||||
if db and not database:
|
||||
database = db
|
||||
|
||||
self.set_host(host)
|
||||
self.set_port(port)
|
||||
self.set_database(database)
|
||||
self.set_getwarnings(get_warnings)
|
||||
self.set_unixsocket(unix_socket)
|
||||
self.set_connection_timeout(connection_timeout)
|
||||
self.set_client_flags(client_flags)
|
||||
self.set_buffered(buffered)
|
||||
|
||||
if user or password:
|
||||
self.set_login(user, password)
|
||||
|
||||
self.disconnect()
|
||||
self._open_connection()
|
||||
self._post_connection()
|
||||
|
||||
def close(self):
|
||||
del self.cursors[:]
|
||||
self.disconnect()
|
||||
|
||||
def remove_cursor(self, c):
|
||||
try:
|
||||
self.cursors.remove(c)
|
||||
except ValueError:
|
||||
raise errors.ProgrammingError(
|
||||
"Cursor could not be removed.")
|
||||
|
||||
def register_cursor(self, c):
|
||||
try:
|
||||
self.cursors.append(c)
|
||||
except:
|
||||
raise
|
||||
|
||||
def cursor(self):
|
||||
if self.buffered:
|
||||
c = (cursor.MySQLCursorBuffered)(self)
|
||||
else:
|
||||
c = (cursor.MySQLCursor)(self)
|
||||
|
||||
self.register_cursor(c)
|
||||
return c
|
||||
|
||||
def commit(self):
|
||||
"""Shortcut for executing COMMIT."""
|
||||
self.protocol.cmd_query("COMMIT")
|
||||
|
||||
def rollback(self):
|
||||
"""Shortcut for executing ROLLBACK"""
|
||||
self.protocol.cmd_query("ROLLBACK")
|
||||
|
||||
|
|
@ -0,0 +1,863 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USAs
|
||||
|
||||
"""Implementing the MySQL Client/Server protocol
|
||||
"""
|
||||
|
||||
import string
|
||||
import socket
|
||||
import re
|
||||
import struct
|
||||
|
||||
try:
|
||||
from hashlib import sha1
|
||||
except ImportError:
|
||||
from sha import new as sha1
|
||||
|
||||
from datetime import datetime
|
||||
from time import strptime
|
||||
from decimal import Decimal
|
||||
|
||||
from constants import *
|
||||
import errors
|
||||
import utils
|
||||
|
||||
class MySQLProtocol(object):
|
||||
"""Class handling the MySQL Protocol.
|
||||
|
||||
MySQL v4.1 Client/Server Protocol is currently supported.
|
||||
"""
|
||||
def __init__(self, conn, handshake=None):
|
||||
self.client_flags = 0
|
||||
self.conn = conn # MySQL Connection
|
||||
if handshake:
|
||||
self.set_handshake(handshake)
|
||||
|
||||
def handle_header(self, buf):
|
||||
"""Takes a buffer and readers information from header.
|
||||
|
||||
Returns a tuple (pktsize, pktnr)
|
||||
"""
|
||||
pktsize = utils.int3read(buf[0:3])
|
||||
pktnr = utils.int1read(buf[3])
|
||||
|
||||
return (pktsize, pktnr)
|
||||
|
||||
def do_auth(self, username=None, password=None, database=None,
|
||||
client_flags=None):
|
||||
"""
|
||||
Make and send the authentication using information found in the
|
||||
handshake packet.
|
||||
"""
|
||||
if not client_flags:
|
||||
client_flags = ClientFlag.get_default()
|
||||
|
||||
auth = Auth(client_flags=client_flags,
|
||||
pktnr=self.handshake.pktnr+1)
|
||||
auth.create(username=username, password=password,
|
||||
seed=self.handshake.info['seed'], database=database)
|
||||
|
||||
self.conn.send(auth.get())
|
||||
buf = self.conn.recv()[0]
|
||||
if self.is_eof(buf):
|
||||
raise errors.InterfaceError("Found EOF after Auth, expecting OK. Using old passwords?")
|
||||
|
||||
connect_with_db = client_flags & ClientFlag.CONNECT_WITH_DB
|
||||
if self.is_ok(buf) and database and not connect_with_db:
|
||||
self.cmd_init_db(database)
|
||||
|
||||
def handle_handshake(self, buf):
|
||||
"""
|
||||
Check whether the buffer is a valid handshake. If it is, we set some
|
||||
member variables for later usage. The handshake packet is returned for later
|
||||
usuage, e.g. authentication.
|
||||
"""
|
||||
|
||||
if self.is_error(buf):
|
||||
# an ErrorPacket is returned by the server
|
||||
self._handle_error(buf)
|
||||
|
||||
handshake = None
|
||||
try:
|
||||
handshake = Handshake(buf)
|
||||
except errors.InterfaceError, msg:
|
||||
raise errors.InterfaceError(msg)
|
||||
self.set_handshake(handshake)
|
||||
|
||||
def set_handshake(self, handshake):
|
||||
"""Gather data from the given handshake."""
|
||||
ver = re.compile("^(\d{1,2})\.(\d{1,2})\.(\d{1,3})(.*)")
|
||||
version = handshake.info['version']
|
||||
m = ver.match(version)
|
||||
if not m:
|
||||
raise errors.InterfaceError("Could not parse MySQL version, was '%s'" % version)
|
||||
else:
|
||||
self.server_version = tuple([ int(v) for v in m.groups()[0:3]])
|
||||
|
||||
self.server_version_original = handshake.info['version']
|
||||
self.server_threadid = handshake.info['thrdid']
|
||||
self.capabilities = handshake.info['capabilities']
|
||||
self.charset = handshake.info['charset']
|
||||
self.threadid = handshake.info['thrdid']
|
||||
self.handshake = handshake
|
||||
|
||||
def _handle_error(self, buf):
|
||||
"""Raise an OperationalError if result is an error
|
||||
"""
|
||||
try:
|
||||
err = ErrorResultPacket(buf)
|
||||
except errors.InterfaceError, e:
|
||||
raise e
|
||||
else:
|
||||
raise errors.OperationalError(err)
|
||||
|
||||
def is_error(self, buf):
|
||||
"""Check if the given buffer is a MySQL Error Packet.
|
||||
|
||||
Buffer should start with \xff.
|
||||
|
||||
Returns boolean.
|
||||
"""
|
||||
if buf and buf[4] == '\xff':
|
||||
self._handle_error(buf)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _handle_ok(self, buf):
|
||||
"""
|
||||
Handle an OK Result Packet. If we got an InterfaceError, raise that
|
||||
instead.
|
||||
"""
|
||||
try:
|
||||
ok = OKResultPacket(buf)
|
||||
except errors.InterfaceError, e:
|
||||
raise e
|
||||
else:
|
||||
self.server_status = ok.server_status
|
||||
self.warning_count = ok.warning_count
|
||||
self.field_count = ok.field_count
|
||||
self.affected_rows = ok.affected_rows
|
||||
self.info_msg = ok.info_msg
|
||||
|
||||
def is_ok(self, buf):
|
||||
"""
|
||||
Check if the given buffer is a MySQL OK Packet. It should
|
||||
start with \x00.
|
||||
|
||||
Returns boolean.
|
||||
"""
|
||||
if buf and buf[4] == '\x00':
|
||||
self._handle_ok(buf)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _handle_fields(self, nrflds):
|
||||
"""Reads a number of fields from a result set."""
|
||||
i = 0
|
||||
fields = []
|
||||
while i < nrflds:
|
||||
buf = self.conn.recv()[0]
|
||||
fld = FieldPacket(buf)
|
||||
fields.append(fld)
|
||||
i += 1
|
||||
return fields
|
||||
|
||||
def is_eof(self, buf):
|
||||
"""
|
||||
Check if the given buffer is a MySQL EOF Packet. It should
|
||||
start with \xfe and be smaller 9 bytes.
|
||||
|
||||
Returns boolean.
|
||||
"""
|
||||
l = utils.read_int(buf, 3)[1]
|
||||
if buf and buf[4] == '\xfe' and l < 9:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _handle_resultset(self, pkt):
|
||||
"""Processes a resultset getting fields information.
|
||||
|
||||
The argument pkt must be a protocol.Packet with length 1, a byte
|
||||
which contains the number of fields.
|
||||
"""
|
||||
if not isinstance(pkt, PacketIn):
|
||||
raise ValueError("%s is not a protocol.PacketIn" % pkt)
|
||||
|
||||
if len(pkt) == 1:
|
||||
(buf,nrflds) = utils.read_lc_int(pkt.data)
|
||||
|
||||
# Get the fields
|
||||
fields = self._handle_fields(nrflds)
|
||||
|
||||
buf = self.conn.recv()[0]
|
||||
eof = EOFPacket(buf)
|
||||
|
||||
return (nrflds, fields, eof)
|
||||
else:
|
||||
raise errors.InterfaceError('Something wrong reading result after query.')
|
||||
|
||||
def result_get_row(self):
|
||||
"""Get data for 1 row
|
||||
|
||||
Get one row's data. Should be called after getting the field
|
||||
descriptions.
|
||||
|
||||
Returns a tuple with 2 elements: a row's data and the
|
||||
EOF packet.
|
||||
"""
|
||||
buf = self.conn.recv()[0]
|
||||
if self.is_eof(buf):
|
||||
eof = EOFPacket(buf)
|
||||
rowdata = None
|
||||
else:
|
||||
eof = None
|
||||
rowdata = utils.read_lc_string_list(buf[4:])
|
||||
return (rowdata, eof)
|
||||
|
||||
def result_get_rows(self, cnt=None):
|
||||
"""Get all rows
|
||||
|
||||
Returns a tuple with 2 elements: a list with all rows and
|
||||
the EOF packet.
|
||||
"""
|
||||
rows = []
|
||||
eof = None
|
||||
rowdata = None
|
||||
while eof is None:
|
||||
(rowdata,eof) = self.result_get_row()
|
||||
if eof is None and rowdata is not None:
|
||||
rows.append(rowdata)
|
||||
return (rows,eof)
|
||||
|
||||
def cmd_query(self, query):
|
||||
"""
|
||||
Sends a query to the server.
|
||||
|
||||
Returns a tuple, when the query returns a result. The tuple
|
||||
consist number of fields and a list containing their descriptions.
|
||||
If the query doesn't return a result set, the an OKResultPacket
|
||||
will be returned.
|
||||
"""
|
||||
try:
|
||||
cmd = CommandPacket()
|
||||
cmd.set_command(ServerCmd.QUERY)
|
||||
cmd.set_argument(query)
|
||||
cmd.create()
|
||||
self.conn.send(cmd.get()) # Errors handled in _handle_error()
|
||||
|
||||
buf = self.conn.recv()[0]
|
||||
if self.is_ok(buf):
|
||||
# Query does not return a result (INSERT/DELETE/..)
|
||||
return OKResultPacket(buf)
|
||||
|
||||
p = PacketIn(buf)
|
||||
(nrflds, fields, eof) = self._handle_resultset(p)
|
||||
except:
|
||||
raise
|
||||
else:
|
||||
return (nrflds, fields)
|
||||
|
||||
return (0, ())
|
||||
|
||||
def _cmd_simple(self, servercmd, arg=''):
|
||||
"""Makes a simple CommandPacket with no arguments"""
|
||||
cmd = CommandPacket()
|
||||
cmd.set_command(servercmd)
|
||||
cmd.set_argument(arg)
|
||||
cmd.create()
|
||||
|
||||
return cmd
|
||||
|
||||
def cmd_refresh(self, opts):
|
||||
"""Send the Refresh command to the MySQL server.
|
||||
|
||||
The argument should be a bitwise value using the protocol.RefreshOption
|
||||
constants.
|
||||
|
||||
Usage:
|
||||
|
||||
RefreshOption = mysql.connector.RefreshOption
|
||||
refresh = RefreshOption.LOG | RefreshOption.THREADS
|
||||
db.cmd_refresh(refresh)
|
||||
|
||||
"""
|
||||
cmd = self._cmd_simple(ServerCmd.REFRESH, opts)
|
||||
try:
|
||||
self.conn.send(cmd.get())
|
||||
buf = self.conn.recv()[0]
|
||||
except:
|
||||
raise
|
||||
|
||||
if self.is_ok(buf):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def cmd_quit(self):
|
||||
"""Closes the current connection with the server."""
|
||||
cmd = self._cmd_simple(ServerCmd.QUIT)
|
||||
self.conn.send(cmd.get())
|
||||
|
||||
def cmd_init_db(self, database):
|
||||
"""
|
||||
Send command to server to change databases.
|
||||
"""
|
||||
cmd = self._cmd_simple(ServerCmd.INIT_DB, database)
|
||||
self.conn.send(cmd.get())
|
||||
self.conn.recv()[0]
|
||||
|
||||
def cmd_shutdown(self):
|
||||
"""Shuts down the MySQL Server.
|
||||
|
||||
Careful with this command if you have SUPER privileges! (Which your
|
||||
scripts probably don't need!)
|
||||
|
||||
Returns True if it succeeds.
|
||||
"""
|
||||
cmd = self._cmd_simple(ServerCmd.SHUTDOWN)
|
||||
try:
|
||||
self.conn.send(cmd.get())
|
||||
buf = self.conn.recv()[0]
|
||||
except:
|
||||
raise
|
||||
|
||||
return True
|
||||
|
||||
def cmd_statistics(self):
|
||||
"""Sends statistics command to the MySQL Server
|
||||
|
||||
Returns a dictionary with various statistical information.
|
||||
"""
|
||||
cmd = self._cmd_simple(ServerCmd.STATISTICS)
|
||||
try:
|
||||
self.conn.send(cmd.get())
|
||||
buf = self.conn.recv()[0]
|
||||
except:
|
||||
raise
|
||||
|
||||
p = Packet(buf)
|
||||
info = str(p.data)
|
||||
|
||||
res = {}
|
||||
pairs = info.split('\x20\x20') # Information is separated by 2 spaces
|
||||
for pair in pairs:
|
||||
(lbl,val) = [ v.strip() for v in pair.split(':') ]
|
||||
# It's either an integer or a decimal
|
||||
try:
|
||||
res[lbl] = long(val)
|
||||
except:
|
||||
try:
|
||||
res[lbl] = Decimal(val)
|
||||
except:
|
||||
raise ValueError(
|
||||
"Got wrong value in COM_STATISTICS information (%s : %s)." % (lbl, val))
|
||||
return res
|
||||
|
||||
def cmd_process_info(self):
|
||||
"""Gets the process list from the MySQL Server.
|
||||
|
||||
Returns a list of dictionaries which corresponds to the output of
|
||||
SHOW PROCESSLIST of MySQL. The data is converted to Python types.
|
||||
"""
|
||||
raise errors.NotSupportedError(
|
||||
"Not implemented. Use a cursor to get processlist information.")
|
||||
|
||||
def cmd_process_kill(self, mypid):
|
||||
"""Kills a MySQL process using it's ID.
|
||||
|
||||
The mypid must be an integer.
|
||||
|
||||
"""
|
||||
cmd = KillPacket(mypid)
|
||||
cmd.create()
|
||||
try:
|
||||
self.conn.send(cmd.get())
|
||||
buf = self.conn.recv()[0]
|
||||
except:
|
||||
raise
|
||||
|
||||
if self.is_eof(buf):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def cmd_debug(self):
|
||||
"""Send DEBUG command to the MySQL Server
|
||||
|
||||
Needs SUPER privileges. The output will go to the MySQL server error log.
|
||||
|
||||
Returns True when it was succesful.
|
||||
"""
|
||||
cmd = self._cmd_simple(ServerCmd.DEBUG)
|
||||
try:
|
||||
self.conn.send(cmd.get())
|
||||
buf = self.conn.recv()[0]
|
||||
except:
|
||||
raise
|
||||
|
||||
if self.is_eof(buf):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def cmd_ping(self):
|
||||
"""
|
||||
Ping the MySQL server to check if the connection is still alive.
|
||||
|
||||
Returns True when alive, False when server doesn't respond.
|
||||
"""
|
||||
cmd = self._cmd_simple(ServerCmd.PING)
|
||||
try:
|
||||
self.conn.send(cmd.get())
|
||||
buf = self.conn.recv()[0]
|
||||
except:
|
||||
return False
|
||||
else:
|
||||
if self.is_ok(buf):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def cmd_change_user(self, username, password, database=None):
|
||||
"""Change the user with given username and password to another optional database.
|
||||
"""
|
||||
if not database:
|
||||
database = self.database
|
||||
|
||||
cmd = ChangeUserPacket()
|
||||
cmd.create(username=username, password=password, database=database,
|
||||
charset=self.charset, seed=self.handshake.info['seed'])
|
||||
try:
|
||||
self.conn.send(cmd.get())
|
||||
buf = self.conn.recv()[0]
|
||||
except:
|
||||
raise
|
||||
|
||||
if not self.is_ok(buf):
|
||||
raise errors.OperationalError(
|
||||
"Failed getting OK Packet after changing user")
|
||||
|
||||
return True
|
||||
|
||||
class BasePacket(object):
|
||||
|
||||
def __len__(self):
|
||||
try:
|
||||
return len(self.data)
|
||||
except:
|
||||
return 0
|
||||
|
||||
def is_valid(self, buf=None):
|
||||
if buf is None:
|
||||
buf = self.data
|
||||
|
||||
(l, n) = (buf[0:3], buf[3])
|
||||
hlength = utils.int3read(l)
|
||||
rlength = len(buf) - 4
|
||||
|
||||
if hlength != rlength:
|
||||
return False
|
||||
|
||||
res = self._is_valid_extra(buf)
|
||||
if res != None:
|
||||
return res
|
||||
|
||||
return True
|
||||
|
||||
def _is_valid_extra(self, buf):
|
||||
return True
|
||||
|
||||
class PacketIn(BasePacket):
|
||||
def __init__(self, buf=None, pktnr=0):
|
||||
self.data = ''
|
||||
self.pktnr = pktnr
|
||||
self.protocol = 10
|
||||
|
||||
if buf:
|
||||
self.is_valid(buf)
|
||||
self.data = buf[4:]
|
||||
|
||||
if self.data:
|
||||
self._parse()
|
||||
|
||||
def _parse(self):
|
||||
pass
|
||||
|
||||
class PacketOut(BasePacket):
|
||||
"""
|
||||
Each packet type used in the MySQL Client Protocol is build on the Packet
|
||||
class. It defines lots of useful functions for parsing and sending
|
||||
data to and from the MySQL Server.
|
||||
"""
|
||||
|
||||
def __init__(self, buf=None, pktnr=0):
|
||||
self.data = ''
|
||||
self.pktnr = pktnr
|
||||
self.protocol = 10
|
||||
|
||||
if buf:
|
||||
self.set(buf)
|
||||
|
||||
if self.data:
|
||||
self._parse()
|
||||
|
||||
def _make_header(self):
|
||||
h = utils.int3store(len(self)) + utils.int1store(self.pktnr)
|
||||
return h
|
||||
|
||||
def _parse(self):
|
||||
pass
|
||||
|
||||
def add(self, s):
|
||||
if not s:
|
||||
self.add_null()
|
||||
else:
|
||||
self.data = self.data + s
|
||||
|
||||
def add_1_int(self, i):
|
||||
self.add(utils.int1store(i))
|
||||
|
||||
def add_2_int(self, i):
|
||||
self.add(utils.int2store(i))
|
||||
|
||||
def add_3_int(self, i):
|
||||
self.add(utils.int3store(i))
|
||||
|
||||
def add_4_int(self, i):
|
||||
self.add(utils.int4store(i))
|
||||
|
||||
def add_null(self, nr=1):
|
||||
self.add('\x00'*nr)
|
||||
|
||||
def get(self):
|
||||
return self._make_header() + self.data
|
||||
|
||||
def get_header(self):
|
||||
return self._make_header()
|
||||
|
||||
def set(self, buf):
|
||||
if not self.is_valid(buf):
|
||||
raise errors.InterfaceError('Packet not valid.')
|
||||
|
||||
self.data = buf[4:]
|
||||
|
||||
def _is_valid_extra(self, buf=None):
|
||||
return None
|
||||
|
||||
class Handshake(PacketIn):
|
||||
|
||||
def __init__(self, buf=None):
|
||||
PacketIn.__init__(self, buf)
|
||||
|
||||
def _parse(self):
|
||||
version = ''
|
||||
options = 0
|
||||
srvstatus = 0
|
||||
|
||||
buf = self.data
|
||||
(buf,self.protocol) = utils.read_int(buf,1)
|
||||
(buf,version) = utils.read_string(buf,end='\x00')
|
||||
(buf,thrdid) = utils.read_int(buf,4)
|
||||
(buf,scramble) = utils.read_bytes(buf, 8)
|
||||
buf = buf[1:] # Filler 1 * \x00
|
||||
(buf,srvcap) = utils.read_int(buf,2)
|
||||
(buf,charset) = utils.read_int(buf,1)
|
||||
(buf,serverstatus) = utils.read_int(buf,2)
|
||||
buf = buf[13:] # Filler 13 * \x00
|
||||
(buf,scramble_next) = utils.read_bytes(buf,12)
|
||||
scramble += scramble_next
|
||||
|
||||
self.info = {
|
||||
'version' : version,
|
||||
'thrdid' : thrdid,
|
||||
'seed' : scramble,
|
||||
'capabilities' : srvcap,
|
||||
'charset' : charset,
|
||||
'serverstatus' : serverstatus,
|
||||
}
|
||||
|
||||
def get_dict(self):
|
||||
self._parse()
|
||||
return self.info
|
||||
|
||||
def _is_valid_extra(self, buf):
|
||||
|
||||
if buf[3] != '\x00':
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
class Auth(PacketOut):
|
||||
|
||||
def __init__(self, packet=None, client_flags=0, pktnr=0):
|
||||
PacketOut.__init__(self, packet, pktnr)
|
||||
self.client_flags = 0
|
||||
self.username = None
|
||||
self.password = None
|
||||
self.database = None
|
||||
if client_flags:
|
||||
self.set_client_flags(client_flags)
|
||||
|
||||
def set_client_flags(self, flags):
|
||||
self.client_flags = flags
|
||||
|
||||
def set_login(self, username, password, database=None):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.database = database
|
||||
|
||||
def scramble(self, passwd, seed):
|
||||
|
||||
hash4 = None
|
||||
try:
|
||||
hash1 = sha1(passwd).digest()
|
||||
hash2 = sha1(hash1).digest() # Password as found in mysql.user()
|
||||
hash3 = sha1(seed + hash2).digest()
|
||||
xored = [ utils.int1read(h1) ^ utils.int1read(h3)
|
||||
for (h1,h3) in zip(hash1, hash3) ]
|
||||
hash4 = struct.pack('20B', *xored)
|
||||
except StandardError, e:
|
||||
raise errors.ProgrammingError('Failed scrambling password; %s' % e)
|
||||
else:
|
||||
return hash4
|
||||
return None
|
||||
|
||||
def create(self, username=None, password=None, database=None, seed=None):
|
||||
self.add_4_int(self.client_flags)
|
||||
self.add_4_int(10 * 1024 * 1024)
|
||||
self.add_1_int(8)
|
||||
self.add_null(23)
|
||||
self.add(username + '\x00')
|
||||
if password:
|
||||
self.add_1_int(20)
|
||||
self.add(self.scramble(password,seed))
|
||||
else:
|
||||
self.add_null(1)
|
||||
|
||||
if database:
|
||||
self.add(database + '\x00')
|
||||
else:
|
||||
self.add_null()
|
||||
|
||||
|
||||
class ChangeUserPacket(Auth):
|
||||
def __init__(self):
|
||||
self.command = ServerCmd.CHANGE_USER
|
||||
Auth.__init__(self)
|
||||
|
||||
def create(self, username=None, password=None, database=None, charset=8, seed=None):
|
||||
self.add_1_int(self.command)
|
||||
self.add(username + '\x00')
|
||||
if password:
|
||||
self.add_1_int(20)
|
||||
self.add(self.scramble(password,seed))
|
||||
else:
|
||||
self.add_null(1)
|
||||
if database:
|
||||
self.add(database + '\x00')
|
||||
else:
|
||||
self.add_null()
|
||||
|
||||
self.add_2_int(charset)
|
||||
|
||||
class ErrorResultPacket(PacketIn):
|
||||
|
||||
def __init__(self, buf=None):
|
||||
self.errno = 0
|
||||
self.errmsg = ''
|
||||
self.sqlstate = None
|
||||
PacketIn.__init__(self, buf)
|
||||
|
||||
def _parse(self):
|
||||
buf = self.data
|
||||
|
||||
if buf[0] != '\xff':
|
||||
raise errors.InterfaceError('Expected an Error Packet.')
|
||||
buf = buf[1:]
|
||||
|
||||
(buf,self.errno) = utils.read_int(buf, 2)
|
||||
|
||||
if buf[0] != '\x23':
|
||||
# Error without SQLState
|
||||
self.errmsg = buf
|
||||
else:
|
||||
(buf,self.sqlstate) = utils.read_bytes(buf[1:],5)
|
||||
self.errmsg = buf
|
||||
|
||||
class OKResultPacket(PacketIn):
|
||||
def __init__(self, buf=None):
|
||||
self.affected_rows = None
|
||||
self.insert_id = None
|
||||
self.server_status = 0
|
||||
self.warning_count = 0
|
||||
self.field_count = 0
|
||||
self.info_msg = ''
|
||||
PacketIn.__init__(self, buf)
|
||||
|
||||
def __str__(self):
|
||||
if self.affected_rows == 1:
|
||||
lbl_rows = 'row'
|
||||
else:
|
||||
lbl_rows = 'rows'
|
||||
|
||||
xtr = []
|
||||
if self.insert_id:
|
||||
xtr.append('last insert: %d ' % self.insert_id)
|
||||
if self.warning_count:
|
||||
xtr.append('warnings: %d' % self.warning_count)
|
||||
|
||||
return "Query OK, %d %s affected %s( sec)" % (self.affected_rows,
|
||||
lbl_rows, ', '.join(xtr))
|
||||
|
||||
def _parse(self):
|
||||
buf = self.data
|
||||
(buf,self.field_count) = utils.read_int(buf,1)
|
||||
(buf,self.affected_rows) = utils.read_lc_int(buf)
|
||||
(buf,self.insert_id) = utils.read_lc_int(buf)
|
||||
(buf,self.server_status) = utils.read_int(buf,2)
|
||||
(buf,self.warning_count) = utils.read_int(buf,2)
|
||||
if buf:
|
||||
(buf,self.info_msg) = utils.read_lc_string(buf)
|
||||
|
||||
class CommandPacket(PacketOut):
|
||||
def __init__(self, cmd=None, arg=None):
|
||||
self.command = cmd
|
||||
self.argument = arg
|
||||
PacketOut.__init__(self)
|
||||
|
||||
def create(self):
|
||||
self.add_1_int(self.command)
|
||||
self.add(str(self.argument))
|
||||
|
||||
def set_command(self, cmd):
|
||||
self.command = cmd
|
||||
|
||||
def set_argument(self, arg):
|
||||
self.argument = arg
|
||||
|
||||
class KillPacket(CommandPacket):
|
||||
|
||||
def __init__(self, arg):
|
||||
CommandPacket.__init__(self)
|
||||
self.set_command(ServerCmd.PROCESS_KILL)
|
||||
self.set_argument(arg)
|
||||
|
||||
def create(self):
|
||||
""""""
|
||||
self.add_1_int(self.command)
|
||||
self.add_4_int(self.argument)
|
||||
|
||||
def set_argument(self, arg):
|
||||
if arg and not isinstance(int, long) and arg > 2**32:
|
||||
raise ValueError, "KillPacket needs integer value as argument not larger than 2^32."
|
||||
self.argument = arg
|
||||
|
||||
class FieldPacket(PacketIn):
|
||||
def __init__(self, buf=None):
|
||||
self.catalog = None
|
||||
self.db = None
|
||||
self.table = None
|
||||
self.org_table = None
|
||||
self.name = None
|
||||
self.length = None
|
||||
self.org_name = None
|
||||
self.charset = None
|
||||
self.type = None
|
||||
self.flags = None
|
||||
PacketIn.__init__(self, buf)
|
||||
|
||||
def __str__(self):
|
||||
flags = []
|
||||
for k,f in FieldFlag.desc.items():
|
||||
if int(self.flags) & f[0]:
|
||||
flags.append(k)
|
||||
return """
|
||||
Field: catalog: %s ; db:%s ; table:%s ; org_table: %s ;
|
||||
name: %s ; org_name: %s ;
|
||||
charset: %s ; lenght: %s ;
|
||||
type: %02x ;
|
||||
flags(%d): %s;
|
||||
""" % (self.catalog,self.db,self.table,self.org_table,self.name,self.org_name,
|
||||
self.charset, len(self), self.type,
|
||||
self.flags, '|'.join(flags))
|
||||
|
||||
def _parse(self):
|
||||
buf = self.data
|
||||
|
||||
(buf,self.catalog) = utils.read_lc_string(buf)
|
||||
(buf,self.db) = utils.read_lc_string(buf)
|
||||
(buf,self.table) = utils.read_lc_string(buf)
|
||||
(buf,self.org_table) = utils.read_lc_string(buf)
|
||||
(buf,self.name) = utils.read_lc_string(buf)
|
||||
(buf,self.org_name) = utils.read_lc_string(buf)
|
||||
buf = buf[1:] # filler 1 * \x00
|
||||
(buf,self.charset) = utils.read_int(buf, 2)
|
||||
(buf,self.length) = utils.read_int(buf, 4)
|
||||
(buf,self.type) = utils.read_int(buf, 1)
|
||||
(buf,self.flags) = utils.read_int(buf, 2)
|
||||
(buf,self.decimal) = utils.read_int(buf, 1)
|
||||
buf = buf[2:] # filler 2 * \x00
|
||||
|
||||
def get_description(self):
|
||||
"""Returns a description as a list useful for cursors.
|
||||
|
||||
This function returns a list as defined in the Python Db API v2.0
|
||||
specification.
|
||||
|
||||
"""
|
||||
return (
|
||||
self.name,
|
||||
self.type,
|
||||
None, # display_size
|
||||
None, # internal_size
|
||||
None, # precision
|
||||
None, # scale
|
||||
~self.flags & FieldFlag.NOT_NULL, # null_ok
|
||||
self.flags, # MySQL specific
|
||||
)
|
||||
|
||||
class EOFPacket(PacketIn):
|
||||
def __init__(self, buf=None):
|
||||
self.warning_count = None
|
||||
self.status_flag = None
|
||||
PacketIn.__init__(self, buf)
|
||||
|
||||
def __str__(self):
|
||||
return "EOFPacket: warnings %d / status: %d" % (self.warning_count,self.status_flag)
|
||||
|
||||
def _is_valid_extra(self, buf=None):
|
||||
if not buf:
|
||||
buf = self.data
|
||||
else:
|
||||
buf = buf[4:]
|
||||
if buf[0] == '\xfe' and len(buf) == 5:
|
||||
# An EOF should always start with \xfe and smaller than 9 bytes
|
||||
return True
|
||||
return False
|
||||
|
||||
def _parse(self):
|
||||
buf = self.data
|
||||
|
||||
buf = buf[1:] # disregard the first checking byte
|
||||
(buf, self.warning_count) = utils.read_int(buf, 2)
|
||||
(buf, self.status_flag) = utils.read_int(buf, 2)
|
|
@ -0,0 +1,392 @@
|
|||
# MySQL Connector/Python - MySQL driver written in Python.
|
||||
# Copyright 2009 Sun Microsystems, Inc. All rights reserved
|
||||
# Use is subject to license terms. (See COPYING)
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation.
|
||||
#
|
||||
# There are special exceptions to the terms and conditions of the GNU
|
||||
# General Public License as it is applied to this software. View the
|
||||
# full text of the exception in file EXCEPTIONS-CLIENT in the directory
|
||||
# of this software distribution or see the FOSS License Exception at
|
||||
# www.mysql.com.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""Utilities
|
||||
"""
|
||||
|
||||
__MYSQL_DEBUG__ = False
|
||||
|
||||
import struct
|
||||
|
||||
def int1read(c):
|
||||
"""
|
||||
Takes a bytes and returns it was an integer.
|
||||
|
||||
Returns integer.
|
||||
"""
|
||||
if isinstance(c,int):
|
||||
if c < 0 or c > 254:
|
||||
raise ValueError('excepts int 0 <= x <= 254')
|
||||
return c
|
||||
elif len(c) > 1:
|
||||
raise ValueError('excepts 1 byte long bytes-object or int')
|
||||
|
||||
return int('%02x' % ord(c),16)
|
||||
|
||||
def int2read(s):
|
||||
"""
|
||||
Takes a string of 2 bytes and unpacks it as unsigned integer.
|
||||
|
||||
Returns integer.
|
||||
"""
|
||||
if len(s) > 2:
|
||||
raise ValueError('int2read require s length of maximum 3 bytes')
|
||||
elif len(s) < 2:
|
||||
s = s + '\x00'
|
||||
return struct.unpack('<H', s)[0]
|
||||
|
||||
def int3read(s):
|
||||
"""
|
||||
Takes a string of 3 bytes and unpacks it as integer.
|
||||
|
||||
Returns integer.
|
||||
"""
|
||||
if len(s) > 3:
|
||||
raise ValueError('int3read require s length of maximum 3 bytes')
|
||||
elif len(s) < 4:
|
||||
s = s + '\x00'*(4-len(s))
|
||||
return struct.unpack('<I', s)[0]
|
||||
|
||||
def int4read(s):
|
||||
"""
|
||||
Takes a string of 4 bytes and unpacks it as integer.
|
||||
|
||||
Returns integer.
|
||||
"""
|
||||
if len(s) > 4:
|
||||
raise ValueError('int4read require s length of maximum 4 bytes')
|
||||
elif len(s) < 4:
|
||||
s = s + '\x00'*(4-len(s))
|
||||
return struct.unpack('<I', s)[0]
|
||||
|
||||
def int8read(s):
|
||||
"""
|
||||
Takes a string of 8 bytes and unpacks it as integer.
|
||||
|
||||
Returns integer.
|
||||
"""
|
||||
if len(s) > 8:
|
||||
raise ValueError('int4read require s length of maximum 8 bytes')
|
||||
elif len(s) < 8:
|
||||
s = s + '\x00'*(8-len(s))
|
||||
return struct.unpack('<Q', s)[0]
|
||||
|
||||
def intread(s):
|
||||
"""
|
||||
Takes a string and unpacks it as an integer.
|
||||
|
||||
This function uses int1read, int2read, int3read and int4read by
|
||||
checking the length of the given string.
|
||||
|
||||
Returns integer.
|
||||
"""
|
||||
l = len(s)
|
||||
if l < 1 or l > 4:
|
||||
raise ValueError('intread expects a string not longer than 4 bytes')
|
||||
if not isinstance(s, str):
|
||||
raise ValueError('intread expects a string')
|
||||
fs = {
|
||||
1 : int1read,
|
||||
2 : int2read,
|
||||
3 : int3read,
|
||||
4 : int4read,
|
||||
8 : int8read,
|
||||
}
|
||||
return fs[l](s)
|
||||
|
||||
def int1store(i):
|
||||
"""
|
||||
Takes an unsigned byte (1 byte) and packs it as string.
|
||||
|
||||
Returns string.
|
||||
"""
|
||||
if i < 0 or i > 255:
|
||||
raise ValueError('int1store requires 0 <= i <= 255')
|
||||
else:
|
||||
return struct.pack('<B',i)
|
||||
|
||||
def int2store(i):
|
||||
"""
|
||||
Takes an unsigned short (2 bytes) and packs it as string.
|
||||
|
||||
Returns string.
|
||||
"""
|
||||
if i < 0 or i > 65535:
|
||||
raise ValueError('int2store requires 0 <= i <= 65535')
|
||||
else:
|
||||
return struct.pack('<H',i)
|
||||
|
||||
def int3store(i):
|
||||
"""
|
||||
Takes an unsigned integer (3 bytes) and packs it as string.
|
||||
|
||||
Returns string.
|
||||
"""
|
||||
if i < 0 or i > 16777215:
|
||||
raise ValueError('int3store requires 0 <= i <= 16777215')
|
||||
else:
|
||||
return struct.pack('<I',i)[0:3]
|
||||
|
||||
def int4store(i):
|
||||
"""
|
||||
Takes an unsigned integer (4 bytes) and packs it as string.
|
||||
|
||||
Returns string.
|
||||
"""
|
||||
if i < 0 or i > 4294967295L:
|
||||
raise ValueError('int4store requires 0 <= i <= 4294967295')
|
||||
else:
|
||||
return struct.pack('<I',i)
|
||||
|
||||
def intstore(i):
|
||||
"""
|
||||
Takes an unsigned integers and packs it as a string.
|
||||
|
||||
This function uses int1store, int2store, int3store and
|
||||
int4store depending on the integer value.
|
||||
|
||||
returns string.
|
||||
"""
|
||||
if i < 0 or i > 4294967295L:
|
||||
raise ValueError('intstore requires 0 <= i <= 4294967295')
|
||||
|
||||
if i <= 255:
|
||||
fs = int1store
|
||||
elif i <= 65535:
|
||||
fs = int2store
|
||||
elif i <= 16777215:
|
||||
fs = int3store
|
||||
else:
|
||||
fs = int4store
|
||||
|
||||
return fs(i)
|
||||
|
||||
def read_bytes(buf, size):
|
||||
"""
|
||||
Reads bytes from a buffer.
|
||||
|
||||
Returns a tuple with buffer less the read bytes, and the bytes.
|
||||
"""
|
||||
s = buf[0:size]
|
||||
return (buf[size:], s)
|
||||
|
||||
def read_lc_string(buf):
|
||||
"""
|
||||
Takes a buffer and reads a length coded string from the start.
|
||||
|
||||
This is how Length coded strings work
|
||||
|
||||
If the string is 250 bytes long or smaller, then it looks like this:
|
||||
|
||||
<-- 1b -->
|
||||
+----------+-------------------------
|
||||
| length | a string goes here
|
||||
+----------+-------------------------
|
||||
|
||||
If the string is bigger than 250, then it looks like this:
|
||||
|
||||
<- 1b -><- 2/3/4 ->
|
||||
+------+-----------+-------------------------
|
||||
| type | length | a string goes here
|
||||
+------+-----------+-------------------------
|
||||
|
||||
if type == \xfc:
|
||||
length is code in next 2 bytes
|
||||
elif type == \xfd:
|
||||
length is code in next 3 bytes
|
||||
elif type == \xfe:
|
||||
length is code in next 4 bytes
|
||||
|
||||
NULL has a special value. If the buffer starts with \xfb then
|
||||
it's a NULL and we return None as value.
|
||||
|
||||
Returns a tuple (trucated buffer, string).
|
||||
"""
|
||||
if buf[0] == '\xfb':
|
||||
# NULL value
|
||||
return (buf[1:], None)
|
||||
|
||||
l = lsize = start = 0
|
||||
fst = buf[0]
|
||||
# Remove the type byte, we got the length information.
|
||||
buf = buf[1:]
|
||||
|
||||
if fst <= '\xFA':
|
||||
# Returns result right away.
|
||||
l = ord(fst)
|
||||
s = buf[:l]
|
||||
return (buf[l:], s)
|
||||
elif fst == '\xFC':
|
||||
lsize = 2
|
||||
elif fst == '\xFD':
|
||||
lsize = 3
|
||||
elif fst == '\xFE':
|
||||
lsize = 4
|
||||
|
||||
l = intread(buf[0:lsize])
|
||||
# Chop of the bytes which hold the length
|
||||
buf = buf[lsize:]
|
||||
# Get the actual string
|
||||
s = buf[0:l]
|
||||
# Set the buffer so we can return it
|
||||
buf = buf[l:]
|
||||
|
||||
return (buf, s)
|
||||
|
||||
def read_lc_string_list(buf):
|
||||
"""
|
||||
Reads all length encoded strings from the given buffer.
|
||||
|
||||
This is exact same function as read_lc_string() but duplicated
|
||||
in hopes for performance gain when reading results.
|
||||
"""
|
||||
strlst = []
|
||||
|
||||
while buf:
|
||||
if buf[0] == '\xfb':
|
||||
# NULL value
|
||||
buf = buf[1:]
|
||||
strlst.append(None)
|
||||
continue
|
||||
|
||||
l = lsize = start = 0
|
||||
fst = buf[0]
|
||||
# Remove the type byte, we got the length information.
|
||||
buf = buf[1:]
|
||||
|
||||
if fst <= '\xFA':
|
||||
# Returns result right away.
|
||||
l = ord(fst)
|
||||
strlst.append(buf[:l])
|
||||
buf = buf[l:]
|
||||
continue
|
||||
elif fst == '\xFC':
|
||||
lsize = 2
|
||||
elif fst == '\xFD':
|
||||
lsize = 3
|
||||
elif fst == '\xFE':
|
||||
lsize = 4
|
||||
|
||||
l = intread(buf[0:lsize])
|
||||
# Chop of the bytes which hold the length
|
||||
buf = buf[lsize:]
|
||||
# Get the actual string
|
||||
s = buf[0:l]
|
||||
# Set the buffer so we can return it
|
||||
buf = buf[l:]
|
||||
|
||||
strlst.append(s)
|
||||
|
||||
return strlst
|
||||
|
||||
def read_string(buf, end=None, size=None):
|
||||
"""
|
||||
Reads a string up until a character or for a given size.
|
||||
|
||||
Returns a tuple (trucated buffer, string).
|
||||
"""
|
||||
if end is None and size is None:
|
||||
raise ValueError('read_string() needs either end or size')
|
||||
|
||||
if end is not None:
|
||||
try:
|
||||
idx = buf.index(end)
|
||||
except (ValueError), e:
|
||||
raise ValueError("end byte not precent in buffer")
|
||||
return (buf[idx+1:], buf[0:idx])
|
||||
elif size is not None:
|
||||
return read_bytes(buf,size)
|
||||
|
||||
raise ValueError('read_string() needs either end or size (weird)')
|
||||
|
||||
def read_int(buf, size):
|
||||
"""
|
||||
Take a buffer and reads an integer of a certain size (1 <= size <= 4).
|
||||
|
||||
Returns a tuple (truncated buffer, int)
|
||||
"""
|
||||
if len(buf) == 0:
|
||||
raise ValueError("Empty buffer.")
|
||||
if not isinstance(size,int) or (size not in [1,2,3,4,8]):
|
||||
raise ValueError('size should be int in range of 1..4 or 8')
|
||||
|
||||
i = None
|
||||
if size == 1:
|
||||
i = int1read(buf[0])
|
||||
elif size == 2:
|
||||
i = int2read(buf[0:2])
|
||||
elif size == 3:
|
||||
i = int3read(buf[0:3])
|
||||
elif size == 4:
|
||||
i = int4read(buf[0:4])
|
||||
elif size == 8:
|
||||
i = int8read(buf[0:8])
|
||||
else:
|
||||
raise ValueError('size should be int in range of 1..4 or 8 (weird)')
|
||||
|
||||
return (buf[size:], int(i))
|
||||
|
||||
def read_lc_int(buf):
|
||||
"""
|
||||
Takes a buffer and reads an length code string from the start.
|
||||
|
||||
Returns a tuple with buffer less the integer and the integer read.
|
||||
"""
|
||||
if len(buf) == 0:
|
||||
raise ValueError("Empty buffer.")
|
||||
|
||||
(buf,s) = read_int(buf,1)
|
||||
if s == 251:
|
||||
l = 0
|
||||
return (buf,None)
|
||||
elif s == 252:
|
||||
(buf,i) = read_int(buf,2)
|
||||
elif s == 253:
|
||||
(buf,i) = read_int(buf,3)
|
||||
elif s == 254:
|
||||
(buf,i) = read_int(buf,8)
|
||||
else:
|
||||
i = s
|
||||
|
||||
return (buf, int(i))
|
||||
|
||||
#
|
||||
# For debugging
|
||||
#
|
||||
def _dump_buffer(buf, label=None):
|
||||
import __main__
|
||||
if not __main__.__dict__.has_key('__MYSQL_DEBUG__'):
|
||||
return
|
||||
else:
|
||||
debug = __main__.__dict__['__MYSQL_DEBUG__']
|
||||
|
||||
try:
|
||||
if debug:
|
||||
if len(buf) == 0:
|
||||
print "%s : EMPTY BUFFER" % label
|
||||
import string
|
||||
print "%s: %s" % (label,string.join( [ "%02x" % ord(c) for c in buf ], ' '))
|
||||
if debug > 1:
|
||||
print "%s: %s" % (label,string.join( [ "%s" % chr(ord(c)) for c in buf ], ''))
|
||||
except:
|
||||
raise
|
|
@ -0,0 +1,793 @@
|
|||
import mc
|
||||
import re
|
||||
import sys
|
||||
import random
|
||||
import time
|
||||
import pickle
|
||||
import mythtv
|
||||
from mythtv import MythError
|
||||
from operator import itemgetter, attrgetter
|
||||
|
||||
|
||||
class MythBoxee:
|
||||
logLevel = 1
|
||||
version = "4.0.1.beta"
|
||||
userAgent = "MythBoxee"
|
||||
tvdb_apikey = "6BEAB4CB5157AAE0"
|
||||
be = None
|
||||
db = None
|
||||
recs = None
|
||||
titles = []
|
||||
recordings = []
|
||||
banners = {}
|
||||
shows = {}
|
||||
series = {}
|
||||
|
||||
|
||||
"""
|
||||
__init__ - Lets get things rolling
|
||||
"""
|
||||
def __init__(self):
|
||||
self.log("def(__init__): Start =========================================================")
|
||||
self.log("def(__init__): MythBoxee Version: " + self.version)
|
||||
self.log("def(__init__): Python Version: " + str(sys.version_info))
|
||||
self.log()
|
||||
|
||||
self.config = mc.GetApp().GetLocalConfig()
|
||||
|
||||
# We'll use this to determine when to reload data.
|
||||
self.config.SetValue("LastRunTime", str(time.time()))
|
||||
self.config.SetValue("CurrentShowItemID", "0")
|
||||
|
||||
# If this is the first time the app is being run, lets set some default options.
|
||||
if not self.config.GetValue("app.firstrun"):
|
||||
self.config.SetValue("SortBy", "Original Air Date")
|
||||
self.config.SetValue("SortDir", "Descending")
|
||||
self.config.SetValue("Filter", "All")
|
||||
self.config.SetValue("StreamMethod", "XML")
|
||||
self.config.SetValue("app.firstrun", "true")
|
||||
|
||||
|
||||
# If dbconn isn't set, we'll assume we haven't found the backend.
|
||||
if not self.config.GetValue("dbconn"):
|
||||
mc.ActivateWindow(14004)
|
||||
else:
|
||||
# Now that the backend has been discovered, lets connect.
|
||||
try:
|
||||
self.log("def(__init__): Attempting Database Connection ...")
|
||||
self.dbconf = eval(self.config.GetValue("dbconn"))
|
||||
self.db = mythtv.MythDB(**self.dbconf)
|
||||
except MythError, e:
|
||||
self.log("def(__init__): Error: " + e.message)
|
||||
mc.ShowDialogNotification("Failed to connect to the MythTV Backend")
|
||||
mc.ActivateWindow(14004)
|
||||
else:
|
||||
self.log("def(__init__): Database Connection Successful")
|
||||
try:
|
||||
self.log("def(__init__): Attempting Connection to Backend")
|
||||
self.be = mythtv.MythBE(db=self.db)
|
||||
except MythError, e:
|
||||
self.log("def(__init__): Connection to Backend FAILED")
|
||||
self.log("def(__init__): MythBE Error: " + e.message)
|
||||
mc.CloseWindow()
|
||||
else:
|
||||
self.log("def(__init__): Connection to Backend Successful")
|
||||
self.GetRecordings()
|
||||
|
||||
self.log("def(__init__): End ===========================================================")
|
||||
|
||||
|
||||
"""
|
||||
DiscoverBackend - just as it sounds
|
||||
|
||||
Attempt to discover the MythTV Backend using UPNP protocol, once found
|
||||
try and gather MySQL database connection information using default PIN
|
||||
via the XML interface. If that fails then prompt user to enter their
|
||||
custom SecurityPin, if we fail to gather database information that way
|
||||
finally prompt user to enter their credentials manually.
|
||||
"""
|
||||
def DiscoverBackend(self):
|
||||
self.log("def(DiscoverBackend): Start =========================================================")
|
||||
|
||||
pin = self.config.GetValue("pin")
|
||||
dbconn = self.config.GetValue("dbconn")
|
||||
|
||||
if not pin:
|
||||
pin = 0000
|
||||
|
||||
try:
|
||||
self.log("def(DiscoverBackend): Attempting Database Connection ...")
|
||||
self.db = mythtv.MythDB(SecurityPin=pin)
|
||||
except Exception, e:
|
||||
self.log("def(DiscoverBackend): Exception: " + str(e.ename))
|
||||
self.log("def(DiscoverBackend): End ===========================================================")
|
||||
return False
|
||||
else:
|
||||
# We have a successful connection, save database information
|
||||
self.config.SetValue("dbconn", str(self.db.dbconn))
|
||||
|
||||
self.log("def(DiscoverBackend): Database Connection Successful")
|
||||
self.log("def(DiscoverBackend): End ===========================================================")
|
||||
return True
|
||||
|
||||
|
||||
"""
|
||||
LoadMain - Loader for Main Window
|
||||
"""
|
||||
def LoadMain(self):
|
||||
self.log("def(LoadMain): Start =========================================================")
|
||||
self.config.SetValue("loadingmain", "true")
|
||||
|
||||
if not self.config.GetValue("dbconn"):
|
||||
return False
|
||||
|
||||
## Put focus on last selected item
|
||||
itemId = int(self.config.GetValue("CurrentShowItemID"))
|
||||
if itemId and itemId != 0:
|
||||
mc.GetWindow(14001).GetList(1030).SetFocusedItem(itemId)
|
||||
|
||||
cacheTime = self.config.GetValue("cache.time")
|
||||
mainItems = len(mc.GetWindow(14001).GetList(1030).GetItems())
|
||||
|
||||
if not cacheTime or mainItems == 0 or cacheTime <= str(time.time() - 2400):
|
||||
self.SetShows()
|
||||
|
||||
self.config.Reset("loadingmain")
|
||||
self.log("def(LoadMain): End ===========================================================")
|
||||
|
||||
|
||||
"""
|
||||
RefreshMain - Refresh the Main Window
|
||||
"""
|
||||
def RefreshMain(self):
|
||||
self.log("def(RefreshMain): Start =========================================================")
|
||||
self.config.SetValue("loadingmain", "true")
|
||||
|
||||
self.config.Reset("cache.time")
|
||||
self.GetRecordings()
|
||||
self.SetShows()
|
||||
|
||||
self.config.Reset("loadingmain")
|
||||
self.log("def(RefreshMain): End ===========================================================")
|
||||
|
||||
|
||||
"""
|
||||
GetRecordings - Pulls all of the recordings out of the backend and prepares them for use in the app.
|
||||
|
||||
This function also creates some dictionarys and lists of information
|
||||
that is used throughout the app for different functions.
|
||||
"""
|
||||
def GetRecordings(self):
|
||||
self.log("def(GetRecordings): Start =========================================================")
|
||||
self.config.SetValue("loadingmain", "true")
|
||||
|
||||
# Empty out crucial info
|
||||
self.titles = []
|
||||
self.banners = {}
|
||||
self.series = {}
|
||||
self.shows = {}
|
||||
|
||||
cacheTime = self.config.GetValue("cache.time")
|
||||
mainItems = len(mc.GetWindow(14001).GetList(1030).GetItems())
|
||||
|
||||
self.recs = self.be.getRecordings()
|
||||
|
||||
self.banners = pickle.loads(self.config.GetValue("cache.banners"))
|
||||
self.series = pickle.loads(self.config.GetValue("cache.series"))
|
||||
|
||||
if not cacheTime or mainItems == 0 or cacheTime <= str(time.time() - 2400):
|
||||
self.log("def(GetRecordings): Cached Expired, Processing Recordings")
|
||||
x=0
|
||||
for recording in self.recs:
|
||||
if recording.title not in self.titles:
|
||||
self.titles.append(str(recording.title))
|
||||
self.shows[str(recording.title)] = []
|
||||
if recording.title not in self.banners:
|
||||
self.banners[str(recording.title)] = self.GetRecordingArtwork(str(recording.title))
|
||||
if recording.title not in self.series:
|
||||
self.series[str(recording.title)] = self.GetRecordingSeriesID(str(recording.title))
|
||||
|
||||
single = [str(recording.title), str(recording.subtitle), str(recording.description), str(recording.chanid), str(recording.airdate), str(recording.starttime), str(recording.endtime), recording.getRecorded().watched, x]
|
||||
self.shows[str(recording.title)].append(single)
|
||||
x = x + 1
|
||||
|
||||
# Lets cache our findings for now.
|
||||
self.config.SetValue("cache.time", str(time.time()))
|
||||
self.config.SetValue("cache.titles", pickle.dumps(self.titles))
|
||||
self.config.SetValue("cache.banners", pickle.dumps(self.banners))
|
||||
self.config.SetValue("cache.series", pickle.dumps(self.series))
|
||||
self.config.SetValue("cache.shows", pickle.dumps(self.shows))
|
||||
else:
|
||||
self.log("def(GetRecordings): Cache OK, Retrieving from Cache")
|
||||
self.titles = pickle.loads(self.config.GetValue("cache.titles"))
|
||||
self.banners = pickle.loads(self.config.GetValue("cache.banners"))
|
||||
self.series = pickle.loads(self.config.GetValue("cache.series"))
|
||||
self.shows = pickle.loads(self.config.GetValue("cache.shows"))
|
||||
|
||||
self.titles.sort()
|
||||
|
||||
self.config.Reset("loadingmain")
|
||||
self.log("def(GetRecordings): End ===========================================================")
|
||||
|
||||
|
||||
"""
|
||||
SetShows - Populate the Shows List on the Main Window
|
||||
"""
|
||||
def SetShows(self):
|
||||
self.log("def(SetShows): Start =========================================================")
|
||||
items = mc.ListItems()
|
||||
for title in self.titles:
|
||||
item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN )
|
||||
item.SetLabel(str(title))
|
||||
item.SetThumbnail(self.banners[title])
|
||||
item.SetProperty("videos", str(len(self.shows[title])))
|
||||
item.SetProperty("seriesid", str(self.series[title]))
|
||||
items.append(item)
|
||||
mc.GetWindow(14001).GetList(1030).SetItems(items)
|
||||
self.log("def(SetShows): End ===========================================================")
|
||||
|
||||
|
||||
"""
|
||||
GetRecordingArtwork - Get the Artwork for a show.
|
||||
"""
|
||||
def GetRecordingArtwork(self, title):
|
||||
self.log("def(GetRecordingArtwork): Start =========================================================")
|
||||
|
||||
sg = mc.Http()
|
||||
sg.SetUserAgent(self.userAgent + " " + self.version)
|
||||
html = sg.Get("http://www.thetvdb.com/api/GetSeries.php?seriesname=" + str(title.replace(" ", "%20")))
|
||||
banners = re.compile("<banner>(.*?)</banner>").findall(html)
|
||||
|
||||
## Sometimes we can't find the show, so we have to provide our own artwork
|
||||
try:
|
||||
artwork = "http://www.thetvdb.com/banners/" + banners[0]
|
||||
except:
|
||||
artwork = "mb_artwork_error.png"
|
||||
|
||||
self.log("def(GetRecordingArtwork): URL: " + str(artwork))
|
||||
self.log("def(GetRecordingArtwork): End =========================================================")
|
||||
|
||||
return artwork
|
||||
|
||||
|
||||
"""
|
||||
GetRecordingSeriesID - Get the Series ID of a show.
|
||||
|
||||
TODO: rewrite this entire function
|
||||
"""
|
||||
def GetRecordingSeriesID(self, title):
|
||||
self.log("def(GetRecordingSeriesID): Start =========================================================")
|
||||
sg = mc.Http()
|
||||
sg.SetUserAgent(self.userAgent + " " + self.version)
|
||||
html = sg.Get("http://www.thetvdb.com/api/GetSeries.php?seriesname=" + title.replace(" ", "%20"))
|
||||
series = re.compile("<seriesid>(.*?)</seriesid>").findall(html)
|
||||
|
||||
## Sometimes we can't determine the series ID
|
||||
try:
|
||||
seriesid = series[0]
|
||||
except:
|
||||
seriesid = 00000
|
||||
|
||||
self.log("def(GetRecordingSeriesID): Title: " + title)
|
||||
self.log("def(GetRecordingSeriesID): SeriesID: " + str(seriesid))
|
||||
self.log("def(GetRecordingSeriesID): End ===========================================================")
|
||||
return seriesid
|
||||
|
||||
|
||||
"""
|
||||
DisplayShow
|
||||
|
||||
TODO: rewrite this entire function
|
||||
"""
|
||||
def DisplayShow(self):
|
||||
self.log("def(DisplaySingleShow): Start =========================================================")
|
||||
|
||||
recordingList = mc.GetWindow(14001).GetList(1030)
|
||||
itemId = recordingList.GetFocusedItem()
|
||||
item = recordingList.GetItem(itemId)
|
||||
title = item.GetLabel()
|
||||
|
||||
# Save the Latest Show Title to what was clicked
|
||||
# this way the show window has a way to load the data.
|
||||
self.config.SetValue("CurrentShowItemID", str(itemId))
|
||||
self.config.SetValue("CurrentShowTitle", title)
|
||||
self.config.SetValue("CurrentShowID", item.GetProperty("seriesid"))
|
||||
|
||||
# Show the Single Show Window
|
||||
mc.ActivateWindow(14002)
|
||||
|
||||
itemList = mc.ListItems()
|
||||
itemList.append(item)
|
||||
|
||||
mc.GetWindow(14002).GetList(2070).SetItems(itemList)
|
||||
|
||||
self.log("def(DisplaySingleShow): Title[" + title + "]")
|
||||
self.log("def(DisplaySingleShow): Current Show Title: " + title)
|
||||
self.log("def(DisplaySingleShow): Current Show Item ID: " + str(itemId))
|
||||
self.log("def(DisplaySingleShow): Current Show Series ID: " + item.GetProperty("seriesid"))
|
||||
self.log("def(DisplaySingleShow): End ===========================================================")
|
||||
|
||||
|
||||
"""
|
||||
LoadShow - init function for Show Window
|
||||
"""
|
||||
def LoadShow(self):
|
||||
self.log("def(LoadShow): Start =========================================================")
|
||||
self.config.SetValue("loading", "true")
|
||||
|
||||
## Get Current Show Information
|
||||
title = self.config.GetValue("CurrentShowTitle")
|
||||
seriesid = self.config.GetValue("CurrentShowID")
|
||||
|
||||
self.log("def(LoadSingleShow): Title[" + title + "]")
|
||||
|
||||
## Setup the Show Window and Populate the Window's Lists
|
||||
self.SetSortableOptions()
|
||||
self.SetSeriesDetails(title, seriesid)
|
||||
self.LoadShowRecordings(title)
|
||||
|
||||
self.config.Reset("loading")
|
||||
self.log("def(LoadShow): End ===========================================================")
|
||||
|
||||
|
||||
"""
|
||||
LoadShowRecordings
|
||||
|
||||
Determine which show is being displayed and find all the recordings for it.
|
||||
Then populate the recording list for the singular show for viewer to watch.
|
||||
"""
|
||||
def LoadShowRecordings(self, title):
|
||||
self.log("def(LoadShowRecordings): Start =========================================================")
|
||||
|
||||
dbconf = eval(self.config.GetValue("dbconn"))
|
||||
|
||||
## Get current sort and filter settings
|
||||
sortBy = self.config.GetValue("SortBy")
|
||||
sortDir = self.config.GetValue("SortDir")
|
||||
theFilter = self.config.GetValue("Filter")
|
||||
|
||||
self.log("def(LoadShowRecordings): Sort By: " + sortBy)
|
||||
self.log("def(LoadShowRecordings): Sort Dir: " + sortDir)
|
||||
self.log("def(LoadShowRecordings): Filter: " + theFilter)
|
||||
|
||||
## Clear the episodes container
|
||||
episodes = None
|
||||
|
||||
## Sort based on Sorting Criteria
|
||||
if sortBy == "Original Air Date" and sortDir == "Ascending":
|
||||
episodes = sorted(self.shows[title], key=itemgetter(4))
|
||||
elif sortBy == "Original Air Date" and sortDir == "Descending":
|
||||
episodes = sorted(self.shows[title], key=itemgetter(4), reverse=True)
|
||||
elif sortBy == "Recorded Date" and sortDir == "Ascending":
|
||||
episodes = sorted(self.shows[title], key=itemgetter(5))
|
||||
elif sortBy == "Recorded Date" and sortDir == "Descending":
|
||||
episodes = sorted(self.shows[title], key=itemgetter(5), reverse=True)
|
||||
elif sortBy == "Title" and sortDir == "Ascending":
|
||||
episodes = sorted(self.shows[title], key=itemgetter(1))
|
||||
elif sortBy == "Title" and sortDir == "Descending":
|
||||
episodes = sorted(self.shows[title], key=itemgetter(1), reverse=True)
|
||||
else:
|
||||
episodes = self.shows[title]
|
||||
|
||||
## Loop through all our recordings, and add them to the list.
|
||||
showitems = mc.ListItems()
|
||||
for title,subtitle,desc,chanid,airdate,starttime,endtime,watched,ref in episodes:
|
||||
#recording = self.recs[ref]
|
||||
|
||||
# Filter the Episodes
|
||||
if theFilter == "Watched" and watched == 0:
|
||||
continue
|
||||
elif theFilter == "Unwatched" and watched == 1:
|
||||
continue
|
||||
|
||||
# Create the Item list and populate it
|
||||
showitem = mc.ListItem( mc.ListItem.MEDIA_VIDEO_EPISODE )
|
||||
showitem.SetLabel(subtitle)
|
||||
showitem.SetTitle(subtitle)
|
||||
showitem.SetTVShowTitle(title)
|
||||
showitem.SetDescription(desc)
|
||||
showitem.SetProperty("starttime", starttime)
|
||||
#showitem.SetProperty("ref", ref)
|
||||
|
||||
## Sometimes dates aren't set, so generate one if not.
|
||||
try:
|
||||
date = airdate.split("-")
|
||||
showitem.SetDate(int(date[0]), int(date[1]), int(date[2]))
|
||||
except:
|
||||
showitem.SetDate(2010, 01, 01)
|
||||
|
||||
|
||||
# Determine Stream Method, Generate Proper Path
|
||||
streamMethod = self.config.GetValue("StreamMethod")
|
||||
if streamMethod == "XML":
|
||||
time = starttime.replace("T", "%20")
|
||||
path = "http://" + self.dbconf['DBHostName'] + ":6544/Myth/GetRecording?ChanId=" + chanid + "&StartTime=" + time
|
||||
showitem.SetThumbnail("http://" + self.dbconf['DBHostName'] + ":6544/Myth/GetPreviewImage?ChanId=" + chanid + "&StartTime=" + starttime.replace("T", "%20"))
|
||||
showitem.SetPath(path)
|
||||
elif streamMethod == "SMB":
|
||||
time = starttime.replace("T", "").replace("-", "").replace(":", "").replace(" ","")
|
||||
path = "smb://" + self.config.GetValue("smb.username") + ":" + self.config.GetValue("smb.password") + "@" + self.dbconf["DBHostName"] + "/" + self.config.GetValue("smb.share") + "/" + chanid + "_" + time + ".mpg"
|
||||
showitem.SetPath(path)
|
||||
#showitem.AddAlternativePath("XML Source", "http://" + self.dbconf['DBHostName'] + ":6544/Myth/GetRecording?ChanId=" + chanid + "&StartTime=" + starttime.replace("T", "%20"), )
|
||||
|
||||
self.log("def(LoadShowRecordings): Thumbnail: " + showitem.GetThumbnail())
|
||||
self.log("def(LoadShowRecordings): Path: " + showitem.GetPath())
|
||||
|
||||
showitems.append(showitem)
|
||||
|
||||
mc.GetWindow(14002).GetList(2040).SetItems(showitems)
|
||||
|
||||
self.log("def(LoadShowRecordings): End ===========================================================")
|
||||
|
||||
|
||||
"""
|
||||
SetSortableOptions - Setup the show options; sort by, sort direction, watched vs unwatched
|
||||
"""
|
||||
def SetSortableOptions(self):
|
||||
self.log("def(SetSortableOptions): Start =========================================================")
|
||||
|
||||
## Setup Sortable Field Options
|
||||
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.GetWindow(14002).GetList(2051).SetItems(mc.ListItems())
|
||||
mc.GetWindow(14002).GetList(2051).SetItems(items)
|
||||
|
||||
## Setup Sortable Direction Options
|
||||
sortableby = ['Ascending', 'Descending']
|
||||
items = mc.ListItems()
|
||||
for sorttype in sortableby:
|
||||
item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN )
|
||||
item.SetLabel(sorttype)
|
||||
items.append(item)
|
||||
mc.GetWindow(14002).GetList(2061).SetItems(mc.ListItems())
|
||||
mc.GetWindow(14002).GetList(2061).SetItems(items)
|
||||
|
||||
## Setup Filter Options
|
||||
filters = ['All', 'Watched', 'Unwatched']
|
||||
items = mc.ListItems()
|
||||
for single_filter in filters:
|
||||
item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN )
|
||||
item.SetLabel(single_filter)
|
||||
items.append(item)
|
||||
mc.GetWindow(14002).GetList(2082).SetItems(mc.ListItems())
|
||||
mc.GetWindow(14002).GetList(2082).SetItems(items)
|
||||
|
||||
## Set the currently selected option in each category
|
||||
mc.GetWindow(14002).GetList(2051).SetSelected(sortable.index(self.config.GetValue("SortBy")), True)
|
||||
mc.GetWindow(14002).GetList(2061).SetSelected(sortableby.index(self.config.GetValue("SortDir")), True)
|
||||
mc.GetWindow(14002).GetList(2082).SetSelected(filters.index(self.config.GetValue("Filter")), True)
|
||||
|
||||
self.log("def(SetSortableOptions): End ===========================================================")
|
||||
|
||||
|
||||
"""
|
||||
SetSeriesDetails - Get Show Series Information
|
||||
|
||||
TODO -- rewrite this entire function
|
||||
"""
|
||||
def SetSeriesDetails(self, title, seriesid):
|
||||
self.log("def(SetSeriesDetails): Start =========================================================")
|
||||
sg = mc.Http()
|
||||
sg.SetUserAgent(self.userAgent + " " + self.version)
|
||||
html = sg.Get("http://thetvdb.com/api/" + self.tvdb_apikey + "/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(title)
|
||||
item.SetTitle(title)
|
||||
try:
|
||||
item.SetDescription(overview[0])
|
||||
item.SetProperty("description", overview[0])
|
||||
except:
|
||||
item.SetDescription("No Description")
|
||||
item.SetProperty("description", "No Description")
|
||||
|
||||
try:
|
||||
item.SetThumbnail("http://www.thetvdb.com/banners/" + poster[0])
|
||||
except:
|
||||
item.SetThumbnail("mb_poster_error.png")
|
||||
|
||||
items.append(item)
|
||||
|
||||
mc.GetWindow(14002).GetList(2070).SetItems(items)
|
||||
self.log("def(SetSeriesDetails): End ===========================================================")
|
||||
|
||||
|
||||
"""
|
||||
FilterEpisodes - Filter the list of Episodes (All, Watched, Unwatched)
|
||||
"""
|
||||
def FilterEpisodes(self):
|
||||
self.log("def(FilterEpisodes): Start =========================================================")
|
||||
|
||||
## Figure out how we need to filter
|
||||
FilterItems = FilterItemNumber = mc.GetWindow(14002).GetList(2082).GetSelected()
|
||||
mc.GetWindow(14002).GetList(2082).UnselectAll()
|
||||
mc.GetWindow(14002).GetList(2082).SetSelected(mc.GetWindow(14002).GetList(2082).GetFocusedItem(), True)
|
||||
|
||||
## Update the Filter Criteria
|
||||
self.config.SetValue("Filter", mc.GetWindow(14002).GetList(2082).GetItem(mc.GetWindow(14002).GetList(2082).GetFocusedItem()).GetLabel())
|
||||
|
||||
## Now that the filter has changed, reload the list of episodes
|
||||
self.LoadShowRecordings(self.config.GetValue("CurrentShowTitle"))
|
||||
self.log("def(FilterEpisodes): End ===========================================================")
|
||||
|
||||
|
||||
"""
|
||||
SortBy - Set the Field that the List of Episodes is Sorted By
|
||||
"""
|
||||
def SortBy(self):
|
||||
self.log("def(SortBy): Start =========================================================")
|
||||
|
||||
## Figure out how we need to Sort
|
||||
sortByItems = sortByItemNumber = mc.GetWindow(14002).GetList(2051).GetSelected()
|
||||
mc.GetWindow(14002).GetList(2051).UnselectAll()
|
||||
mc.GetWindow(14002).GetList(2051).SetSelected(mc.GetWindow(14002).GetList(2051).GetFocusedItem(), True)
|
||||
|
||||
## Update the Sort Criteria
|
||||
self.config.SetValue("SortBy", mc.GetWindow(14002).GetList(2051).GetItem(mc.GetWindow(14002).GetList(2051).GetFocusedItem()).GetLabel())
|
||||
|
||||
self.log("def(SortBy): Direction: " + self.config.GetValue("SortBy"))
|
||||
|
||||
## Now that we have updated the Sort Criteria, reload the shows
|
||||
self.LoadShowRecordings(self.config.GetValue("CurrentShowTitle"))
|
||||
self.log("def(SortBy): End ===========================================================")
|
||||
|
||||
"""
|
||||
SortDir - Set the Direction that the List of Episodes is Sorted By
|
||||
"""
|
||||
def SortDir(self):
|
||||
self.log("def(SortDir): Start =========================================================")
|
||||
|
||||
## Figure out how we need to Sort
|
||||
sortDirectionItems = sortDirectionItemNumber = mc.GetWindow(14002).GetList(2061).GetSelected()
|
||||
mc.GetWindow(14002).GetList(2061).UnselectAll()
|
||||
mc.GetWindow(14002).GetList(2061).SetSelected(mc.GetWindow(14002).GetList(2061).GetFocusedItem(), True)
|
||||
|
||||
## Update the Sort Criteria
|
||||
self.config.SetValue("SortDir", mc.GetWindow(14002).GetList(2061).GetItem(mc.GetWindow(14002).GetList(2061).GetFocusedItem()).GetLabel())
|
||||
|
||||
self.log("def(SortDir): Direction: " + self.config.GetValue("SortDir"))
|
||||
|
||||
## Now that we have updated the Sort Criteria, reload the shows
|
||||
self.LoadShowRecordings(self.config.GetValue("CurrentShowTitle"))
|
||||
self.log("def(SortDir): End =========================================================")
|
||||
|
||||
|
||||
"""
|
||||
LoadSettings - Stuff that needs to be executed when settings window is loaded
|
||||
"""
|
||||
def LoadSettings(self):
|
||||
self.log("def(LoadSettings): Start =========================================================")
|
||||
|
||||
## Grab the current StreamMethod
|
||||
streamMethod = self.config.GetValue("StreamMethod")
|
||||
|
||||
self.log("def(LoadSettings): Stream Method: " + streamMethod)
|
||||
|
||||
# Grab and Set the Database Information
|
||||
if self.config.GetValue("dbconn"):
|
||||
dbconf = eval(self.config.GetValue("dbconn"))
|
||||
mc.GetWindow(14004).GetEdit(1042).SetText(dbconf['DBHostName'])
|
||||
mc.GetWindow(14004).GetEdit(1043).SetText(dbconf['DBUserName'])
|
||||
mc.GetWindow(14004).GetEdit(1044).SetText(dbconf['DBPassword'])
|
||||
mc.GetWindow(14004).GetEdit(1045).SetText(dbconf['DBName'])
|
||||
mc.GetWindow(14004).GetControl(1032).SetFocus()
|
||||
else:
|
||||
mc.GetWindow(14004).GetControl(1042).SetFocus()
|
||||
|
||||
# Setup Stream Methods for user to choose
|
||||
methods = ['XML', 'SMB']
|
||||
items = mc.ListItems()
|
||||
for method in methods:
|
||||
item = mc.ListItem( mc.ListItem.MEDIA_UNKNOWN )
|
||||
item.SetLabel(method)
|
||||
items.append(item)
|
||||
mc.GetWindow(14004).GetList(1022).SetItems(items)
|
||||
mc.GetWindow(14004).GetList(1022).SetSelected(methods.index(streamMethod), True)
|
||||
|
||||
# Depending on StreamMethod Enable Options
|
||||
if not streamMethod or streamMethod == "XML":
|
||||
mc.GetWindow(14004).GetControl(1032).SetEnabled(False)
|
||||
mc.GetWindow(14004).GetControl(1033).SetEnabled(False)
|
||||
mc.GetWindow(14004).GetControl(1034).SetEnabled(False)
|
||||
else:
|
||||
if not self.config.GetValue("smb.username"):
|
||||
self.config.SetValue("smb.username", "guest")
|
||||
if not self.config.GetValue("smb.password"):
|
||||
self.config.SetValue("smb.password", "guest")
|
||||
|
||||
self.log("def(LoadSettings): smb.share: " + self.config.GetValue("smb.share"))
|
||||
self.log("def(LoadSettings): smb.username: " + self.config.GetValue("smb.username"))
|
||||
self.log("def(LoadSettings): smb.password: " + self.config.GetValue("smb.password"))
|
||||
|
||||
## Since the Stream Method is SMB enable controls for setting info
|
||||
mc.GetWindow(14004).GetControl(1032).SetEnabled(True)
|
||||
mc.GetWindow(14004).GetControl(1033).SetEnabled(True)
|
||||
mc.GetWindow(14004).GetControl(1034).SetEnabled(True)
|
||||
|
||||
## Update the fields with current SMB settings.
|
||||
mc.GetWindow(14004).GetEdit(1032).SetText(self.config.GetValue("smb.share"))
|
||||
mc.GetWindow(14004).GetEdit(1033).SetText(self.config.GetValue("smb.username"))
|
||||
mc.GetWindow(14004).GetEdit(1034).SetText(self.config.GetValue("smb.password"))
|
||||
|
||||
self.log("def(LoadSettings): End ===========================================================")
|
||||
|
||||
|
||||
"""
|
||||
SetStreamMethod - Change the Streaming Method
|
||||
"""
|
||||
def SetStreamMethod(self):
|
||||
self.log("def(SetStreamMethod): Start =========================================================")
|
||||
|
||||
## Figure out what Stream Method the user has selected
|
||||
streamMethodItems = streamMethodItemNumber = mc.GetWindow(14004).GetList(1022).GetSelected()
|
||||
mc.GetWindow(14004).GetList(1022).UnselectAll()
|
||||
mc.GetWindow(14004).GetList(1022).SetSelected(mc.GetWindow(14004).GetList(1022).GetFocusedItem(), True)
|
||||
streamMethod = mc.GetWindow(14004).GetList(1022).GetItem(mc.GetWindow(14004).GetList(1022).GetFocusedItem()).GetLabel()
|
||||
|
||||
## Disabled some UI pieces depending on stream method
|
||||
if not streamMethod or streamMethod == "XML":
|
||||
mc.GetWindow(14004).GetControl(1032).SetEnabled(False)
|
||||
mc.GetWindow(14004).GetControl(1033).SetEnabled(False)
|
||||
mc.GetWindow(14004).GetControl(1034).SetEnabled(False)
|
||||
else:
|
||||
mc.GetWindow(14004).GetControl(1032).SetEnabled(True)
|
||||
mc.GetWindow(14004).GetControl(1033).SetEnabled(True)
|
||||
mc.GetWindow(14004).GetControl(1034).SetEnabled(True)
|
||||
|
||||
## Save the Stream Method
|
||||
self.config.SetValue("StreamMethod", streamMethod)
|
||||
|
||||
## Notify the User
|
||||
mc.ShowDialogNotification("Stream Method Changed to " + streamMethod)
|
||||
|
||||
self.log("def(SetStreamMethod): Stream Method Changed to -- " + streamMethod)
|
||||
self.log("def(SetStreamMethod): End =========================================================")
|
||||
|
||||
|
||||
"""
|
||||
SaveDbSettings - Save Database Settings
|
||||
"""
|
||||
def SaveDbSettings(self):
|
||||
self.log("def(SaveDbSettings): Start =========================================================")
|
||||
|
||||
dbconf = {}
|
||||
dbconf['DBHostName'] = mc.GetWindow(14004).GetEdit(1042).GetText()
|
||||
dbconf['DBUserName'] = mc.GetWindow(14004).GetEdit(1043).GetText()
|
||||
dbconf['DBPassword'] = mc.GetWindow(14004).GetEdit(1044).GetText()
|
||||
dbconf['DBName'] = mc.GetWindow(14004).GetEdit(1045).GetText()
|
||||
|
||||
self.config.SetValue("dbconn", str(dbconf))
|
||||
|
||||
## Notify the user that the changes have been saved.
|
||||
mc.ShowDialogNotification("Database Settings Saved")
|
||||
|
||||
self.log("def(SaveDbSettings): End ===========================================================")
|
||||
|
||||
|
||||
"""
|
||||
TestDbSettings - Test Database Settings
|
||||
"""
|
||||
def TestDbSettings(self):
|
||||
self.log("def(TestDbSettings): Start =========================================================")
|
||||
self.config.SetValue("loadingsettings", "true")
|
||||
mc.GetWindow(14004).GetLabel(9002).SetLabel("Attempting Database Connection ...")
|
||||
|
||||
dbconf = {}
|
||||
dbconf['DBHostName'] = mc.GetWindow(14004).GetEdit(1042).GetText()
|
||||
dbconf['DBUserName'] = mc.GetWindow(14004).GetEdit(1043).GetText()
|
||||
dbconf['DBPassword'] = mc.GetWindow(14004).GetEdit(1044).GetText()
|
||||
dbconf['DBName'] = mc.GetWindow(14004).GetEdit(1045).GetText()
|
||||
|
||||
try:
|
||||
self.log("def(TestDbSettings): Attempting Database Connection ...")
|
||||
mythtv.MythDB(**dbconf)
|
||||
except MythError, e:
|
||||
self.log("def(TestDbSettings): Error: " + e.message)
|
||||
mc.ShowDialogNotification("Failed to connect to the MythTV Backend")
|
||||
else:
|
||||
self.SaveDbSettings()
|
||||
mc.ShowDialogNotification("Connection to MythTV Backend Success. Settings Saved.")
|
||||
|
||||
self.config.Reset("loadingsettings")
|
||||
self.log("def(TestDbSettings): End ===========================================================")
|
||||
|
||||
|
||||
"""
|
||||
SaveSMBSettings - Saves the SMB settings the user inputted.
|
||||
"""
|
||||
def SaveSMBSettings(self):
|
||||
self.log("def(SetStreamMethod): Start =========================================================")
|
||||
|
||||
## Save SMB settings the user inputted
|
||||
self.config.SetValue("smb.share", mc.GetWindow(14004).GetEdit(1032).GetText())
|
||||
self.config.SetValue("smb.username", mc.GetWindow(14004).GetEdit(1033).GetText())
|
||||
self.config.SetValue("smb.password", mc.GetWindow(14004).GetEdit(1034).GetText())
|
||||
|
||||
## Notify the user that the changes have been saved.
|
||||
mc.ShowDialogNotification("SMB Share Settings Saved")
|
||||
|
||||
self.log("def(SetStreamMethod): End ===========================================================")
|
||||
|
||||
|
||||
"""
|
||||
SettingsInit - Init function for Settings Window
|
||||
"""
|
||||
def SettingsInit(self):
|
||||
self.log("def(SettingsInit): Start =========================================================")
|
||||
self.config.SetValue("loadingsettings", "true")
|
||||
if not self.config.GetValue("dbconn"):
|
||||
mc.ShowDialogOk("MythBoxee", "Welcome to MythBoxee! Looks like this is the first time you have run this app. Please fill out all the settings and you'll be on your way to using this app.")
|
||||
response = mc.ShowDialogConfirm("MythBoxee", "Do you know what your Security Pin is for your MythTV Backend? If not, we'll try the default, if that fails, you'll need to fill your database information in manually.", "No", "Yes")
|
||||
if response:
|
||||
pin = mc.ShowDialogKeyboard("Security Pin", "", True)
|
||||
self.config.SetValue("pin", str(pin))
|
||||
else:
|
||||
pin = 0000
|
||||
self.config.SetValue("pin", str(pin))
|
||||
|
||||
mc.GetWindow(14004).GetLabel(9002).SetLabel("Connecting to Backend ...")
|
||||
if self.DiscoverBackend() == False:
|
||||
mc.ShowDialogOk("MythBoxee", "Unfortunately MythBoxee wasn't able to auto-discover your MythTV backend and database credentials. Please enter them in manually.")
|
||||
mc.GetWindow(14004).GetLabel(9002).SetLabel("LOADING...")
|
||||
self.LoadSettings()
|
||||
else:
|
||||
mc.ShowDialogOk("MythBoxee", "MythBoxee auto-discovered your MythTV backend. Enjoy your recordings!")
|
||||
self.config.Reset("app.lastruntime")
|
||||
mc.CloseWindow()
|
||||
else:
|
||||
self.LoadSettings()
|
||||
self.config.Reset("loadingsettings")
|
||||
self.log("def(SettingsInit): End ===========================================================")
|
||||
|
||||
|
||||
"""
|
||||
StatusInit -- Function called when Status Window is opened.
|
||||
|
||||
This function pulls status information from the MythTV backend and displays it to the user.
|
||||
Status information includes load, uptime, free space, upcoming recordings, guide data, etc ...
|
||||
"""
|
||||
def StatusInit(self):
|
||||
self.log("def(StatusInit): Start =========================================================")
|
||||
self.config.SetValue("loadingstatus", "true")
|
||||
|
||||
uptime = self.be.getUptime()
|
||||
load = self.be.getLoad()
|
||||
freespace = self.be.getFreeSpace()
|
||||
guidedata = self.be.getLastGuideData()
|
||||
isRecording = self.be.isRecording(1)
|
||||
freespacesummary = self.be.getFreeSpaceSummary()
|
||||
recorders = self.be.getRecorderList()
|
||||
upcoming = self.be.getUpcomingRecordings()
|
||||
|
||||
self.log("def(StatusInit): Recorders: " + str(recorders))
|
||||
self.log("def(StatusInit): Uptime: " + str(uptime))
|
||||
self.log("def(StatusInit): Load: " + str(load))
|
||||
self.log("def(StatusInit): Free Space: " + str(freespace))
|
||||
self.log("def(StatusInit): Guide Data: " + str(guidedata))
|
||||
self.log("def(StatusInit): Summary: " + str(freespacesummary))
|
||||
self.log("def(StatusInit): Upcoming: " + str(upcoming))
|
||||
self.log("def(StatusInit): Recording: " + str(isRecording))
|
||||
|
||||
self.config.Reset("loadingstatus")
|
||||
self.log("def(StatusInit): End ===========================================================")
|
||||
|
||||
|
||||
"""
|
||||
log - logging function mainly for debugging
|
||||
"""
|
||||
def log(self, message):
|
||||
if self.logLevel == 3:
|
||||
mc.ShowDialogNotification(message)
|
||||
|
||||
if self.logLevel >= 2:
|
||||
mc.LogDebug(">>> MythBoxee: " + message)
|
||||
|
||||
if self.logLevel == 1:
|
||||
mc.LogInfo(">>> MythBoxee: " + message)
|
||||
print ">>> MythBoxee: " + message
|
|
@ -0,0 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Contains any static and global variables for MythTV Python Bindings
|
||||
"""
|
||||
|
||||
OWN_VERSION = (0,23,1,0)
|
||||
SCHEMA_VERSION = 1254
|
||||
MVSCHEMA_VERSION = 1032
|
||||
NVSCHEMA_VERSION = 1004
|
||||
PROTO_VERSION = 56
|
||||
PROGRAM_FIELDS = 47
|
||||
BACKEND_SEP = '[]:[]'
|
|
@ -0,0 +1,54 @@
|
|||
#!/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)
|
||||
|
||||
__version__ = OWN_VERSION
|
||||
#MythStatic.mysqldb = MySQLdb.__version__
|
||||
|
||||
if __name__ == '__main__':
|
||||
banner = 'MythTV Python interactive shell.'
|
||||
import code
|
||||
try:
|
||||
import readline, rlcompleter
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
readline.parse_and_bind("tab: complete")
|
||||
banner += ' TAB completion available.'
|
||||
namespace = globals().copy()
|
||||
namespace.update(locals())
|
||||
code.InteractiveConsole(namespace).interact(banner)
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
# ----------------------
|
||||
# Name: tmdb_exceptions - Custom exceptions used or raised by tmdb_api
|
||||
# Python Script
|
||||
# Author: dbr/Ben modified by R.D. Vaughan
|
||||
# Purpose: Custom exceptions used or raised by tmdb_api
|
||||
#
|
||||
# License:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
#-------------------------------------
|
||||
__title__ ="tmdb_exceptions - Custom exceptions used or raised by tmdb_api";
|
||||
__author__="dbr/Ben modified by R.D. Vaughan"
|
||||
__version__="v0.1.4"
|
||||
# 0.1.0 Initial development
|
||||
# 0.1.1 Alpha Release
|
||||
# 0.1.2 Release bump - no changes to this code
|
||||
# 0.1.3 Release bump - no changes to this code
|
||||
# 0.1.4 Release bump - no changes to this code
|
||||
|
||||
__all__ = ["TmdBaseError", "TmdHttpError", "TmdXmlError", "TmdbUiAbort", "TmdbMovieOrPersonNotFound", ]
|
||||
|
||||
# Start of code used to access themoviedb.org api
|
||||
class TmdBaseError(Exception):
|
||||
pass
|
||||
|
||||
class TmdHttpError(TmdBaseError):
|
||||
def __repr__(self): # Display the type of error
|
||||
return None
|
||||
# end __repr__
|
||||
|
||||
class TmdXmlError(TmdBaseError):
|
||||
def __repr__(self): # Display the type of error
|
||||
return None
|
||||
# end __repr__
|
||||
|
||||
class TmdbMovieOrPersonNotFound(TmdBaseError):
|
||||
def __repr__(self):
|
||||
return None
|
||||
# end __repr__
|
||||
|
||||
class TmdbUiAbort(TmdBaseError):
|
||||
def __repr__(self):
|
||||
return None
|
||||
# end __repr__
|
|
@ -0,0 +1,266 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
# ----------------------
|
||||
# Name: tmdb_ui.py Is a simple console user interface for the tmdb_api. The interface is used selection
|
||||
# of a movie from themoviesdb.org.
|
||||
# Python Script
|
||||
# Author: dbr/Ben modified by R.D. Vaughan
|
||||
# Purpose: Console interface for selecting a movie from themoviedb.org. This interface would be invoked when
|
||||
# an exact match is not found and the invoking script has specified the "interactive = True" when
|
||||
# creating an instance of MovieDb().
|
||||
#
|
||||
# License:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
#-------------------------------------
|
||||
__title__ ="tmdb_ui - Is a simple console user interface for the tmdb_api. The interface is used selection of a movie oe person from themoviesdb.org.";
|
||||
__author__="dbr/Ben modified by R.D. Vaughan"
|
||||
__purpose__='''Console interface for selecting a movie from themoviedb.org. This interface would be invoked when an exact match is not found and the invoking script has specified the "interactive = True" when creating an instance of MovieDb().
|
||||
'''
|
||||
__version__="v0.1.5"
|
||||
# 0.1.0 Initial development
|
||||
# 0.1.1 Alpha Release
|
||||
# 0.1.2 Release bump - no changes to this code
|
||||
# 0.1.3 Release bump - no changes to this code
|
||||
# 0.1.4 Release bump - no changes to this code
|
||||
# 0.1.5 Modified automated selection when there was only one search result. It was causing
|
||||
# too many incorrect selections.
|
||||
# Also removed a duplicate initialization of the version number.
|
||||
|
||||
|
||||
"""Contains included user interfaces for tmdb movie/person selection.
|
||||
|
||||
A UI is a callback. A class, it's __init__ function takes two arguments:
|
||||
|
||||
- config, which is the tmdb config dict, setup in tmdb_api.py
|
||||
- log, which is tmdb's logger instance (which uses the logging module). You can
|
||||
call log.info() log.warning() etc
|
||||
|
||||
It pass a dictionary "allElements", this is passed a list of dicts, each dict
|
||||
contains at least the keys "name" (human readable movie name), and "id" (the movies/person)
|
||||
ID as on themoviedb.org). for movies only if the key 'released' is included the year will be added
|
||||
to the movie title like 'Avatar (2009)' all other keys will be ignored.
|
||||
For example:
|
||||
|
||||
[{'name': u'Avatar', 'id': u'19995', 'released': '2009-12-25'},
|
||||
{'name': u'Avatar - Sequel', 'id': u'73181'}]
|
||||
OR
|
||||
[{'name': u'Tom Cruise', 'id': u'500'},
|
||||
{'name': u'Cruise Moylan', 'id': u'77716'}]
|
||||
|
||||
|
||||
The "selectMovieOrPerson" method must return the appropriate dict, or it can raise
|
||||
TmdbUiAbort (if the selection is aborted), TmdbMovieOrPersonNotFound (if the movie
|
||||
or person cannot be found).
|
||||
|
||||
A simple example callback, which returns a random movie:
|
||||
|
||||
>>> import random
|
||||
>>> from tmdb_api_ui import BaseUI
|
||||
>>> class RandomUI(BaseUI):
|
||||
... def selectMovieOrPerson(self, allElements):
|
||||
... import random
|
||||
... return random.choice(allElements)
|
||||
|
||||
Then to use it..
|
||||
|
||||
>>> from tmdb_api import MovieDb
|
||||
>>> t = MovieDb(custom_ui = RandomUI)
|
||||
>>> random_matching_movie = t.searchTitle('Avatar')
|
||||
>>> type(random_matching_movie)
|
||||
[{"Avatar",'19995'}]
|
||||
"""
|
||||
|
||||
import os, struct, sys, string
|
||||
from tmdb_exceptions import TmdbUiAbort
|
||||
|
||||
class OutStreamEncoder(object):
|
||||
"""Wraps a stream with an encoder"""
|
||||
def __init__(self, outstream, encoding=None):
|
||||
self.out = outstream
|
||||
if not encoding:
|
||||
self.encoding = sys.getfilesystemencoding()
|
||||
else:
|
||||
self.encoding = encoding
|
||||
|
||||
def write(self, obj):
|
||||
"""Wraps the output stream, encoding Unicode strings with the specified encoding"""
|
||||
if isinstance(obj, unicode):
|
||||
try:
|
||||
self.out.write(obj.encode(self.encoding))
|
||||
except IOError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
self.out.write(obj)
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
def __getattr__(self, attr):
|
||||
"""Delegate everything but write to the stream"""
|
||||
return getattr(self.out, attr)
|
||||
sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
|
||||
sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
|
||||
|
||||
# Two routines used for movie title search and matching
|
||||
def is_punct_char(char):
|
||||
'''check if char is punctuation char
|
||||
return True if char is punctuation
|
||||
return False if char is not punctuation
|
||||
'''
|
||||
return char in string.punctuation
|
||||
|
||||
def is_not_punct_char(char):
|
||||
'''check if char is not punctuation char
|
||||
return True if char is not punctuation
|
||||
return False if chaar is punctuation
|
||||
'''
|
||||
return not is_punct_char(char)
|
||||
|
||||
|
||||
def makeDict(movieORperson):
|
||||
'''Make a dictionary out of the chosen movie data
|
||||
return a dictionary of the movie and ancillary data
|
||||
'''
|
||||
selection = {}
|
||||
for key in movieORperson.keys():
|
||||
selection[key] = movieORperson[key]
|
||||
return selection
|
||||
# end makeDict()
|
||||
|
||||
|
||||
class BaseUI:
|
||||
"""Default non-interactive UI, which auto-selects first results
|
||||
"""
|
||||
def __init__(self, config, log, searchTerm=None):
|
||||
self.config = config
|
||||
self.log = log
|
||||
self.searchTerm = searchTerm
|
||||
|
||||
def selectMovieOrPerson(self, allElements):
|
||||
return makeDict([allElements[0]])
|
||||
|
||||
|
||||
class ConsoleUI(BaseUI):
|
||||
"""Interactively allows the user to select a movie or person from a console based UI
|
||||
"""
|
||||
|
||||
def removeCommonWords(self, title):
|
||||
'''Remove common words from a title
|
||||
return title striped of common words
|
||||
'''
|
||||
if not title:
|
||||
return u' '
|
||||
wordList = [u'the ', u'a ', u' '] # common word list. Leave double space as the last value.
|
||||
title = title.lower()
|
||||
for word in wordList:
|
||||
title = title.replace(word, u'')
|
||||
if not title:
|
||||
return u' '
|
||||
title = title.strip()
|
||||
return filter(is_not_punct_char, title)
|
||||
# end removeCommonWords()
|
||||
|
||||
|
||||
def _displayMovie(self, allElements):
|
||||
"""Helper function, lists movies or people with corresponding ID
|
||||
"""
|
||||
print u"themoviedb.org Search Results:"
|
||||
for i in range(len(allElements[:15])): # list first 15 search results
|
||||
i_show = i + 1 # Start at more human readable number 1 (not 0)
|
||||
self.log.debug('Showing allElements[%s] = %s)' % (i_show, allElements[i]))
|
||||
if allElements[i]['name'] == u'User choses to ignore video':
|
||||
print u"% 2s -> %s # %s" % (
|
||||
i_show,
|
||||
'99999999', "Set this video to be ignored by Jamu with a reference number of '99999999'"
|
||||
)
|
||||
continue
|
||||
if not allElements[i].has_key('released'):
|
||||
title = allElements[i]['name']
|
||||
elif len(allElements[i]['released']) > 3:
|
||||
title = u"%s (%s)" % (allElements[i]['name'], allElements[i]['released'][:4])
|
||||
else:
|
||||
title = allElements[i]['name']
|
||||
if allElements[i]['url'].find('/person/') > -1:
|
||||
format = u"%2s -> %-30s # http://www.themoviedb.org/person/%s"
|
||||
else:
|
||||
format = u"%2s -> %-50s # http://www.themoviedb.org/movie/%s"
|
||||
print format % (
|
||||
i_show,
|
||||
title,
|
||||
allElements[i]['id']
|
||||
)
|
||||
print u"Direct search of themoviedb.org # http://themoviedb.org/"
|
||||
# end _displayMovie()
|
||||
|
||||
def selectMovieOrPerson(self, allElements):
|
||||
if allElements[0]['url'].find('/movie/') > -1:
|
||||
morp = u'movie'
|
||||
else:
|
||||
morp = u'person'
|
||||
# Add the ability to select the skip inetref of '99999999' for movies only
|
||||
if morp == u'movie':
|
||||
allElements.append( {'id': '99999999', 'name': u'User choses to ignore video'} )
|
||||
|
||||
self._displayMovie(allElements)
|
||||
|
||||
refsize = 5 # The number of digits required in a TMDB number is directly entered
|
||||
if len(allElements) == 2 and morp == u'movie':
|
||||
data = makeDict(allElements[0])
|
||||
if self.removeCommonWords(data['name']) == self.removeCommonWords(self.searchTerm) and data.has_key('released'):
|
||||
# Single result, return it!
|
||||
print u"Automatically selecting only result"
|
||||
return [data]
|
||||
|
||||
if len(allElements) == 1 and morp == u'person':
|
||||
data = makeDict(allElements[0])
|
||||
if self.removeCommonWords(data['name']) == self.removeCommonWords(self.searchTerm):
|
||||
# Single result, return it!
|
||||
print u"Automatically selecting only result"
|
||||
return [data]
|
||||
|
||||
if self.config['select_first'] is True:
|
||||
print u"Automatically returning first search result"
|
||||
return [makeDict(allElements[0])]
|
||||
|
||||
while True: # return breaks this loop
|
||||
try:
|
||||
print u'Enter choice:\n("Enter" key equals first selection (1)) or input a zero padded 5 digit %s TMDB id number, ? for help):' % morp
|
||||
ans = raw_input()
|
||||
except KeyboardInterrupt:
|
||||
raise TmdbUiAbort("User aborted (^c keyboard interupt)")
|
||||
except EOFError:
|
||||
raise TmdbUiAbort("User aborted (EOF received)")
|
||||
|
||||
self.log.debug(u'Got choice of: %s' % (ans))
|
||||
try:
|
||||
if ans == '': # Enter pressed which equates to the first selection
|
||||
selected_id = 0
|
||||
else:
|
||||
if int(ans) == 0:
|
||||
raise ValueError
|
||||
selected_id = int(ans) - 1 # The human entered 1 as first result, not zero
|
||||
except ValueError: # Input was not number
|
||||
if ans == "q":
|
||||
self.log.debug(u'Got quit command (q)')
|
||||
raise TmdbUiAbort("User aborted ('q' quit command)")
|
||||
elif ans == "?":
|
||||
print u"## Help"
|
||||
print u"# Enter the number that corresponds to the correct movie."
|
||||
print u"# Paste a TMDB %s ID number (pad with leading zeros to make 5 digits) from themoviedb.org and hit 'Enter'" % morp
|
||||
print u"# ? - this help"
|
||||
print u"# q - abort/skip movie selection"
|
||||
else:
|
||||
print '! Unknown/Invalid keypress "%s"\n' % (ans)
|
||||
self.log.debug('Unknown keypress %s' % (ans))
|
||||
else:
|
||||
self.log.debug('Trying to return ID: %d' % (selected_id))
|
||||
try:
|
||||
data = makeDict(allElements[selected_id])
|
||||
data['userResponse'] = u'User selected'
|
||||
return [data]
|
||||
except IndexError:
|
||||
if len(ans) == refsize:
|
||||
return [{'userResponse': u'User input', 'id': u'%d' % int(ans)}]
|
||||
#end try
|
||||
#end while not valid_input
|
||||
# end selectMovieOrPerson()
|
|
@ -0,0 +1,232 @@
|
|||
#!/usr/bin/env python
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben
|
||||
#project:tvdb_api
|
||||
#repository:http://github.com/dbr/tvdb_api
|
||||
#license:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
|
||||
"""
|
||||
urllib2 caching handler
|
||||
Modified from http://code.activestate.com/recipes/491261/
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
|
||||
__author__ = "dbr/Ben"
|
||||
__version__ = "1.2.1"
|
||||
|
||||
import os
|
||||
import time
|
||||
import errno
|
||||
import httplib
|
||||
import urllib2
|
||||
import StringIO
|
||||
from hashlib import md5
|
||||
from threading import RLock
|
||||
|
||||
cache_lock = RLock()
|
||||
|
||||
def locked_function(origfunc):
|
||||
"""Decorator to execute function under lock"""
|
||||
def wrapped(*args, **kwargs):
|
||||
cache_lock.acquire()
|
||||
try:
|
||||
return origfunc(*args, **kwargs)
|
||||
finally:
|
||||
cache_lock.release()
|
||||
return wrapped
|
||||
|
||||
def calculate_cache_path(cache_location, url):
|
||||
"""Checks if [cache_location]/[hash_of_url].headers and .body exist
|
||||
"""
|
||||
thumb = md5(url).hexdigest()
|
||||
header = os.path.join(cache_location, thumb + ".headers")
|
||||
body = os.path.join(cache_location, thumb + ".body")
|
||||
return header, body
|
||||
|
||||
def check_cache_time(path, max_age):
|
||||
"""Checks if a file has been created/modified in the [last max_age] seconds.
|
||||
False means the file is too old (or doesn't exist), True means it is
|
||||
up-to-date and valid"""
|
||||
if not os.path.isfile(path):
|
||||
return False
|
||||
cache_modified_time = os.stat(path).st_mtime
|
||||
time_now = time.time()
|
||||
if cache_modified_time < time_now - max_age:
|
||||
# Cache is old
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
@locked_function
|
||||
def exists_in_cache(cache_location, url, max_age):
|
||||
"""Returns if header AND body cache file exist (and are up-to-date)"""
|
||||
hpath, bpath = calculate_cache_path(cache_location, url)
|
||||
if os.path.exists(hpath) and os.path.exists(bpath):
|
||||
return(
|
||||
check_cache_time(hpath, max_age)
|
||||
and check_cache_time(bpath, max_age)
|
||||
)
|
||||
else:
|
||||
# File does not exist
|
||||
return False
|
||||
|
||||
@locked_function
|
||||
def store_in_cache(cache_location, url, response):
|
||||
"""Tries to store response in cache."""
|
||||
hpath, bpath = calculate_cache_path(cache_location, url)
|
||||
try:
|
||||
outf = open(hpath, "w")
|
||||
headers = str(response.info())
|
||||
outf.write(headers)
|
||||
outf.close()
|
||||
|
||||
outf = open(bpath, "w")
|
||||
outf.write(response.read())
|
||||
outf.close()
|
||||
except IOError:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
class CacheHandler(urllib2.BaseHandler):
|
||||
"""Stores responses in a persistant on-disk cache.
|
||||
|
||||
If a subsequent GET request is made for the same URL, the stored
|
||||
response is returned, saving time, resources and bandwidth
|
||||
"""
|
||||
@locked_function
|
||||
def __init__(self, cache_location, max_age = 21600):
|
||||
"""The location of the cache directory"""
|
||||
self.max_age = max_age
|
||||
self.cache_location = cache_location
|
||||
if not os.path.exists(self.cache_location):
|
||||
try:
|
||||
os.mkdir(self.cache_location)
|
||||
except OSError, e:
|
||||
if e.errno == errno.EEXIST and os.path.isdir(self.cache_location):
|
||||
# File exists, and it's a directory,
|
||||
# another process beat us to creating this dir, that's OK.
|
||||
pass
|
||||
else:
|
||||
# Our target dir is already a file, or different error,
|
||||
# relay the error!
|
||||
raise OSError(e)
|
||||
|
||||
def default_open(self, request):
|
||||
"""Handles GET requests, if the response is cached it returns it
|
||||
"""
|
||||
if request.get_method() is not "GET":
|
||||
return None # let the next handler try to handle the request
|
||||
|
||||
if exists_in_cache(
|
||||
self.cache_location, request.get_full_url(), self.max_age
|
||||
):
|
||||
return CachedResponse(
|
||||
self.cache_location,
|
||||
request.get_full_url(),
|
||||
set_cache_header = True
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
def http_response(self, request, response):
|
||||
"""Gets a HTTP response, if it was a GET request and the status code
|
||||
starts with 2 (200 OK etc) it caches it and returns a CachedResponse
|
||||
"""
|
||||
if (request.get_method() == "GET"
|
||||
and str(response.code).startswith("2")
|
||||
):
|
||||
if 'x-local-cache' not in response.info():
|
||||
# Response is not cached
|
||||
set_cache_header = store_in_cache(
|
||||
self.cache_location,
|
||||
request.get_full_url(),
|
||||
response
|
||||
)
|
||||
else:
|
||||
set_cache_header = True
|
||||
#end if x-cache in response
|
||||
|
||||
return CachedResponse(
|
||||
self.cache_location,
|
||||
request.get_full_url(),
|
||||
set_cache_header = set_cache_header
|
||||
)
|
||||
else:
|
||||
return response
|
||||
|
||||
class CachedResponse(StringIO.StringIO):
|
||||
"""An urllib2.response-like object for cached responses.
|
||||
|
||||
To determine if a response is cached or coming directly from
|
||||
the network, check the x-local-cache header rather than the object type.
|
||||
"""
|
||||
|
||||
@locked_function
|
||||
def __init__(self, cache_location, url, set_cache_header=True):
|
||||
self.cache_location = cache_location
|
||||
hpath, bpath = calculate_cache_path(cache_location, url)
|
||||
|
||||
StringIO.StringIO.__init__(self, file(bpath).read())
|
||||
|
||||
self.url = url
|
||||
self.code = 200
|
||||
self.msg = "OK"
|
||||
headerbuf = file(hpath).read()
|
||||
if set_cache_header:
|
||||
headerbuf += "x-local-cache: %s\r\n" % (bpath)
|
||||
self.headers = httplib.HTTPMessage(StringIO.StringIO(headerbuf))
|
||||
|
||||
def info(self):
|
||||
"""Returns headers
|
||||
"""
|
||||
return self.headers
|
||||
|
||||
def geturl(self):
|
||||
"""Returns original URL
|
||||
"""
|
||||
return self.url
|
||||
|
||||
@locked_function
|
||||
def recache(self):
|
||||
new_request = urllib2.urlopen(self.url)
|
||||
set_cache_header = store_in_cache(
|
||||
self.cache_location,
|
||||
new_request.url,
|
||||
new_request
|
||||
)
|
||||
CachedResponse.__init__(self, self.cache_location, self.url, True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
def main():
|
||||
"""Quick test/example of CacheHandler"""
|
||||
opener = urllib2.build_opener(CacheHandler("/tmp/"))
|
||||
response = opener.open("http://google.com")
|
||||
print response.headers
|
||||
print "Response:", response.read()
|
||||
|
||||
response.recache()
|
||||
print response.headers
|
||||
print "After recache:", response.read()
|
||||
|
||||
# Test usage in threads
|
||||
from threading import Thread
|
||||
class CacheThreadTest(Thread):
|
||||
lastdata = None
|
||||
def run(self):
|
||||
req = opener.open("http://google.com")
|
||||
newdata = req.read()
|
||||
if self.lastdata is None:
|
||||
self.lastdata = newdata
|
||||
assert self.lastdata == newdata, "Data was not consistent, uhoh"
|
||||
req.recache()
|
||||
threads = [CacheThreadTest() for x in range(50)]
|
||||
print "Starting threads"
|
||||
[t.start() for t in threads]
|
||||
print "..done"
|
||||
print "Joining threads"
|
||||
[t.join() for t in threads]
|
||||
print "..done"
|
||||
main()
|
|
@ -0,0 +1,109 @@
|
|||
[File ttvdb-example.conf]
|
||||
#-------------------------------------
|
||||
# Name: ttvdb-example.conf
|
||||
# Project: ttvdb
|
||||
# Configuration file
|
||||
# Author: R.D. Vaughan
|
||||
# Version: 0.1.0 - Initial alpha release
|
||||
# Version: 0.8.9 - version changed to match the current ttvdb release number
|
||||
# Version: 0.9.5 - Changed regex pattern strings to support multi-langiage file names
|
||||
#
|
||||
# License:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
#-------------------------------------
|
||||
#
|
||||
# START season name overrides section --------------------------------------------------------------
|
||||
#####
|
||||
# PURPOSE: TV program sources such as Schedule Direct series names do not always match the series
|
||||
# names on thetvdb.com. This section allow you to override series names to reduce the
|
||||
# need for manual interaction and more accurate matching with thetvdb.com TV series wiki.
|
||||
#
|
||||
# FORMAT:
|
||||
# Any line starting with "#" is treated as a comment
|
||||
# Any blank line is ignored
|
||||
# All other lines must have two fields in this specific order:
|
||||
# 1st) The series name or the %TITLE% field as passed by MythTV and a trailing ':' character
|
||||
# 2nd) The thetvdb.com series id (SID) as specified for the series on thetvdb.com wiki. This will
|
||||
# override the series name to eliminate the need for manual interaction.
|
||||
# NOTE: Included here. but disabled are examples of current (2009) TV shows that Schedule Direct
|
||||
# has different TV series names then what is on thetvdb.com wiki. If you are searching for
|
||||
# series based on the Schedule Direct names as MythTV does then the wrong TV series will be
|
||||
# identified. You can pick the correct series by using interactive mode but this does not
|
||||
# help with unattended processing. This section allows you to specify the correct TV series
|
||||
# on thetvbd.com despite the name in Schedule Direct. Luckily there are not many TV series
|
||||
# that require overrides.
|
||||
#####
|
||||
[series_name_override]
|
||||
# Specify recorded "Life On Mars" shows as the US version
|
||||
#Life on Mars:82289
|
||||
# For overnight episode updates when a filename is used
|
||||
#Life on Mars (US):82289
|
||||
|
||||
# Specify recorded "Eleventh Hour" shows as the US version
|
||||
#Eleventh Hour:83066
|
||||
# For overnight episode updates when a filename is used
|
||||
#Eleventh Hour (US):83066
|
||||
|
||||
# Specify recorded "Frontline" or "Frontline/World" shows as the "Frontline PBS" version
|
||||
#Frontline/World:80646
|
||||
#Frontline:80646
|
||||
|
||||
# Specify recorded "The Beast" shows are the 2009 version
|
||||
#The Beast:83805
|
||||
# For overnight episode updates when a filename is used
|
||||
#The Beast (2009):83805
|
||||
|
||||
# Specify recorded "Castle" shows are the 2009 version
|
||||
#Castle:83462
|
||||
# For overnight episode updates when a filename is used
|
||||
#Castle (2009):83462
|
||||
|
||||
# Specify recorded "Battlestar Galactica" shows are the 2003 version
|
||||
#Battlestar Galactica:73545
|
||||
# For overnight episode updates when a filename is used
|
||||
#Battlestar Galactica (2003):73545
|
||||
# END season name overrides section --------------------------------------------------------------
|
||||
|
||||
# START episode name massaging section --------------------------------------------------------------
|
||||
#####
|
||||
# PURPOSE: TV program sources such as Schedule Direct episode names do not always match the episode
|
||||
# names on thetvdb.com. This section allow you to massage episode names to reduce the
|
||||
# need for manual interaction and more accurate matching with thetvdb.com TV series wiki.
|
||||
# Alter the episode names for a series to reduce the need for manual interaction
|
||||
# and more accurate matching with thetvdb.com TV series wiki. See example below.
|
||||
#
|
||||
# FORMAT:
|
||||
# All lines must in the following format:
|
||||
# 1st) The series name or the %TITLE% field as passed by MythTV and a trailing ':' character
|
||||
# 2nd) Pairs of values separated by commas. The first value is the search text to match to text within
|
||||
# the episode name such as the %SUBTITLE% field passed by MythTV and the text to replace the
|
||||
# matched searched text. If the replacement text contains a space then surround that text with
|
||||
# the '"' double quotation characters.
|
||||
# E.g. "24": PM," PM", AM, " AM" will turn the episode name
|
||||
# "Day 7: 11:00AM to 12:00PM" into "Day 7: 11:00 AM to 12:00 PM"
|
||||
#
|
||||
#####
|
||||
[ep_name_massage]
|
||||
#24: PM," PM", AM, " AM",M-,"M - "
|
||||
# END episode name massaging section --------------------------------------------------------------
|
||||
|
||||
# START regex section------------------------------------------------------------------------------
|
||||
#####
|
||||
# NOTE: If you do not understand regex expressions DO NOT USE this section.
|
||||
# PURPOSE: This section adds regex strings used to parse video file names when extracting
|
||||
# the following: series name, season number, episode number. Essential when downloading
|
||||
# metadata from mythtvfrontend->mythvideo when using ttvdb. You only need to add a regex
|
||||
# string if ttvdb cannot extract the required information from your video file names.
|
||||
# NOTE: ANY % percent sign in the expression must be doubled (e.g. a % must be changed to %% )
|
||||
# NOTE: The key value (e.g. "regex##") must be unique for each value.
|
||||
# NOTE: DO NOT surround the regex pattern string with the traditional ''' three single quotes
|
||||
#
|
||||
# "regex01" is an example video file name "foo_S01_12" where:
|
||||
# series name is "foo", season number starts at "S" and episode number starts after '_'
|
||||
# foo_[s01]_[e01]
|
||||
#####
|
||||
[regex]
|
||||
# foo_S01_12
|
||||
regex01: ^(.+?)[ \._\-][Ss]([0-9]+)_([0-9]+)[^\\/]*$
|
||||
# END regex section------------------------------------------------------------------------------
|
||||
|
|
@ -0,0 +1,805 @@
|
|||
#!/usr/bin/env python
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben
|
||||
#project:tvdb_api
|
||||
#repository:http://github.com/dbr/tvdb_api
|
||||
#license:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
|
||||
"""Simple-to-use Python interface to The TVDB's API (www.thetvdb.com)
|
||||
|
||||
Example usage:
|
||||
|
||||
>>> from tvdb_api import Tvdb
|
||||
>>> t = Tvdb()
|
||||
>>> t['Lost'][4][11]['episodename']
|
||||
u'Cabin Fever'
|
||||
"""
|
||||
__author__ = "dbr/Ben"
|
||||
__version__ = "1.2.1"
|
||||
|
||||
import os
|
||||
import sys
|
||||
import urllib
|
||||
import urllib2
|
||||
import tempfile
|
||||
import logging
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as ElementTree
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ElementTree
|
||||
|
||||
from cache import CacheHandler
|
||||
|
||||
from tvdb_ui import BaseUI, ConsoleUI
|
||||
from tvdb_exceptions import (tvdb_error, tvdb_userabort, tvdb_shownotfound,
|
||||
tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound)
|
||||
|
||||
class ShowContainer(dict):
|
||||
"""Simple dict that holds a series of Show instances
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Show(dict):
|
||||
"""Holds a dict of seasons, and show data.
|
||||
"""
|
||||
def __init__(self):
|
||||
dict.__init__(self)
|
||||
self.data = {}
|
||||
|
||||
def __repr__(self):
|
||||
return "<Show %s (containing %s seasons)>" % (
|
||||
self.data.get(u'seriesname', 'instance'),
|
||||
len(self)
|
||||
)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key in self:
|
||||
# Key is an episode, return it
|
||||
return dict.__getitem__(self, key)
|
||||
|
||||
if key in self.data:
|
||||
# Non-numeric request is for show-data
|
||||
return dict.__getitem__(self.data, key)
|
||||
|
||||
# Data wasn't found, raise appropriate error
|
||||
if isinstance(key, int) or key.isdigit():
|
||||
# Episode number x was not found
|
||||
raise tvdb_seasonnotfound("Could not find season %s" % (repr(key)))
|
||||
else:
|
||||
# If it's not numeric, it must be an attribute name, which
|
||||
# doesn't exist, so attribute error.
|
||||
raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key)))
|
||||
|
||||
def search(self, term = None, key = None):
|
||||
"""
|
||||
Search all episodes in show. Can search all data, or a specific key (for
|
||||
example, episodename)
|
||||
|
||||
Always returns an array (can be empty). First index contains the first
|
||||
match, and so on.
|
||||
|
||||
Each array index is an Episode() instance, so doing
|
||||
search_results[0]['episodename'] will retrieve the episode name of the
|
||||
first match.
|
||||
|
||||
Search terms are converted to lower case (unicode) strings.
|
||||
|
||||
# Examples
|
||||
|
||||
These examples assume t is an instance of Tvdb():
|
||||
|
||||
>>> t = Tvdb()
|
||||
>>>
|
||||
|
||||
To search for all episodes of Scrubs with a bit of data
|
||||
containing "my first day":
|
||||
|
||||
>>> t['Scrubs'].search("my first day")
|
||||
[<Episode 01x01 - My First Day>]
|
||||
>>>
|
||||
|
||||
Search for "My Name Is Earl" episode named "Faked His Own Death":
|
||||
|
||||
>>> t['My Name Is Earl'].search('Faked His Own Death', key = 'episodename')
|
||||
[<Episode 01x04 - Faked His Own Death>]
|
||||
>>>
|
||||
|
||||
To search Scrubs for all episodes with "mentor" in the episode name:
|
||||
|
||||
>>> t['scrubs'].search('mentor', key = 'episodename')
|
||||
[<Episode 01x02 - My Mentor>, <Episode 03x15 - My Tormented Mentor>]
|
||||
>>>
|
||||
|
||||
# Using search results
|
||||
|
||||
>>> results = t['Scrubs'].search("my first")
|
||||
>>> print results[0]['episodename']
|
||||
My First Day
|
||||
>>> for x in results: print x['episodename']
|
||||
My First Day
|
||||
My First Step
|
||||
My First Kill
|
||||
>>>
|
||||
"""
|
||||
results = []
|
||||
for cur_season in self.values():
|
||||
searchresult = cur_season.search(term = term, key = key)
|
||||
if len(searchresult) != 0:
|
||||
results.extend(searchresult)
|
||||
#end for cur_season
|
||||
return results
|
||||
|
||||
|
||||
class Season(dict):
|
||||
def __repr__(self):
|
||||
return "<Season instance (containing %s episodes)>" % (
|
||||
len(self.keys())
|
||||
)
|
||||
|
||||
def __getitem__(self, episode_number):
|
||||
if episode_number not in self:
|
||||
raise tvdb_episodenotfound("Could not find episode %s" % (repr(episode_number)))
|
||||
else:
|
||||
return dict.__getitem__(self, episode_number)
|
||||
|
||||
def search(self, term = None, key = None):
|
||||
"""Search all episodes in season, returns a list of matching Episode
|
||||
instances.
|
||||
|
||||
>>> t = Tvdb()
|
||||
>>> t['scrubs'][1].search('first day')
|
||||
[<Episode 01x01 - My First Day>]
|
||||
>>>
|
||||
|
||||
See Show.search documentation for further information on search
|
||||
"""
|
||||
results = []
|
||||
for ep in self.values():
|
||||
searchresult = ep.search(term = term, key = key)
|
||||
if searchresult is not None:
|
||||
results.append(
|
||||
searchresult
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
class Episode(dict):
|
||||
def __repr__(self):
|
||||
seasno = int(self.get(u'seasonnumber', 0))
|
||||
epno = int(self.get(u'episodenumber', 0))
|
||||
epname = self.get(u'episodename')
|
||||
if epname is not None:
|
||||
return "<Episode %02dx%02d - %s>" % (seasno, epno, epname)
|
||||
else:
|
||||
return "<Episode %02dx%02d>" % (seasno, epno)
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return dict.__getitem__(self, key)
|
||||
except KeyError:
|
||||
raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key)))
|
||||
|
||||
def search(self, term = None, key = None):
|
||||
"""Search episode data for term, if it matches, return the Episode (self).
|
||||
The key parameter can be used to limit the search to a specific element,
|
||||
for example, episodename.
|
||||
|
||||
This primarily for use use by Show.search and Season.search. See
|
||||
Show.search for further information on search
|
||||
|
||||
Simple example:
|
||||
|
||||
>>> e = Episode()
|
||||
>>> e['episodename'] = "An Example"
|
||||
>>> e.search("examp")
|
||||
<Episode 00x00 - An Example>
|
||||
>>>
|
||||
|
||||
Limiting by key:
|
||||
|
||||
>>> e.search("examp", key = "episodename")
|
||||
<Episode 00x00 - An Example>
|
||||
>>>
|
||||
"""
|
||||
if term == None:
|
||||
raise TypeError("must supply string to search for (contents)")
|
||||
|
||||
term = unicode(term).lower()
|
||||
for cur_key, cur_value in self.items():
|
||||
cur_key, cur_value = unicode(cur_key).lower(), unicode(cur_value).lower()
|
||||
if key is not None and cur_key != key:
|
||||
# Do not search this key
|
||||
continue
|
||||
if cur_value.find( unicode(term).lower() ) > -1:
|
||||
return self
|
||||
#end if cur_value.find()
|
||||
#end for cur_key, cur_value
|
||||
|
||||
|
||||
class Actors(list):
|
||||
"""Holds all Actor instances for a show
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Actor(dict):
|
||||
"""Represents a single actor. Should contain..
|
||||
|
||||
id,
|
||||
image,
|
||||
name,
|
||||
role,
|
||||
sortorder
|
||||
"""
|
||||
def __repr__(self):
|
||||
return "<Actor \"%s\">" % (self.get("name"))
|
||||
|
||||
|
||||
class Tvdb:
|
||||
"""Create easy-to-use interface to name of season/episode name
|
||||
>>> t = Tvdb()
|
||||
>>> t['Scrubs'][1][24]['episodename']
|
||||
u'My Last Day'
|
||||
"""
|
||||
def __init__(self,
|
||||
interactive = False,
|
||||
select_first = False,
|
||||
debug = False,
|
||||
cache = True,
|
||||
banners = False,
|
||||
actors = False,
|
||||
custom_ui = None,
|
||||
language = None,
|
||||
search_all_languages = False,
|
||||
apikey = None):
|
||||
"""interactive (True/False):
|
||||
When True, uses built-in console UI is used to select the correct show.
|
||||
When False, the first search result is used.
|
||||
|
||||
select_first (True/False):
|
||||
Automatically selects the first series search result (rather
|
||||
than showing the user a list of more than one series).
|
||||
Is overridden by interactive = False, or specifying a custom_ui
|
||||
|
||||
debug (True/False):
|
||||
shows verbose debugging information
|
||||
|
||||
cache (True/False/str/unicode):
|
||||
Retrieved XML are persisted to to disc. If true, stores in tvdb_api
|
||||
folder under your systems TEMP_DIR, if set to str/unicode instance it
|
||||
will use this as the cache location. If False, disables caching.
|
||||
|
||||
banners (True/False):
|
||||
Retrieves the banners for a show. These are accessed
|
||||
via the _banners key of a Show(), for example:
|
||||
|
||||
>>> Tvdb(banners=True)['scrubs']['_banners'].keys()
|
||||
['fanart', 'poster', 'series', 'season']
|
||||
|
||||
actors (True/False):
|
||||
Retrieves a list of the actors for a show. These are accessed
|
||||
via the _actors key of a Show(), for example:
|
||||
|
||||
>>> t = Tvdb(actors=True)
|
||||
>>> t['scrubs']['_actors'][0]['name']
|
||||
u'Zach Braff'
|
||||
|
||||
custom_ui (tvdb_ui.BaseUI subclass):
|
||||
A callable subclass of tvdb_ui.BaseUI (overrides interactive option)
|
||||
|
||||
language (2 character language abbreviation):
|
||||
The language of the returned data. Is also the language search
|
||||
uses. Default is "en" (English). For full list, run..
|
||||
|
||||
>>> Tvdb().config['valid_languages'] #doctest: +ELLIPSIS
|
||||
['da', 'fi', 'nl', ...]
|
||||
|
||||
search_all_languages (True/False):
|
||||
By default, Tvdb will only search in the language specified using
|
||||
the language option. When this is True, it will search for the
|
||||
show in and language
|
||||
|
||||
apikey (str/unicode):
|
||||
Override the default thetvdb.com API key. By default it will use
|
||||
tvdb_api's own key (fine for small scripts), but you can use your
|
||||
own key if desired - this is recommended if you are embedding
|
||||
tvdb_api in a larger application)
|
||||
See http://thetvdb.com/?tab=apiregister to get your own key
|
||||
"""
|
||||
self.shows = ShowContainer() # Holds all Show classes
|
||||
self.corrections = {} # Holds show-name to show_id mapping
|
||||
|
||||
self.config = {}
|
||||
|
||||
if apikey is not None:
|
||||
self.config['apikey'] = apikey
|
||||
else:
|
||||
self.config['apikey'] = "0629B785CE550C8D" # tvdb_api's API key
|
||||
|
||||
self.config['debug_enabled'] = debug # show debugging messages
|
||||
|
||||
self.config['custom_ui'] = custom_ui
|
||||
|
||||
self.config['interactive'] = interactive # prompt for correct series?
|
||||
|
||||
self.config['select_first'] = select_first
|
||||
|
||||
self.config['search_all_languages'] = search_all_languages
|
||||
|
||||
if cache is True:
|
||||
self.config['cache_enabled'] = True
|
||||
self.config['cache_location'] = self._getTempDir()
|
||||
elif isinstance(cache, basestring):
|
||||
self.config['cache_enabled'] = True
|
||||
self.config['cache_location'] = cache
|
||||
else:
|
||||
self.config['cache_enabled'] = False
|
||||
|
||||
if self.config['cache_enabled']:
|
||||
self.urlopener = urllib2.build_opener(
|
||||
CacheHandler(self.config['cache_location'])
|
||||
)
|
||||
else:
|
||||
self.urlopener = urllib2.build_opener()
|
||||
|
||||
self.config['banners_enabled'] = banners
|
||||
self.config['actors_enabled'] = actors
|
||||
|
||||
self.log = self._initLogger() # Setups the logger (self.log.debug() etc)
|
||||
|
||||
# List of language from http://www.thetvdb.com/api/0629B785CE550C8D/languages.xml
|
||||
# Hard-coded here as it is realtively static, and saves another HTTP request, as
|
||||
# recommended on http://thetvdb.com/wiki/index.php/API:languages.xml
|
||||
self.config['valid_languages'] = [
|
||||
"da", "fi", "nl", "de", "it", "es", "fr","pl", "hu","el","tr",
|
||||
"ru","he","ja","pt","zh","cs","sl", "hr","ko","en","sv","no"
|
||||
]
|
||||
|
||||
if language is None:
|
||||
self.config['language'] = "en"
|
||||
elif language not in self.config['valid_languages']:
|
||||
raise ValueError("Invalid language %s, options are: %s" % (
|
||||
language, self.config['valid_languages']
|
||||
))
|
||||
else:
|
||||
self.config['language'] = language
|
||||
|
||||
# The following url_ configs are based of the
|
||||
# http://thetvdb.com/wiki/index.php/Programmers_API
|
||||
self.config['base_url'] = "http://www.thetvdb.com"
|
||||
|
||||
if self.config['search_all_languages']:
|
||||
self.config['url_getSeries'] = "%(base_url)s/api/GetSeries.php?seriesname=%%s&language=all" % self.config
|
||||
else:
|
||||
self.config['url_getSeries'] = "%(base_url)s/api/GetSeries.php?seriesname=%%s&language=%(language)s" % self.config
|
||||
|
||||
self.config['url_epInfo'] = "%(base_url)s/api/%(apikey)s/series/%%s/all/%(language)s.xml" % self.config
|
||||
|
||||
self.config['url_seriesInfo'] = "%(base_url)s/api/%(apikey)s/series/%%s/%(language)s.xml" % self.config
|
||||
self.config['url_actorsInfo'] = "%(base_url)s/api/%(apikey)s/series/%%s/actors.xml" % self.config
|
||||
|
||||
self.config['url_seriesBanner'] = "%(base_url)s/api/%(apikey)s/series/%%s/banners.xml" % self.config
|
||||
self.config['url_artworkPrefix'] = "%(base_url)s/banners/%%s" % self.config
|
||||
|
||||
#end __init__
|
||||
|
||||
def _initLogger(self):
|
||||
"""Setups a logger using the logging module, returns a log object
|
||||
"""
|
||||
logger = logging.getLogger("tvdb")
|
||||
formatter = logging.Formatter('%(asctime)s) %(levelname)s %(message)s')
|
||||
|
||||
hdlr = logging.StreamHandler(sys.stdout)
|
||||
|
||||
hdlr.setFormatter(formatter)
|
||||
logger.addHandler(hdlr)
|
||||
|
||||
if self.config['debug_enabled']:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
logger.setLevel(logging.WARNING)
|
||||
return logger
|
||||
#end initLogger
|
||||
|
||||
def _getTempDir(self):
|
||||
"""Returns the [system temp dir]/tvdb_api
|
||||
"""
|
||||
return os.path.join(tempfile.gettempdir(), "tvdb_api")
|
||||
|
||||
def _loadUrl(self, url, recache = False):
|
||||
try:
|
||||
self.log.debug("Retrieving URL %s" % url)
|
||||
resp = self.urlopener.open(url)
|
||||
if 'x-local-cache' in resp.headers:
|
||||
self.log.debug("URL %s was cached in %s" % (
|
||||
url,
|
||||
resp.headers['x-local-cache'])
|
||||
)
|
||||
if recache:
|
||||
self.log.debug("Attempting to recache %s" % url)
|
||||
resp.recache()
|
||||
except urllib2.URLError, errormsg:
|
||||
raise tvdb_error("Could not connect to server: %s" % (errormsg))
|
||||
#end try
|
||||
|
||||
return resp.read()
|
||||
|
||||
def _getetsrc(self, url):
|
||||
"""Loads a URL sing caching, returns an ElementTree of the source
|
||||
"""
|
||||
src = self._loadUrl(url)
|
||||
try:
|
||||
return ElementTree.fromstring(src)
|
||||
except SyntaxError:
|
||||
src = self._loadUrl(url, recache=True)
|
||||
try:
|
||||
return ElementTree.fromstring(src)
|
||||
except SyntaxError, exceptionmsg:
|
||||
errormsg = "There was an error with the XML retrieved from thetvdb.com:\n%s" % (
|
||||
exceptionmsg
|
||||
)
|
||||
|
||||
if self.config['cache_enabled']:
|
||||
errormsg += "\nFirst try emptying the cache folder at..\n%s" % (
|
||||
self.config['cache_location']
|
||||
)
|
||||
|
||||
errormsg += "\nIf this does not resolve the issue, please try again later. If the error persists, report a bug on"
|
||||
errormsg += "\nhttp://dbr.lighthouseapp.com/projects/13342-tvdb_api/overview\n"
|
||||
raise tvdb_error(errormsg)
|
||||
#end _getetsrc
|
||||
|
||||
def _setItem(self, sid, seas, ep, attrib, value):
|
||||
"""Creates a new episode, creating Show(), Season() and
|
||||
Episode()s as required. Called by _getShowData to populute
|
||||
|
||||
Since the nice-to-use tvdb[1][24]['name] interface
|
||||
makes it impossible to do tvdb[1][24]['name] = "name"
|
||||
and still be capable of checking if an episode exists
|
||||
so we can raise tvdb_shownotfound, we have a slightly
|
||||
less pretty method of setting items.. but since the API
|
||||
is supposed to be read-only, this is the best way to
|
||||
do it!
|
||||
The problem is that calling tvdb[1][24]['episodename'] = "name"
|
||||
calls __getitem__ on tvdb[1], there is no way to check if
|
||||
tvdb.__dict__ should have a key "1" before we auto-create it
|
||||
"""
|
||||
if sid not in self.shows:
|
||||
self.shows[sid] = Show()
|
||||
if seas not in self.shows[sid]:
|
||||
self.shows[sid][seas] = Season()
|
||||
if ep not in self.shows[sid][seas]:
|
||||
self.shows[sid][seas][ep] = Episode()
|
||||
self.shows[sid][seas][ep][attrib] = value
|
||||
#end _set_item
|
||||
|
||||
def _setShowData(self, sid, key, value):
|
||||
"""Sets self.shows[sid] to a new Show instance, or sets the data
|
||||
"""
|
||||
if sid not in self.shows:
|
||||
self.shows[sid] = Show()
|
||||
self.shows[sid].data[key] = value
|
||||
|
||||
def _cleanData(self, data):
|
||||
"""Cleans up strings returned by TheTVDB.com
|
||||
|
||||
Issues corrected:
|
||||
- Replaces & with &
|
||||
- Trailing whitespace
|
||||
"""
|
||||
data = data.replace(u"&", u"&")
|
||||
data = data.strip()
|
||||
return data
|
||||
#end _cleanData
|
||||
|
||||
def _getSeries(self, series):
|
||||
"""This searches TheTVDB.com for the series name,
|
||||
If a custom_ui UI is configured, it uses this to select the correct
|
||||
series. If not, and interactive == True, ConsoleUI is used, if not
|
||||
BaseUI is used to select the first result.
|
||||
"""
|
||||
series = urllib.quote(series.encode("utf-8"))
|
||||
self.log.debug("Searching for show %s" % series)
|
||||
seriesEt = self._getetsrc(self.config['url_getSeries'] % (series))
|
||||
allSeries = []
|
||||
for series in seriesEt:
|
||||
sn = series.find('SeriesName')
|
||||
value = self._cleanData(sn.text)
|
||||
cur_sid = series.find('id').text
|
||||
self.log.debug('Found series %s (id: %s)' % (value, cur_sid))
|
||||
allSeries.append( {'sid':cur_sid, 'name':value} )
|
||||
#end for series
|
||||
|
||||
if len(allSeries) == 0:
|
||||
self.log.debug('Series result returned zero')
|
||||
raise tvdb_shownotfound("Show-name search returned zero results (cannot find show on TVDB)")
|
||||
|
||||
if self.config['custom_ui'] is not None:
|
||||
self.log.debug("Using custom UI %s" % (repr(self.config['custom_ui'])))
|
||||
ui = self.config['custom_ui'](config = self.config, log = self.log)
|
||||
else:
|
||||
if not self.config['interactive']:
|
||||
self.log.debug('Auto-selecting first search result using BaseUI')
|
||||
ui = BaseUI(config = self.config, log = self.log)
|
||||
else:
|
||||
self.log.debug('Interactivily selecting show using ConsoleUI')
|
||||
ui = ConsoleUI(config = self.config, log = self.log)
|
||||
#end if config['interactive]
|
||||
#end if custom_ui != None
|
||||
|
||||
return ui.selectSeries(allSeries)
|
||||
|
||||
#end _getSeries
|
||||
|
||||
def _parseBanners(self, sid):
|
||||
"""Parses banners XML, from
|
||||
http://www.thetvdb.com/api/[APIKEY]/series/[SERIES ID]/banners.xml
|
||||
|
||||
Banners are retrieved using t['show name]['_banners'], for example:
|
||||
|
||||
>>> t = Tvdb(banners = True)
|
||||
>>> t['scrubs']['_banners'].keys()
|
||||
['fanart', 'poster', 'series', 'season']
|
||||
>>> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath']
|
||||
'http://www.thetvdb.com/banners/posters/76156-2.jpg'
|
||||
>>>
|
||||
|
||||
Any key starting with an underscore has been processed (not the raw
|
||||
data from the XML)
|
||||
|
||||
This interface will be improved in future versions.
|
||||
"""
|
||||
self.log.debug('Getting season banners for %s' % (sid))
|
||||
bannersEt = self._getetsrc( self.config['url_seriesBanner'] % (sid) )
|
||||
banners = {}
|
||||
for cur_banner in bannersEt.findall('Banner'):
|
||||
bid = cur_banner.find('id').text
|
||||
btype = cur_banner.find('BannerType')
|
||||
btype2 = cur_banner.find('BannerType2')
|
||||
if btype is None or btype2 is None:
|
||||
continue
|
||||
btype, btype2 = btype.text, btype2.text
|
||||
if not btype in banners:
|
||||
banners[btype] = {}
|
||||
if not btype2 in banners[btype]:
|
||||
banners[btype][btype2] = {}
|
||||
if not bid in banners[btype][btype2]:
|
||||
banners[btype][btype2][bid] = {}
|
||||
|
||||
self.log.debug("Banner: %s", bid)
|
||||
for cur_element in cur_banner.getchildren():
|
||||
tag = cur_element.tag.lower()
|
||||
value = cur_element.text
|
||||
if tag is None or value is None:
|
||||
continue
|
||||
tag, value = tag.lower(), value.lower()
|
||||
self.log.debug("Banner info: %s = %s" % (tag, value))
|
||||
banners[btype][btype2][bid][tag] = value
|
||||
|
||||
for k, v in banners[btype][btype2][bid].items():
|
||||
if k.endswith("path"):
|
||||
new_key = "_%s" % (k)
|
||||
self.log.debug("Transforming %s to %s" % (k, new_key))
|
||||
new_url = self.config['url_artworkPrefix'] % (v)
|
||||
self.log.debug("New banner URL: %s" % (new_url))
|
||||
banners[btype][btype2][bid][new_key] = new_url
|
||||
|
||||
self._setShowData(sid, "_banners", banners)
|
||||
|
||||
|
||||
# Alternate tvdb_api's method for retrieving graphics URLs but returned as a list that preserves
|
||||
# the user rating order highest rated to lowest rated
|
||||
def ttvdb_parseBanners(self, sid):
|
||||
"""Parses banners XML, from
|
||||
http://www.thetvdb.com/api/[APIKEY]/series/[SERIES ID]/banners.xml
|
||||
|
||||
Banners are retrieved using t['show name]['_banners'], for example:
|
||||
|
||||
>>> t = Tvdb(banners = True)
|
||||
>>> t['scrubs']['_banners'].keys()
|
||||
['fanart', 'poster', 'series', 'season']
|
||||
>>> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath']
|
||||
'http://www.thetvdb.com/banners/posters/76156-2.jpg'
|
||||
>>>
|
||||
|
||||
Any key starting with an underscore has been processed (not the raw
|
||||
data from the XML)
|
||||
|
||||
This interface will be improved in future versions.
|
||||
Changed in this interface is that a list or URLs is created to preserve the user rating order from
|
||||
top rated to lowest rated.
|
||||
"""
|
||||
|
||||
self.log.debug('Getting season banners for %s' % (sid))
|
||||
bannersEt = self._getetsrc( self.config['url_seriesBanner'] % (sid) )
|
||||
banners = {}
|
||||
bid_order = {'fanart': [], 'poster': [], 'series': [], 'season': []}
|
||||
for cur_banner in bannersEt.findall('Banner'):
|
||||
bid = cur_banner.find('id').text
|
||||
btype = cur_banner.find('BannerType')
|
||||
btype2 = cur_banner.find('BannerType2')
|
||||
if btype is None or btype2 is None:
|
||||
continue
|
||||
btype, btype2 = btype.text, btype2.text
|
||||
if not btype in banners:
|
||||
banners[btype] = {}
|
||||
if not btype2 in banners[btype]:
|
||||
banners[btype][btype2] = {}
|
||||
if not bid in banners[btype][btype2]:
|
||||
banners[btype][btype2][bid] = {}
|
||||
if btype in bid_order.keys():
|
||||
if btype2 != u'blank':
|
||||
bid_order[btype].append([bid, btype2])
|
||||
|
||||
self.log.debug("Banner: %s", bid)
|
||||
for cur_element in cur_banner.getchildren():
|
||||
tag = cur_element.tag.lower()
|
||||
value = cur_element.text
|
||||
if tag is None or value is None:
|
||||
continue
|
||||
tag, value = tag.lower(), value.lower()
|
||||
self.log.debug("Banner info: %s = %s" % (tag, value))
|
||||
banners[btype][btype2][bid][tag] = value
|
||||
|
||||
for k, v in banners[btype][btype2][bid].items():
|
||||
if k.endswith("path"):
|
||||
new_key = "_%s" % (k)
|
||||
self.log.debug("Transforming %s to %s" % (k, new_key))
|
||||
new_url = self.config['url_artworkPrefix'] % (v)
|
||||
self.log.debug("New banner URL: %s" % (new_url))
|
||||
banners[btype][btype2][bid][new_key] = new_url
|
||||
|
||||
graphics_in_order = {'fanart': [], 'poster': [], 'series': [], 'season': []}
|
||||
for key in bid_order.keys():
|
||||
for bid in bid_order[key]:
|
||||
graphics_in_order[key].append(banners[key][bid[1]][bid[0]])
|
||||
return graphics_in_order
|
||||
# end ttvdb_parseBanners()
|
||||
|
||||
|
||||
def _parseActors(self, sid):
|
||||
"""Parsers actors XML, from
|
||||
http://www.thetvdb.com/api/[APIKEY]/series/[SERIES ID]/actors.xml
|
||||
|
||||
Actors are retrieved using t['show name]['_actors'], for example:
|
||||
|
||||
>>> t = Tvdb(actors = True)
|
||||
>>> actors = t['scrubs']['_actors']
|
||||
>>> type(actors)
|
||||
<class 'tvdb_api.Actors'>
|
||||
>>> type(actors[0])
|
||||
<class 'tvdb_api.Actor'>
|
||||
>>> actors[0]
|
||||
<Actor "Zach Braff">
|
||||
>>> sorted(actors[0].keys())
|
||||
['id', 'image', 'name', 'role', 'sortorder']
|
||||
>>> actors[0]['name']
|
||||
u'Zach Braff'
|
||||
>>> actors[0]['image']
|
||||
'http://www.thetvdb.com/banners/actors/43640.jpg'
|
||||
|
||||
Any key starting with an underscore has been processed (not the raw
|
||||
data from the XML)
|
||||
"""
|
||||
self.log.debug("Getting actors for %s" % (sid))
|
||||
actorsEt = self._getetsrc(self.config['url_actorsInfo'] % (sid))
|
||||
|
||||
cur_actors = Actors()
|
||||
for curActorItem in actorsEt.findall("Actor"):
|
||||
curActor = Actor()
|
||||
for curInfo in curActorItem:
|
||||
tag = curInfo.tag.lower()
|
||||
value = curInfo.text
|
||||
if value is not None:
|
||||
if tag == "image":
|
||||
value = self.config['url_artworkPrefix'] % (value)
|
||||
else:
|
||||
value = self._cleanData(value)
|
||||
curActor[tag] = value
|
||||
cur_actors.append(curActor)
|
||||
self._setShowData(sid, '_actors', cur_actors)
|
||||
|
||||
def _getShowData(self, sid):
|
||||
"""Takes a series ID, gets the epInfo URL and parses the TVDB
|
||||
XML file into the shows dict in layout:
|
||||
shows[series_id][season_number][episode_number]
|
||||
"""
|
||||
|
||||
# Parse show information
|
||||
self.log.debug('Getting all series data for %s' % (sid))
|
||||
seriesInfoEt = self._getetsrc(self.config['url_seriesInfo'] % (sid))
|
||||
for curInfo in seriesInfoEt.findall("Series")[0]:
|
||||
tag = curInfo.tag.lower()
|
||||
value = curInfo.text
|
||||
|
||||
if value is not None:
|
||||
if tag in ['banner', 'fanart', 'poster']:
|
||||
value = self.config['url_artworkPrefix'] % (value)
|
||||
else:
|
||||
value = self._cleanData(value)
|
||||
|
||||
self._setShowData(sid, tag, value)
|
||||
self.log.debug("Got info: %s = %s" % (tag, value))
|
||||
#end for series
|
||||
|
||||
# Parse banners
|
||||
if self.config['banners_enabled']:
|
||||
self._parseBanners(sid)
|
||||
|
||||
# Parse actors
|
||||
if self.config['actors_enabled']:
|
||||
self._parseActors(sid)
|
||||
|
||||
# Parse episode data
|
||||
self.log.debug('Getting all episodes of %s' % (sid))
|
||||
epsEt = self._getetsrc( self.config['url_epInfo'] % (sid) )
|
||||
|
||||
for cur_ep in epsEt.findall("Episode"):
|
||||
seas_no = int(cur_ep.find('SeasonNumber').text)
|
||||
ep_no = int(cur_ep.find('EpisodeNumber').text)
|
||||
for cur_item in cur_ep.getchildren():
|
||||
tag = cur_item.tag.lower()
|
||||
value = cur_item.text
|
||||
if value is not None:
|
||||
if tag == 'filename':
|
||||
value = self.config['url_artworkPrefix'] % (value)
|
||||
else:
|
||||
value = self._cleanData(value)
|
||||
self._setItem(sid, seas_no, ep_no, tag, value)
|
||||
#end for cur_ep
|
||||
#end _geEps
|
||||
|
||||
def _nameToSid(self, name):
|
||||
"""Takes show name, returns the correct series ID (if the show has
|
||||
already been grabbed), or grabs all episodes and returns
|
||||
the correct SID.
|
||||
"""
|
||||
if name in self.corrections:
|
||||
self.log.debug('Correcting %s to %s' % (name, self.corrections[name]) )
|
||||
sid = self.corrections[name]
|
||||
else:
|
||||
self.log.debug('Getting show %s' % (name))
|
||||
selected_series = self._getSeries( name )
|
||||
sname, sid = selected_series['name'], selected_series['sid']
|
||||
self.log.debug('Got %s, sid %s' % (sname, sid))
|
||||
|
||||
self.corrections[name] = sid
|
||||
self._getShowData(sid)
|
||||
#end if name in self.corrections
|
||||
return sid
|
||||
#end _nameToSid
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Handles tvdb_instance['seriesname'] calls.
|
||||
The dict index should be the show id
|
||||
"""
|
||||
if isinstance(key, (int, long)):
|
||||
# Item is integer, treat as show id
|
||||
if key not in self.shows:
|
||||
self._getShowData(key)
|
||||
return self.shows[key]
|
||||
|
||||
key = key.lower() # make key lower case
|
||||
sid = self._nameToSid(key)
|
||||
self.log.debug('Got series id %s' % (sid))
|
||||
return self.shows[sid]
|
||||
#end __getitem__
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.shows)
|
||||
#end __repr__
|
||||
#end Tvdb
|
||||
|
||||
def main():
|
||||
"""Simple example of using tvdb_api - it just
|
||||
grabs an episode name interactively.
|
||||
"""
|
||||
tvdb_instance = Tvdb(interactive=True, debug=True, cache=False)
|
||||
print tvdb_instance['Lost']['seriesname']
|
||||
print tvdb_instance['Lost'][1][4]['episodename']
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env python
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben
|
||||
#project:tvdb_api
|
||||
#repository:http://github.com/dbr/tvdb_api
|
||||
#license:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
|
||||
"""Custom exceptions used or raised by tvdb_api
|
||||
"""
|
||||
|
||||
__author__ = "dbr/Ben"
|
||||
__version__ = "1.2.1"
|
||||
|
||||
__all__ = ["tvdb_error", "tvdb_userabort", "tvdb_shownotfound",
|
||||
"tvdb_seasonnotfound", "tvdb_episodenotfound", "tvdb_attributenotfound"]
|
||||
|
||||
class tvdb_error(Exception):
|
||||
"""An error with www.thetvdb.com (Cannot connect, for example)
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_userabort(Exception):
|
||||
"""User aborted the interactive selection (via
|
||||
the q command, ^c etc)
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_shownotfound(Exception):
|
||||
"""Show cannot be found on www.thetvdb.com (non-existant show)
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_seasonnotfound(Exception):
|
||||
"""Season cannot be found on www.thetvdb.com
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_episodenotfound(Exception):
|
||||
"""Episode cannot be found on www.thetvdb.com
|
||||
"""
|
||||
pass
|
||||
|
||||
class tvdb_attributenotfound(Exception):
|
||||
"""Raised if an episode does not have the requested
|
||||
attribute (such as a episode name)
|
||||
"""
|
||||
pass
|
|
@ -0,0 +1,124 @@
|
|||
#!/usr/bin/env python
|
||||
#encoding:utf-8
|
||||
#author:dbr/Ben
|
||||
#project:tvdb_api
|
||||
#repository:http://github.com/dbr/tvdb_api
|
||||
#license:Creative Commons GNU GPL v2
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
|
||||
"""Contains included user interfaces for Tvdb show selection.
|
||||
|
||||
A UI is a callback. A class, it's __init__ function takes two arguments:
|
||||
|
||||
- config, which is the Tvdb config dict, setup in tvdb_api.py
|
||||
- log, which is Tvdb's logger instance (which uses the logging module). You can
|
||||
call log.info() log.warning() etc
|
||||
|
||||
It must have a method "selectSeries", this is passed a list of dicts, each dict
|
||||
contains the the keys "name" (human readable show name), and "sid" (the shows
|
||||
ID as on thetvdb.com). For example:
|
||||
|
||||
[{'name': u'Lost', 'sid': u'73739'},
|
||||
{'name': u'Lost Universe', 'sid': u'73181'}]
|
||||
|
||||
The "selectSeries" method must return the appropriate dict, or it can raise
|
||||
tvdb_userabort (if the selection is aborted), tvdb_shownotfound (if the show
|
||||
cannot be found).
|
||||
|
||||
A simple example callback, which returns a random series:
|
||||
|
||||
>>> import random
|
||||
>>> from tvdb_ui import BaseUI
|
||||
>>> class RandomUI(BaseUI):
|
||||
... def selectSeries(self, allSeries):
|
||||
... import random
|
||||
... return random.choice(allSeries)
|
||||
|
||||
Then to use it..
|
||||
|
||||
>>> from tvdb_api import Tvdb
|
||||
>>> t = Tvdb(custom_ui = RandomUI)
|
||||
>>> random_matching_series = t['Lost']
|
||||
>>> type(random_matching_series)
|
||||
<class 'tvdb_api.Show'>
|
||||
"""
|
||||
|
||||
__author__ = "dbr/Ben"
|
||||
__version__ = "1.2.1"
|
||||
|
||||
from tvdb_exceptions import tvdb_userabort
|
||||
|
||||
class BaseUI:
|
||||
"""Default non-interactive UI, which auto-selects first results
|
||||
"""
|
||||
def __init__(self, config, log):
|
||||
self.config = config
|
||||
self.log = log
|
||||
|
||||
def selectSeries(self, allSeries):
|
||||
return allSeries[0]
|
||||
|
||||
|
||||
class ConsoleUI(BaseUI):
|
||||
"""Interactively allows the user to select a show from a console based UI
|
||||
"""
|
||||
|
||||
def _displaySeries(self, allSeries):
|
||||
"""Helper function, lists series with corresponding ID
|
||||
"""
|
||||
print "TVDB Search Results:"
|
||||
for i in range(len(allSeries[:6])): # list first 6 search results
|
||||
i_show = i + 1 # Start at more human readable number 1 (not 0)
|
||||
self.log.debug('Showing allSeries[%s] = %s)' % (i_show, allSeries[i]))
|
||||
print "%s -> %s # http://thetvdb.com/?tab=series&id=%s" % (
|
||||
i_show,
|
||||
allSeries[i]['name'].encode("UTF-8","ignore"),
|
||||
allSeries[i]['sid'].encode("UTF-8","ignore")
|
||||
)
|
||||
|
||||
def selectSeries(self, allSeries):
|
||||
self._displaySeries(allSeries)
|
||||
|
||||
if len(allSeries) == 1:
|
||||
# Single result, return it!
|
||||
print "Automatically selecting only result"
|
||||
return allSeries[0]
|
||||
|
||||
if self.config['select_first'] is True:
|
||||
print "Automatically returning first search result"
|
||||
return allSeries[0]
|
||||
|
||||
while True: # return breaks this loop
|
||||
try:
|
||||
print "Enter choice (first number, ? for help):"
|
||||
ans = raw_input()
|
||||
except KeyboardInterrupt:
|
||||
raise tvdb_userabort("User aborted (^c keyboard interupt)")
|
||||
except EOFError:
|
||||
raise tvdb_userabort("User aborted (EOF received)")
|
||||
|
||||
self.log.debug('Got choice of: %s' % (ans))
|
||||
try:
|
||||
selected_id = int(ans) - 1 # The human entered 1 as first result, not zero
|
||||
except ValueError: # Input was not number
|
||||
if ans == "q":
|
||||
self.log.debug('Got quit command (q)')
|
||||
raise tvdb_userabort("User aborted ('q' quit command)")
|
||||
elif ans == "?":
|
||||
print "## Help"
|
||||
print "# Enter the number that corresponds to the correct show."
|
||||
print "# ? - this help"
|
||||
print "# q - abort tvnamer"
|
||||
else:
|
||||
self.log.debug('Unknown keypress %s' % (ans))
|
||||
else:
|
||||
self.log.debug('Trying to return ID: %d' % (selected_id))
|
||||
try:
|
||||
return allSeries[ selected_id ]
|
||||
except IndexError:
|
||||
self.log.debug('Invalid show number entered!')
|
||||
print "Invalid number (%s) selected!"
|
||||
self._displaySeries(allSeries)
|
||||
#end try
|
||||
#end while not valid_input
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
--- mythtv_old/MythBase.py 2010-07-31 08:46:35.000000000 -0400
|
||||
+++ mythtv/MythBase.py 2010-08-17 21:49:43.000000000 -0400
|
||||
@@ -7,18 +7,15 @@
|
||||
from MythStatic import *
|
||||
|
||||
import os, re, socket, sys, locale, weakref
|
||||
-from xml import *
|
||||
+import xml.etree.cElementTree as etree
|
||||
from datetime import datetime
|
||||
from time import sleep, time
|
||||
from urllib import urlopen
|
||||
from subprocess import Popen
|
||||
from sys import version_info
|
||||
|
||||
-#import MySQLdb, MySQLdb.cursors
|
||||
-#MySQLdb.__version__ = tuple([v for v in MySQLdb.__version__.split('.')])
|
||||
-
|
||||
-import mysql.connector
|
||||
-import mysql.connector.cursor
|
||||
+import MySQLdb, MySQLdb.cursors
|
||||
+MySQLdb.__version__ = tuple([v for v in MySQLdb.__version__.split('.')])
|
||||
|
||||
class DictData( object ):
|
||||
"""
|
||||
@@ -742,19 +739,19 @@
|
||||
del self.hash[index]
|
||||
del self.data[index]
|
||||
|
||||
-class MythDBCursor( mysql.connector.cursor.MySQLCursor ):
|
||||
+class MythDBCursor( MySQLdb.cursors.Cursor ):
|
||||
"""
|
||||
Custom cursor, offering logging and error handling
|
||||
"""
|
||||
def __init__(self, connection):
|
||||
self.log = None
|
||||
- mysql.connector.cursor.MySQLCursor.__init__(self, connection)
|
||||
+ MySQLdb.cursors.Cursor.__init__(self, connection)
|
||||
|
||||
def execute(self, query, args=None):
|
||||
- #if MySQLdb.__version__ >= ('1','2','2'):
|
||||
- # self.connection.ping(True)
|
||||
- #else:
|
||||
- # self.connection.ping()
|
||||
+ if MySQLdb.__version__ >= ('1','2','2'):
|
||||
+ self.connection.ping(True)
|
||||
+ else:
|
||||
+ self.connection.ping()
|
||||
if self.log == None:
|
||||
self.log = MythLog('Python Database Connection')
|
||||
if args:
|
||||
@@ -762,21 +759,21 @@
|
||||
else:
|
||||
self.log(self.log.DATABASE, ' '.join(query.split()))
|
||||
try:
|
||||
- return mysql.connector.cursor.MySQLCursor.execute(self, query, args)
|
||||
+ return MySQLdb.cursors.Cursor.execute(self, query, args)
|
||||
except Exception, e:
|
||||
raise MythDBError(MythDBError.DB_RAW, e.args)
|
||||
|
||||
def executemany(self, query, args):
|
||||
- #if MySQLdb.__version__ >= ('1','2','2'):
|
||||
- # self.connection.ping(True)
|
||||
- #else:
|
||||
- # self.connection.ping()
|
||||
+ if MySQLdb.__version__ >= ('1','2','2'):
|
||||
+ self.connection.ping(True)
|
||||
+ else:
|
||||
+ self.connection.ping()
|
||||
if self.log == None:
|
||||
self.log = MythLog('Python Database Connection')
|
||||
for arg in args:
|
||||
self.log(self.log.DATABASE, ' '.join(query.split()), str(arg))
|
||||
try:
|
||||
- return mysql.connector.cursor.MySQLCursor.executemany(self, query, args)
|
||||
+ return MySQLdb.cursors.Cursor.executemany(self, query, args)
|
||||
except Exception, e:
|
||||
raise MythDBError(MythDBError.DB_RAW, e.args)
|
||||
|
||||
@@ -793,23 +790,22 @@
|
||||
try:
|
||||
self.log(MythLog.DATABASE, "Attempting connection",
|
||||
str(dbconn))
|
||||
- self.db = mysql.connector.connect(user=dbconn['DBUserName'],
|
||||
- host=dbconn['DBHostName'],
|
||||
- password=dbconn['DBPassword'],
|
||||
- db=dbconn['DBName'],
|
||||
- port=dbconn['DBPort'],
|
||||
- use_unicode=True,
|
||||
- charset='utf8')
|
||||
+ self.db = MySQLdb.connect( user= dbconn['DBUserName'],
|
||||
+ host= dbconn['DBHostName'],
|
||||
+ passwd= dbconn['DBPassword'],
|
||||
+ db= dbconn['DBName'],
|
||||
+ port= dbconn['DBPort'],
|
||||
+ use_unicode=True,
|
||||
+ charset='utf8')
|
||||
except:
|
||||
raise MythDBError(MythError.DB_CONNECTION, dbconn)
|
||||
|
||||
def cursor(self, log=None, type=MythDBCursor):
|
||||
- #if MySQLdb.__version__ >= ('1','2','2'):
|
||||
- # self.db.ping(True)
|
||||
- #else:
|
||||
- # self.db.ping()
|
||||
- #c = self.db.cursor(type)
|
||||
- c = self.db.cursor()
|
||||
+ if MySQLdb.__version__ >= ('1','2','2'):
|
||||
+ self.db.ping(True)
|
||||
+ else:
|
||||
+ self.db.ping()
|
||||
+ c = self.db.cursor(type)
|
||||
if log:
|
||||
c.log = log
|
||||
else:
|
||||
@@ -1000,6 +996,7 @@
|
||||
else:
|
||||
# fall back to UPnP
|
||||
dbconn = self._listenUPNP(dbconn['SecurityPin'], 5.0)
|
||||
+
|
||||
# push data to new settings file
|
||||
settings = [dbconn[key] for key in \
|
||||
('SecurityPin','DBHostName','DBUserName',
|
||||
@@ -1449,7 +1446,7 @@
|
||||
ind1 = xmlstr.find('xmlns')
|
||||
ind2 = xmlstr.find('"', ind1+7) + 1
|
||||
xmlstr = xmlstr[:ind1] + xmlstr[ind2:]
|
||||
- return etree.ElementTree.fromstring(xmlstr)
|
||||
+ return etree.fromstring(xmlstr)
|
||||
|
||||
def getConnectionInfo(self, pin=0):
|
||||
"""Return dbconn dict from backend connection info."""
|
|
@ -0,0 +1,28 @@
|
|||
--- mythtv_old/MythData.py 2010-08-07 21:55:58.000000000 -0400
|
||||
+++ mythtv/MythData.py 2010-08-17 21:49:43.000000000 -0400
|
||||
@@ -175,7 +175,7 @@
|
||||
if not self.open:
|
||||
return
|
||||
self.control.backendCommand('QUERY_FILETRANSFER '\
|
||||
- +BACKEND_SEP.join([str(self.sockno), 'JOIN']))
|
||||
+ +BACKEND_SEP.join([str(self.sockno), 'DONE']))
|
||||
self.socket.shutdown(1)
|
||||
self.socket.close()
|
||||
self.open = False
|
||||
@@ -521,12 +521,14 @@
|
||||
"""Program.getRecorded() -> Recorded object"""
|
||||
return Recorded((self.chanid,self.recstartts), db=self.db)
|
||||
|
||||
- def open(self, type='r', dba=None):
|
||||
+ def open(self, type='r'):
|
||||
"""Program.open(type='r') -> file or FileTransfer object"""
|
||||
if type != 'r':
|
||||
raise MythFileError(MythError.FILE_FAILED_WRITE, self.filename,
|
||||
'Program () objects cannot be opened for writing')
|
||||
- return ftopen(self.filename, 'r', forceremote=False, nooverwrite=False, db=dba)
|
||||
+ if not self.filename.startswith('myth://'):
|
||||
+ self.filename = 'myth://%s/%s' % (self.hostname, self.filename)
|
||||
+ return ftopen(self.filename, 'r')
|
||||
|
||||
class Record( DBDataWrite ):
|
||||
"""
|
|
@ -0,0 +1,199 @@
|
|||
<?xml version="1.0"?>
|
||||
<window type="window" id="14001">
|
||||
<defaultcontrol always="true">1030</defaultcontrol>
|
||||
<allowoverlay>yes</allowoverlay>
|
||||
<onload lang="python"><![CDATA[
|
||||
from mythboxee import MythBoxee
|
||||
mb = MythBoxee()
|
||||
mb.LoadMain()
|
||||
]]></onload>
|
||||
<controls>
|
||||
<control type="group" id="1020">
|
||||
<control type="image" id="1021">
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<texture>mb_bg.png</texture>
|
||||
</control>
|
||||
<control type="image" id="1022">
|
||||
<posx>10</posx>
|
||||
<posy>10</posy>
|
||||
<width>244</width>
|
||||
<height>65</height>
|
||||
<texture>logo.png</texture>
|
||||
</control>
|
||||
</control>
|
||||
<control type="grouplist" id="1040">
|
||||
<posy>28</posy>
|
||||
<posx>780</posx>
|
||||
<width>500</width>
|
||||
<itemgap>10</itemgap>
|
||||
<orientation>horizontal</orientation>
|
||||
<ondown>1030</ondown>
|
||||
<control type="button" id="1041">
|
||||
<label>Refresh</label>
|
||||
<texturefocus>bg_btn.png</texturefocus>
|
||||
<texturenofocus></texturenofocus>
|
||||
<height>30</height>
|
||||
<align>center</align>
|
||||
<onclick lang="python"><![CDATA[mb.RefreshMain()]]></onclick>
|
||||
</control>
|
||||
<control type="button" id="1042">
|
||||
<label>Settings</label>
|
||||
<texturefocus>bg_btn.png</texturefocus>
|
||||
<texturenofocus></texturenofocus>
|
||||
<height>30</height>
|
||||
<align>center</align>
|
||||
<onclick lang="python"><![CDATA[mc.ActivateWindow(14004)]]></onclick>
|
||||
</control>
|
||||
<control type="button" id="1043">
|
||||
<label>Status</label>
|
||||
<texturefocus>bg_btn.png</texturefocus>
|
||||
<texturenofocus></texturenofocus>
|
||||
<height>29</height>
|
||||
<align>center</align>
|
||||
<onclick lang="python"><![CDATA[mc.ActivateWindow(14003)]]></onclick>
|
||||
</control>
|
||||
</control>
|
||||
<control type="panel" id="1030">
|
||||
<posx>22</posx>
|
||||
<posy>82</posy>
|
||||
<width>1280</width>
|
||||
<height>692</height>
|
||||
<onleft>-</onleft>
|
||||
<onright>-</onright>
|
||||
<onup>1040</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>250</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>385</posx>
|
||||
<posy>76</posy>
|
||||
<width>152</width>
|
||||
<height>60</height>
|
||||
<align>right</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(1030)</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(1030)</visible>
|
||||
</control>
|
||||
<control type="label">
|
||||
<description>show title</description>
|
||||
<posx>10</posx>
|
||||
<posy>75</posy>
|
||||
<width>250</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>385</posx>
|
||||
<posy>76</posy>
|
||||
<width>152</width>
|
||||
<height>60</height>
|
||||
<align>right</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[mb.DisplayShow()]]></onclick>
|
||||
</content>
|
||||
</control>
|
||||
<control type="group">
|
||||
<visible>App.HasSetting(loadingmain)</visible>
|
||||
<animation effect="fade" start="0" end="100" time="150">VisibleChange</animation>
|
||||
<control type="image">
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<texture>black.png</texture>
|
||||
<animation effect="fade" start="80" end="80" time="0" condition="true">Conditional</animation>
|
||||
</control>
|
||||
<control type="label">
|
||||
<posx>530</posx>
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<align>cetner</align>
|
||||
<aligny>center</aligny>
|
||||
<font>sans40b</font>
|
||||
<label>LOADING...</label>
|
||||
<textcolor>ffffffff</textcolor>
|
||||
</control>
|
||||
</control>
|
||||
</controls>
|
||||
</window>
|
|
@ -0,0 +1,310 @@
|
|||
<window type="window" id="14004">
|
||||
<defaultcontrol always="true">1030</defaultcontrol>
|
||||
<allowoverlay>yes</allowoverlay>
|
||||
<onload lang="python"><![CDATA[
|
||||
import mc
|
||||
import mythboxee
|
||||
|
||||
mb.SettingsInit()
|
||||
]]></onload>
|
||||
<controls>
|
||||
<control type="group" id="1010">
|
||||
<control type="image" id="1011">
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<texture>mb_bg_settings.png</texture>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
<!-- START: BACKEND INFORMATION -->
|
||||
|
||||
<control type="group" id="1040">
|
||||
<control type="label" id="1041">
|
||||
<posx>820</posx>
|
||||
<posy>150</posy>
|
||||
<width>250</width>
|
||||
<label>Backend Information</label>
|
||||
<font>font23</font>
|
||||
</control>
|
||||
<control type="edit" id="1042">
|
||||
<posx>820</posx>
|
||||
<posy>175</posy>
|
||||
<width>300</width>
|
||||
<label>Hostname/IP:</label>
|
||||
<align>left</align>
|
||||
<font>font18</font>
|
||||
<ondown>1043</ondown>
|
||||
</control>
|
||||
<control type="edit" id="1043">
|
||||
<posx>820</posx>
|
||||
<posy>205</posy>
|
||||
<width>300</width>
|
||||
<label>Username:</label>
|
||||
<align>left</align>
|
||||
<font>font18</font>
|
||||
<onup>1042</onup>
|
||||
<ondown>1044</ondown>
|
||||
</control>
|
||||
<control type="edit" id="1044">
|
||||
<posx>820</posx>
|
||||
<posy>235</posy>
|
||||
<width>300</width>
|
||||
<align>left</align>
|
||||
<label>Password:</label>
|
||||
<password>true</password>
|
||||
<font>font18</font>
|
||||
<onup>1043</onup>
|
||||
<ondown>1045</ondown>
|
||||
</control>
|
||||
<control type="edit" id="1045">
|
||||
<posx>820</posx>
|
||||
<posy>265</posy>
|
||||
<width>300</width>
|
||||
<align>left</align>
|
||||
<label>Database:</label>
|
||||
<font>font18</font>
|
||||
<onup>1044</onup>
|
||||
<ondown>1047</ondown>
|
||||
</control>
|
||||
|
||||
</control>
|
||||
|
||||
<control type="grouplist" id="1046">
|
||||
<posx>820</posx>
|
||||
<posy>295</posy>
|
||||
<width>300</width>
|
||||
<itemgap>0</itemgap>
|
||||
<orientation>horizontal</orientation>
|
||||
<ondown>1022</ondown>
|
||||
<control type="button" id="1047">
|
||||
<label>Test Connection</label>
|
||||
<texturefocus>bg_btn.png</texturefocus>
|
||||
<texturenofocus></texturenofocus>
|
||||
<font>font14</font>
|
||||
<height>30</height>
|
||||
<align>center</align>
|
||||
<aligny>center</aligny>
|
||||
<onclick lang="python"><![CDATA[mb.TestDbSettings()]]></onclick>
|
||||
</control>
|
||||
<control type="button" id="1048">
|
||||
<label>Save DB Settings</label>
|
||||
<texturefocus>bg_btn.png</texturefocus>
|
||||
<texturenofocus></texturenofocus>
|
||||
<font>font14</font>
|
||||
<height>30</height>
|
||||
<align>center</align>
|
||||
<aligny>center</aligny>
|
||||
<onclick lang="python"><![CDATA[mb.SaveDbSettings()]]></onclick>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
<!-- END: BACKEND INFORMATION -->
|
||||
|
||||
<!-- START: STREAM METHOD -->
|
||||
|
||||
<control type="group" id="1020">
|
||||
<control type="label" id="1021">
|
||||
<posx>820</posx>
|
||||
<posy>360</posy>
|
||||
<width>250</width>
|
||||
<align>left</align>
|
||||
<label>Stream Method</label>
|
||||
<font>font23</font>
|
||||
</control>
|
||||
<control type="list" id="1022">
|
||||
<posx>830</posx>
|
||||
<posy>395</posy>
|
||||
<width>164</width>
|
||||
<height>100</height>
|
||||
<onup>1044</onup>
|
||||
<ondown>1032</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>font18</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>font18</font>
|
||||
<textcolor>green</textcolor>
|
||||
<visible>Control.HasFocus(1022)</visible>
|
||||
<selectedcolor>green</selectedcolor>
|
||||
</control>
|
||||
<control type="label">
|
||||
<visible>!Control.HasFocus(1022)</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>font18</font>
|
||||
<textcolor>FF999999</textcolor>
|
||||
<selectedcolor>FFFFFFFF</selectedcolor>
|
||||
</control>
|
||||
</focusedlayout>
|
||||
<content type="action">
|
||||
<onclick lang="python"><![CDATA[mb.SetStreamMethod()]]></onclick>
|
||||
</content>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
<!-- END: STREAM METHOD -->
|
||||
|
||||
<!-- START: SMB SETTINGS -->
|
||||
|
||||
<control type="group" id="1030">
|
||||
<control type="label" id="1031">
|
||||
<posx>820</posx>
|
||||
<posy>480</posy>
|
||||
<width>250</width>
|
||||
<align>left</align>
|
||||
<label>SMB Share</label>
|
||||
<font>font23</font>
|
||||
</control>
|
||||
<control type="edit" id="1032">
|
||||
<posx>820</posx>
|
||||
<posy>505</posy>
|
||||
<width>300</width>
|
||||
<label>Share:</label>
|
||||
<align>left</align>
|
||||
<font>font18</font>
|
||||
<onup>1022</onup>
|
||||
<ondown>1033</ondown>
|
||||
</control>
|
||||
<control type="edit" id="1033">
|
||||
<posx>820</posx>
|
||||
<posy>535</posy>
|
||||
<width>300</width>
|
||||
<label>Username:</label>
|
||||
<align>left</align>
|
||||
<font>font18</font>
|
||||
<onup>1032</onup>
|
||||
<ondown>1034</ondown>
|
||||
</control>
|
||||
<control type="edit" id="1034">
|
||||
<posx>820</posx>
|
||||
<posy>565</posy>
|
||||
<width>300</width>
|
||||
<label>Password:</label>
|
||||
<align>left</align>
|
||||
<font>font18</font>
|
||||
<password>true</password>
|
||||
<onup>1033</onup>
|
||||
<ondown>1071</ondown>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
<control type="grouplist" id="1070">
|
||||
<posx>895</posx>
|
||||
<posy>600</posy>
|
||||
<width>300</width>
|
||||
<itemgap>0</itemgap>
|
||||
<orientation>horizontal</orientation>
|
||||
<onup>1034</onup>
|
||||
<control type="button" id="1071">
|
||||
<label>Save SMB Settings</label>
|
||||
<texturefocus>bg_btn.png</texturefocus>
|
||||
<texturenofocus></texturenofocus>
|
||||
<font>font14</font>
|
||||
<height>30</height>
|
||||
<align>center</align>
|
||||
<aligny>center</aligny>
|
||||
<onclick lang="python"><![CDATA[mb.SaveSMBSettings()]]></onclick>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
<!-- END: SMB SETTINGS -->
|
||||
|
||||
<!-- START: CACHE SETTINGS -->
|
||||
<!--
|
||||
<control type="group" id="1050">
|
||||
<control type="label" id="1051">
|
||||
<posx>820</posx>
|
||||
<posy>560</posy>
|
||||
<width>250</width>
|
||||
<align>left</align>
|
||||
<label>Cache Settings</label>
|
||||
<font>font23</font>
|
||||
</control>
|
||||
<control type="togglebutton" id="1052">
|
||||
<posx>820</posx>
|
||||
<posy>565</posy>
|
||||
<width>300</width>
|
||||
<label>Share:</label>
|
||||
<align>left</align>
|
||||
<font>font18</font>
|
||||
<onup>1022</onup>
|
||||
<ondown>1033</ondown>
|
||||
<ontextchange lang="python"><![CDATA[mb.SaveCacheSettings()]]></ontextchange>
|
||||
</control>
|
||||
<control type="edit" id="1053">
|
||||
<posx>820</posx>
|
||||
<posy>590</posy>
|
||||
<width>300</width>
|
||||
<label>Username:</label>
|
||||
<align>left</align>
|
||||
<font>font18</font>
|
||||
<onup>1032</onup>
|
||||
<ondown>1034</ondown>
|
||||
<ontextchange lang="python"><![CDATA[mb.SaveCacheSettings()]]></ontextchange>
|
||||
</control>
|
||||
<control type="edit" id="1054">
|
||||
<posx>820</posx>
|
||||
<posy>615</posy>
|
||||
<width>300</width>
|
||||
<label>Password:</label>
|
||||
<align>left</align>
|
||||
<font>font18</font>
|
||||
<password>true</password>
|
||||
<onup>1033</onup>
|
||||
<ontextchange lang="python"><![CDATA[mb.SaveCacheSettings()]]></ontextchange>
|
||||
</control>
|
||||
</control>
|
||||
-->
|
||||
<!-- END: CACHE SETTINGS -->
|
||||
|
||||
<control type="group" id="9000">
|
||||
<visible>App.HasSetting(loadingsettings)</visible>
|
||||
<animation effect="fade" start="0" end="100" time="150">VisibleChange</animation>
|
||||
<control type="image" id="9001">
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<texture>black.png</texture>
|
||||
<animation effect="fade" start="80" end="80" time="0" condition="true">Conditional</animation>
|
||||
</control>
|
||||
<control type="label" id="9002">
|
||||
<posy>0</posy>
|
||||
<posx>0</posx>
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<align>center</align>
|
||||
<aligny>center</aligny>
|
||||
<font>sans40b</font>
|
||||
<label>LOADING...</label>
|
||||
<textcolor>ffffffff</textcolor>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
</controls>
|
||||
</window>
|
|
@ -0,0 +1,526 @@
|
|||
<?xml version="1.0"?>
|
||||
<window type="window" id="14002">
|
||||
<defaultcontrol always="true">2040</defaultcontrol>
|
||||
<allowoverlay>true</allowoverlay>
|
||||
<onload lang="python"><![CDATA[
|
||||
import mythboxee
|
||||
mb.LoadShow()
|
||||
]]></onload>
|
||||
<controls>
|
||||
<control type="group" id="2020">
|
||||
<control type="image" id="2021">
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<texture>mb_bg.png</texture>
|
||||
</control>
|
||||
<control type="image" id="2022">
|
||||
<posx>10</posx>
|
||||
<posy>10</posy>
|
||||
<width>244</width>
|
||||
<height>65</height>
|
||||
<texture>logo.png</texture>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
<!-- START: TOP MENU -->
|
||||
|
||||
<control type="grouplist" id="1040">
|
||||
<posy>28</posy>
|
||||
<posx>1106</posx>
|
||||
<width>166</width>
|
||||
<itemgap>10</itemgap>
|
||||
<orientation>horizontal</orientation>
|
||||
<ondown>2040</ondown>
|
||||
<!--
|
||||
940/334
|
||||
<control type="button" id="1041">
|
||||
<label>Refresh</label>
|
||||
<texturefocus>bg_btn.png</texturefocus>
|
||||
<texturenofocus></texturenofocus>
|
||||
<height>30</height>
|
||||
<align>center</align>
|
||||
<onclick lang="python"><![CDATA[mb.RefreshMain()]]></onclick>
|
||||
</control>
|
||||
-->
|
||||
<control type="button" id="1042">
|
||||
<label>Settings</label>
|
||||
<texturefocus>bg_btn.png</texturefocus>
|
||||
<texturenofocus></texturenofocus>
|
||||
<height>30</height>
|
||||
<align>center</align>
|
||||
<onclick lang="python"><![CDATA[mc.ActivateWindow(14004)]]></onclick>
|
||||
</control>
|
||||
<!--
|
||||
780/500
|
||||
<control type="button" id="1043">
|
||||
<label>Send Logs</label>
|
||||
<texturefocus>bg_btn.png</texturefocus>
|
||||
<texturenofocus></texturenofocus>
|
||||
<height>29</height>
|
||||
<align>center</align>
|
||||
<onclick lang="python"><![CDATA[mb.SendLogs()]]></onclick>
|
||||
</control>
|
||||
-->
|
||||
</control>
|
||||
|
||||
<!-- END: TOP MENU -->
|
||||
|
||||
<control type="group" id="2030">
|
||||
<description>details box</description>
|
||||
<posx>28</posx>
|
||||
<posy>81</posy>
|
||||
<onleft>-</onleft>
|
||||
<onright>2040</onright>
|
||||
<onup>1040</onup>
|
||||
<ondown>-</ondown>
|
||||
<control type="image">
|
||||
<description>details box's background</description>
|
||||
<posx>0</posx>
|
||||
<posy>0</posy>
|
||||
<width>283</width>
|
||||
<height>630</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(2070).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(2070).ListItem.Label]</label>
|
||||
<font>font18</font>
|
||||
<textcolor>FFFEFEFE</textcolor>
|
||||
<wrapmultiline>true</wrapmultiline>
|
||||
</control>
|
||||
<control type="textbox">
|
||||
<description>description</description>
|
||||
<posx>22</posx>
|
||||
<posy>395</posy>
|
||||
<width>228</width>
|
||||
<height>215</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<scroll>true</scroll>
|
||||
<label>$INFO[Container(2070).ListItem.property(description)]</label>
|
||||
<font>font16</font>
|
||||
<textcolor>FFCCCCCC</textcolor>
|
||||
<autoscroll delay="3000" time="2000" repeat="10000">Control.HasFocus(2040)</autoscroll>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
<!-- end SHOW DETAILS -->
|
||||
|
||||
<!-- begin EPISODES LIST -->
|
||||
|
||||
<control type="list" id="2040">
|
||||
<posx>345</posx>
|
||||
<posy>82</posy>
|
||||
<width>700</width>
|
||||
<height>630</height>
|
||||
<onleft>-</onleft>
|
||||
<onright>2051</onright>
|
||||
<onup>1040</onup>
|
||||
<ondown>-</ondown>
|
||||
<itemlayout width="700" height="105">
|
||||
<control type="image">
|
||||
<description>background</description>
|
||||
<posx>0</posx>
|
||||
<posy>0</posy>
|
||||
<width>700</width>
|
||||
<height>102</height>
|
||||
<texture border="5">mb_item_big.png</texture>
|
||||
</control>
|
||||
<control type="image">
|
||||
<description>thumbnail</description>
|
||||
<posx>10</posx>
|
||||
<posy>10</posy>
|
||||
<width>126</width>
|
||||
<height>82</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>148</posx>
|
||||
<posy>6</posy>
|
||||
<width>440</width>
|
||||
<height>30</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<info>ListItem.Label</info>
|
||||
<font>font18b</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>font16</font>
|
||||
<textcolor>FFCCCCCC</textcolor>
|
||||
</control>
|
||||
<control type="label">
|
||||
<description>description</description>
|
||||
<posx>148</posx>
|
||||
<posy>31</posy>
|
||||
<width>560</width>
|
||||
<height>60</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<label>$INFO[ListItem.property(description)]</label>
|
||||
<font>font16</font>
|
||||
<textcolor>FFCCCCCC</textcolor>
|
||||
<wrapmultiline>true</wrapmultiline>
|
||||
</control>
|
||||
</itemlayout>
|
||||
<focusedlayout width="700" height="105">
|
||||
<control type="togglebutton">
|
||||
<description>background</description>
|
||||
<posx>0</posx>
|
||||
<posy>0</posy>
|
||||
<width>700</width>
|
||||
<height>102</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(2040)</usealttexture>
|
||||
</control>
|
||||
<control type="image">
|
||||
<description>thumbnail</description>
|
||||
<posx>10</posx>
|
||||
<posy>10</posy>
|
||||
<width>126</width>
|
||||
<height>82</height>
|
||||
<info>Listitem.Thumb</info>
|
||||
<aspectratio>scale</aspectratio>
|
||||
<bordertexture>mb_thumb_bg.png</bordertexture>
|
||||
<bordersize>3</bordersize>
|
||||
<visible>!Control.HasFocus(2040)</visible>
|
||||
</control>
|
||||
<control type="image">
|
||||
<description>thumbnail hover</description>
|
||||
<posx>10</posx>
|
||||
<posy>10</posy>
|
||||
<width>126</width>
|
||||
<height>82</height>
|
||||
<info>Listitem.Thumb</info>
|
||||
<aspectratio>scale</aspectratio>
|
||||
<bordertexture>mb_thumb_hover_bg.png</bordertexture>
|
||||
<bordersize>3</bordersize>
|
||||
<visible>Control.HasFocus(2040)</visible>
|
||||
</control>
|
||||
<control type="label">
|
||||
<description>episode title</description>
|
||||
<posx>148</posx>
|
||||
<posy>6</posy>
|
||||
<width>440</width>
|
||||
<height>30</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<info>ListItem.Label</info>
|
||||
<font>font18b</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>font16</font>
|
||||
<textcolor>FFCCCCCC</textcolor>
|
||||
</control>
|
||||
<control type="label">
|
||||
<description>description</description>
|
||||
<posx>148</posx>
|
||||
<posy>31</posy>
|
||||
<width>560</width>
|
||||
<height>60</height>
|
||||
<align>left</align>
|
||||
<aligny>top</aligny>
|
||||
<label>$INFO[ListItem.property(description)]</label>
|
||||
<font>font16</font>
|
||||
<textcolor>FFCCCCCC</textcolor>
|
||||
<wrapmultiline>true</wrapmultiline>
|
||||
</control>
|
||||
</focusedlayout>
|
||||
</control>
|
||||
|
||||
<!-- end EPISODES LIST -->
|
||||
|
||||
<!-- begin SORT LIST -->
|
||||
|
||||
<control type="group" id="2050">
|
||||
<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="2051">
|
||||
<posx>1070</posx>
|
||||
<posy>100</posy>
|
||||
<width>164</width>
|
||||
<height>100</height>
|
||||
<onleft>2040</onleft>
|
||||
<onright>-</onright>
|
||||
<onup>-</onup>
|
||||
<ondown>2061</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(2051)</visible>
|
||||
<selectedcolor>FFFFFFFF</selectedcolor>
|
||||
</control>
|
||||
<control type="label">
|
||||
<visible>!Control.HasFocus(2051)</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[mb.SortBy()]]></onclick>
|
||||
</content>
|
||||
</control>
|
||||
|
||||
</control>
|
||||
|
||||
<!-- end SORT LIST -->
|
||||
|
||||
<control type="group" id="2060">
|
||||
<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="2061">
|
||||
<posx>1070</posx>
|
||||
<posy>225</posy>
|
||||
<width>164</width>
|
||||
<height>100</height>
|
||||
<onleft>2040</onleft>
|
||||
<onright>-</onright>
|
||||
<onup>2051</onup>
|
||||
<ondown>2082</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(2061)</visible>
|
||||
<selectedcolor>FFFFFFFF</selectedcolor>
|
||||
</control>
|
||||
<control type="label">
|
||||
<visible>!Control.HasFocus(2061)</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[mb.SortDir()]]></onclick>
|
||||
</content>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
<control type="group" id="2080">
|
||||
<control type="label" id="2081">
|
||||
<posx>1079</posx>
|
||||
<posy>300</posy>
|
||||
<label>FILTERS:</label>
|
||||
<font>font14b</font>
|
||||
<aligny>top</aligny>
|
||||
<textcolor>FF999999</textcolor>
|
||||
</control>
|
||||
|
||||
<control type="list" id="2082">
|
||||
<posx>1070</posx>
|
||||
<posy>325</posy>
|
||||
<width>164</width>
|
||||
<height>100</height>
|
||||
<onleft>2040</onleft>
|
||||
<onright>-</onright>
|
||||
<onup>2061</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(2082)</visible>
|
||||
<selectedcolor>FFFFFFFF</selectedcolor>
|
||||
</control>
|
||||
<control type="label">
|
||||
<visible>!Control.HasFocus(2082)</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[mb.FilterEpisodes()]]></onclick>
|
||||
</content>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
<!-- begin SHOW DETAILS CONTAINER -->
|
||||
|
||||
<control type="list" id="2070">
|
||||
<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="group">
|
||||
<visible>App.HasSetting(loading)</visible>
|
||||
<animation effect="fade" start="0" end="100" time="150">VisibleChange</animation>
|
||||
<control type="image">
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<texture>black.png</texture>
|
||||
<animation effect="fade" start="80" end="80" time="0" condition="true">Conditional</animation>
|
||||
</control>
|
||||
<control type="label">
|
||||
<posx>530</posx>
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<align>cetner</align>
|
||||
<aligny>center</aligny>
|
||||
<font>sans40b</font>
|
||||
<label>LOADING...</label>
|
||||
<textcolor>ffffffff</textcolor>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
</controls>
|
||||
</window>
|
|
@ -0,0 +1,132 @@
|
|||
<window type="window" id="14003">
|
||||
<allowoverlay>yes</allowoverlay>
|
||||
<onload lang="python"><![CDATA[
|
||||
import mc
|
||||
import mythboxee
|
||||
|
||||
mb.StatusInit()
|
||||
]]></onload>
|
||||
<controls>
|
||||
<control type="group" id="1010">
|
||||
<control type="image" id="1011">
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<texture>mb_bg.png</texture>
|
||||
</control>
|
||||
<control type="image" id="1012">
|
||||
<posx>10</posx>
|
||||
<posy>10</posy>
|
||||
<width>244</width>
|
||||
<height>65</height>
|
||||
<texture>logo.png</texture>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
<!-- BEGIN ENCODER STATUS -->
|
||||
|
||||
<control type="group" id="1020">
|
||||
<control type="image" id="1021">
|
||||
<posx>20</posx>
|
||||
<posy>120</posy>
|
||||
<height>255</height>
|
||||
<width>610</width>
|
||||
<texture>bg_box.png</texture>
|
||||
</control>
|
||||
<control type="label" id="1022">
|
||||
<posx>30</posx>
|
||||
<posy>90</posy>
|
||||
<label>Encoder Status</label>
|
||||
<font>font28b</font>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
<!-- END ENCODE STATUS -->
|
||||
|
||||
<!-- BEGIN SYSTEM INFORMATION -->
|
||||
|
||||
<control type="group" id="1030">
|
||||
<control type="image" id="1031">
|
||||
<posx>650</posx>
|
||||
<posy>120</posy>
|
||||
<height>255</height>
|
||||
<width>610</width>
|
||||
<texture>bg_box.png</texture>
|
||||
</control>
|
||||
<control type="label" id="1032">
|
||||
<posx>660</posx>
|
||||
<posy>90</posy>
|
||||
<label>System Information</label>
|
||||
<font>font28b</font>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
<!-- END SYSTEM INFORMATION -->
|
||||
|
||||
<!-- BEGIN SCHEDULE -->
|
||||
|
||||
<control type="group" id="1040">
|
||||
<control type="image" id="1041">
|
||||
<posx>20</posx>
|
||||
<posy>460</posy>
|
||||
<height>240</height>
|
||||
<width>610</width>
|
||||
<texture>bg_box.png</texture>
|
||||
</control>
|
||||
<control type="label" id="1042">
|
||||
<posx>30</posx>
|
||||
<posy>430</posy>
|
||||
<label>Upcoming Schedule</label>
|
||||
<font>font28b</font>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
<!-- END SCHEDULE -->
|
||||
|
||||
<!-- BEGIN JOB QUEUE -->
|
||||
|
||||
<control type="group" id="1050">
|
||||
<control type="image" id="1051">
|
||||
<posx>650</posx>
|
||||
<posy>460</posy>
|
||||
<height>240</height>
|
||||
<width>610</width>
|
||||
<texture>bg_box.png</texture>
|
||||
</control>
|
||||
<control type="label" id="1052">
|
||||
<posx>660</posx>
|
||||
<posy>430</posy>
|
||||
<label>Job Queue</label>
|
||||
<font>font28b</font>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
<!-- END JOB QUEUE -->
|
||||
|
||||
<!-- BEGIN LOADING OVERLAY -->
|
||||
|
||||
<control type="group" id="9000">
|
||||
<visible>App.HasSetting(loadingstatus)</visible>
|
||||
<animation effect="fade" start="0" end="100" time="150">VisibleChange</animation>
|
||||
<control type="image" id="9001">
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<texture>black.png</texture>
|
||||
<animation effect="fade" start="80" end="80" time="0" condition="true">Conditional</animation>
|
||||
</control>
|
||||
<control type="label" id="9002">
|
||||
<posy>0</posy>
|
||||
<posx>0</posx>
|
||||
<width>1280</width>
|
||||
<height>720</height>
|
||||
<align>center</align>
|
||||
<aligny>center</aligny>
|
||||
<font>sans40b</font>
|
||||
<label>LOADING...</label>
|
||||
<textcolor>ffffffff</textcolor>
|
||||
</control>
|
||||
</control>
|
||||
|
||||
<!-- END LOADING OVERLAY -->
|
||||
|
||||
</controls>
|
||||
</window>
|
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 859 B |
After Width: | Height: | Size: 859 B |
After Width: | Height: | Size: 350 B |
After Width: | Height: | Size: 363 B |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 558 B |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 116 B |
After Width: | Height: | Size: 113 B |
|
@ -0,0 +1,51 @@
|
|||
"""Core XML support for Python.
|
||||
|
||||
This package contains four sub-packages:
|
||||
|
||||
dom -- The W3C Document Object Model. This supports DOM Level 1 +
|
||||
Namespaces.
|
||||
|
||||
parsers -- Python wrappers for XML parsers (currently only supports Expat).
|
||||
|
||||
sax -- The Simple API for XML, developed by XML-Dev, led by David
|
||||
Megginson and ported to Python by Lars Marius Garshol. This
|
||||
supports the SAX 2 API.
|
||||
|
||||
etree -- The ElementTree XML library. This is a subset of the full
|
||||
ElementTree XML release.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
__all__ = ["dom", "parsers", "sax", "etree"]
|
||||
|
||||
# When being checked-out without options, this has the form
|
||||
# "<dollar>Revision: x.y </dollar>"
|
||||
# When exported using -kv, it is "x.y".
|
||||
__version__ = "$Revision: 41660 $".split()[-2:][0]
|
||||
|
||||
|
||||
_MINIMUM_XMLPLUS_VERSION = (0, 8, 4)
|
||||
|
||||
|
||||
import os
|
||||
|
||||
# only prefer _xmlplus if the environment variable PY_USE_XMLPLUS is defined
|
||||
if 'PY_USE_XMLPLUS' in os.environ:
|
||||
try:
|
||||
import _xmlplus
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
v = _xmlplus.version_info
|
||||
except AttributeError:
|
||||
# _xmlplus is too old; ignore it
|
||||
pass
|
||||
else:
|
||||
if v >= _MINIMUM_XMLPLUS_VERSION:
|
||||
import sys
|
||||
_xmlplus.__path__.extend(__path__)
|
||||
sys.modules[__name__] = _xmlplus
|
||||
else:
|
||||
del v
|
|
@ -0,0 +1,27 @@
|
|||
# This is the Python mapping for interface NodeFilter from
|
||||
# DOM2-Traversal-Range. It contains only constants.
|
||||
|
||||
class NodeFilter:
|
||||
"""
|
||||
This is the DOM2 NodeFilter interface. It contains only constants.
|
||||
"""
|
||||
FILTER_ACCEPT = 1
|
||||
FILTER_REJECT = 2
|
||||
FILTER_SKIP = 3
|
||||
|
||||
SHOW_ALL = 0xFFFFFFFFL
|
||||
SHOW_ELEMENT = 0x00000001
|
||||
SHOW_ATTRIBUTE = 0x00000002
|
||||
SHOW_TEXT = 0x00000004
|
||||
SHOW_CDATA_SECTION = 0x00000008
|
||||
SHOW_ENTITY_REFERENCE = 0x00000010
|
||||
SHOW_ENTITY = 0x00000020
|
||||
SHOW_PROCESSING_INSTRUCTION = 0x00000040
|
||||
SHOW_COMMENT = 0x00000080
|
||||
SHOW_DOCUMENT = 0x00000100
|
||||
SHOW_DOCUMENT_TYPE = 0x00000200
|
||||
SHOW_DOCUMENT_FRAGMENT = 0x00000400
|
||||
SHOW_NOTATION = 0x00000800
|
||||
|
||||
def acceptNode(self, node):
|
||||
raise NotImplementedError
|
|
@ -0,0 +1,139 @@
|
|||
"""W3C Document Object Model implementation for Python.
|
||||
|
||||
The Python mapping of the Document Object Model is documented in the
|
||||
Python Library Reference in the section on the xml.dom package.
|
||||
|
||||
This package contains the following modules:
|
||||
|
||||
minidom -- A simple implementation of the Level 1 DOM with namespace
|
||||
support added (based on the Level 2 specification) and other
|
||||
minor Level 2 functionality.
|
||||
|
||||
pulldom -- DOM builder supporting on-demand tree-building for selected
|
||||
subtrees of the document.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class Node:
|
||||
"""Class giving the NodeType constants."""
|
||||
|
||||
# DOM implementations may use this as a base class for their own
|
||||
# Node implementations. If they don't, the constants defined here
|
||||
# should still be used as the canonical definitions as they match
|
||||
# the values given in the W3C recommendation. Client code can
|
||||
# safely refer to these values in all tests of Node.nodeType
|
||||
# values.
|
||||
|
||||
ELEMENT_NODE = 1
|
||||
ATTRIBUTE_NODE = 2
|
||||
TEXT_NODE = 3
|
||||
CDATA_SECTION_NODE = 4
|
||||
ENTITY_REFERENCE_NODE = 5
|
||||
ENTITY_NODE = 6
|
||||
PROCESSING_INSTRUCTION_NODE = 7
|
||||
COMMENT_NODE = 8
|
||||
DOCUMENT_NODE = 9
|
||||
DOCUMENT_TYPE_NODE = 10
|
||||
DOCUMENT_FRAGMENT_NODE = 11
|
||||
NOTATION_NODE = 12
|
||||
|
||||
|
||||
#ExceptionCode
|
||||
INDEX_SIZE_ERR = 1
|
||||
DOMSTRING_SIZE_ERR = 2
|
||||
HIERARCHY_REQUEST_ERR = 3
|
||||
WRONG_DOCUMENT_ERR = 4
|
||||
INVALID_CHARACTER_ERR = 5
|
||||
NO_DATA_ALLOWED_ERR = 6
|
||||
NO_MODIFICATION_ALLOWED_ERR = 7
|
||||
NOT_FOUND_ERR = 8
|
||||
NOT_SUPPORTED_ERR = 9
|
||||
INUSE_ATTRIBUTE_ERR = 10
|
||||
INVALID_STATE_ERR = 11
|
||||
SYNTAX_ERR = 12
|
||||
INVALID_MODIFICATION_ERR = 13
|
||||
NAMESPACE_ERR = 14
|
||||
INVALID_ACCESS_ERR = 15
|
||||
VALIDATION_ERR = 16
|
||||
|
||||
|
||||
class DOMException(Exception):
|
||||
"""Abstract base class for DOM exceptions.
|
||||
Exceptions with specific codes are specializations of this class."""
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
if self.__class__ is DOMException:
|
||||
raise RuntimeError(
|
||||
"DOMException should not be instantiated directly")
|
||||
Exception.__init__(self, *args, **kw)
|
||||
|
||||
def _get_code(self):
|
||||
return self.code
|
||||
|
||||
|
||||
class IndexSizeErr(DOMException):
|
||||
code = INDEX_SIZE_ERR
|
||||
|
||||
class DomstringSizeErr(DOMException):
|
||||
code = DOMSTRING_SIZE_ERR
|
||||
|
||||
class HierarchyRequestErr(DOMException):
|
||||
code = HIERARCHY_REQUEST_ERR
|
||||
|
||||
class WrongDocumentErr(DOMException):
|
||||
code = WRONG_DOCUMENT_ERR
|
||||
|
||||
class InvalidCharacterErr(DOMException):
|
||||
code = INVALID_CHARACTER_ERR
|
||||
|
||||
class NoDataAllowedErr(DOMException):
|
||||
code = NO_DATA_ALLOWED_ERR
|
||||
|
||||
class NoModificationAllowedErr(DOMException):
|
||||
code = NO_MODIFICATION_ALLOWED_ERR
|
||||
|
||||
class NotFoundErr(DOMException):
|
||||
code = NOT_FOUND_ERR
|
||||
|
||||
class NotSupportedErr(DOMException):
|
||||
code = NOT_SUPPORTED_ERR
|
||||
|
||||
class InuseAttributeErr(DOMException):
|
||||
code = INUSE_ATTRIBUTE_ERR
|
||||
|
||||
class InvalidStateErr(DOMException):
|
||||
code = INVALID_STATE_ERR
|
||||
|
||||
class SyntaxErr(DOMException):
|
||||
code = SYNTAX_ERR
|
||||
|
||||
class InvalidModificationErr(DOMException):
|
||||
code = INVALID_MODIFICATION_ERR
|
||||
|
||||
class NamespaceErr(DOMException):
|
||||
code = NAMESPACE_ERR
|
||||
|
||||
class InvalidAccessErr(DOMException):
|
||||
code = INVALID_ACCESS_ERR
|
||||
|
||||
class ValidationErr(DOMException):
|
||||
code = VALIDATION_ERR
|
||||
|
||||
class UserDataHandler:
|
||||
"""Class giving the operation constants for UserDataHandler.handle()."""
|
||||
|
||||
# Based on DOM Level 3 (WD 9 April 2002)
|
||||
|
||||
NODE_CLONED = 1
|
||||
NODE_IMPORTED = 2
|
||||
NODE_DELETED = 3
|
||||
NODE_RENAMED = 4
|
||||
|
||||
XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace"
|
||||
XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/"
|
||||
XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml"
|
||||
EMPTY_NAMESPACE = None
|
||||
EMPTY_PREFIX = None
|
||||
|
||||
from domreg import getDOMImplementation,registerDOMImplementation
|
|
@ -0,0 +1,99 @@
|
|||
"""Registration facilities for DOM. This module should not be used
|
||||
directly. Instead, the functions getDOMImplementation and
|
||||
registerDOMImplementation should be imported from xml.dom."""
|
||||
|
||||
from xml.dom.minicompat import * # isinstance, StringTypes
|
||||
|
||||
# This is a list of well-known implementations. Well-known names
|
||||
# should be published by posting to xml-sig@python.org, and are
|
||||
# subsequently recorded in this file.
|
||||
|
||||
well_known_implementations = {
|
||||
'minidom':'xml.dom.minidom',
|
||||
'4DOM': 'xml.dom.DOMImplementation',
|
||||
}
|
||||
|
||||
# DOM implementations not officially registered should register
|
||||
# themselves with their
|
||||
|
||||
registered = {}
|
||||
|
||||
def registerDOMImplementation(name, factory):
|
||||
"""registerDOMImplementation(name, factory)
|
||||
|
||||
Register the factory function with the name. The factory function
|
||||
should return an object which implements the DOMImplementation
|
||||
interface. The factory function can either return the same object,
|
||||
or a new one (e.g. if that implementation supports some
|
||||
customization)."""
|
||||
|
||||
registered[name] = factory
|
||||
|
||||
def _good_enough(dom, features):
|
||||
"_good_enough(dom, features) -> Return 1 if the dom offers the features"
|
||||
for f,v in features:
|
||||
if not dom.hasFeature(f,v):
|
||||
return 0
|
||||
return 1
|
||||
|
||||
def getDOMImplementation(name = None, features = ()):
|
||||
"""getDOMImplementation(name = None, features = ()) -> DOM implementation.
|
||||
|
||||
Return a suitable DOM implementation. The name is either
|
||||
well-known, the module name of a DOM implementation, or None. If
|
||||
it is not None, imports the corresponding module and returns
|
||||
DOMImplementation object if the import succeeds.
|
||||
|
||||
If name is not given, consider the available implementations to
|
||||
find one with the required feature set. If no implementation can
|
||||
be found, raise an ImportError. The features list must be a sequence
|
||||
of (feature, version) pairs which are passed to hasFeature."""
|
||||
|
||||
import os
|
||||
creator = None
|
||||
mod = well_known_implementations.get(name)
|
||||
if mod:
|
||||
mod = __import__(mod, {}, {}, ['getDOMImplementation'])
|
||||
return mod.getDOMImplementation()
|
||||
elif name:
|
||||
return registered[name]()
|
||||
elif os.environ.has_key("PYTHON_DOM"):
|
||||
return getDOMImplementation(name = os.environ["PYTHON_DOM"])
|
||||
|
||||
# User did not specify a name, try implementations in arbitrary
|
||||
# order, returning the one that has the required features
|
||||
if isinstance(features, StringTypes):
|
||||
features = _parse_feature_string(features)
|
||||
for creator in registered.values():
|
||||
dom = creator()
|
||||
if _good_enough(dom, features):
|
||||
return dom
|
||||
|
||||
for creator in well_known_implementations.keys():
|
||||
try:
|
||||
dom = getDOMImplementation(name = creator)
|
||||
except StandardError: # typically ImportError, or AttributeError
|
||||
continue
|
||||
if _good_enough(dom, features):
|
||||
return dom
|
||||
|
||||
raise ImportError,"no suitable DOM implementation found"
|
||||
|
||||
def _parse_feature_string(s):
|
||||
features = []
|
||||
parts = s.split()
|
||||
i = 0
|
||||
length = len(parts)
|
||||
while i < length:
|
||||
feature = parts[i]
|
||||
if feature[0] in "0123456789":
|
||||
raise ValueError, "bad feature name: %r" % (feature,)
|
||||
i = i + 1
|
||||
version = None
|
||||
if i < length:
|
||||
v = parts[i]
|
||||
if v[0] in "0123456789":
|
||||
i = i + 1
|
||||
version = v
|
||||
features.append((feature, version))
|
||||
return tuple(features)
|
|
@ -0,0 +1,983 @@
|
|||
"""Facility to use the Expat parser to load a minidom instance
|
||||
from a string or file.
|
||||
|
||||
This avoids all the overhead of SAX and pulldom to gain performance.
|
||||
"""
|
||||
|
||||
# Warning!
|
||||
#
|
||||
# This module is tightly bound to the implementation details of the
|
||||
# minidom DOM and can't be used with other DOM implementations. This
|
||||
# is due, in part, to a lack of appropriate methods in the DOM (there is
|
||||
# no way to create Entity and Notation nodes via the DOM Level 2
|
||||
# interface), and for performance. The later is the cause of some fairly
|
||||
# cryptic code.
|
||||
#
|
||||
# Performance hacks:
|
||||
#
|
||||
# - .character_data_handler() has an extra case in which continuing
|
||||
# data is appended to an existing Text node; this can be a
|
||||
# speedup since pyexpat can break up character data into multiple
|
||||
# callbacks even though we set the buffer_text attribute on the
|
||||
# parser. This also gives us the advantage that we don't need a
|
||||
# separate normalization pass.
|
||||
#
|
||||
# - Determining that a node exists is done using an identity comparison
|
||||
# with None rather than a truth test; this avoids searching for and
|
||||
# calling any methods on the node object if it exists. (A rather
|
||||
# nice speedup is achieved this way as well!)
|
||||
|
||||
from xml.dom import xmlbuilder, minidom, Node
|
||||
from xml.dom import EMPTY_NAMESPACE, EMPTY_PREFIX, XMLNS_NAMESPACE
|
||||
from xml.parsers import expat
|
||||
from xml.dom.minidom import _append_child, _set_attribute_node
|
||||
from xml.dom.NodeFilter import NodeFilter
|
||||
|
||||
from xml.dom.minicompat import *
|
||||
|
||||
TEXT_NODE = Node.TEXT_NODE
|
||||
CDATA_SECTION_NODE = Node.CDATA_SECTION_NODE
|
||||
DOCUMENT_NODE = Node.DOCUMENT_NODE
|
||||
|
||||
FILTER_ACCEPT = xmlbuilder.DOMBuilderFilter.FILTER_ACCEPT
|
||||
FILTER_REJECT = xmlbuilder.DOMBuilderFilter.FILTER_REJECT
|
||||
FILTER_SKIP = xmlbuilder.DOMBuilderFilter.FILTER_SKIP
|
||||
FILTER_INTERRUPT = xmlbuilder.DOMBuilderFilter.FILTER_INTERRUPT
|
||||
|
||||
theDOMImplementation = minidom.getDOMImplementation()
|
||||
|
||||
# Expat typename -> TypeInfo
|
||||
_typeinfo_map = {
|
||||
"CDATA": minidom.TypeInfo(None, "cdata"),
|
||||
"ENUM": minidom.TypeInfo(None, "enumeration"),
|
||||
"ENTITY": minidom.TypeInfo(None, "entity"),
|
||||
"ENTITIES": minidom.TypeInfo(None, "entities"),
|
||||
"ID": minidom.TypeInfo(None, "id"),
|
||||
"IDREF": minidom.TypeInfo(None, "idref"),
|
||||
"IDREFS": minidom.TypeInfo(None, "idrefs"),
|
||||
"NMTOKEN": minidom.TypeInfo(None, "nmtoken"),
|
||||
"NMTOKENS": minidom.TypeInfo(None, "nmtokens"),
|
||||
}
|
||||
|
||||
class ElementInfo(object):
|
||||
__slots__ = '_attr_info', '_model', 'tagName'
|
||||
|
||||
def __init__(self, tagName, model=None):
|
||||
self.tagName = tagName
|
||||
self._attr_info = []
|
||||
self._model = model
|
||||
|
||||
def __getstate__(self):
|
||||
return self._attr_info, self._model, self.tagName
|
||||
|
||||
def __setstate__(self, state):
|
||||
self._attr_info, self._model, self.tagName = state
|
||||
|
||||
def getAttributeType(self, aname):
|
||||
for info in self._attr_info:
|
||||
if info[1] == aname:
|
||||
t = info[-2]
|
||||
if t[0] == "(":
|
||||
return _typeinfo_map["ENUM"]
|
||||
else:
|
||||
return _typeinfo_map[info[-2]]
|
||||
return minidom._no_type
|
||||
|
||||
def getAttributeTypeNS(self, namespaceURI, localName):
|
||||
return minidom._no_type
|
||||
|
||||
def isElementContent(self):
|
||||
if self._model:
|
||||
type = self._model[0]
|
||||
return type not in (expat.model.XML_CTYPE_ANY,
|
||||
expat.model.XML_CTYPE_MIXED)
|
||||
else:
|
||||
return False
|
||||
|
||||
def isEmpty(self):
|
||||
if self._model:
|
||||
return self._model[0] == expat.model.XML_CTYPE_EMPTY
|
||||
else:
|
||||
return False
|
||||
|
||||
def isId(self, aname):
|
||||
for info in self._attr_info:
|
||||
if info[1] == aname:
|
||||
return info[-2] == "ID"
|
||||
return False
|
||||
|
||||
def isIdNS(self, euri, ename, auri, aname):
|
||||
# not sure this is meaningful
|
||||
return self.isId((auri, aname))
|
||||
|
||||
def _intern(builder, s):
|
||||
return builder._intern_setdefault(s, s)
|
||||
|
||||
def _parse_ns_name(builder, name):
|
||||
assert ' ' in name
|
||||
parts = name.split(' ')
|
||||
intern = builder._intern_setdefault
|
||||
if len(parts) == 3:
|
||||
uri, localname, prefix = parts
|
||||
prefix = intern(prefix, prefix)
|
||||
qname = "%s:%s" % (prefix, localname)
|
||||
qname = intern(qname, qname)
|
||||
localname = intern(localname, localname)
|
||||
else:
|
||||
uri, localname = parts
|
||||
prefix = EMPTY_PREFIX
|
||||
qname = localname = intern(localname, localname)
|
||||
return intern(uri, uri), localname, prefix, qname
|
||||
|
||||
|
||||
class ExpatBuilder:
|
||||
"""Document builder that uses Expat to build a ParsedXML.DOM document
|
||||
instance."""
|
||||
|
||||
def __init__(self, options=None):
|
||||
if options is None:
|
||||
options = xmlbuilder.Options()
|
||||
self._options = options
|
||||
if self._options.filter is not None:
|
||||
self._filter = FilterVisibilityController(self._options.filter)
|
||||
else:
|
||||
self._filter = None
|
||||
# This *really* doesn't do anything in this case, so
|
||||
# override it with something fast & minimal.
|
||||
self._finish_start_element = id
|
||||
self._parser = None
|
||||
self.reset()
|
||||
|
||||
def createParser(self):
|
||||
"""Create a new parser object."""
|
||||
return expat.ParserCreate()
|
||||
|
||||
def getParser(self):
|
||||
"""Return the parser object, creating a new one if needed."""
|
||||
if not self._parser:
|
||||
self._parser = self.createParser()
|
||||
self._intern_setdefault = self._parser.intern.setdefault
|
||||
self._parser.buffer_text = True
|
||||
self._parser.ordered_attributes = True
|
||||
self._parser.specified_attributes = True
|
||||
self.install(self._parser)
|
||||
return self._parser
|
||||
|
||||
def reset(self):
|
||||
"""Free all data structures used during DOM construction."""
|
||||
self.document = theDOMImplementation.createDocument(
|
||||
EMPTY_NAMESPACE, None, None)
|
||||
self.curNode = self.document
|
||||
self._elem_info = self.document._elem_info
|
||||
self._cdata = False
|
||||
|
||||
def install(self, parser):
|
||||
"""Install the callbacks needed to build the DOM into the parser."""
|
||||
# This creates circular references!
|
||||
parser.StartDoctypeDeclHandler = self.start_doctype_decl_handler
|
||||
parser.StartElementHandler = self.first_element_handler
|
||||
parser.EndElementHandler = self.end_element_handler
|
||||
parser.ProcessingInstructionHandler = self.pi_handler
|
||||
if self._options.entities:
|
||||
parser.EntityDeclHandler = self.entity_decl_handler
|
||||
parser.NotationDeclHandler = self.notation_decl_handler
|
||||
if self._options.comments:
|
||||
parser.CommentHandler = self.comment_handler
|
||||
if self._options.cdata_sections:
|
||||
parser.StartCdataSectionHandler = self.start_cdata_section_handler
|
||||
parser.EndCdataSectionHandler = self.end_cdata_section_handler
|
||||
parser.CharacterDataHandler = self.character_data_handler_cdata
|
||||
else:
|
||||
parser.CharacterDataHandler = self.character_data_handler
|
||||
parser.ExternalEntityRefHandler = self.external_entity_ref_handler
|
||||
parser.XmlDeclHandler = self.xml_decl_handler
|
||||
parser.ElementDeclHandler = self.element_decl_handler
|
||||
parser.AttlistDeclHandler = self.attlist_decl_handler
|
||||
|
||||
def parseFile(self, file):
|
||||
"""Parse a document from a file object, returning the document
|
||||
node."""
|
||||
parser = self.getParser()
|
||||
first_buffer = True
|
||||
try:
|
||||
while 1:
|
||||
buffer = file.read(16*1024)
|
||||
if not buffer:
|
||||
break
|
||||
parser.Parse(buffer, 0)
|
||||
if first_buffer and self.document.documentElement:
|
||||
self._setup_subset(buffer)
|
||||
first_buffer = False
|
||||
parser.Parse("", True)
|
||||
except ParseEscape:
|
||||
pass
|
||||
doc = self.document
|
||||
self.reset()
|
||||
self._parser = None
|
||||
return doc
|
||||
|
||||
def parseString(self, string):
|
||||
"""Parse a document from a string, returning the document node."""
|
||||
parser = self.getParser()
|
||||
try:
|
||||
parser.Parse(string, True)
|
||||
self._setup_subset(string)
|
||||
except ParseEscape:
|
||||
pass
|
||||
doc = self.document
|
||||
self.reset()
|
||||
self._parser = None
|
||||
return doc
|
||||
|
||||
def _setup_subset(self, buffer):
|
||||
"""Load the internal subset if there might be one."""
|
||||
if self.document.doctype:
|
||||
extractor = InternalSubsetExtractor()
|
||||
extractor.parseString(buffer)
|
||||
subset = extractor.getSubset()
|
||||
self.document.doctype.internalSubset = subset
|
||||
|
||||
def start_doctype_decl_handler(self, doctypeName, systemId, publicId,
|
||||
has_internal_subset):
|
||||
doctype = self.document.implementation.createDocumentType(
|
||||
doctypeName, publicId, systemId)
|
||||
doctype.ownerDocument = self.document
|
||||
self.document.childNodes.append(doctype)
|
||||
self.document.doctype = doctype
|
||||
if self._filter and self._filter.acceptNode(doctype) == FILTER_REJECT:
|
||||
self.document.doctype = None
|
||||
del self.document.childNodes[-1]
|
||||
doctype = None
|
||||
self._parser.EntityDeclHandler = None
|
||||
self._parser.NotationDeclHandler = None
|
||||
if has_internal_subset:
|
||||
if doctype is not None:
|
||||
doctype.entities._seq = []
|
||||
doctype.notations._seq = []
|
||||
self._parser.CommentHandler = None
|
||||
self._parser.ProcessingInstructionHandler = None
|
||||
self._parser.EndDoctypeDeclHandler = self.end_doctype_decl_handler
|
||||
|
||||
def end_doctype_decl_handler(self):
|
||||
if self._options.comments:
|
||||
self._parser.CommentHandler = self.comment_handler
|
||||
self._parser.ProcessingInstructionHandler = self.pi_handler
|
||||
if not (self._elem_info or self._filter):
|
||||
self._finish_end_element = id
|
||||
|
||||
def pi_handler(self, target, data):
|
||||
node = self.document.createProcessingInstruction(target, data)
|
||||
_append_child(self.curNode, node)
|
||||
if self._filter and self._filter.acceptNode(node) == FILTER_REJECT:
|
||||
self.curNode.removeChild(node)
|
||||
|
||||
def character_data_handler_cdata(self, data):
|
||||
childNodes = self.curNode.childNodes
|
||||
if self._cdata:
|
||||
if ( self._cdata_continue
|
||||
and childNodes[-1].nodeType == CDATA_SECTION_NODE):
|
||||
childNodes[-1].appendData(data)
|
||||
return
|
||||
node = self.document.createCDATASection(data)
|
||||
self._cdata_continue = True
|
||||
elif childNodes and childNodes[-1].nodeType == TEXT_NODE:
|
||||
node = childNodes[-1]
|
||||
value = node.data + data
|
||||
d = node.__dict__
|
||||
d['data'] = d['nodeValue'] = value
|
||||
return
|
||||
else:
|
||||
node = minidom.Text()
|
||||
d = node.__dict__
|
||||
d['data'] = d['nodeValue'] = data
|
||||
d['ownerDocument'] = self.document
|
||||
_append_child(self.curNode, node)
|
||||
|
||||
def character_data_handler(self, data):
|
||||
childNodes = self.curNode.childNodes
|
||||
if childNodes and childNodes[-1].nodeType == TEXT_NODE:
|
||||
node = childNodes[-1]
|
||||
d = node.__dict__
|
||||
d['data'] = d['nodeValue'] = node.data + data
|
||||
return
|
||||
node = minidom.Text()
|
||||
d = node.__dict__
|
||||
d['data'] = d['nodeValue'] = node.data + data
|
||||
d['ownerDocument'] = self.document
|
||||
_append_child(self.curNode, node)
|
||||
|
||||
def entity_decl_handler(self, entityName, is_parameter_entity, value,
|
||||
base, systemId, publicId, notationName):
|
||||
if is_parameter_entity:
|
||||
# we don't care about parameter entities for the DOM
|
||||
return
|
||||
if not self._options.entities:
|
||||
return
|
||||
node = self.document._create_entity(entityName, publicId,
|
||||
systemId, notationName)
|
||||
if value is not None:
|
||||
# internal entity
|
||||
# node *should* be readonly, but we'll cheat
|
||||
child = self.document.createTextNode(value)
|
||||
node.childNodes.append(child)
|
||||
self.document.doctype.entities._seq.append(node)
|
||||
if self._filter and self._filter.acceptNode(node) == FILTER_REJECT:
|
||||
del self.document.doctype.entities._seq[-1]
|
||||
|
||||
def notation_decl_handler(self, notationName, base, systemId, publicId):
|
||||
node = self.document._create_notation(notationName, publicId, systemId)
|
||||
self.document.doctype.notations._seq.append(node)
|
||||
if self._filter and self._filter.acceptNode(node) == FILTER_ACCEPT:
|
||||
del self.document.doctype.notations._seq[-1]
|
||||
|
||||
def comment_handler(self, data):
|
||||
node = self.document.createComment(data)
|
||||
_append_child(self.curNode, node)
|
||||
if self._filter and self._filter.acceptNode(node) == FILTER_REJECT:
|
||||
self.curNode.removeChild(node)
|
||||
|
||||
def start_cdata_section_handler(self):
|
||||
self._cdata = True
|
||||
self._cdata_continue = False
|
||||
|
||||
def end_cdata_section_handler(self):
|
||||
self._cdata = False
|
||||
self._cdata_continue = False
|
||||
|
||||
def external_entity_ref_handler(self, context, base, systemId, publicId):
|
||||
return 1
|
||||
|
||||
def first_element_handler(self, name, attributes):
|
||||
if self._filter is None and not self._elem_info:
|
||||
self._finish_end_element = id
|
||||
self.getParser().StartElementHandler = self.start_element_handler
|
||||
self.start_element_handler(name, attributes)
|
||||
|
||||
def start_element_handler(self, name, attributes):
|
||||
node = self.document.createElement(name)
|
||||
_append_child(self.curNode, node)
|
||||
self.curNode = node
|
||||
|
||||
if attributes:
|
||||
for i in range(0, len(attributes), 2):
|
||||
a = minidom.Attr(attributes[i], EMPTY_NAMESPACE,
|
||||
None, EMPTY_PREFIX)
|
||||
value = attributes[i+1]
|
||||
d = a.childNodes[0].__dict__
|
||||
d['data'] = d['nodeValue'] = value
|
||||
d = a.__dict__
|
||||
d['value'] = d['nodeValue'] = value
|
||||
d['ownerDocument'] = self.document
|
||||
_set_attribute_node(node, a)
|
||||
|
||||
if node is not self.document.documentElement:
|
||||
self._finish_start_element(node)
|
||||
|
||||
def _finish_start_element(self, node):
|
||||
if self._filter:
|
||||
# To be general, we'd have to call isSameNode(), but this
|
||||
# is sufficient for minidom:
|
||||
if node is self.document.documentElement:
|
||||
return
|
||||
filt = self._filter.startContainer(node)
|
||||
if filt == FILTER_REJECT:
|
||||
# ignore this node & all descendents
|
||||
Rejecter(self)
|
||||
elif filt == FILTER_SKIP:
|
||||
# ignore this node, but make it's children become
|
||||
# children of the parent node
|
||||
Skipper(self)
|
||||
else:
|
||||
return
|
||||
self.curNode = node.parentNode
|
||||
node.parentNode.removeChild(node)
|
||||
node.unlink()
|
||||
|
||||
# If this ever changes, Namespaces.end_element_handler() needs to
|
||||
# be changed to match.
|
||||
#
|
||||
def end_element_handler(self, name):
|
||||
curNode = self.curNode
|
||||
self.curNode = curNode.parentNode
|
||||
self._finish_end_element(curNode)
|
||||
|
||||
def _finish_end_element(self, curNode):
|
||||
info = self._elem_info.get(curNode.tagName)
|
||||
if info:
|
||||
self._handle_white_text_nodes(curNode, info)
|
||||
if self._filter:
|
||||
if curNode is self.document.documentElement:
|
||||
return
|
||||
if self._filter.acceptNode(curNode) == FILTER_REJECT:
|
||||
self.curNode.removeChild(curNode)
|
||||
curNode.unlink()
|
||||
|
||||
def _handle_white_text_nodes(self, node, info):
|
||||
if (self._options.whitespace_in_element_content
|
||||
or not info.isElementContent()):
|
||||
return
|
||||
|
||||
# We have element type information and should remove ignorable
|
||||
# whitespace; identify for text nodes which contain only
|
||||
# whitespace.
|
||||
L = []
|
||||
for child in node.childNodes:
|
||||
if child.nodeType == TEXT_NODE and not child.data.strip():
|
||||
L.append(child)
|
||||
|
||||
# Remove ignorable whitespace from the tree.
|
||||
for child in L:
|
||||
node.removeChild(child)
|
||||
|
||||
def element_decl_handler(self, name, model):
|
||||
info = self._elem_info.get(name)
|
||||
if info is None:
|
||||
self._elem_info[name] = ElementInfo(name, model)
|
||||
else:
|
||||
assert info._model is None
|
||||
info._model = model
|
||||
|
||||
def attlist_decl_handler(self, elem, name, type, default, required):
|
||||
info = self._elem_info.get(elem)
|
||||
if info is None:
|
||||
info = ElementInfo(elem)
|
||||
self._elem_info[elem] = info
|
||||
info._attr_info.append(
|
||||
[None, name, None, None, default, 0, type, required])
|
||||
|
||||
def xml_decl_handler(self, version, encoding, standalone):
|
||||
self.document.version = version
|
||||
self.document.encoding = encoding
|
||||
# This is still a little ugly, thanks to the pyexpat API. ;-(
|
||||
if standalone >= 0:
|
||||
if standalone:
|
||||
self.document.standalone = True
|
||||
else:
|
||||
self.document.standalone = False
|
||||
|
||||
|
||||
# Don't include FILTER_INTERRUPT, since that's checked separately
|
||||
# where allowed.
|
||||
_ALLOWED_FILTER_RETURNS = (FILTER_ACCEPT, FILTER_REJECT, FILTER_SKIP)
|
||||
|
||||
class FilterVisibilityController(object):
|
||||
"""Wrapper around a DOMBuilderFilter which implements the checks
|
||||
to make the whatToShow filter attribute work."""
|
||||
|
||||
__slots__ = 'filter',
|
||||
|
||||
def __init__(self, filter):
|
||||
self.filter = filter
|
||||
|
||||
def startContainer(self, node):
|
||||
mask = self._nodetype_mask[node.nodeType]
|
||||
if self.filter.whatToShow & mask:
|
||||
val = self.filter.startContainer(node)
|
||||
if val == FILTER_INTERRUPT:
|
||||
raise ParseEscape
|
||||
if val not in _ALLOWED_FILTER_RETURNS:
|
||||
raise ValueError, \
|
||||
"startContainer() returned illegal value: " + repr(val)
|
||||
return val
|
||||
else:
|
||||
return FILTER_ACCEPT
|
||||
|
||||
def acceptNode(self, node):
|
||||
mask = self._nodetype_mask[node.nodeType]
|
||||
if self.filter.whatToShow & mask:
|
||||
val = self.filter.acceptNode(node)
|
||||
if val == FILTER_INTERRUPT:
|
||||
raise ParseEscape
|
||||
if val == FILTER_SKIP:
|
||||
# move all child nodes to the parent, and remove this node
|
||||
parent = node.parentNode
|
||||
for child in node.childNodes[:]:
|
||||
parent.appendChild(child)
|
||||
# node is handled by the caller
|
||||
return FILTER_REJECT
|
||||
if val not in _ALLOWED_FILTER_RETURNS:
|
||||
raise ValueError, \
|
||||
"acceptNode() returned illegal value: " + repr(val)
|
||||
return val
|
||||
else:
|
||||
return FILTER_ACCEPT
|
||||
|
||||
_nodetype_mask = {
|
||||
Node.ELEMENT_NODE: NodeFilter.SHOW_ELEMENT,
|
||||
Node.ATTRIBUTE_NODE: NodeFilter.SHOW_ATTRIBUTE,
|
||||
Node.TEXT_NODE: NodeFilter.SHOW_TEXT,
|
||||
Node.CDATA_SECTION_NODE: NodeFilter.SHOW_CDATA_SECTION,
|
||||
Node.ENTITY_REFERENCE_NODE: NodeFilter.SHOW_ENTITY_REFERENCE,
|
||||
Node.ENTITY_NODE: NodeFilter.SHOW_ENTITY,
|
||||
Node.PROCESSING_INSTRUCTION_NODE: NodeFilter.SHOW_PROCESSING_INSTRUCTION,
|
||||
Node.COMMENT_NODE: NodeFilter.SHOW_COMMENT,
|
||||
Node.DOCUMENT_NODE: NodeFilter.SHOW_DOCUMENT,
|
||||
Node.DOCUMENT_TYPE_NODE: NodeFilter.SHOW_DOCUMENT_TYPE,
|
||||
Node.DOCUMENT_FRAGMENT_NODE: NodeFilter.SHOW_DOCUMENT_FRAGMENT,
|
||||
Node.NOTATION_NODE: NodeFilter.SHOW_NOTATION,
|
||||
}
|
||||
|
||||
|
||||
class FilterCrutch(object):
|
||||
__slots__ = '_builder', '_level', '_old_start', '_old_end'
|
||||
|
||||
def __init__(self, builder):
|
||||
self._level = 0
|
||||
self._builder = builder
|
||||
parser = builder._parser
|
||||
self._old_start = parser.StartElementHandler
|
||||
self._old_end = parser.EndElementHandler
|
||||
parser.StartElementHandler = self.start_element_handler
|
||||
parser.EndElementHandler = self.end_element_handler
|
||||
|
||||
class Rejecter(FilterCrutch):
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, builder):
|
||||
FilterCrutch.__init__(self, builder)
|
||||
parser = builder._parser
|
||||
for name in ("ProcessingInstructionHandler",
|
||||
"CommentHandler",
|
||||
"CharacterDataHandler",
|
||||
"StartCdataSectionHandler",
|
||||
"EndCdataSectionHandler",
|
||||
"ExternalEntityRefHandler",
|
||||
):
|
||||
setattr(parser, name, None)
|
||||
|
||||
def start_element_handler(self, *args):
|
||||
self._level = self._level + 1
|
||||
|
||||
def end_element_handler(self, *args):
|
||||
if self._level == 0:
|
||||
# restore the old handlers
|
||||
parser = self._builder._parser
|
||||
self._builder.install(parser)
|
||||
parser.StartElementHandler = self._old_start
|
||||
parser.EndElementHandler = self._old_end
|
||||
else:
|
||||
self._level = self._level - 1
|
||||
|
||||
class Skipper(FilterCrutch):
|
||||
__slots__ = ()
|
||||
|
||||
def start_element_handler(self, *args):
|
||||
node = self._builder.curNode
|
||||
self._old_start(*args)
|
||||
if self._builder.curNode is not node:
|
||||
self._level = self._level + 1
|
||||
|
||||
def end_element_handler(self, *args):
|
||||
if self._level == 0:
|
||||
# We're popping back out of the node we're skipping, so we
|
||||
# shouldn't need to do anything but reset the handlers.
|
||||
self._builder._parser.StartElementHandler = self._old_start
|
||||
self._builder._parser.EndElementHandler = self._old_end
|
||||
self._builder = None
|
||||
else:
|
||||
self._level = self._level - 1
|
||||
self._old_end(*args)
|
||||
|
||||
|
||||
# framework document used by the fragment builder.
|
||||
# Takes a string for the doctype, subset string, and namespace attrs string.
|
||||
|
||||
_FRAGMENT_BUILDER_INTERNAL_SYSTEM_ID = \
|
||||
"http://xml.python.org/entities/fragment-builder/internal"
|
||||
|
||||
_FRAGMENT_BUILDER_TEMPLATE = (
|
||||
'''\
|
||||
<!DOCTYPE wrapper
|
||||
%%s [
|
||||
<!ENTITY fragment-builder-internal
|
||||
SYSTEM "%s">
|
||||
%%s
|
||||
]>
|
||||
<wrapper %%s
|
||||
>&fragment-builder-internal;</wrapper>'''
|
||||
% _FRAGMENT_BUILDER_INTERNAL_SYSTEM_ID)
|
||||
|
||||
|
||||
class FragmentBuilder(ExpatBuilder):
|
||||
"""Builder which constructs document fragments given XML source
|
||||
text and a context node.
|
||||
|
||||
The context node is expected to provide information about the
|
||||
namespace declarations which are in scope at the start of the
|
||||
fragment.
|
||||
"""
|
||||
|
||||
def __init__(self, context, options=None):
|
||||
if context.nodeType == DOCUMENT_NODE:
|
||||
self.originalDocument = context
|
||||
self.context = context
|
||||
else:
|
||||
self.originalDocument = context.ownerDocument
|
||||
self.context = context
|
||||
ExpatBuilder.__init__(self, options)
|
||||
|
||||
def reset(self):
|
||||
ExpatBuilder.reset(self)
|
||||
self.fragment = None
|
||||
|
||||
def parseFile(self, file):
|
||||
"""Parse a document fragment from a file object, returning the
|
||||
fragment node."""
|
||||
return self.parseString(file.read())
|
||||
|
||||
def parseString(self, string):
|
||||
"""Parse a document fragment from a string, returning the
|
||||
fragment node."""
|
||||
self._source = string
|
||||
parser = self.getParser()
|
||||
doctype = self.originalDocument.doctype
|
||||
ident = ""
|
||||
if doctype:
|
||||
subset = doctype.internalSubset or self._getDeclarations()
|
||||
if doctype.publicId:
|
||||
ident = ('PUBLIC "%s" "%s"'
|
||||
% (doctype.publicId, doctype.systemId))
|
||||
elif doctype.systemId:
|
||||
ident = 'SYSTEM "%s"' % doctype.systemId
|
||||
else:
|
||||
subset = ""
|
||||
nsattrs = self._getNSattrs() # get ns decls from node's ancestors
|
||||
document = _FRAGMENT_BUILDER_TEMPLATE % (ident, subset, nsattrs)
|
||||
try:
|
||||
parser.Parse(document, 1)
|
||||
except:
|
||||
self.reset()
|
||||
raise
|
||||
fragment = self.fragment
|
||||
self.reset()
|
||||
## self._parser = None
|
||||
return fragment
|
||||
|
||||
def _getDeclarations(self):
|
||||
"""Re-create the internal subset from the DocumentType node.
|
||||
|
||||
This is only needed if we don't already have the
|
||||
internalSubset as a string.
|
||||
"""
|
||||
doctype = self.context.ownerDocument.doctype
|
||||
s = ""
|
||||
if doctype:
|
||||
for i in range(doctype.notations.length):
|
||||
notation = doctype.notations.item(i)
|
||||
if s:
|
||||
s = s + "\n "
|
||||
s = "%s<!NOTATION %s" % (s, notation.nodeName)
|
||||
if notation.publicId:
|
||||
s = '%s PUBLIC "%s"\n "%s">' \
|
||||
% (s, notation.publicId, notation.systemId)
|
||||
else:
|
||||
s = '%s SYSTEM "%s">' % (s, notation.systemId)
|
||||
for i in range(doctype.entities.length):
|
||||
entity = doctype.entities.item(i)
|
||||
if s:
|
||||
s = s + "\n "
|
||||
s = "%s<!ENTITY %s" % (s, entity.nodeName)
|
||||
if entity.publicId:
|
||||
s = '%s PUBLIC "%s"\n "%s"' \
|
||||
% (s, entity.publicId, entity.systemId)
|
||||
elif entity.systemId:
|
||||
s = '%s SYSTEM "%s"' % (s, entity.systemId)
|
||||
else:
|
||||
s = '%s "%s"' % (s, entity.firstChild.data)
|
||||
if entity.notationName:
|
||||
s = "%s NOTATION %s" % (s, entity.notationName)
|
||||
s = s + ">"
|
||||
return s
|
||||
|
||||
def _getNSattrs(self):
|
||||
return ""
|
||||
|
||||
def external_entity_ref_handler(self, context, base, systemId, publicId):
|
||||
if systemId == _FRAGMENT_BUILDER_INTERNAL_SYSTEM_ID:
|
||||
# this entref is the one that we made to put the subtree
|
||||
# in; all of our given input is parsed in here.
|
||||
old_document = self.document
|
||||
old_cur_node = self.curNode
|
||||
parser = self._parser.ExternalEntityParserCreate(context)
|
||||
# put the real document back, parse into the fragment to return
|
||||
self.document = self.originalDocument
|
||||
self.fragment = self.document.createDocumentFragment()
|
||||
self.curNode = self.fragment
|
||||
try:
|
||||
parser.Parse(self._source, 1)
|
||||
finally:
|
||||
self.curNode = old_cur_node
|
||||
self.document = old_document
|
||||
self._source = None
|
||||
return -1
|
||||
else:
|
||||
return ExpatBuilder.external_entity_ref_handler(
|
||||
self, context, base, systemId, publicId)
|
||||
|
||||
|
||||
class Namespaces:
|
||||
"""Mix-in class for builders; adds support for namespaces."""
|
||||
|
||||
def _initNamespaces(self):
|
||||
# list of (prefix, uri) ns declarations. Namespace attrs are
|
||||
# constructed from this and added to the element's attrs.
|
||||
self._ns_ordered_prefixes = []
|
||||
|
||||
def createParser(self):
|
||||
"""Create a new namespace-handling parser."""
|
||||
parser = expat.ParserCreate(namespace_separator=" ")
|
||||
parser.namespace_prefixes = True
|
||||
return parser
|
||||
|
||||
def install(self, parser):
|
||||
"""Insert the namespace-handlers onto the parser."""
|
||||
ExpatBuilder.install(self, parser)
|
||||
if self._options.namespace_declarations:
|
||||
parser.StartNamespaceDeclHandler = (
|
||||
self.start_namespace_decl_handler)
|
||||
|
||||
def start_namespace_decl_handler(self, prefix, uri):
|
||||
"""Push this namespace declaration on our storage."""
|
||||
self._ns_ordered_prefixes.append((prefix, uri))
|
||||
|
||||
def start_element_handler(self, name, attributes):
|
||||
if ' ' in name:
|
||||
uri, localname, prefix, qname = _parse_ns_name(self, name)
|
||||
else:
|
||||
uri = EMPTY_NAMESPACE
|
||||
qname = name
|
||||
localname = None
|
||||
prefix = EMPTY_PREFIX
|
||||
node = minidom.Element(qname, uri, prefix, localname)
|
||||
node.ownerDocument = self.document
|
||||
_append_child(self.curNode, node)
|
||||
self.curNode = node
|
||||
|
||||
if self._ns_ordered_prefixes:
|
||||
for prefix, uri in self._ns_ordered_prefixes:
|
||||
if prefix:
|
||||
a = minidom.Attr(_intern(self, 'xmlns:' + prefix),
|
||||
XMLNS_NAMESPACE, prefix, "xmlns")
|
||||
else:
|
||||
a = minidom.Attr("xmlns", XMLNS_NAMESPACE,
|
||||
"xmlns", EMPTY_PREFIX)
|
||||
d = a.childNodes[0].__dict__
|
||||
d['data'] = d['nodeValue'] = uri
|
||||
d = a.__dict__
|
||||
d['value'] = d['nodeValue'] = uri
|
||||
d['ownerDocument'] = self.document
|
||||
_set_attribute_node(node, a)
|
||||
del self._ns_ordered_prefixes[:]
|
||||
|
||||
if attributes:
|
||||
_attrs = node._attrs
|
||||
_attrsNS = node._attrsNS
|
||||
for i in range(0, len(attributes), 2):
|
||||
aname = attributes[i]
|
||||
value = attributes[i+1]
|
||||
if ' ' in aname:
|
||||
uri, localname, prefix, qname = _parse_ns_name(self, aname)
|
||||
a = minidom.Attr(qname, uri, localname, prefix)
|
||||
_attrs[qname] = a
|
||||
_attrsNS[(uri, localname)] = a
|
||||
else:
|
||||
a = minidom.Attr(aname, EMPTY_NAMESPACE,
|
||||
aname, EMPTY_PREFIX)
|
||||
_attrs[aname] = a
|
||||
_attrsNS[(EMPTY_NAMESPACE, aname)] = a
|
||||
d = a.childNodes[0].__dict__
|
||||
d['data'] = d['nodeValue'] = value
|
||||
d = a.__dict__
|
||||
d['ownerDocument'] = self.document
|
||||
d['value'] = d['nodeValue'] = value
|
||||
d['ownerElement'] = node
|
||||
|
||||
if __debug__:
|
||||
# This only adds some asserts to the original
|
||||
# end_element_handler(), so we only define this when -O is not
|
||||
# used. If changing one, be sure to check the other to see if
|
||||
# it needs to be changed as well.
|
||||
#
|
||||
def end_element_handler(self, name):
|
||||
curNode = self.curNode
|
||||
if ' ' in name:
|
||||
uri, localname, prefix, qname = _parse_ns_name(self, name)
|
||||
assert (curNode.namespaceURI == uri
|
||||
and curNode.localName == localname
|
||||
and curNode.prefix == prefix), \
|
||||
"element stack messed up! (namespace)"
|
||||
else:
|
||||
assert curNode.nodeName == name, \
|
||||
"element stack messed up - bad nodeName"
|
||||
assert curNode.namespaceURI == EMPTY_NAMESPACE, \
|
||||
"element stack messed up - bad namespaceURI"
|
||||
self.curNode = curNode.parentNode
|
||||
self._finish_end_element(curNode)
|
||||
|
||||
|
||||
class ExpatBuilderNS(Namespaces, ExpatBuilder):
|
||||
"""Document builder that supports namespaces."""
|
||||
|
||||
def reset(self):
|
||||
ExpatBuilder.reset(self)
|
||||
self._initNamespaces()
|
||||
|
||||
|
||||
class FragmentBuilderNS(Namespaces, FragmentBuilder):
|
||||
"""Fragment builder that supports namespaces."""
|
||||
|
||||
def reset(self):
|
||||
FragmentBuilder.reset(self)
|
||||
self._initNamespaces()
|
||||
|
||||
def _getNSattrs(self):
|
||||
"""Return string of namespace attributes from this element and
|
||||
ancestors."""
|
||||
# XXX This needs to be re-written to walk the ancestors of the
|
||||
# context to build up the namespace information from
|
||||
# declarations, elements, and attributes found in context.
|
||||
# Otherwise we have to store a bunch more data on the DOM
|
||||
# (though that *might* be more reliable -- not clear).
|
||||
attrs = ""
|
||||
context = self.context
|
||||
L = []
|
||||
while context:
|
||||
if hasattr(context, '_ns_prefix_uri'):
|
||||
for prefix, uri in context._ns_prefix_uri.items():
|
||||
# add every new NS decl from context to L and attrs string
|
||||
if prefix in L:
|
||||
continue
|
||||
L.append(prefix)
|
||||
if prefix:
|
||||
declname = "xmlns:" + prefix
|
||||
else:
|
||||
declname = "xmlns"
|
||||
if attrs:
|
||||
attrs = "%s\n %s='%s'" % (attrs, declname, uri)
|
||||
else:
|
||||
attrs = " %s='%s'" % (declname, uri)
|
||||
context = context.parentNode
|
||||
return attrs
|
||||
|
||||
|
||||
class ParseEscape(Exception):
|
||||
"""Exception raised to short-circuit parsing in InternalSubsetExtractor."""
|
||||
pass
|
||||
|
||||
class InternalSubsetExtractor(ExpatBuilder):
|
||||
"""XML processor which can rip out the internal document type subset."""
|
||||
|
||||
subset = None
|
||||
|
||||
def getSubset(self):
|
||||
"""Return the internal subset as a string."""
|
||||
return self.subset
|
||||
|
||||
def parseFile(self, file):
|
||||
try:
|
||||
ExpatBuilder.parseFile(self, file)
|
||||
except ParseEscape:
|
||||
pass
|
||||
|
||||
def parseString(self, string):
|
||||
try:
|
||||
ExpatBuilder.parseString(self, string)
|
||||
except ParseEscape:
|
||||
pass
|
||||
|
||||
def install(self, parser):
|
||||
parser.StartDoctypeDeclHandler = self.start_doctype_decl_handler
|
||||
parser.StartElementHandler = self.start_element_handler
|
||||
|
||||
def start_doctype_decl_handler(self, name, publicId, systemId,
|
||||
has_internal_subset):
|
||||
if has_internal_subset:
|
||||
parser = self.getParser()
|
||||
self.subset = []
|
||||
parser.DefaultHandler = self.subset.append
|
||||
parser.EndDoctypeDeclHandler = self.end_doctype_decl_handler
|
||||
else:
|
||||
raise ParseEscape()
|
||||
|
||||
def end_doctype_decl_handler(self):
|
||||
s = ''.join(self.subset).replace('\r\n', '\n').replace('\r', '\n')
|
||||
self.subset = s
|
||||
raise ParseEscape()
|
||||
|
||||
def start_element_handler(self, name, attrs):
|
||||
raise ParseEscape()
|
||||
|
||||
|
||||
def parse(file, namespaces=True):
|
||||
"""Parse a document, returning the resulting Document node.
|
||||
|
||||
'file' may be either a file name or an open file object.
|
||||
"""
|
||||
if namespaces:
|
||||
builder = ExpatBuilderNS()
|
||||
else:
|
||||
builder = ExpatBuilder()
|
||||
|
||||
if isinstance(file, StringTypes):
|
||||
fp = open(file, 'rb')
|
||||
try:
|
||||
result = builder.parseFile(fp)
|
||||
finally:
|
||||
fp.close()
|
||||
else:
|
||||
result = builder.parseFile(file)
|
||||
return result
|
||||
|
||||
|
||||
def parseString(string, namespaces=True):
|
||||
"""Parse a document from a string, returning the resulting
|
||||
Document node.
|
||||
"""
|
||||
if namespaces:
|
||||
builder = ExpatBuilderNS()
|
||||
else:
|
||||
builder = ExpatBuilder()
|
||||
return builder.parseString(string)
|
||||
|
||||
|
||||
def parseFragment(file, context, namespaces=True):
|
||||
"""Parse a fragment of a document, given the context from which it
|
||||
was originally extracted. context should be the parent of the
|
||||
node(s) which are in the fragment.
|
||||
|
||||
'file' may be either a file name or an open file object.
|
||||
"""
|
||||
if namespaces:
|
||||
builder = FragmentBuilderNS(context)
|
||||
else:
|
||||
builder = FragmentBuilder(context)
|
||||
|
||||
if isinstance(file, StringTypes):
|
||||
fp = open(file, 'rb')
|
||||
try:
|
||||
result = builder.parseFile(fp)
|
||||
finally:
|
||||
fp.close()
|
||||
else:
|
||||
result = builder.parseFile(file)
|
||||
return result
|
||||
|
||||
|
||||
def parseFragmentString(string, context, namespaces=True):
|
||||
"""Parse a fragment of a document from a string, given the context
|
||||
from which it was originally extracted. context should be the
|
||||
parent of the node(s) which are in the fragment.
|
||||
"""
|
||||
if namespaces:
|
||||
builder = FragmentBuilderNS(context)
|
||||
else:
|
||||
builder = FragmentBuilder(context)
|
||||
return builder.parseString(string)
|
||||
|
||||
|
||||
def makeBuilder(options):
|
||||
"""Create a builder based on an Options object."""
|
||||
if options.namespaces:
|
||||
return ExpatBuilderNS(options)
|
||||
else:
|
||||
return ExpatBuilder(options)
|
|
@ -0,0 +1,110 @@
|
|||
"""Python version compatibility support for minidom."""
|
||||
|
||||
# This module should only be imported using "import *".
|
||||
#
|
||||
# The following names are defined:
|
||||
#
|
||||
# NodeList -- lightest possible NodeList implementation
|
||||
#
|
||||
# EmptyNodeList -- lightest possible NodeList that is guarateed to
|
||||
# remain empty (immutable)
|
||||
#
|
||||
# StringTypes -- tuple of defined string types
|
||||
#
|
||||
# defproperty -- function used in conjunction with GetattrMagic;
|
||||
# using these together is needed to make them work
|
||||
# as efficiently as possible in both Python 2.2+
|
||||
# and older versions. For example:
|
||||
#
|
||||
# class MyClass(GetattrMagic):
|
||||
# def _get_myattr(self):
|
||||
# return something
|
||||
#
|
||||
# defproperty(MyClass, "myattr",
|
||||
# "return some value")
|
||||
#
|
||||
# For Python 2.2 and newer, this will construct a
|
||||
# property object on the class, which avoids
|
||||
# needing to override __getattr__(). It will only
|
||||
# work for read-only attributes.
|
||||
#
|
||||
# For older versions of Python, inheriting from
|
||||
# GetattrMagic will use the traditional
|
||||
# __getattr__() hackery to achieve the same effect,
|
||||
# but less efficiently.
|
||||
#
|
||||
# defproperty() should be used for each version of
|
||||
# the relevant _get_<property>() function.
|
||||
|
||||
__all__ = ["NodeList", "EmptyNodeList", "StringTypes", "defproperty"]
|
||||
|
||||
import xml.dom
|
||||
|
||||
try:
|
||||
unicode
|
||||
except NameError:
|
||||
StringTypes = type(''),
|
||||
else:
|
||||
StringTypes = type(''), type(unicode(''))
|
||||
|
||||
|
||||
class NodeList(list):
|
||||
__slots__ = ()
|
||||
|
||||
def item(self, index):
|
||||
if 0 <= index < len(self):
|
||||
return self[index]
|
||||
|
||||
def _get_length(self):
|
||||
return len(self)
|
||||
|
||||
def _set_length(self, value):
|
||||
raise xml.dom.NoModificationAllowedErr(
|
||||
"attempt to modify read-only attribute 'length'")
|
||||
|
||||
length = property(_get_length, _set_length,
|
||||
doc="The number of nodes in the NodeList.")
|
||||
|
||||
def __getstate__(self):
|
||||
return list(self)
|
||||
|
||||
def __setstate__(self, state):
|
||||
self[:] = state
|
||||
|
||||
|
||||
class EmptyNodeList(tuple):
|
||||
__slots__ = ()
|
||||
|
||||
def __add__(self, other):
|
||||
NL = NodeList()
|
||||
NL.extend(other)
|
||||
return NL
|
||||
|
||||
def __radd__(self, other):
|
||||
NL = NodeList()
|
||||
NL.extend(other)
|
||||
return NL
|
||||
|
||||
def item(self, index):
|
||||
return None
|
||||
|
||||
def _get_length(self):
|
||||
return 0
|
||||
|
||||
def _set_length(self, value):
|
||||
raise xml.dom.NoModificationAllowedErr(
|
||||
"attempt to modify read-only attribute 'length'")
|
||||
|
||||
length = property(_get_length, _set_length,
|
||||
doc="The number of nodes in the NodeList.")
|
||||
|
||||
|
||||
def defproperty(klass, name, doc):
|
||||
get = getattr(klass, ("_get_" + name)).im_func
|
||||
def set(self, value, name=name):
|
||||
raise xml.dom.NoModificationAllowedErr(
|
||||
"attempt to modify read-only attribute " + repr(name))
|
||||
assert not hasattr(klass, "_set_" + name), \
|
||||
"expected not to find _set_" + name
|
||||
prop = property(get, set, doc=doc)
|
||||
setattr(klass, name, prop)
|
|
@ -0,0 +1,351 @@
|
|||
import xml.sax
|
||||
import xml.sax.handler
|
||||
import types
|
||||
|
||||
try:
|
||||
_StringTypes = [types.StringType, types.UnicodeType]
|
||||
except AttributeError:
|
||||
_StringTypes = [types.StringType]
|
||||
|
||||
START_ELEMENT = "START_ELEMENT"
|
||||
END_ELEMENT = "END_ELEMENT"
|
||||
COMMENT = "COMMENT"
|
||||
START_DOCUMENT = "START_DOCUMENT"
|
||||
END_DOCUMENT = "END_DOCUMENT"
|
||||
PROCESSING_INSTRUCTION = "PROCESSING_INSTRUCTION"
|
||||
IGNORABLE_WHITESPACE = "IGNORABLE_WHITESPACE"
|
||||
CHARACTERS = "CHARACTERS"
|
||||
|
||||
class PullDOM(xml.sax.ContentHandler):
|
||||
_locator = None
|
||||
document = None
|
||||
|
||||
def __init__(self, documentFactory=None):
|
||||
from xml.dom import XML_NAMESPACE
|
||||
self.documentFactory = documentFactory
|
||||
self.firstEvent = [None, None]
|
||||
self.lastEvent = self.firstEvent
|
||||
self.elementStack = []
|
||||
self.push = self.elementStack.append
|
||||
try:
|
||||
self.pop = self.elementStack.pop
|
||||
except AttributeError:
|
||||
# use class' pop instead
|
||||
pass
|
||||
self._ns_contexts = [{XML_NAMESPACE:'xml'}] # contains uri -> prefix dicts
|
||||
self._current_context = self._ns_contexts[-1]
|
||||
self.pending_events = []
|
||||
|
||||
def pop(self):
|
||||
result = self.elementStack[-1]
|
||||
del self.elementStack[-1]
|
||||
return result
|
||||
|
||||
def setDocumentLocator(self, locator):
|
||||
self._locator = locator
|
||||
|
||||
def startPrefixMapping(self, prefix, uri):
|
||||
if not hasattr(self, '_xmlns_attrs'):
|
||||
self._xmlns_attrs = []
|
||||
self._xmlns_attrs.append((prefix or 'xmlns', uri))
|
||||
self._ns_contexts.append(self._current_context.copy())
|
||||
self._current_context[uri] = prefix or None
|
||||
|
||||
def endPrefixMapping(self, prefix):
|
||||
self._current_context = self._ns_contexts.pop()
|
||||
|
||||
def startElementNS(self, name, tagName , attrs):
|
||||
# Retrieve xml namespace declaration attributes.
|
||||
xmlns_uri = 'http://www.w3.org/2000/xmlns/'
|
||||
xmlns_attrs = getattr(self, '_xmlns_attrs', None)
|
||||
if xmlns_attrs is not None:
|
||||
for aname, value in xmlns_attrs:
|
||||
attrs._attrs[(xmlns_uri, aname)] = value
|
||||
self._xmlns_attrs = []
|
||||
uri, localname = name
|
||||
if uri:
|
||||
# When using namespaces, the reader may or may not
|
||||
# provide us with the original name. If not, create
|
||||
# *a* valid tagName from the current context.
|
||||
if tagName is None:
|
||||
prefix = self._current_context[uri]
|
||||
if prefix:
|
||||
tagName = prefix + ":" + localname
|
||||
else:
|
||||
tagName = localname
|
||||
if self.document:
|
||||
node = self.document.createElementNS(uri, tagName)
|
||||
else:
|
||||
node = self.buildDocument(uri, tagName)
|
||||
else:
|
||||
# When the tagname is not prefixed, it just appears as
|
||||
# localname
|
||||
if self.document:
|
||||
node = self.document.createElement(localname)
|
||||
else:
|
||||
node = self.buildDocument(None, localname)
|
||||
|
||||
for aname,value in attrs.items():
|
||||
a_uri, a_localname = aname
|
||||
if a_uri == xmlns_uri:
|
||||
if a_localname == 'xmlns':
|
||||
qname = a_localname
|
||||
else:
|
||||
qname = 'xmlns:' + a_localname
|
||||
attr = self.document.createAttributeNS(a_uri, qname)
|
||||
node.setAttributeNodeNS(attr)
|
||||
elif a_uri:
|
||||
prefix = self._current_context[a_uri]
|
||||
if prefix:
|
||||
qname = prefix + ":" + a_localname
|
||||
else:
|
||||
qname = a_localname
|
||||
attr = self.document.createAttributeNS(a_uri, qname)
|
||||
node.setAttributeNodeNS(attr)
|
||||
else:
|
||||
attr = self.document.createAttribute(a_localname)
|
||||
node.setAttributeNode(attr)
|
||||
attr.value = value
|
||||
|
||||
self.lastEvent[1] = [(START_ELEMENT, node), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
self.push(node)
|
||||
|
||||
def endElementNS(self, name, tagName):
|
||||
self.lastEvent[1] = [(END_ELEMENT, self.pop()), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
|
||||
def startElement(self, name, attrs):
|
||||
if self.document:
|
||||
node = self.document.createElement(name)
|
||||
else:
|
||||
node = self.buildDocument(None, name)
|
||||
|
||||
for aname,value in attrs.items():
|
||||
attr = self.document.createAttribute(aname)
|
||||
attr.value = value
|
||||
node.setAttributeNode(attr)
|
||||
|
||||
self.lastEvent[1] = [(START_ELEMENT, node), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
self.push(node)
|
||||
|
||||
def endElement(self, name):
|
||||
self.lastEvent[1] = [(END_ELEMENT, self.pop()), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
|
||||
def comment(self, s):
|
||||
if self.document:
|
||||
node = self.document.createComment(s)
|
||||
self.lastEvent[1] = [(COMMENT, node), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
else:
|
||||
event = [(COMMENT, s), None]
|
||||
self.pending_events.append(event)
|
||||
|
||||
def processingInstruction(self, target, data):
|
||||
if self.document:
|
||||
node = self.document.createProcessingInstruction(target, data)
|
||||
self.lastEvent[1] = [(PROCESSING_INSTRUCTION, node), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
else:
|
||||
event = [(PROCESSING_INSTRUCTION, target, data), None]
|
||||
self.pending_events.append(event)
|
||||
|
||||
def ignorableWhitespace(self, chars):
|
||||
node = self.document.createTextNode(chars)
|
||||
self.lastEvent[1] = [(IGNORABLE_WHITESPACE, node), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
|
||||
def characters(self, chars):
|
||||
node = self.document.createTextNode(chars)
|
||||
self.lastEvent[1] = [(CHARACTERS, node), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
|
||||
def startDocument(self):
|
||||
if self.documentFactory is None:
|
||||
import xml.dom.minidom
|
||||
self.documentFactory = xml.dom.minidom.Document.implementation
|
||||
|
||||
def buildDocument(self, uri, tagname):
|
||||
# Can't do that in startDocument, since we need the tagname
|
||||
# XXX: obtain DocumentType
|
||||
node = self.documentFactory.createDocument(uri, tagname, None)
|
||||
self.document = node
|
||||
self.lastEvent[1] = [(START_DOCUMENT, node), None]
|
||||
self.lastEvent = self.lastEvent[1]
|
||||
self.push(node)
|
||||
# Put everything we have seen so far into the document
|
||||
for e in self.pending_events:
|
||||
if e[0][0] == PROCESSING_INSTRUCTION:
|
||||
_,target,data = e[0]
|
||||
n = self.document.createProcessingInstruction(target, data)
|
||||
e[0] = (PROCESSING_INSTRUCTION, n)
|
||||
elif e[0][0] == COMMENT:
|
||||
n = self.document.createComment(e[0][1])
|
||||
e[0] = (COMMENT, n)
|
||||
else:
|
||||
raise AssertionError("Unknown pending event ",e[0][0])
|
||||
self.lastEvent[1] = e
|
||||
self.lastEvent = e
|
||||
self.pending_events = None
|
||||
return node.firstChild
|
||||
|
||||
def endDocument(self):
|
||||
self.lastEvent[1] = [(END_DOCUMENT, self.document), None]
|
||||
self.pop()
|
||||
|
||||
def clear(self):
|
||||
"clear(): Explicitly release parsing structures"
|
||||
self.document = None
|
||||
|
||||
class ErrorHandler:
|
||||
def warning(self, exception):
|
||||
print exception
|
||||
def error(self, exception):
|
||||
raise exception
|
||||
def fatalError(self, exception):
|
||||
raise exception
|
||||
|
||||
class DOMEventStream:
|
||||
def __init__(self, stream, parser, bufsize):
|
||||
self.stream = stream
|
||||
self.parser = parser
|
||||
self.bufsize = bufsize
|
||||
if not hasattr(self.parser, 'feed'):
|
||||
self.getEvent = self._slurp
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.pulldom = PullDOM()
|
||||
# This content handler relies on namespace support
|
||||
self.parser.setFeature(xml.sax.handler.feature_namespaces, 1)
|
||||
self.parser.setContentHandler(self.pulldom)
|
||||
|
||||
def __getitem__(self, pos):
|
||||
rc = self.getEvent()
|
||||
if rc:
|
||||
return rc
|
||||
raise IndexError
|
||||
|
||||
def next(self):
|
||||
rc = self.getEvent()
|
||||
if rc:
|
||||
return rc
|
||||
raise StopIteration
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def expandNode(self, node):
|
||||
event = self.getEvent()
|
||||
parents = [node]
|
||||
while event:
|
||||
token, cur_node = event
|
||||
if cur_node is node:
|
||||
return
|
||||
if token != END_ELEMENT:
|
||||
parents[-1].appendChild(cur_node)
|
||||
if token == START_ELEMENT:
|
||||
parents.append(cur_node)
|
||||
elif token == END_ELEMENT:
|
||||
del parents[-1]
|
||||
event = self.getEvent()
|
||||
|
||||
def getEvent(self):
|
||||
# use IncrementalParser interface, so we get the desired
|
||||
# pull effect
|
||||
if not self.pulldom.firstEvent[1]:
|
||||
self.pulldom.lastEvent = self.pulldom.firstEvent
|
||||
while not self.pulldom.firstEvent[1]:
|
||||
buf = self.stream.read(self.bufsize)
|
||||
if not buf:
|
||||
self.parser.close()
|
||||
return None
|
||||
self.parser.feed(buf)
|
||||
rc = self.pulldom.firstEvent[1][0]
|
||||
self.pulldom.firstEvent[1] = self.pulldom.firstEvent[1][1]
|
||||
return rc
|
||||
|
||||
def _slurp(self):
|
||||
""" Fallback replacement for getEvent() using the
|
||||
standard SAX2 interface, which means we slurp the
|
||||
SAX events into memory (no performance gain, but
|
||||
we are compatible to all SAX parsers).
|
||||
"""
|
||||
self.parser.parse(self.stream)
|
||||
self.getEvent = self._emit
|
||||
return self._emit()
|
||||
|
||||
def _emit(self):
|
||||
""" Fallback replacement for getEvent() that emits
|
||||
the events that _slurp() read previously.
|
||||
"""
|
||||
rc = self.pulldom.firstEvent[1][0]
|
||||
self.pulldom.firstEvent[1] = self.pulldom.firstEvent[1][1]
|
||||
return rc
|
||||
|
||||
def clear(self):
|
||||
"""clear(): Explicitly release parsing objects"""
|
||||
self.pulldom.clear()
|
||||
del self.pulldom
|
||||
self.parser = None
|
||||
self.stream = None
|
||||
|
||||
class SAX2DOM(PullDOM):
|
||||
|
||||
def startElementNS(self, name, tagName , attrs):
|
||||
PullDOM.startElementNS(self, name, tagName, attrs)
|
||||
curNode = self.elementStack[-1]
|
||||
parentNode = self.elementStack[-2]
|
||||
parentNode.appendChild(curNode)
|
||||
|
||||
def startElement(self, name, attrs):
|
||||
PullDOM.startElement(self, name, attrs)
|
||||
curNode = self.elementStack[-1]
|
||||
parentNode = self.elementStack[-2]
|
||||
parentNode.appendChild(curNode)
|
||||
|
||||
def processingInstruction(self, target, data):
|
||||
PullDOM.processingInstruction(self, target, data)
|
||||
node = self.lastEvent[0][1]
|
||||
parentNode = self.elementStack[-1]
|
||||
parentNode.appendChild(node)
|
||||
|
||||
def ignorableWhitespace(self, chars):
|
||||
PullDOM.ignorableWhitespace(self, chars)
|
||||
node = self.lastEvent[0][1]
|
||||
parentNode = self.elementStack[-1]
|
||||
parentNode.appendChild(node)
|
||||
|
||||
def characters(self, chars):
|
||||
PullDOM.characters(self, chars)
|
||||
node = self.lastEvent[0][1]
|
||||
parentNode = self.elementStack[-1]
|
||||
parentNode.appendChild(node)
|
||||
|
||||
|
||||
default_bufsize = (2 ** 14) - 20
|
||||
|
||||
def parse(stream_or_string, parser=None, bufsize=None):
|
||||
if bufsize is None:
|
||||
bufsize = default_bufsize
|
||||
if type(stream_or_string) in _StringTypes:
|
||||
stream = open(stream_or_string)
|
||||
else:
|
||||
stream = stream_or_string
|
||||
if not parser:
|
||||
parser = xml.sax.make_parser()
|
||||
return DOMEventStream(stream, parser, bufsize)
|
||||
|
||||
def parseString(string, parser=None):
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
bufsize = len(string)
|
||||
buf = StringIO(string)
|
||||
if not parser:
|
||||
parser = xml.sax.make_parser()
|
||||
return DOMEventStream(buf, parser, bufsize)
|
|
@ -0,0 +1,386 @@
|
|||
"""Implementation of the DOM Level 3 'LS-Load' feature."""
|
||||
|
||||
import copy
|
||||
import xml.dom
|
||||
|
||||
from xml.dom.NodeFilter import NodeFilter
|
||||
|
||||
|
||||
__all__ = ["DOMBuilder", "DOMEntityResolver", "DOMInputSource"]
|
||||
|
||||
|
||||
class Options:
|
||||
"""Features object that has variables set for each DOMBuilder feature.
|
||||
|
||||
The DOMBuilder class uses an instance of this class to pass settings to
|
||||
the ExpatBuilder class.
|
||||
"""
|
||||
|
||||
# Note that the DOMBuilder class in LoadSave constrains which of these
|
||||
# values can be set using the DOM Level 3 LoadSave feature.
|
||||
|
||||
namespaces = 1
|
||||
namespace_declarations = True
|
||||
validation = False
|
||||
external_parameter_entities = True
|
||||
external_general_entities = True
|
||||
external_dtd_subset = True
|
||||
validate_if_schema = False
|
||||
validate = False
|
||||
datatype_normalization = False
|
||||
create_entity_ref_nodes = True
|
||||
entities = True
|
||||
whitespace_in_element_content = True
|
||||
cdata_sections = True
|
||||
comments = True
|
||||
charset_overrides_xml_encoding = True
|
||||
infoset = False
|
||||
supported_mediatypes_only = False
|
||||
|
||||
errorHandler = None
|
||||
filter = None
|
||||
|
||||
|
||||
class DOMBuilder:
|
||||
entityResolver = None
|
||||
errorHandler = None
|
||||
filter = None
|
||||
|
||||
ACTION_REPLACE = 1
|
||||
ACTION_APPEND_AS_CHILDREN = 2
|
||||
ACTION_INSERT_AFTER = 3
|
||||
ACTION_INSERT_BEFORE = 4
|
||||
|
||||
_legal_actions = (ACTION_REPLACE, ACTION_APPEND_AS_CHILDREN,
|
||||
ACTION_INSERT_AFTER, ACTION_INSERT_BEFORE)
|
||||
|
||||
def __init__(self):
|
||||
self._options = Options()
|
||||
|
||||
def _get_entityResolver(self):
|
||||
return self.entityResolver
|
||||
def _set_entityResolver(self, entityResolver):
|
||||
self.entityResolver = entityResolver
|
||||
|
||||
def _get_errorHandler(self):
|
||||
return self.errorHandler
|
||||
def _set_errorHandler(self, errorHandler):
|
||||
self.errorHandler = errorHandler
|
||||
|
||||
def _get_filter(self):
|
||||
return self.filter
|
||||
def _set_filter(self, filter):
|
||||
self.filter = filter
|
||||
|
||||
def setFeature(self, name, state):
|
||||
if self.supportsFeature(name):
|
||||
state = state and 1 or 0
|
||||
try:
|
||||
settings = self._settings[(_name_xform(name), state)]
|
||||
except KeyError:
|
||||
raise xml.dom.NotSupportedErr(
|
||||
"unsupported feature: %r" % (name,))
|
||||
else:
|
||||
for name, value in settings:
|
||||
setattr(self._options, name, value)
|
||||
else:
|
||||
raise xml.dom.NotFoundErr("unknown feature: " + repr(name))
|
||||
|
||||
def supportsFeature(self, name):
|
||||
return hasattr(self._options, _name_xform(name))
|
||||
|
||||
def canSetFeature(self, name, state):
|
||||
key = (_name_xform(name), state and 1 or 0)
|
||||
return self._settings.has_key(key)
|
||||
|
||||
# This dictionary maps from (feature,value) to a list of
|
||||
# (option,value) pairs that should be set on the Options object.
|
||||
# If a (feature,value) setting is not in this dictionary, it is
|
||||
# not supported by the DOMBuilder.
|
||||
#
|
||||
_settings = {
|
||||
("namespace_declarations", 0): [
|
||||
("namespace_declarations", 0)],
|
||||
("namespace_declarations", 1): [
|
||||
("namespace_declarations", 1)],
|
||||
("validation", 0): [
|
||||
("validation", 0)],
|
||||
("external_general_entities", 0): [
|
||||
("external_general_entities", 0)],
|
||||
("external_general_entities", 1): [
|
||||
("external_general_entities", 1)],
|
||||
("external_parameter_entities", 0): [
|
||||
("external_parameter_entities", 0)],
|
||||
("external_parameter_entities", 1): [
|
||||
("external_parameter_entities", 1)],
|
||||
("validate_if_schema", 0): [
|
||||
("validate_if_schema", 0)],
|
||||
("create_entity_ref_nodes", 0): [
|
||||
("create_entity_ref_nodes", 0)],
|
||||
("create_entity_ref_nodes", 1): [
|
||||
("create_entity_ref_nodes", 1)],
|
||||
("entities", 0): [
|
||||
("create_entity_ref_nodes", 0),
|
||||
("entities", 0)],
|
||||
("entities", 1): [
|
||||
("entities", 1)],
|
||||
("whitespace_in_element_content", 0): [
|
||||
("whitespace_in_element_content", 0)],
|
||||
("whitespace_in_element_content", 1): [
|
||||
("whitespace_in_element_content", 1)],
|
||||
("cdata_sections", 0): [
|
||||
("cdata_sections", 0)],
|
||||
("cdata_sections", 1): [
|
||||
("cdata_sections", 1)],
|
||||
("comments", 0): [
|
||||
("comments", 0)],
|
||||
("comments", 1): [
|
||||
("comments", 1)],
|
||||
("charset_overrides_xml_encoding", 0): [
|
||||
("charset_overrides_xml_encoding", 0)],
|
||||
("charset_overrides_xml_encoding", 1): [
|
||||
("charset_overrides_xml_encoding", 1)],
|
||||
("infoset", 0): [],
|
||||
("infoset", 1): [
|
||||
("namespace_declarations", 0),
|
||||
("validate_if_schema", 0),
|
||||
("create_entity_ref_nodes", 0),
|
||||
("entities", 0),
|
||||
("cdata_sections", 0),
|
||||
("datatype_normalization", 1),
|
||||
("whitespace_in_element_content", 1),
|
||||
("comments", 1),
|
||||
("charset_overrides_xml_encoding", 1)],
|
||||
("supported_mediatypes_only", 0): [
|
||||
("supported_mediatypes_only", 0)],
|
||||
("namespaces", 0): [
|
||||
("namespaces", 0)],
|
||||
("namespaces", 1): [
|
||||
("namespaces", 1)],
|
||||
}
|
||||
|
||||
def getFeature(self, name):
|
||||
xname = _name_xform(name)
|
||||
try:
|
||||
return getattr(self._options, xname)
|
||||
except AttributeError:
|
||||
if name == "infoset":
|
||||
options = self._options
|
||||
return (options.datatype_normalization
|
||||
and options.whitespace_in_element_content
|
||||
and options.comments
|
||||
and options.charset_overrides_xml_encoding
|
||||
and not (options.namespace_declarations
|
||||
or options.validate_if_schema
|
||||
or options.create_entity_ref_nodes
|
||||
or options.entities
|
||||
or options.cdata_sections))
|
||||
raise xml.dom.NotFoundErr("feature %s not known" % repr(name))
|
||||
|
||||
def parseURI(self, uri):
|
||||
if self.entityResolver:
|
||||
input = self.entityResolver.resolveEntity(None, uri)
|
||||
else:
|
||||
input = DOMEntityResolver().resolveEntity(None, uri)
|
||||
return self.parse(input)
|
||||
|
||||
def parse(self, input):
|
||||
options = copy.copy(self._options)
|
||||
options.filter = self.filter
|
||||
options.errorHandler = self.errorHandler
|
||||
fp = input.byteStream
|
||||
if fp is None and options.systemId:
|
||||
import urllib2
|
||||
fp = urllib2.urlopen(input.systemId)
|
||||
return self._parse_bytestream(fp, options)
|
||||
|
||||
def parseWithContext(self, input, cnode, action):
|
||||
if action not in self._legal_actions:
|
||||
raise ValueError("not a legal action")
|
||||
raise NotImplementedError("Haven't written this yet...")
|
||||
|
||||
def _parse_bytestream(self, stream, options):
|
||||
import xml.dom.expatbuilder
|
||||
builder = xml.dom.expatbuilder.makeBuilder(options)
|
||||
return builder.parseFile(stream)
|
||||
|
||||
|
||||
def _name_xform(name):
|
||||
return name.lower().replace('-', '_')
|
||||
|
||||
|
||||
class DOMEntityResolver(object):
|
||||
__slots__ = '_opener',
|
||||
|
||||
def resolveEntity(self, publicId, systemId):
|
||||
assert systemId is not None
|
||||
source = DOMInputSource()
|
||||
source.publicId = publicId
|
||||
source.systemId = systemId
|
||||
source.byteStream = self._get_opener().open(systemId)
|
||||
|
||||
# determine the encoding if the transport provided it
|
||||
source.encoding = self._guess_media_encoding(source)
|
||||
|
||||
# determine the base URI is we can
|
||||
import posixpath, urlparse
|
||||
parts = urlparse.urlparse(systemId)
|
||||
scheme, netloc, path, params, query, fragment = parts
|
||||
# XXX should we check the scheme here as well?
|
||||
if path and not path.endswith("/"):
|
||||
path = posixpath.dirname(path) + "/"
|
||||
parts = scheme, netloc, path, params, query, fragment
|
||||
source.baseURI = urlparse.urlunparse(parts)
|
||||
|
||||
return source
|
||||
|
||||
def _get_opener(self):
|
||||
try:
|
||||
return self._opener
|
||||
except AttributeError:
|
||||
self._opener = self._create_opener()
|
||||
return self._opener
|
||||
|
||||
def _create_opener(self):
|
||||
import urllib2
|
||||
return urllib2.build_opener()
|
||||
|
||||
def _guess_media_encoding(self, source):
|
||||
info = source.byteStream.info()
|
||||
if info.has_key("Content-Type"):
|
||||
for param in info.getplist():
|
||||
if param.startswith("charset="):
|
||||
return param.split("=", 1)[1].lower()
|
||||
|
||||
|
||||
class DOMInputSource(object):
|
||||
__slots__ = ('byteStream', 'characterStream', 'stringData',
|
||||
'encoding', 'publicId', 'systemId', 'baseURI')
|
||||
|
||||
def __init__(self):
|
||||
self.byteStream = None
|
||||
self.characterStream = None
|
||||
self.stringData = None
|
||||
self.encoding = None
|
||||
self.publicId = None
|
||||
self.systemId = None
|
||||
self.baseURI = None
|
||||
|
||||
def _get_byteStream(self):
|
||||
return self.byteStream
|
||||
def _set_byteStream(self, byteStream):
|
||||
self.byteStream = byteStream
|
||||
|
||||
def _get_characterStream(self):
|
||||
return self.characterStream
|
||||
def _set_characterStream(self, characterStream):
|
||||
self.characterStream = characterStream
|
||||
|
||||
def _get_stringData(self):
|
||||
return self.stringData
|
||||
def _set_stringData(self, data):
|
||||
self.stringData = data
|
||||
|
||||
def _get_encoding(self):
|
||||
return self.encoding
|
||||
def _set_encoding(self, encoding):
|
||||
self.encoding = encoding
|
||||
|
||||
def _get_publicId(self):
|
||||
return self.publicId
|
||||
def _set_publicId(self, publicId):
|
||||
self.publicId = publicId
|
||||
|
||||
def _get_systemId(self):
|
||||
return self.systemId
|
||||
def _set_systemId(self, systemId):
|
||||
self.systemId = systemId
|
||||
|
||||
def _get_baseURI(self):
|
||||
return self.baseURI
|
||||
def _set_baseURI(self, uri):
|
||||
self.baseURI = uri
|
||||
|
||||
|
||||
class DOMBuilderFilter:
|
||||
"""Element filter which can be used to tailor construction of
|
||||
a DOM instance.
|
||||
"""
|
||||
|
||||
# There's really no need for this class; concrete implementations
|
||||
# should just implement the endElement() and startElement()
|
||||
# methods as appropriate. Using this makes it easy to only
|
||||
# implement one of them.
|
||||
|
||||
FILTER_ACCEPT = 1
|
||||
FILTER_REJECT = 2
|
||||
FILTER_SKIP = 3
|
||||
FILTER_INTERRUPT = 4
|
||||
|
||||
whatToShow = NodeFilter.SHOW_ALL
|
||||
|
||||
def _get_whatToShow(self):
|
||||
return self.whatToShow
|
||||
|
||||
def acceptNode(self, element):
|
||||
return self.FILTER_ACCEPT
|
||||
|
||||
def startContainer(self, element):
|
||||
return self.FILTER_ACCEPT
|
||||
|
||||
del NodeFilter
|
||||
|
||||
|
||||
class DocumentLS:
|
||||
"""Mixin to create documents that conform to the load/save spec."""
|
||||
|
||||
async = False
|
||||
|
||||
def _get_async(self):
|
||||
return False
|
||||
def _set_async(self, async):
|
||||
if async:
|
||||
raise xml.dom.NotSupportedErr(
|
||||
"asynchronous document loading is not supported")
|
||||
|
||||
def abort(self):
|
||||
# What does it mean to "clear" a document? Does the
|
||||
# documentElement disappear?
|
||||
raise NotImplementedError(
|
||||
"haven't figured out what this means yet")
|
||||
|
||||
def load(self, uri):
|
||||
raise NotImplementedError("haven't written this yet")
|
||||
|
||||
def loadXML(self, source):
|
||||
raise NotImplementedError("haven't written this yet")
|
||||
|
||||
def saveXML(self, snode):
|
||||
if snode is None:
|
||||
snode = self
|
||||
elif snode.ownerDocument is not self:
|
||||
raise xml.dom.WrongDocumentErr()
|
||||
return snode.toxml()
|
||||
|
||||
|
||||
class DOMImplementationLS:
|
||||
MODE_SYNCHRONOUS = 1
|
||||
MODE_ASYNCHRONOUS = 2
|
||||
|
||||
def createDOMBuilder(self, mode, schemaType):
|
||||
if schemaType is not None:
|
||||
raise xml.dom.NotSupportedErr(
|
||||
"schemaType not yet supported")
|
||||
if mode == self.MODE_SYNCHRONOUS:
|
||||
return DOMBuilder()
|
||||
if mode == self.MODE_ASYNCHRONOUS:
|
||||
raise xml.dom.NotSupportedErr(
|
||||
"asynchronous builders are not supported")
|
||||
raise ValueError("unknown value for mode")
|
||||
|
||||
def createDOMWriter(self):
|
||||
raise NotImplementedError(
|
||||
"the writer interface hasn't been written yet!")
|
||||
|
||||
def createDOMInputSource(self):
|
||||
return DOMInputSource()
|
|
@ -0,0 +1,143 @@
|
|||
#
|
||||
# ElementTree
|
||||
# $Id: ElementInclude.py 1862 2004-06-18 07:31:02Z Fredrik $
|
||||
#
|
||||
# limited xinclude support for element trees
|
||||
#
|
||||
# history:
|
||||
# 2003-08-15 fl created
|
||||
# 2003-11-14 fl fixed default loader
|
||||
#
|
||||
# Copyright (c) 2003-2004 by Fredrik Lundh. All rights reserved.
|
||||
#
|
||||
# fredrik@pythonware.com
|
||||
# http://www.pythonware.com
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# The ElementTree toolkit is
|
||||
#
|
||||
# Copyright (c) 1999-2004 by Fredrik Lundh
|
||||
#
|
||||
# By obtaining, using, and/or copying this software and/or its
|
||||
# associated documentation, you agree that you have read, understood,
|
||||
# and will comply with the following terms and conditions:
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its associated documentation for any purpose and without fee is
|
||||
# hereby granted, provided that the above copyright notice appears in
|
||||
# all copies, and that both that copyright notice and this permission
|
||||
# notice appear in supporting documentation, and that the name of
|
||||
# Secret Labs AB or the author not be used in advertising or publicity
|
||||
# pertaining to distribution of the software without specific, written
|
||||
# prior permission.
|
||||
#
|
||||
# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
|
||||
# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
|
||||
# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
|
||||
# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
|
||||
# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
|
||||
# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
|
||||
# OF THIS SOFTWARE.
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See http://www.python.org/2.4/license for licensing details.
|
||||
|
||||
##
|
||||
# Limited XInclude support for the ElementTree package.
|
||||
##
|
||||
|
||||
import copy
|
||||
import ElementTree
|
||||
|
||||
XINCLUDE = "{http://www.w3.org/2001/XInclude}"
|
||||
|
||||
XINCLUDE_INCLUDE = XINCLUDE + "include"
|
||||
XINCLUDE_FALLBACK = XINCLUDE + "fallback"
|
||||
|
||||
##
|
||||
# Fatal include error.
|
||||
|
||||
class FatalIncludeError(SyntaxError):
|
||||
pass
|
||||
|
||||
##
|
||||
# Default loader. This loader reads an included resource from disk.
|
||||
#
|
||||
# @param href Resource reference.
|
||||
# @param parse Parse mode. Either "xml" or "text".
|
||||
# @param encoding Optional text encoding.
|
||||
# @return The expanded resource. If the parse mode is "xml", this
|
||||
# is an ElementTree instance. If the parse mode is "text", this
|
||||
# is a Unicode string. If the loader fails, it can return None
|
||||
# or raise an IOError exception.
|
||||
# @throws IOError If the loader fails to load the resource.
|
||||
|
||||
def default_loader(href, parse, encoding=None):
|
||||
file = open(href)
|
||||
if parse == "xml":
|
||||
data = ElementTree.parse(file).getroot()
|
||||
else:
|
||||
data = file.read()
|
||||
if encoding:
|
||||
data = data.decode(encoding)
|
||||
file.close()
|
||||
return data
|
||||
|
||||
##
|
||||
# Expand XInclude directives.
|
||||
#
|
||||
# @param elem Root element.
|
||||
# @param loader Optional resource loader. If omitted, it defaults
|
||||
# to {@link default_loader}. If given, it should be a callable
|
||||
# that implements the same interface as <b>default_loader</b>.
|
||||
# @throws FatalIncludeError If the function fails to include a given
|
||||
# resource, or if the tree contains malformed XInclude elements.
|
||||
# @throws IOError If the function fails to load a given resource.
|
||||
|
||||
def include(elem, loader=None):
|
||||
if loader is None:
|
||||
loader = default_loader
|
||||
# look for xinclude elements
|
||||
i = 0
|
||||
while i < len(elem):
|
||||
e = elem[i]
|
||||
if e.tag == XINCLUDE_INCLUDE:
|
||||
# process xinclude directive
|
||||
href = e.get("href")
|
||||
parse = e.get("parse", "xml")
|
||||
if parse == "xml":
|
||||
node = loader(href, parse)
|
||||
if node is None:
|
||||
raise FatalIncludeError(
|
||||
"cannot load %r as %r" % (href, parse)
|
||||
)
|
||||
node = copy.copy(node)
|
||||
if e.tail:
|
||||
node.tail = (node.tail or "") + e.tail
|
||||
elem[i] = node
|
||||
elif parse == "text":
|
||||
text = loader(href, parse, e.get("encoding"))
|
||||
if text is None:
|
||||
raise FatalIncludeError(
|
||||
"cannot load %r as %r" % (href, parse)
|
||||
)
|
||||
if i:
|
||||
node = elem[i-1]
|
||||
node.tail = (node.tail or "") + text
|
||||
else:
|
||||
elem.text = (elem.text or "") + text + (e.tail or "")
|
||||
del elem[i]
|
||||
continue
|
||||
else:
|
||||
raise FatalIncludeError(
|
||||
"unknown parse type in xi:include tag (%r)" % parse
|
||||
)
|
||||
elif e.tag == XINCLUDE_FALLBACK:
|
||||
raise FatalIncludeError(
|
||||
"xi:fallback tag must be child of xi:include (%r)" % e.tag
|
||||
)
|
||||
else:
|
||||
include(e, loader)
|
||||
i = i + 1
|
|
@ -0,0 +1,198 @@
|
|||
#
|
||||
# ElementTree
|
||||
# $Id: ElementPath.py 1858 2004-06-17 21:31:41Z Fredrik $
|
||||
#
|
||||
# limited xpath support for element trees
|
||||
#
|
||||
# history:
|
||||
# 2003-05-23 fl created
|
||||
# 2003-05-28 fl added support for // etc
|
||||
# 2003-08-27 fl fixed parsing of periods in element names
|
||||
#
|
||||
# Copyright (c) 2003-2004 by Fredrik Lundh. All rights reserved.
|
||||
#
|
||||
# fredrik@pythonware.com
|
||||
# http://www.pythonware.com
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# The ElementTree toolkit is
|
||||
#
|
||||
# Copyright (c) 1999-2004 by Fredrik Lundh
|
||||
#
|
||||
# By obtaining, using, and/or copying this software and/or its
|
||||
# associated documentation, you agree that you have read, understood,
|
||||
# and will comply with the following terms and conditions:
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its associated documentation for any purpose and without fee is
|
||||
# hereby granted, provided that the above copyright notice appears in
|
||||
# all copies, and that both that copyright notice and this permission
|
||||
# notice appear in supporting documentation, and that the name of
|
||||
# Secret Labs AB or the author not be used in advertising or publicity
|
||||
# pertaining to distribution of the software without specific, written
|
||||
# prior permission.
|
||||
#
|
||||
# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
|
||||
# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
|
||||
# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
|
||||
# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
|
||||
# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
|
||||
# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
|
||||
# OF THIS SOFTWARE.
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See http://www.python.org/2.4/license for licensing details.
|
||||
|
||||
##
|
||||
# Implementation module for XPath support. There's usually no reason
|
||||
# to import this module directly; the <b>ElementTree</b> does this for
|
||||
# you, if needed.
|
||||
##
|
||||
|
||||
import re
|
||||
|
||||
xpath_tokenizer = re.compile(
|
||||
"(::|\.\.|\(\)|[/.*:\[\]\(\)@=])|((?:\{[^}]+\})?[^/:\[\]\(\)@=\s]+)|\s+"
|
||||
).findall
|
||||
|
||||
class xpath_descendant_or_self:
|
||||
pass
|
||||
|
||||
##
|
||||
# Wrapper for a compiled XPath.
|
||||
|
||||
class Path:
|
||||
|
||||
##
|
||||
# Create an Path instance from an XPath expression.
|
||||
|
||||
def __init__(self, path):
|
||||
tokens = xpath_tokenizer(path)
|
||||
# the current version supports 'path/path'-style expressions only
|
||||
self.path = []
|
||||
self.tag = None
|
||||
if tokens and tokens[0][0] == "/":
|
||||
raise SyntaxError("cannot use absolute path on element")
|
||||
while tokens:
|
||||
op, tag = tokens.pop(0)
|
||||
if tag or op == "*":
|
||||
self.path.append(tag or op)
|
||||
elif op == ".":
|
||||
pass
|
||||
elif op == "/":
|
||||
self.path.append(xpath_descendant_or_self())
|
||||
continue
|
||||
else:
|
||||
raise SyntaxError("unsupported path syntax (%s)" % op)
|
||||
if tokens:
|
||||
op, tag = tokens.pop(0)
|
||||
if op != "/":
|
||||
raise SyntaxError(
|
||||
"expected path separator (%s)" % (op or tag)
|
||||
)
|
||||
if self.path and isinstance(self.path[-1], xpath_descendant_or_self):
|
||||
raise SyntaxError("path cannot end with //")
|
||||
if len(self.path) == 1 and isinstance(self.path[0], type("")):
|
||||
self.tag = self.path[0]
|
||||
|
||||
##
|
||||
# Find first matching object.
|
||||
|
||||
def find(self, element):
|
||||
tag = self.tag
|
||||
if tag is None:
|
||||
nodeset = self.findall(element)
|
||||
if not nodeset:
|
||||
return None
|
||||
return nodeset[0]
|
||||
for elem in element:
|
||||
if elem.tag == tag:
|
||||
return elem
|
||||
return None
|
||||
|
||||
##
|
||||
# Find text for first matching object.
|
||||
|
||||
def findtext(self, element, default=None):
|
||||
tag = self.tag
|
||||
if tag is None:
|
||||
nodeset = self.findall(element)
|
||||
if not nodeset:
|
||||
return default
|
||||
return nodeset[0].text or ""
|
||||
for elem in element:
|
||||
if elem.tag == tag:
|
||||
return elem.text or ""
|
||||
return default
|
||||
|
||||
##
|
||||
# Find all matching objects.
|
||||
|
||||
def findall(self, element):
|
||||
nodeset = [element]
|
||||
index = 0
|
||||
while 1:
|
||||
try:
|
||||
path = self.path[index]
|
||||
index = index + 1
|
||||
except IndexError:
|
||||
return nodeset
|
||||
set = []
|
||||
if isinstance(path, xpath_descendant_or_self):
|
||||
try:
|
||||
tag = self.path[index]
|
||||
if not isinstance(tag, type("")):
|
||||
tag = None
|
||||
else:
|
||||
index = index + 1
|
||||
except IndexError:
|
||||
tag = None # invalid path
|
||||
for node in nodeset:
|
||||
new = list(node.getiterator(tag))
|
||||
if new and new[0] is node:
|
||||
set.extend(new[1:])
|
||||
else:
|
||||
set.extend(new)
|
||||
else:
|
||||
for node in nodeset:
|
||||
for node in node:
|
||||
if path == "*" or node.tag == path:
|
||||
set.append(node)
|
||||
if not set:
|
||||
return []
|
||||
nodeset = set
|
||||
|
||||
_cache = {}
|
||||
|
||||
##
|
||||
# (Internal) Compile path.
|
||||
|
||||
def _compile(path):
|
||||
p = _cache.get(path)
|
||||
if p is not None:
|
||||
return p
|
||||
p = Path(path)
|
||||
if len(_cache) >= 100:
|
||||
_cache.clear()
|
||||
_cache[path] = p
|
||||
return p
|
||||
|
||||
##
|
||||
# Find first matching object.
|
||||
|
||||
def find(element, path):
|
||||
return _compile(path).find(element)
|
||||
|
||||
##
|
||||
# Find text for first matching object.
|
||||
|
||||
def findtext(element, path, default=None):
|
||||
return _compile(path).findtext(element, default)
|
||||
|
||||
##
|
||||
# Find all matching objects.
|
||||
|
||||
def findall(element, path):
|
||||
return _compile(path).findall(element)
|
|
@ -0,0 +1,33 @@
|
|||
# $Id: __init__.py 1821 2004-06-03 16:57:49Z fredrik $
|
||||
# elementtree package
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# The ElementTree toolkit is
|
||||
#
|
||||
# Copyright (c) 1999-2004 by Fredrik Lundh
|
||||
#
|
||||
# By obtaining, using, and/or copying this software and/or its
|
||||
# associated documentation, you agree that you have read, understood,
|
||||
# and will comply with the following terms and conditions:
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its associated documentation for any purpose and without fee is
|
||||
# hereby granted, provided that the above copyright notice appears in
|
||||
# all copies, and that both that copyright notice and this permission
|
||||
# notice appear in supporting documentation, and that the name of
|
||||
# Secret Labs AB or the author not be used in advertising or publicity
|
||||
# pertaining to distribution of the software without specific, written
|
||||
# prior permission.
|
||||
#
|
||||
# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
|
||||
# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
|
||||
# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
|
||||
# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
|
||||
# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
|
||||
# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
|
||||
# OF THIS SOFTWARE.
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See http://www.python.org/2.4/license for licensing details.
|
|
@ -0,0 +1,3 @@
|
|||
# Wrapper module for _elementtree
|
||||
|
||||
from ElementTree import *
|
|
@ -0,0 +1,8 @@
|
|||
"""Python interfaces to XML parsers.
|
||||
|
||||
This package contains one module:
|
||||
|
||||
expat -- Python wrapper for James Clark's Expat parser, with namespace
|
||||
support.
|
||||
|
||||
"""
|
|
@ -0,0 +1,4 @@
|
|||
"""Interface to the Expat non-validating XML parser."""
|
||||
__version__ = '$Revision: 17640 $'
|
||||
|
||||
from pyexpat import *
|
|
@ -0,0 +1,108 @@
|
|||
"""Simple API for XML (SAX) implementation for Python.
|
||||
|
||||
This module provides an implementation of the SAX 2 interface;
|
||||
information about the Java version of the interface can be found at
|
||||
http://www.megginson.com/SAX/. The Python version of the interface is
|
||||
documented at <...>.
|
||||
|
||||
This package contains the following modules:
|
||||
|
||||
handler -- Base classes and constants which define the SAX 2 API for
|
||||
the 'client-side' of SAX for Python.
|
||||
|
||||
saxutils -- Implementation of the convenience classes commonly used to
|
||||
work with SAX.
|
||||
|
||||
xmlreader -- Base classes and constants which define the SAX 2 API for
|
||||
the parsers used with SAX for Python.
|
||||
|
||||
expatreader -- Driver that allows use of the Expat parser with SAX.
|
||||
"""
|
||||
|
||||
from xmlreader import InputSource
|
||||
from handler import ContentHandler, ErrorHandler
|
||||
from _exceptions import SAXException, SAXNotRecognizedException, \
|
||||
SAXParseException, SAXNotSupportedException, \
|
||||
SAXReaderNotAvailable
|
||||
|
||||
|
||||
def parse(source, handler, errorHandler=ErrorHandler()):
|
||||
parser = make_parser()
|
||||
parser.setContentHandler(handler)
|
||||
parser.setErrorHandler(errorHandler)
|
||||
parser.parse(source)
|
||||
|
||||
def parseString(string, handler, errorHandler=ErrorHandler()):
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
if errorHandler is None:
|
||||
errorHandler = ErrorHandler()
|
||||
parser = make_parser()
|
||||
parser.setContentHandler(handler)
|
||||
parser.setErrorHandler(errorHandler)
|
||||
|
||||
inpsrc = InputSource()
|
||||
inpsrc.setByteStream(StringIO(string))
|
||||
parser.parse(inpsrc)
|
||||
|
||||
# this is the parser list used by the make_parser function if no
|
||||
# alternatives are given as parameters to the function
|
||||
|
||||
default_parser_list = ["xml.sax.expatreader"]
|
||||
|
||||
# tell modulefinder that importing sax potentially imports expatreader
|
||||
_false = 0
|
||||
if _false:
|
||||
import xml.sax.expatreader
|
||||
|
||||
import os, sys
|
||||
if os.environ.has_key("PY_SAX_PARSER"):
|
||||
default_parser_list = os.environ["PY_SAX_PARSER"].split(",")
|
||||
del os
|
||||
|
||||
_key = "python.xml.sax.parser"
|
||||
if sys.platform[:4] == "java" and sys.registry.containsKey(_key):
|
||||
default_parser_list = sys.registry.getProperty(_key).split(",")
|
||||
|
||||
|
||||
def make_parser(parser_list = []):
|
||||
"""Creates and returns a SAX parser.
|
||||
|
||||
Creates the first parser it is able to instantiate of the ones
|
||||
given in the list created by doing parser_list +
|
||||
default_parser_list. The lists must contain the names of Python
|
||||
modules containing both a SAX parser and a create_parser function."""
|
||||
|
||||
for parser_name in parser_list + default_parser_list:
|
||||
try:
|
||||
return _create_parser(parser_name)
|
||||
except ImportError,e:
|
||||
import sys
|
||||
if parser_name in sys.modules:
|
||||
# The parser module was found, but importing it
|
||||
# failed unexpectedly, pass this exception through
|
||||
raise
|
||||
except SAXReaderNotAvailable:
|
||||
# The parser module detected that it won't work properly,
|
||||
# so try the next one
|
||||
pass
|
||||
|
||||
raise SAXReaderNotAvailable("No parsers found", None)
|
||||
|
||||
# --- Internal utility methods used by make_parser
|
||||
|
||||
if sys.platform[ : 4] == "java":
|
||||
def _create_parser(parser_name):
|
||||
from org.python.core import imp
|
||||
drv_module = imp.importName(parser_name, 0, globals())
|
||||
return drv_module.create_parser()
|
||||
|
||||
else:
|
||||
def _create_parser(parser_name):
|
||||
drv_module = __import__(parser_name,{},{},['create_parser'])
|
||||
return drv_module.create_parser()
|
||||
|
||||
del sys
|
|
@ -0,0 +1,131 @@
|
|||
"""Different kinds of SAX Exceptions"""
|
||||
import sys
|
||||
if sys.platform[:4] == "java":
|
||||
from java.lang import Exception
|
||||
del sys
|
||||
|
||||
# ===== SAXEXCEPTION =====
|
||||
|
||||
class SAXException(Exception):
|
||||
"""Encapsulate an XML error or warning. This class can contain
|
||||
basic error or warning information from either the XML parser or
|
||||
the application: you can subclass it to provide additional
|
||||
functionality, or to add localization. Note that although you will
|
||||
receive a SAXException as the argument to the handlers in the
|
||||
ErrorHandler interface, you are not actually required to throw
|
||||
the exception; instead, you can simply read the information in
|
||||
it."""
|
||||
|
||||
def __init__(self, msg, exception=None):
|
||||
"""Creates an exception. The message is required, but the exception
|
||||
is optional."""
|
||||
self._msg = msg
|
||||
self._exception = exception
|
||||
Exception.__init__(self, msg)
|
||||
|
||||
def getMessage(self):
|
||||
"Return a message for this exception."
|
||||
return self._msg
|
||||
|
||||
def getException(self):
|
||||
"Return the embedded exception, or None if there was none."
|
||||
return self._exception
|
||||
|
||||
def __str__(self):
|
||||
"Create a string representation of the exception."
|
||||
return self._msg
|
||||
|
||||
def __getitem__(self, ix):
|
||||
"""Avoids weird error messages if someone does exception[ix] by
|
||||
mistake, since Exception has __getitem__ defined."""
|
||||
raise AttributeError("__getitem__")
|
||||
|
||||
|
||||
# ===== SAXPARSEEXCEPTION =====
|
||||
|
||||
class SAXParseException(SAXException):
|
||||
"""Encapsulate an XML parse error or warning.
|
||||
|
||||
This exception will include information for locating the error in
|
||||
the original XML document. Note that although the application will
|
||||
receive a SAXParseException as the argument to the handlers in the
|
||||
ErrorHandler interface, the application is not actually required
|
||||
to throw the exception; instead, it can simply read the
|
||||
information in it and take a different action.
|
||||
|
||||
Since this exception is a subclass of SAXException, it inherits
|
||||
the ability to wrap another exception."""
|
||||
|
||||
def __init__(self, msg, exception, locator):
|
||||
"Creates the exception. The exception parameter is allowed to be None."
|
||||
SAXException.__init__(self, msg, exception)
|
||||
self._locator = locator
|
||||
|
||||
# We need to cache this stuff at construction time.
|
||||
# If this exception is thrown, the objects through which we must
|
||||
# traverse to get this information may be deleted by the time
|
||||
# it gets caught.
|
||||
self._systemId = self._locator.getSystemId()
|
||||
self._colnum = self._locator.getColumnNumber()
|
||||
self._linenum = self._locator.getLineNumber()
|
||||
|
||||
def getColumnNumber(self):
|
||||
"""The column number of the end of the text where the exception
|
||||
occurred."""
|
||||
return self._colnum
|
||||
|
||||
def getLineNumber(self):
|
||||
"The line number of the end of the text where the exception occurred."
|
||||
return self._linenum
|
||||
|
||||
def getPublicId(self):
|
||||
"Get the public identifier of the entity where the exception occurred."
|
||||
return self._locator.getPublicId()
|
||||
|
||||
def getSystemId(self):
|
||||
"Get the system identifier of the entity where the exception occurred."
|
||||
return self._systemId
|
||||
|
||||
def __str__(self):
|
||||
"Create a string representation of the exception."
|
||||
sysid = self.getSystemId()
|
||||
if sysid is None:
|
||||
sysid = "<unknown>"
|
||||
linenum = self.getLineNumber()
|
||||
if linenum is None:
|
||||
linenum = "?"
|
||||
colnum = self.getColumnNumber()
|
||||
if colnum is None:
|
||||
colnum = "?"
|
||||
return "%s:%s:%s: %s" % (sysid, linenum, colnum, self._msg)
|
||||
|
||||
|
||||
# ===== SAXNOTRECOGNIZEDEXCEPTION =====
|
||||
|
||||
class SAXNotRecognizedException(SAXException):
|
||||
"""Exception class for an unrecognized identifier.
|
||||
|
||||
An XMLReader will raise this exception when it is confronted with an
|
||||
unrecognized feature or property. SAX applications and extensions may
|
||||
use this class for similar purposes."""
|
||||
|
||||
|
||||
# ===== SAXNOTSUPPORTEDEXCEPTION =====
|
||||
|
||||
class SAXNotSupportedException(SAXException):
|
||||
"""Exception class for an unsupported operation.
|
||||
|
||||
An XMLReader will raise this exception when a service it cannot
|
||||
perform is requested (specifically setting a state or value). SAX
|
||||
applications and extensions may use this class for similar
|
||||
purposes."""
|
||||
|
||||
# ===== SAXNOTSUPPORTEDEXCEPTION =====
|
||||
|
||||
class SAXReaderNotAvailable(SAXNotSupportedException):
|
||||
"""Exception class for a missing driver.
|
||||
|
||||
An XMLReader module (driver) should raise this exception when it
|
||||
is first imported, e.g. when a support module cannot be imported.
|
||||
It also may be raised during parsing, e.g. if executing an external
|
||||
program is not permitted."""
|
|
@ -0,0 +1,414 @@
|
|||
"""
|
||||
SAX driver for the pyexpat C module. This driver works with
|
||||
pyexpat.__version__ == '2.22'.
|
||||
"""
|
||||
|
||||
version = "0.20"
|
||||
|
||||
from xml.sax._exceptions import *
|
||||
from xml.sax.handler import feature_validation, feature_namespaces
|
||||
from xml.sax.handler import feature_namespace_prefixes
|
||||
from xml.sax.handler import feature_external_ges, feature_external_pes
|
||||
from xml.sax.handler import feature_string_interning
|
||||
from xml.sax.handler import property_xml_string, property_interning_dict
|
||||
|
||||
# xml.parsers.expat does not raise ImportError in Jython
|
||||
import sys
|
||||
if sys.platform[:4] == "java":
|
||||
raise SAXReaderNotAvailable("expat not available in Java", None)
|
||||
del sys
|
||||
|
||||
try:
|
||||
from xml.parsers import expat
|
||||
except ImportError:
|
||||
raise SAXReaderNotAvailable("expat not supported", None)
|
||||
else:
|
||||
if not hasattr(expat, "ParserCreate"):
|
||||
raise SAXReaderNotAvailable("expat not supported", None)
|
||||
from xml.sax import xmlreader, saxutils, handler
|
||||
|
||||
AttributesImpl = xmlreader.AttributesImpl
|
||||
AttributesNSImpl = xmlreader.AttributesNSImpl
|
||||
|
||||
# If we're using a sufficiently recent version of Python, we can use
|
||||
# weak references to avoid cycles between the parser and content
|
||||
# handler, otherwise we'll just have to pretend.
|
||||
try:
|
||||
import _weakref
|
||||
except ImportError:
|
||||
def _mkproxy(o):
|
||||
return o
|
||||
else:
|
||||
import weakref
|
||||
_mkproxy = weakref.proxy
|
||||
del weakref, _weakref
|
||||
|
||||
# --- ExpatLocator
|
||||
|
||||
class ExpatLocator(xmlreader.Locator):
|
||||
"""Locator for use with the ExpatParser class.
|
||||
|
||||
This uses a weak reference to the parser object to avoid creating
|
||||
a circular reference between the parser and the content handler.
|
||||
"""
|
||||
def __init__(self, parser):
|
||||
self._ref = _mkproxy(parser)
|
||||
|
||||
def getColumnNumber(self):
|
||||
parser = self._ref
|
||||
if parser._parser is None:
|
||||
return None
|
||||
return parser._parser.ErrorColumnNumber
|
||||
|
||||
def getLineNumber(self):
|
||||
parser = self._ref
|
||||
if parser._parser is None:
|
||||
return 1
|
||||
return parser._parser.ErrorLineNumber
|
||||
|
||||
def getPublicId(self):
|
||||
parser = self._ref
|
||||
if parser is None:
|
||||
return None
|
||||
return parser._source.getPublicId()
|
||||
|
||||
def getSystemId(self):
|
||||
parser = self._ref
|
||||
if parser is None:
|
||||
return None
|
||||
return parser._source.getSystemId()
|
||||
|
||||
|
||||
# --- ExpatParser
|
||||
|
||||
class ExpatParser(xmlreader.IncrementalParser, xmlreader.Locator):
|
||||
"""SAX driver for the pyexpat C module."""
|
||||
|
||||
def __init__(self, namespaceHandling=0, bufsize=2**16-20):
|
||||
xmlreader.IncrementalParser.__init__(self, bufsize)
|
||||
self._source = xmlreader.InputSource()
|
||||
self._parser = None
|
||||
self._namespaces = namespaceHandling
|
||||
self._lex_handler_prop = None
|
||||
self._parsing = 0
|
||||
self._entity_stack = []
|
||||
self._external_ges = 1
|
||||
self._interning = None
|
||||
|
||||
# XMLReader methods
|
||||
|
||||
def parse(self, source):
|
||||
"Parse an XML document from a URL or an InputSource."
|
||||
source = saxutils.prepare_input_source(source)
|
||||
|
||||
self._source = source
|
||||
self.reset()
|
||||
self._cont_handler.setDocumentLocator(ExpatLocator(self))
|
||||
xmlreader.IncrementalParser.parse(self, source)
|
||||
|
||||
def prepareParser(self, source):
|
||||
if source.getSystemId() is not None:
|
||||
self._parser.SetBase(source.getSystemId())
|
||||
|
||||
# Redefined setContentHandler to allow changing handlers during parsing
|
||||
|
||||
def setContentHandler(self, handler):
|
||||
xmlreader.IncrementalParser.setContentHandler(self, handler)
|
||||
if self._parsing:
|
||||
self._reset_cont_handler()
|
||||
|
||||
def getFeature(self, name):
|
||||
if name == feature_namespaces:
|
||||
return self._namespaces
|
||||
elif name == feature_string_interning:
|
||||
return self._interning is not None
|
||||
elif name in (feature_validation, feature_external_pes,
|
||||
feature_namespace_prefixes):
|
||||
return 0
|
||||
elif name == feature_external_ges:
|
||||
return self._external_ges
|
||||
raise SAXNotRecognizedException("Feature '%s' not recognized" % name)
|
||||
|
||||
def setFeature(self, name, state):
|
||||
if self._parsing:
|
||||
raise SAXNotSupportedException("Cannot set features while parsing")
|
||||
|
||||
if name == feature_namespaces:
|
||||
self._namespaces = state
|
||||
elif name == feature_external_ges:
|
||||
self._external_ges = state
|
||||
elif name == feature_string_interning:
|
||||
if state:
|
||||
if self._interning is None:
|
||||
self._interning = {}
|
||||
else:
|
||||
self._interning = None
|
||||
elif name == feature_validation:
|
||||
if state:
|
||||
raise SAXNotSupportedException(
|
||||
"expat does not support validation")
|
||||
elif name == feature_external_pes:
|
||||
if state:
|
||||
raise SAXNotSupportedException(
|
||||
"expat does not read external parameter entities")
|
||||
elif name == feature_namespace_prefixes:
|
||||
if state:
|
||||
raise SAXNotSupportedException(
|
||||
"expat does not report namespace prefixes")
|
||||
else:
|
||||
raise SAXNotRecognizedException(
|
||||
"Feature '%s' not recognized" % name)
|
||||
|
||||
def getProperty(self, name):
|
||||
if name == handler.property_lexical_handler:
|
||||
return self._lex_handler_prop
|
||||
elif name == property_interning_dict:
|
||||
return self._interning
|
||||
elif name == property_xml_string:
|
||||
if self._parser:
|
||||
if hasattr(self._parser, "GetInputContext"):
|
||||
return self._parser.GetInputContext()
|
||||
else:
|
||||
raise SAXNotRecognizedException(
|
||||
"This version of expat does not support getting"
|
||||
" the XML string")
|
||||
else:
|
||||
raise SAXNotSupportedException(
|
||||
"XML string cannot be returned when not parsing")
|
||||
raise SAXNotRecognizedException("Property '%s' not recognized" % name)
|
||||
|
||||
def setProperty(self, name, value):
|
||||
if name == handler.property_lexical_handler:
|
||||
self._lex_handler_prop = value
|
||||
if self._parsing:
|
||||
self._reset_lex_handler_prop()
|
||||
elif name == property_interning_dict:
|
||||
self._interning = value
|
||||
elif name == property_xml_string:
|
||||
raise SAXNotSupportedException("Property '%s' cannot be set" %
|
||||
name)
|
||||
else:
|
||||
raise SAXNotRecognizedException("Property '%s' not recognized" %
|
||||
name)
|
||||
|
||||
# IncrementalParser methods
|
||||
|
||||
def feed(self, data, isFinal = 0):
|
||||
if not self._parsing:
|
||||
self.reset()
|
||||
self._parsing = 1
|
||||
self._cont_handler.startDocument()
|
||||
|
||||
try:
|
||||
# The isFinal parameter is internal to the expat reader.
|
||||
# If it is set to true, expat will check validity of the entire
|
||||
# document. When feeding chunks, they are not normally final -
|
||||
# except when invoked from close.
|
||||
self._parser.Parse(data, isFinal)
|
||||
except expat.error, e:
|
||||
exc = SAXParseException(expat.ErrorString(e.code), e, self)
|
||||
# FIXME: when to invoke error()?
|
||||
self._err_handler.fatalError(exc)
|
||||
|
||||
def close(self):
|
||||
if self._entity_stack:
|
||||
# If we are completing an external entity, do nothing here
|
||||
return
|
||||
self.feed("", isFinal = 1)
|
||||
self._cont_handler.endDocument()
|
||||
self._parsing = 0
|
||||
# break cycle created by expat handlers pointing to our methods
|
||||
self._parser = None
|
||||
|
||||
def _reset_cont_handler(self):
|
||||
self._parser.ProcessingInstructionHandler = \
|
||||
self._cont_handler.processingInstruction
|
||||
self._parser.CharacterDataHandler = self._cont_handler.characters
|
||||
|
||||
def _reset_lex_handler_prop(self):
|
||||
lex = self._lex_handler_prop
|
||||
parser = self._parser
|
||||
if lex is None:
|
||||
parser.CommentHandler = None
|
||||
parser.StartCdataSectionHandler = None
|
||||
parser.EndCdataSectionHandler = None
|
||||
parser.StartDoctypeDeclHandler = None
|
||||
parser.EndDoctypeDeclHandler = None
|
||||
else:
|
||||
parser.CommentHandler = lex.comment
|
||||
parser.StartCdataSectionHandler = lex.startCDATA
|
||||
parser.EndCdataSectionHandler = lex.endCDATA
|
||||
parser.StartDoctypeDeclHandler = self.start_doctype_decl
|
||||
parser.EndDoctypeDeclHandler = lex.endDTD
|
||||
|
||||
def reset(self):
|
||||
if self._namespaces:
|
||||
self._parser = expat.ParserCreate(self._source.getEncoding(), " ",
|
||||
intern=self._interning)
|
||||
self._parser.namespace_prefixes = 1
|
||||
self._parser.StartElementHandler = self.start_element_ns
|
||||
self._parser.EndElementHandler = self.end_element_ns
|
||||
else:
|
||||
self._parser = expat.ParserCreate(self._source.getEncoding(),
|
||||
intern = self._interning)
|
||||
self._parser.StartElementHandler = self.start_element
|
||||
self._parser.EndElementHandler = self.end_element
|
||||
|
||||
self._reset_cont_handler()
|
||||
self._parser.UnparsedEntityDeclHandler = self.unparsed_entity_decl
|
||||
self._parser.NotationDeclHandler = self.notation_decl
|
||||
self._parser.StartNamespaceDeclHandler = self.start_namespace_decl
|
||||
self._parser.EndNamespaceDeclHandler = self.end_namespace_decl
|
||||
|
||||
self._decl_handler_prop = None
|
||||
if self._lex_handler_prop:
|
||||
self._reset_lex_handler_prop()
|
||||
# self._parser.DefaultHandler =
|
||||
# self._parser.DefaultHandlerExpand =
|
||||
# self._parser.NotStandaloneHandler =
|
||||
self._parser.ExternalEntityRefHandler = self.external_entity_ref
|
||||
try:
|
||||
self._parser.SkippedEntityHandler = self.skipped_entity_handler
|
||||
except AttributeError:
|
||||
# This pyexpat does not support SkippedEntity
|
||||
pass
|
||||
self._parser.SetParamEntityParsing(
|
||||
expat.XML_PARAM_ENTITY_PARSING_UNLESS_STANDALONE)
|
||||
|
||||
self._parsing = 0
|
||||
self._entity_stack = []
|
||||
|
||||
# Locator methods
|
||||
|
||||
def getColumnNumber(self):
|
||||
if self._parser is None:
|
||||
return None
|
||||
return self._parser.ErrorColumnNumber
|
||||
|
||||
def getLineNumber(self):
|
||||
if self._parser is None:
|
||||
return 1
|
||||
return self._parser.ErrorLineNumber
|
||||
|
||||
def getPublicId(self):
|
||||
return self._source.getPublicId()
|
||||
|
||||
def getSystemId(self):
|
||||
return self._source.getSystemId()
|
||||
|
||||
# event handlers
|
||||
def start_element(self, name, attrs):
|
||||
self._cont_handler.startElement(name, AttributesImpl(attrs))
|
||||
|
||||
def end_element(self, name):
|
||||
self._cont_handler.endElement(name)
|
||||
|
||||
def start_element_ns(self, name, attrs):
|
||||
pair = name.split()
|
||||
if len(pair) == 1:
|
||||
# no namespace
|
||||
pair = (None, name)
|
||||
elif len(pair) == 3:
|
||||
pair = pair[0], pair[1]
|
||||
else:
|
||||
# default namespace
|
||||
pair = tuple(pair)
|
||||
|
||||
newattrs = {}
|
||||
qnames = {}
|
||||
for (aname, value) in attrs.items():
|
||||
parts = aname.split()
|
||||
length = len(parts)
|
||||
if length == 1:
|
||||
# no namespace
|
||||
qname = aname
|
||||
apair = (None, aname)
|
||||
elif length == 3:
|
||||
qname = "%s:%s" % (parts[2], parts[1])
|
||||
apair = parts[0], parts[1]
|
||||
else:
|
||||
# default namespace
|
||||
qname = parts[1]
|
||||
apair = tuple(parts)
|
||||
|
||||
newattrs[apair] = value
|
||||
qnames[apair] = qname
|
||||
|
||||
self._cont_handler.startElementNS(pair, None,
|
||||
AttributesNSImpl(newattrs, qnames))
|
||||
|
||||
def end_element_ns(self, name):
|
||||
pair = name.split()
|
||||
if len(pair) == 1:
|
||||
pair = (None, name)
|
||||
elif len(pair) == 3:
|
||||
pair = pair[0], pair[1]
|
||||
else:
|
||||
pair = tuple(pair)
|
||||
|
||||
self._cont_handler.endElementNS(pair, None)
|
||||
|
||||
# this is not used (call directly to ContentHandler)
|
||||
def processing_instruction(self, target, data):
|
||||
self._cont_handler.processingInstruction(target, data)
|
||||
|
||||
# this is not used (call directly to ContentHandler)
|
||||
def character_data(self, data):
|
||||
self._cont_handler.characters(data)
|
||||
|
||||
def start_namespace_decl(self, prefix, uri):
|
||||
self._cont_handler.startPrefixMapping(prefix, uri)
|
||||
|
||||
def end_namespace_decl(self, prefix):
|
||||
self._cont_handler.endPrefixMapping(prefix)
|
||||
|
||||
def start_doctype_decl(self, name, sysid, pubid, has_internal_subset):
|
||||
self._lex_handler_prop.startDTD(name, pubid, sysid)
|
||||
|
||||
def unparsed_entity_decl(self, name, base, sysid, pubid, notation_name):
|
||||
self._dtd_handler.unparsedEntityDecl(name, pubid, sysid, notation_name)
|
||||
|
||||
def notation_decl(self, name, base, sysid, pubid):
|
||||
self._dtd_handler.notationDecl(name, pubid, sysid)
|
||||
|
||||
def external_entity_ref(self, context, base, sysid, pubid):
|
||||
if not self._external_ges:
|
||||
return 1
|
||||
|
||||
source = self._ent_handler.resolveEntity(pubid, sysid)
|
||||
source = saxutils.prepare_input_source(source,
|
||||
self._source.getSystemId() or
|
||||
"")
|
||||
|
||||
self._entity_stack.append((self._parser, self._source))
|
||||
self._parser = self._parser.ExternalEntityParserCreate(context)
|
||||
self._source = source
|
||||
|
||||
try:
|
||||
xmlreader.IncrementalParser.parse(self, source)
|
||||
except:
|
||||
return 0 # FIXME: save error info here?
|
||||
|
||||
(self._parser, self._source) = self._entity_stack[-1]
|
||||
del self._entity_stack[-1]
|
||||
return 1
|
||||
|
||||
def skipped_entity_handler(self, name, is_pe):
|
||||
if is_pe:
|
||||
# The SAX spec requires to report skipped PEs with a '%'
|
||||
name = '%'+name
|
||||
self._cont_handler.skippedEntity(name)
|
||||
|
||||
# ---
|
||||
|
||||
def create_parser(*args, **kwargs):
|
||||
return ExpatParser(*args, **kwargs)
|
||||
|
||||
# ---
|
||||
|
||||
if __name__ == "__main__":
|
||||
import xml.sax
|
||||
p = create_parser()
|
||||
p.setContentHandler(xml.sax.XMLGenerator())
|
||||
p.setErrorHandler(xml.sax.ErrorHandler())
|
||||
p.parse("../../../hamlet.xml")
|
|
@ -0,0 +1,342 @@
|
|||
"""
|
||||
This module contains the core classes of version 2.0 of SAX for Python.
|
||||
This file provides only default classes with absolutely minimum
|
||||
functionality, from which drivers and applications can be subclassed.
|
||||
|
||||
Many of these classes are empty and are included only as documentation
|
||||
of the interfaces.
|
||||
|
||||
$Id: handler.py 35816 2004-05-06 03:47:48Z fdrake $
|
||||
"""
|
||||
|
||||
version = '2.0beta'
|
||||
|
||||
#============================================================================
|
||||
#
|
||||
# HANDLER INTERFACES
|
||||
#
|
||||
#============================================================================
|
||||
|
||||
# ===== ERRORHANDLER =====
|
||||
|
||||
class ErrorHandler:
|
||||
"""Basic interface for SAX error handlers.
|
||||
|
||||
If you create an object that implements this interface, then
|
||||
register the object with your XMLReader, the parser will call the
|
||||
methods in your object to report all warnings and errors. There
|
||||
are three levels of errors available: warnings, (possibly)
|
||||
recoverable errors, and unrecoverable errors. All methods take a
|
||||
SAXParseException as the only parameter."""
|
||||
|
||||
def error(self, exception):
|
||||
"Handle a recoverable error."
|
||||
raise exception
|
||||
|
||||
def fatalError(self, exception):
|
||||
"Handle a non-recoverable error."
|
||||
raise exception
|
||||
|
||||
def warning(self, exception):
|
||||
"Handle a warning."
|
||||
print exception
|
||||
|
||||
|
||||
# ===== CONTENTHANDLER =====
|
||||
|
||||
class ContentHandler:
|
||||
"""Interface for receiving logical document content events.
|
||||
|
||||
This is the main callback interface in SAX, and the one most
|
||||
important to applications. The order of events in this interface
|
||||
mirrors the order of the information in the document."""
|
||||
|
||||
def __init__(self):
|
||||
self._locator = None
|
||||
|
||||
def setDocumentLocator(self, locator):
|
||||
"""Called by the parser to give the application a locator for
|
||||
locating the origin of document events.
|
||||
|
||||
SAX parsers are strongly encouraged (though not absolutely
|
||||
required) to supply a locator: if it does so, it must supply
|
||||
the locator to the application by invoking this method before
|
||||
invoking any of the other methods in the DocumentHandler
|
||||
interface.
|
||||
|
||||
The locator allows the application to determine the end
|
||||
position of any document-related event, even if the parser is
|
||||
not reporting an error. Typically, the application will use
|
||||
this information for reporting its own errors (such as
|
||||
character content that does not match an application's
|
||||
business rules). The information returned by the locator is
|
||||
probably not sufficient for use with a search engine.
|
||||
|
||||
Note that the locator will return correct information only
|
||||
during the invocation of the events in this interface. The
|
||||
application should not attempt to use it at any other time."""
|
||||
self._locator = locator
|
||||
|
||||
def startDocument(self):
|
||||
"""Receive notification of the beginning of a document.
|
||||
|
||||
The SAX parser will invoke this method only once, before any
|
||||
other methods in this interface or in DTDHandler (except for
|
||||
setDocumentLocator)."""
|
||||
|
||||
def endDocument(self):
|
||||
"""Receive notification of the end of a document.
|
||||
|
||||
The SAX parser will invoke this method only once, and it will
|
||||
be the last method invoked during the parse. The parser shall
|
||||
not invoke this method until it has either abandoned parsing
|
||||
(because of an unrecoverable error) or reached the end of
|
||||
input."""
|
||||
|
||||
def startPrefixMapping(self, prefix, uri):
|
||||
"""Begin the scope of a prefix-URI Namespace mapping.
|
||||
|
||||
The information from this event is not necessary for normal
|
||||
Namespace processing: the SAX XML reader will automatically
|
||||
replace prefixes for element and attribute names when the
|
||||
http://xml.org/sax/features/namespaces feature is true (the
|
||||
default).
|
||||
|
||||
There are cases, however, when applications need to use
|
||||
prefixes in character data or in attribute values, where they
|
||||
cannot safely be expanded automatically; the
|
||||
start/endPrefixMapping event supplies the information to the
|
||||
application to expand prefixes in those contexts itself, if
|
||||
necessary.
|
||||
|
||||
Note that start/endPrefixMapping events are not guaranteed to
|
||||
be properly nested relative to each-other: all
|
||||
startPrefixMapping events will occur before the corresponding
|
||||
startElement event, and all endPrefixMapping events will occur
|
||||
after the corresponding endElement event, but their order is
|
||||
not guaranteed."""
|
||||
|
||||
def endPrefixMapping(self, prefix):
|
||||
"""End the scope of a prefix-URI mapping.
|
||||
|
||||
See startPrefixMapping for details. This event will always
|
||||
occur after the corresponding endElement event, but the order
|
||||
of endPrefixMapping events is not otherwise guaranteed."""
|
||||
|
||||
def startElement(self, name, attrs):
|
||||
"""Signals the start of an element in non-namespace mode.
|
||||
|
||||
The name parameter contains the raw XML 1.0 name of the
|
||||
element type as a string and the attrs parameter holds an
|
||||
instance of the Attributes class containing the attributes of
|
||||
the element."""
|
||||
|
||||
def endElement(self, name):
|
||||
"""Signals the end of an element in non-namespace mode.
|
||||
|
||||
The name parameter contains the name of the element type, just
|
||||
as with the startElement event."""
|
||||
|
||||
def startElementNS(self, name, qname, attrs):
|
||||
"""Signals the start of an element in namespace mode.
|
||||
|
||||
The name parameter contains the name of the element type as a
|
||||
(uri, localname) tuple, the qname parameter the raw XML 1.0
|
||||
name used in the source document, and the attrs parameter
|
||||
holds an instance of the Attributes class containing the
|
||||
attributes of the element.
|
||||
|
||||
The uri part of the name tuple is None for elements which have
|
||||
no namespace."""
|
||||
|
||||
def endElementNS(self, name, qname):
|
||||
"""Signals the end of an element in namespace mode.
|
||||
|
||||
The name parameter contains the name of the element type, just
|
||||
as with the startElementNS event."""
|
||||
|
||||
def characters(self, content):
|
||||
"""Receive notification of character data.
|
||||
|
||||
The Parser will call this method to report each chunk of
|
||||
character data. SAX parsers may return all contiguous
|
||||
character data in a single chunk, or they may split it into
|
||||
several chunks; however, all of the characters in any single
|
||||
event must come from the same external entity so that the
|
||||
Locator provides useful information."""
|
||||
|
||||
def ignorableWhitespace(self, whitespace):
|
||||
"""Receive notification of ignorable whitespace in element content.
|
||||
|
||||
Validating Parsers must use this method to report each chunk
|
||||
of ignorable whitespace (see the W3C XML 1.0 recommendation,
|
||||
section 2.10): non-validating parsers may also use this method
|
||||
if they are capable of parsing and using content models.
|
||||
|
||||
SAX parsers may return all contiguous whitespace in a single
|
||||
chunk, or they may split it into several chunks; however, all
|
||||
of the characters in any single event must come from the same
|
||||
external entity, so that the Locator provides useful
|
||||
information."""
|
||||
|
||||
def processingInstruction(self, target, data):
|
||||
"""Receive notification of a processing instruction.
|
||||
|
||||
The Parser will invoke this method once for each processing
|
||||
instruction found: note that processing instructions may occur
|
||||
before or after the main document element.
|
||||
|
||||
A SAX parser should never report an XML declaration (XML 1.0,
|
||||
section 2.8) or a text declaration (XML 1.0, section 4.3.1)
|
||||
using this method."""
|
||||
|
||||
def skippedEntity(self, name):
|
||||
"""Receive notification of a skipped entity.
|
||||
|
||||
The Parser will invoke this method once for each entity
|
||||
skipped. Non-validating processors may skip entities if they
|
||||
have not seen the declarations (because, for example, the
|
||||
entity was declared in an external DTD subset). All processors
|
||||
may skip external entities, depending on the values of the
|
||||
http://xml.org/sax/features/external-general-entities and the
|
||||
http://xml.org/sax/features/external-parameter-entities
|
||||
properties."""
|
||||
|
||||
|
||||
# ===== DTDHandler =====
|
||||
|
||||
class DTDHandler:
|
||||
"""Handle DTD events.
|
||||
|
||||
This interface specifies only those DTD events required for basic
|
||||
parsing (unparsed entities and attributes)."""
|
||||
|
||||
def notationDecl(self, name, publicId, systemId):
|
||||
"Handle a notation declaration event."
|
||||
|
||||
def unparsedEntityDecl(self, name, publicId, systemId, ndata):
|
||||
"Handle an unparsed entity declaration event."
|
||||
|
||||
|
||||
# ===== ENTITYRESOLVER =====
|
||||
|
||||
class EntityResolver:
|
||||
"""Basic interface for resolving entities. If you create an object
|
||||
implementing this interface, then register the object with your
|
||||
Parser, the parser will call the method in your object to
|
||||
resolve all external entities. Note that DefaultHandler implements
|
||||
this interface with the default behaviour."""
|
||||
|
||||
def resolveEntity(self, publicId, systemId):
|
||||
"""Resolve the system identifier of an entity and return either
|
||||
the system identifier to read from as a string, or an InputSource
|
||||
to read from."""
|
||||
return systemId
|
||||
|
||||
|
||||
#============================================================================
|
||||
#
|
||||
# CORE FEATURES
|
||||
#
|
||||
#============================================================================
|
||||
|
||||
feature_namespaces = "http://xml.org/sax/features/namespaces"
|
||||
# true: Perform Namespace processing (default).
|
||||
# false: Optionally do not perform Namespace processing
|
||||
# (implies namespace-prefixes).
|
||||
# access: (parsing) read-only; (not parsing) read/write
|
||||
|
||||
feature_namespace_prefixes = "http://xml.org/sax/features/namespace-prefixes"
|
||||
# true: Report the original prefixed names and attributes used for Namespace
|
||||
# declarations.
|
||||
# false: Do not report attributes used for Namespace declarations, and
|
||||
# optionally do not report original prefixed names (default).
|
||||
# access: (parsing) read-only; (not parsing) read/write
|
||||
|
||||
feature_string_interning = "http://xml.org/sax/features/string-interning"
|
||||
# true: All element names, prefixes, attribute names, Namespace URIs, and
|
||||
# local names are interned using the built-in intern function.
|
||||
# false: Names are not necessarily interned, although they may be (default).
|
||||
# access: (parsing) read-only; (not parsing) read/write
|
||||
|
||||
feature_validation = "http://xml.org/sax/features/validation"
|
||||
# true: Report all validation errors (implies external-general-entities and
|
||||
# external-parameter-entities).
|
||||
# false: Do not report validation errors.
|
||||
# access: (parsing) read-only; (not parsing) read/write
|
||||
|
||||
feature_external_ges = "http://xml.org/sax/features/external-general-entities"
|
||||
# true: Include all external general (text) entities.
|
||||
# false: Do not include external general entities.
|
||||
# access: (parsing) read-only; (not parsing) read/write
|
||||
|
||||
feature_external_pes = "http://xml.org/sax/features/external-parameter-entities"
|
||||
# true: Include all external parameter entities, including the external
|
||||
# DTD subset.
|
||||
# false: Do not include any external parameter entities, even the external
|
||||
# DTD subset.
|
||||
# access: (parsing) read-only; (not parsing) read/write
|
||||
|
||||
all_features = [feature_namespaces,
|
||||
feature_namespace_prefixes,
|
||||
feature_string_interning,
|
||||
feature_validation,
|
||||
feature_external_ges,
|
||||
feature_external_pes]
|
||||
|
||||
|
||||
#============================================================================
|
||||
#
|
||||
# CORE PROPERTIES
|
||||
#
|
||||
#============================================================================
|
||||
|
||||
property_lexical_handler = "http://xml.org/sax/properties/lexical-handler"
|
||||
# data type: xml.sax.sax2lib.LexicalHandler
|
||||
# description: An optional extension handler for lexical events like comments.
|
||||
# access: read/write
|
||||
|
||||
property_declaration_handler = "http://xml.org/sax/properties/declaration-handler"
|
||||
# data type: xml.sax.sax2lib.DeclHandler
|
||||
# description: An optional extension handler for DTD-related events other
|
||||
# than notations and unparsed entities.
|
||||
# access: read/write
|
||||
|
||||
property_dom_node = "http://xml.org/sax/properties/dom-node"
|
||||
# data type: org.w3c.dom.Node
|
||||
# description: When parsing, the current DOM node being visited if this is
|
||||
# a DOM iterator; when not parsing, the root DOM node for
|
||||
# iteration.
|
||||
# access: (parsing) read-only; (not parsing) read/write
|
||||
|
||||
property_xml_string = "http://xml.org/sax/properties/xml-string"
|
||||
# data type: String
|
||||
# description: The literal string of characters that was the source for
|
||||
# the current event.
|
||||
# access: read-only
|
||||
|
||||
property_encoding = "http://www.python.org/sax/properties/encoding"
|
||||
# data type: String
|
||||
# description: The name of the encoding to assume for input data.
|
||||
# access: write: set the encoding, e.g. established by a higher-level
|
||||
# protocol. May change during parsing (e.g. after
|
||||
# processing a META tag)
|
||||
# read: return the current encoding (possibly established through
|
||||
# auto-detection.
|
||||
# initial value: UTF-8
|
||||
#
|
||||
|
||||
property_interning_dict = "http://www.python.org/sax/properties/interning-dict"
|
||||
# data type: Dictionary
|
||||
# description: The dictionary used to intern common strings in the document
|
||||
# access: write: Request that the parser uses a specific dictionary, to
|
||||
# allow interning across different documents
|
||||
# read: return the current interning dictionary, or None
|
||||
#
|
||||
|
||||
all_properties = [property_lexical_handler,
|
||||
property_dom_node,
|
||||
property_declaration_handler,
|
||||
property_xml_string,
|
||||
property_encoding,
|
||||
property_interning_dict]
|
|
@ -0,0 +1,302 @@
|
|||
"""\
|
||||
A library of useful helper classes to the SAX classes, for the
|
||||
convenience of application and driver writers.
|
||||
"""
|
||||
|
||||
import os, urlparse, urllib, types
|
||||
import handler
|
||||
import xmlreader
|
||||
|
||||
try:
|
||||
_StringTypes = [types.StringType, types.UnicodeType]
|
||||
except AttributeError:
|
||||
_StringTypes = [types.StringType]
|
||||
|
||||
# See whether the xmlcharrefreplace error handler is
|
||||
# supported
|
||||
try:
|
||||
from codecs import xmlcharrefreplace_errors
|
||||
_error_handling = "xmlcharrefreplace"
|
||||
del xmlcharrefreplace_errors
|
||||
except ImportError:
|
||||
_error_handling = "strict"
|
||||
|
||||
def __dict_replace(s, d):
|
||||
"""Replace substrings of a string using a dictionary."""
|
||||
for key, value in d.items():
|
||||
s = s.replace(key, value)
|
||||
return s
|
||||
|
||||
def escape(data, entities={}):
|
||||
"""Escape &, <, and > in a string of data.
|
||||
|
||||
You can escape other strings of data by passing a dictionary as
|
||||
the optional entities parameter. The keys and values must all be
|
||||
strings; each key will be replaced with its corresponding value.
|
||||
"""
|
||||
|
||||
# must do ampersand first
|
||||
data = data.replace("&", "&")
|
||||
data = data.replace(">", ">")
|
||||
data = data.replace("<", "<")
|
||||
if entities:
|
||||
data = __dict_replace(data, entities)
|
||||
return data
|
||||
|
||||
def unescape(data, entities={}):
|
||||
"""Unescape &, <, and > in a string of data.
|
||||
|
||||
You can unescape other strings of data by passing a dictionary as
|
||||
the optional entities parameter. The keys and values must all be
|
||||
strings; each key will be replaced with its corresponding value.
|
||||
"""
|
||||
data = data.replace("<", "<")
|
||||
data = data.replace(">", ">")
|
||||
if entities:
|
||||
data = __dict_replace(data, entities)
|
||||
# must do ampersand last
|
||||
return data.replace("&", "&")
|
||||
|
||||
def quoteattr(data, entities={}):
|
||||
"""Escape and quote an attribute value.
|
||||
|
||||
Escape &, <, and > in a string of data, then quote it for use as
|
||||
an attribute value. The \" character will be escaped as well, if
|
||||
necessary.
|
||||
|
||||
You can escape other strings of data by passing a dictionary as
|
||||
the optional entities parameter. The keys and values must all be
|
||||
strings; each key will be replaced with its corresponding value.
|
||||
"""
|
||||
entities = entities.copy()
|
||||
entities.update({'\n': ' ', '\r': ' ', '\t':'	'})
|
||||
data = escape(data, entities)
|
||||
if '"' in data:
|
||||
if "'" in data:
|
||||
data = '"%s"' % data.replace('"', """)
|
||||
else:
|
||||
data = "'%s'" % data
|
||||
else:
|
||||
data = '"%s"' % data
|
||||
return data
|
||||
|
||||
|
||||
class XMLGenerator(handler.ContentHandler):
|
||||
|
||||
def __init__(self, out=None, encoding="iso-8859-1"):
|
||||
if out is None:
|
||||
import sys
|
||||
out = sys.stdout
|
||||
handler.ContentHandler.__init__(self)
|
||||
self._out = out
|
||||
self._ns_contexts = [{}] # contains uri -> prefix dicts
|
||||
self._current_context = self._ns_contexts[-1]
|
||||
self._undeclared_ns_maps = []
|
||||
self._encoding = encoding
|
||||
|
||||
def _write(self, text):
|
||||
if isinstance(text, str):
|
||||
self._out.write(text)
|
||||
else:
|
||||
self._out.write(text.encode(self._encoding, _error_handling))
|
||||
|
||||
def _qname(self, name):
|
||||
"""Builds a qualified name from a (ns_url, localname) pair"""
|
||||
if name[0]:
|
||||
# The name is in a non-empty namespace
|
||||
prefix = self._current_context[name[0]]
|
||||
if prefix:
|
||||
# If it is not the default namespace, prepend the prefix
|
||||
return prefix + ":" + name[1]
|
||||
# Return the unqualified name
|
||||
return name[1]
|
||||
|
||||
# ContentHandler methods
|
||||
|
||||
def startDocument(self):
|
||||
self._write('<?xml version="1.0" encoding="%s"?>\n' %
|
||||
self._encoding)
|
||||
|
||||
def startPrefixMapping(self, prefix, uri):
|
||||
self._ns_contexts.append(self._current_context.copy())
|
||||
self._current_context[uri] = prefix
|
||||
self._undeclared_ns_maps.append((prefix, uri))
|
||||
|
||||
def endPrefixMapping(self, prefix):
|
||||
self._current_context = self._ns_contexts[-1]
|
||||
del self._ns_contexts[-1]
|
||||
|
||||
def startElement(self, name, attrs):
|
||||
self._write('<' + name)
|
||||
for (name, value) in attrs.items():
|
||||
self._write(' %s=%s' % (name, quoteattr(value)))
|
||||
self._write('>')
|
||||
|
||||
def endElement(self, name):
|
||||
self._write('</%s>' % name)
|
||||
|
||||
def startElementNS(self, name, qname, attrs):
|
||||
self._write('<' + self._qname(name))
|
||||
|
||||
for prefix, uri in self._undeclared_ns_maps:
|
||||
if prefix:
|
||||
self._out.write(' xmlns:%s="%s"' % (prefix, uri))
|
||||
else:
|
||||
self._out.write(' xmlns="%s"' % uri)
|
||||
self._undeclared_ns_maps = []
|
||||
|
||||
for (name, value) in attrs.items():
|
||||
self._write(' %s=%s' % (self._qname(name), quoteattr(value)))
|
||||
self._write('>')
|
||||
|
||||
def endElementNS(self, name, qname):
|
||||
self._write('</%s>' % self._qname(name))
|
||||
|
||||
def characters(self, content):
|
||||
self._write(escape(content))
|
||||
|
||||
def ignorableWhitespace(self, content):
|
||||
self._write(content)
|
||||
|
||||
def processingInstruction(self, target, data):
|
||||
self._write('<?%s %s?>' % (target, data))
|
||||
|
||||
|
||||
class XMLFilterBase(xmlreader.XMLReader):
|
||||
"""This class is designed to sit between an XMLReader and the
|
||||
client application's event handlers. By default, it does nothing
|
||||
but pass requests up to the reader and events on to the handlers
|
||||
unmodified, but subclasses can override specific methods to modify
|
||||
the event stream or the configuration requests as they pass
|
||||
through."""
|
||||
|
||||
def __init__(self, parent = None):
|
||||
xmlreader.XMLReader.__init__(self)
|
||||
self._parent = parent
|
||||
|
||||
# ErrorHandler methods
|
||||
|
||||
def error(self, exception):
|
||||
self._err_handler.error(exception)
|
||||
|
||||
def fatalError(self, exception):
|
||||
self._err_handler.fatalError(exception)
|
||||
|
||||
def warning(self, exception):
|
||||
self._err_handler.warning(exception)
|
||||
|
||||
# ContentHandler methods
|
||||
|
||||
def setDocumentLocator(self, locator):
|
||||
self._cont_handler.setDocumentLocator(locator)
|
||||
|
||||
def startDocument(self):
|
||||
self._cont_handler.startDocument()
|
||||
|
||||
def endDocument(self):
|
||||
self._cont_handler.endDocument()
|
||||
|
||||
def startPrefixMapping(self, prefix, uri):
|
||||
self._cont_handler.startPrefixMapping(prefix, uri)
|
||||
|
||||
def endPrefixMapping(self, prefix):
|
||||
self._cont_handler.endPrefixMapping(prefix)
|
||||
|
||||
def startElement(self, name, attrs):
|
||||
self._cont_handler.startElement(name, attrs)
|
||||
|
||||
def endElement(self, name):
|
||||
self._cont_handler.endElement(name)
|
||||
|
||||
def startElementNS(self, name, qname, attrs):
|
||||
self._cont_handler.startElementNS(name, qname, attrs)
|
||||
|
||||
def endElementNS(self, name, qname):
|
||||
self._cont_handler.endElementNS(name, qname)
|
||||
|
||||
def characters(self, content):
|
||||
self._cont_handler.characters(content)
|
||||
|
||||
def ignorableWhitespace(self, chars):
|
||||
self._cont_handler.ignorableWhitespace(chars)
|
||||
|
||||
def processingInstruction(self, target, data):
|
||||
self._cont_handler.processingInstruction(target, data)
|
||||
|
||||
def skippedEntity(self, name):
|
||||
self._cont_handler.skippedEntity(name)
|
||||
|
||||
# DTDHandler methods
|
||||
|
||||
def notationDecl(self, name, publicId, systemId):
|
||||
self._dtd_handler.notationDecl(name, publicId, systemId)
|
||||
|
||||
def unparsedEntityDecl(self, name, publicId, systemId, ndata):
|
||||
self._dtd_handler.unparsedEntityDecl(name, publicId, systemId, ndata)
|
||||
|
||||
# EntityResolver methods
|
||||
|
||||
def resolveEntity(self, publicId, systemId):
|
||||
return self._ent_handler.resolveEntity(publicId, systemId)
|
||||
|
||||
# XMLReader methods
|
||||
|
||||
def parse(self, source):
|
||||
self._parent.setContentHandler(self)
|
||||
self._parent.setErrorHandler(self)
|
||||
self._parent.setEntityResolver(self)
|
||||
self._parent.setDTDHandler(self)
|
||||
self._parent.parse(source)
|
||||
|
||||
def setLocale(self, locale):
|
||||
self._parent.setLocale(locale)
|
||||
|
||||
def getFeature(self, name):
|
||||
return self._parent.getFeature(name)
|
||||
|
||||
def setFeature(self, name, state):
|
||||
self._parent.setFeature(name, state)
|
||||
|
||||
def getProperty(self, name):
|
||||
return self._parent.getProperty(name)
|
||||
|
||||
def setProperty(self, name, value):
|
||||
self._parent.setProperty(name, value)
|
||||
|
||||
# XMLFilter methods
|
||||
|
||||
def getParent(self):
|
||||
return self._parent
|
||||
|
||||
def setParent(self, parent):
|
||||
self._parent = parent
|
||||
|
||||
# --- Utility functions
|
||||
|
||||
def prepare_input_source(source, base = ""):
|
||||
"""This function takes an InputSource and an optional base URL and
|
||||
returns a fully resolved InputSource object ready for reading."""
|
||||
|
||||
if type(source) in _StringTypes:
|
||||
source = xmlreader.InputSource(source)
|
||||
elif hasattr(source, "read"):
|
||||
f = source
|
||||
source = xmlreader.InputSource()
|
||||
source.setByteStream(f)
|
||||
if hasattr(f, "name"):
|
||||
source.setSystemId(f.name)
|
||||
|
||||
if source.getByteStream() is None:
|
||||
sysid = source.getSystemId()
|
||||
basehead = os.path.dirname(os.path.normpath(base))
|
||||
sysidfilename = os.path.join(basehead, sysid)
|
||||
if os.path.isfile(sysidfilename):
|
||||
source.setSystemId(sysidfilename)
|
||||
f = open(sysidfilename, "rb")
|
||||
else:
|
||||
source.setSystemId(urlparse.urljoin(base, sysid))
|
||||
f = urllib.urlopen(source.getSystemId())
|
||||
|
||||
source.setByteStream(f)
|
||||
|
||||
return source
|
|
@ -0,0 +1,381 @@
|
|||
"""An XML Reader is the SAX 2 name for an XML parser. XML Parsers
|
||||
should be based on this code. """
|
||||
|
||||
import handler
|
||||
|
||||
from _exceptions import SAXNotSupportedException, SAXNotRecognizedException
|
||||
|
||||
|
||||
# ===== XMLREADER =====
|
||||
|
||||
class XMLReader:
|
||||
"""Interface for reading an XML document using callbacks.
|
||||
|
||||
XMLReader is the interface that an XML parser's SAX2 driver must
|
||||
implement. This interface allows an application to set and query
|
||||
features and properties in the parser, to register event handlers
|
||||
for document processing, and to initiate a document parse.
|
||||
|
||||
All SAX interfaces are assumed to be synchronous: the parse
|
||||
methods must not return until parsing is complete, and readers
|
||||
must wait for an event-handler callback to return before reporting
|
||||
the next event."""
|
||||
|
||||
def __init__(self):
|
||||
self._cont_handler = handler.ContentHandler()
|
||||
self._dtd_handler = handler.DTDHandler()
|
||||
self._ent_handler = handler.EntityResolver()
|
||||
self._err_handler = handler.ErrorHandler()
|
||||
|
||||
def parse(self, source):
|
||||
"Parse an XML document from a system identifier or an InputSource."
|
||||
raise NotImplementedError("This method must be implemented!")
|
||||
|
||||
def getContentHandler(self):
|
||||
"Returns the current ContentHandler."
|
||||
return self._cont_handler
|
||||
|
||||
def setContentHandler(self, handler):
|
||||
"Registers a new object to receive document content events."
|
||||
self._cont_handler = handler
|
||||
|
||||
def getDTDHandler(self):
|
||||
"Returns the current DTD handler."
|
||||
return self._dtd_handler
|
||||
|
||||
def setDTDHandler(self, handler):
|
||||
"Register an object to receive basic DTD-related events."
|
||||
self._dtd_handler = handler
|
||||
|
||||
def getEntityResolver(self):
|
||||
"Returns the current EntityResolver."
|
||||
return self._ent_handler
|
||||
|
||||
def setEntityResolver(self, resolver):
|
||||
"Register an object to resolve external entities."
|
||||
self._ent_handler = resolver
|
||||
|
||||
def getErrorHandler(self):
|
||||
"Returns the current ErrorHandler."
|
||||
return self._err_handler
|
||||
|
||||
def setErrorHandler(self, handler):
|
||||
"Register an object to receive error-message events."
|
||||
self._err_handler = handler
|
||||
|
||||
def setLocale(self, locale):
|
||||
"""Allow an application to set the locale for errors and warnings.
|
||||
|
||||
SAX parsers are not required to provide localization for errors
|
||||
and warnings; if they cannot support the requested locale,
|
||||
however, they must throw a SAX exception. Applications may
|
||||
request a locale change in the middle of a parse."""
|
||||
raise SAXNotSupportedException("Locale support not implemented")
|
||||
|
||||
def getFeature(self, name):
|
||||
"Looks up and returns the state of a SAX2 feature."
|
||||
raise SAXNotRecognizedException("Feature '%s' not recognized" % name)
|
||||
|
||||
def setFeature(self, name, state):
|
||||
"Sets the state of a SAX2 feature."
|
||||
raise SAXNotRecognizedException("Feature '%s' not recognized" % name)
|
||||
|
||||
def getProperty(self, name):
|
||||
"Looks up and returns the value of a SAX2 property."
|
||||
raise SAXNotRecognizedException("Property '%s' not recognized" % name)
|
||||
|
||||
def setProperty(self, name, value):
|
||||
"Sets the value of a SAX2 property."
|
||||
raise SAXNotRecognizedException("Property '%s' not recognized" % name)
|
||||
|
||||
class IncrementalParser(XMLReader):
|
||||
"""This interface adds three extra methods to the XMLReader
|
||||
interface that allow XML parsers to support incremental
|
||||
parsing. Support for this interface is optional, since not all
|
||||
underlying XML parsers support this functionality.
|
||||
|
||||
When the parser is instantiated it is ready to begin accepting
|
||||
data from the feed method immediately. After parsing has been
|
||||
finished with a call to close the reset method must be called to
|
||||
make the parser ready to accept new data, either from feed or
|
||||
using the parse method.
|
||||
|
||||
Note that these methods must _not_ be called during parsing, that
|
||||
is, after parse has been called and before it returns.
|
||||
|
||||
By default, the class also implements the parse method of the XMLReader
|
||||
interface using the feed, close and reset methods of the
|
||||
IncrementalParser interface as a convenience to SAX 2.0 driver
|
||||
writers."""
|
||||
|
||||
def __init__(self, bufsize=2**16):
|
||||
self._bufsize = bufsize
|
||||
XMLReader.__init__(self)
|
||||
|
||||
def parse(self, source):
|
||||
import saxutils
|
||||
source = saxutils.prepare_input_source(source)
|
||||
|
||||
self.prepareParser(source)
|
||||
file = source.getByteStream()
|
||||
buffer = file.read(self._bufsize)
|
||||
while buffer != "":
|
||||
self.feed(buffer)
|
||||
buffer = file.read(self._bufsize)
|
||||
self.close()
|
||||
|
||||
def feed(self, data):
|
||||
"""This method gives the raw XML data in the data parameter to
|
||||
the parser and makes it parse the data, emitting the
|
||||
corresponding events. It is allowed for XML constructs to be
|
||||
split across several calls to feed.
|
||||
|
||||
feed may raise SAXException."""
|
||||
raise NotImplementedError("This method must be implemented!")
|
||||
|
||||
def prepareParser(self, source):
|
||||
"""This method is called by the parse implementation to allow
|
||||
the SAX 2.0 driver to prepare itself for parsing."""
|
||||
raise NotImplementedError("prepareParser must be overridden!")
|
||||
|
||||
def close(self):
|
||||
"""This method is called when the entire XML document has been
|
||||
passed to the parser through the feed method, to notify the
|
||||
parser that there are no more data. This allows the parser to
|
||||
do the final checks on the document and empty the internal
|
||||
data buffer.
|
||||
|
||||
The parser will not be ready to parse another document until
|
||||
the reset method has been called.
|
||||
|
||||
close may raise SAXException."""
|
||||
raise NotImplementedError("This method must be implemented!")
|
||||
|
||||
def reset(self):
|
||||
"""This method is called after close has been called to reset
|
||||
the parser so that it is ready to parse new documents. The
|
||||
results of calling parse or feed after close without calling
|
||||
reset are undefined."""
|
||||
raise NotImplementedError("This method must be implemented!")
|
||||
|
||||
# ===== LOCATOR =====
|
||||
|
||||
class Locator:
|
||||
"""Interface for associating a SAX event with a document
|
||||
location. A locator object will return valid results only during
|
||||
calls to DocumentHandler methods; at any other time, the
|
||||
results are unpredictable."""
|
||||
|
||||
def getColumnNumber(self):
|
||||
"Return the column number where the current event ends."
|
||||
return -1
|
||||
|
||||
def getLineNumber(self):
|
||||
"Return the line number where the current event ends."
|
||||
return -1
|
||||
|
||||
def getPublicId(self):
|
||||
"Return the public identifier for the current event."
|
||||
return None
|
||||
|
||||
def getSystemId(self):
|
||||
"Return the system identifier for the current event."
|
||||
return None
|
||||
|
||||
# ===== INPUTSOURCE =====
|
||||
|
||||
class InputSource:
|
||||
"""Encapsulation of the information needed by the XMLReader to
|
||||
read entities.
|
||||
|
||||
This class may include information about the public identifier,
|
||||
system identifier, byte stream (possibly with character encoding
|
||||
information) and/or the character stream of an entity.
|
||||
|
||||
Applications will create objects of this class for use in the
|
||||
XMLReader.parse method and for returning from
|
||||
EntityResolver.resolveEntity.
|
||||
|
||||
An InputSource belongs to the application, the XMLReader is not
|
||||
allowed to modify InputSource objects passed to it from the
|
||||
application, although it may make copies and modify those."""
|
||||
|
||||
def __init__(self, system_id = None):
|
||||
self.__system_id = system_id
|
||||
self.__public_id = None
|
||||
self.__encoding = None
|
||||
self.__bytefile = None
|
||||
self.__charfile = None
|
||||
|
||||
def setPublicId(self, public_id):
|
||||
"Sets the public identifier of this InputSource."
|
||||
self.__public_id = public_id
|
||||
|
||||
def getPublicId(self):
|
||||
"Returns the public identifier of this InputSource."
|
||||
return self.__public_id
|
||||
|
||||
def setSystemId(self, system_id):
|
||||
"Sets the system identifier of this InputSource."
|
||||
self.__system_id = system_id
|
||||
|
||||
def getSystemId(self):
|
||||
"Returns the system identifier of this InputSource."
|
||||
return self.__system_id
|
||||
|
||||
def setEncoding(self, encoding):
|
||||
"""Sets the character encoding of this InputSource.
|
||||
|
||||
The encoding must be a string acceptable for an XML encoding
|
||||
declaration (see section 4.3.3 of the XML recommendation).
|
||||
|
||||
The encoding attribute of the InputSource is ignored if the
|
||||
InputSource also contains a character stream."""
|
||||
self.__encoding = encoding
|
||||
|
||||
def getEncoding(self):
|
||||
"Get the character encoding of this InputSource."
|
||||
return self.__encoding
|
||||
|
||||
def setByteStream(self, bytefile):
|
||||
"""Set the byte stream (a Python file-like object which does
|
||||
not perform byte-to-character conversion) for this input
|
||||
source.
|
||||
|
||||
The SAX parser will ignore this if there is also a character
|
||||
stream specified, but it will use a byte stream in preference
|
||||
to opening a URI connection itself.
|
||||
|
||||
If the application knows the character encoding of the byte
|
||||
stream, it should set it with the setEncoding method."""
|
||||
self.__bytefile = bytefile
|
||||
|
||||
def getByteStream(self):
|
||||
"""Get the byte stream for this input source.
|
||||
|
||||
The getEncoding method will return the character encoding for
|
||||
this byte stream, or None if unknown."""
|
||||
return self.__bytefile
|
||||
|
||||
def setCharacterStream(self, charfile):
|
||||
"""Set the character stream for this input source. (The stream
|
||||
must be a Python 2.0 Unicode-wrapped file-like that performs
|
||||
conversion to Unicode strings.)
|
||||
|
||||
If there is a character stream specified, the SAX parser will
|
||||
ignore any byte stream and will not attempt to open a URI
|
||||
connection to the system identifier."""
|
||||
self.__charfile = charfile
|
||||
|
||||
def getCharacterStream(self):
|
||||
"Get the character stream for this input source."
|
||||
return self.__charfile
|
||||
|
||||
# ===== ATTRIBUTESIMPL =====
|
||||
|
||||
class AttributesImpl:
|
||||
|
||||
def __init__(self, attrs):
|
||||
"""Non-NS-aware implementation.
|
||||
|
||||
attrs should be of the form {name : value}."""
|
||||
self._attrs = attrs
|
||||
|
||||
def getLength(self):
|
||||
return len(self._attrs)
|
||||
|
||||
def getType(self, name):
|
||||
return "CDATA"
|
||||
|
||||
def getValue(self, name):
|
||||
return self._attrs[name]
|
||||
|
||||
def getValueByQName(self, name):
|
||||
return self._attrs[name]
|
||||
|
||||
def getNameByQName(self, name):
|
||||
if not name in self._attrs:
|
||||
raise KeyError, name
|
||||
return name
|
||||
|
||||
def getQNameByName(self, name):
|
||||
if not name in self._attrs:
|
||||
raise KeyError, name
|
||||
return name
|
||||
|
||||
def getNames(self):
|
||||
return self._attrs.keys()
|
||||
|
||||
def getQNames(self):
|
||||
return self._attrs.keys()
|
||||
|
||||
def __len__(self):
|
||||
return len(self._attrs)
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self._attrs[name]
|
||||
|
||||
def keys(self):
|
||||
return self._attrs.keys()
|
||||
|
||||
def has_key(self, name):
|
||||
return name in self._attrs
|
||||
|
||||
def __contains__(self, name):
|
||||
return self._attrs.has_key(name)
|
||||
|
||||
def get(self, name, alternative=None):
|
||||
return self._attrs.get(name, alternative)
|
||||
|
||||
def copy(self):
|
||||
return self.__class__(self._attrs)
|
||||
|
||||
def items(self):
|
||||
return self._attrs.items()
|
||||
|
||||
def values(self):
|
||||
return self._attrs.values()
|
||||
|
||||
# ===== ATTRIBUTESNSIMPL =====
|
||||
|
||||
class AttributesNSImpl(AttributesImpl):
|
||||
|
||||
def __init__(self, attrs, qnames):
|
||||
"""NS-aware implementation.
|
||||
|
||||
attrs should be of the form {(ns_uri, lname): value, ...}.
|
||||
qnames of the form {(ns_uri, lname): qname, ...}."""
|
||||
self._attrs = attrs
|
||||
self._qnames = qnames
|
||||
|
||||
def getValueByQName(self, name):
|
||||
for (nsname, qname) in self._qnames.items():
|
||||
if qname == name:
|
||||
return self._attrs[nsname]
|
||||
|
||||
raise KeyError, name
|
||||
|
||||
def getNameByQName(self, name):
|
||||
for (nsname, qname) in self._qnames.items():
|
||||
if qname == name:
|
||||
return nsname
|
||||
|
||||
raise KeyError, name
|
||||
|
||||
def getQNameByName(self, name):
|
||||
return self._qnames[name]
|
||||
|
||||
def getQNames(self):
|
||||
return self._qnames.values()
|
||||
|
||||
def copy(self):
|
||||
return self.__class__(self._attrs, self._qnames)
|
||||
|
||||
|
||||
def _test():
|
||||
XMLReader()
|
||||
IncrementalParser()
|
||||
Locator()
|
||||
|
||||
if __name__ == "__main__":
|
||||
_test()
|