From 09a34696d529e1686970df5cf1555e160fd0c8f3 Mon Sep 17 00:00:00 2001 From: Six Date: Sat, 11 Dec 2010 18:01:32 -0500 Subject: reorg of files, added traditional setup.py and removed pavement files --- MANIFEST.in | 2 - README | 24 +- dodai/__init__.py | 72 -- dodai/config/__init__.py | 76 -- dodai/config/databases/__init__.py | 285 ------- dodai/config/databases/sa.py | 90 -- dodai/config/files.py | 106 --- dodai/config/log.py | 150 ---- dodai/config/option.py | 61 -- dodai/config/sections.py | 117 --- dodai/db.py | 30 - dodai/exception.py | 289 ------- dodai/tools/__init__.py | 143 ---- dodai/tools/himo.py | 174 ---- dodai/tools/odict.py | 1399 -------------------------------- lib/dodai/__init__.py | 72 ++ lib/dodai/config/__init__.py | 76 ++ lib/dodai/config/databases/__init__.py | 285 +++++++ lib/dodai/config/databases/sa.py | 90 ++ lib/dodai/config/files.py | 106 +++ lib/dodai/config/log.py | 150 ++++ lib/dodai/config/option.py | 61 ++ lib/dodai/config/sections.py | 117 +++ lib/dodai/db.py | 30 + lib/dodai/exception.py | 289 +++++++ lib/dodai/tools/__init__.py | 143 ++++ lib/dodai/tools/himo.py | 174 ++++ lib/dodai/tools/odict.py | 1399 ++++++++++++++++++++++++++++++++ pavement.py | 116 --- setup.py | 216 +++++ 30 files changed, 3212 insertions(+), 3130 deletions(-) delete mode 100644 dodai/__init__.py delete mode 100644 dodai/config/__init__.py delete mode 100644 dodai/config/databases/__init__.py delete mode 100644 dodai/config/databases/sa.py delete mode 100644 dodai/config/files.py delete mode 100644 dodai/config/log.py delete mode 100644 dodai/config/option.py delete mode 100644 dodai/config/sections.py delete mode 100644 dodai/db.py delete mode 100644 dodai/exception.py delete mode 100644 dodai/tools/__init__.py delete mode 100644 dodai/tools/himo.py delete mode 100644 dodai/tools/odict.py create mode 100644 lib/dodai/__init__.py create mode 100644 lib/dodai/config/__init__.py create mode 100644 lib/dodai/config/databases/__init__.py create mode 100644 lib/dodai/config/databases/sa.py create mode 100644 lib/dodai/config/files.py create mode 100644 lib/dodai/config/log.py create mode 100644 lib/dodai/config/option.py create mode 100644 lib/dodai/config/sections.py create mode 100644 lib/dodai/db.py create mode 100644 lib/dodai/exception.py create mode 100644 lib/dodai/tools/__init__.py create mode 100644 lib/dodai/tools/himo.py create mode 100644 lib/dodai/tools/odict.py delete mode 100644 pavement.py create mode 100644 setup.py diff --git a/MANIFEST.in b/MANIFEST.in index 4e3618d..f40b39b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,3 @@ include setup.py -include pavement.py -include paver-minilib.zip include LICENSE prune test diff --git a/README b/README index cc8e664..ed50ecc 100644 --- a/README +++ b/README @@ -1,10 +1,9 @@ DODAI -A Python module to help with writing command line scripts - -This module is to be a foundation to your command line python -scripts. This module provides for quick access to logger, configparser, -optionparse and databases via sqlalchemy. +Module code for quick easy access to parsed text based config file data +and configured database engines. All config file data is returned ordered +and transformed to unicode objects and database connections are returned as +sqlalchemy or couchdb objects. INSTALLATION @@ -22,18 +21,3 @@ There are three ways to install dodai tar zxvf dodai-* cd dodai-* python setup.py install - -3. If you have Paver installed then download compressed file from - http://code.google.com/p/dodai/downloads/list - - tar zxvf dodai-* - cd dodai-* - paver install - - -The installation will also install sqlalchemy. This installation will not -install the required system and python libs needed to get sqlalchemy to work -with your desired database. This includes things like: cx_Oracle, psycopg2, -pyodbc, mysql-python etc. For more information on installing these check out -the docs at http://code.google.com/p/dodai - diff --git a/dodai/__init__.py b/dodai/__init__.py deleted file mode 100644 index 55bb57d..0000000 --- a/dodai/__init__.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright (C) 2010 Leonard Thomas -# -# This file is part of Dodai. -# -# Dodai 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, either version 3 of the License, or -# (at your option) any later version. -# -# Dodai 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 Dodai. If not, see . - -import os -import sys -from dodai.config import Config -from dodai.tools import config_directories - -class Configure(Config): - - CONFIG_FILES = ['config', 'configs', 'configure', 'connect', - 'connections', 'connection', 'setup'] - CONFIG_EXTENSIONS = ['cfg', 'txt', 'ini'] - - - def __init__(self, project_name, config_files=None): - self.project_name = project_name - self._load_config_files() - self._add_files(config_files) - self._add_config_files_to_database_handler() - - def _config_files(self): - # Returns a list of possible config file names - out = [] - for name in self.CONFIG_FILES: - out.append(name) - for ext in self.CONFIG_EXTENSIONS: - name = "{0}.{1}".format(name, ext) - out.append(name) - return out - - def _load_config_files(self): - # Adds any default config file if it exists - out = [] - directories = config_directories(self.project_name) - for dir in directories: - if os.path.isdir(dir): - for name in self._config_files(): - path = os.path.join(dir, name) - self._add_file(path) - - def _add_files(self, filenames): - # Adds a list or tuple of filenames - if filenames: - if isinstance(filenames, list) or isinstance(filenames, tuple): - for filename in filenames: - self._add_file(filename) - else: - self._add_file(filenames) - - def _add_file(self, filename): - # Adds the given filename to the dodai.config.files object - if os.path.isfile(filename): - self.files.add(filename) - - def _add_config_files_to_database_handler(self): - sections = self.files.load() - self.databases.add(sections) diff --git a/dodai/config/__init__.py b/dodai/config/__init__.py deleted file mode 100644 index e849f8e..0000000 --- a/dodai/config/__init__.py +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright (C) 2010 Leonard Thomas -# -# This file is part of Dodai. -# -# Dodai 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, either version 3 of the License, or -# (at your option) any later version. -# -# Dodai 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 Dodai. If not, see . - - -class Config(object): - - def __call__(self): - obj = ConfigResults() - if hasattr(self, 'vars'): - for key, val in self.vars.items(): - setattr(obj, key, val) - return obj - - @property - def files(self): - if not hasattr(self, '_files'): - from dodai.config.files import ConfigFiles - from dodai.tools.odict import OrderedDict - from dodai.tools import Section - from dodai.tools.himo import String2Himo - from ConfigParser import ConfigParser - s2h = String2Himo() - self._files = ConfigFiles(OrderedDict, Section, s2h) - self._files.register_parser_object(ConfigParser) - return self._files - - @property - def options(self): - if not hasattr(self, '_options'): - from dodai.config.option import ConfigOption - self._options = ConfigOption() - return self._options - - @property - def logs(self): - if not hasattr(self, '_logs'): - from dodai.config.log import ConfigLog - self._logs = ConfigLog() - return self._logs - - @property - def databases(self): - if not hasattr(self, '_databases'): - # Wire up the sqlalchemy objects to use in the sa object - # which will be used as the default database handler - from dodai.config.databases import ConfigDatabases - from dodai.config.databases.sa import Sa - from sqlalchemy.orm import sessionmaker - from sqlalchemy import create_engine - from dodai.db import Db - sa = Sa(create_engine, sessionmaker, Db) - self._databases = ConfigDatabases(sa, 'sa') - return self._databases - - def set(self, key, val): - if not hasattr(self, 'vars'): - self.vars = {} - self.vars[key] = val - - -class ConfigResults(object): - pass diff --git a/dodai/config/databases/__init__.py b/dodai/config/databases/__init__.py deleted file mode 100644 index c91e77f..0000000 --- a/dodai/config/databases/__init__.py +++ /dev/null @@ -1,285 +0,0 @@ -# Copyright (C) 2010 Leonard Thomas -# -# This file is part of Dodai. -# -# Dodai 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, either version 3 of the License, or -# (at your option) any later version. -# -# Dodai 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 Dodai. If not, see . - - -from dodai.exception import DatabaseEmptyOptionException -from dodai.exception import DatabasePortException -from dodai.exception import DatabaseHostnameException -from dodai.exception import DatabaseProtocolException -from dodai.exception import DatabaseConnectionException -from dodai.exception import UnknownDatabaseConnectionException -from dodai.tools import list_to_english - -class ConfigDatabases(object): - """An object that is used for creating database connection objects - - """ - def __init__(self, default_handler, default_name): - self.connections = {} - self._handlers = {} - DatabaseConnection.DEFAULT_HANDLER = default_name - self.add_handler(default_name, default_handler) - - def add(self, sections): - """Adds a dictionary of sections to this object that could - be used for creating database connection objects. The dictionary - should be formatted as follows: - - sections[name] = {option: value, option: value, ....} - - The 'name' is the section name. It is an identifier that is used - to load the database: - - The options and values can have the following: - - protocol: The database engine (postgresql, mysql, sqlite, - oracle, mssql etc..) - - hostname: The hostname or ip address of the database server - - port: The port that should be used to connect to the - database server should be a number between 1 - and 65535 - - username: The username used to connect to the database server - - password: The password used to connect to the database server - - database: The database to connect to once connected to the - server - - schema: The schema that will be used. This option does - not do anything but will exist in the db object - for use - - filename: The file path to the database file (sqlite) - - protocol_extra: Used to tell the handler which python db engine - to use. For example one might want to tell - the sqlalchemy handler to use the psycopg2 - python object. - - handler: The handler used to build the orm object. Dodai - only works with sqlalchemy so the default 'sa' - is used. - - """ - for name, section in sections.items(): - self.connections[name] = DatabaseConnection(section, name, - self._handlers) - - def load(self, section_name): - """Returns a database connection object of the given section_name. - Throws Exceptions for any type of configuration errors or missing - configuration data - - """ - if section_name in self.connections: - return self.connections[section_name].load() - else: - raise UnknownDatabaseConnectionException(section_name) - - def add_handler(self, name, handler): - """Adds the given handler and name to this objects handlers - - """ - self._handlers[name] = handler - -class DatabaseConnection(object): - - DEFAULT_HANDLER = None - - def __init__(self, section, name, handlers): - self.section = section - self.name = name - self.handlers = handlers - self.validate = DatabaseConnectionValidator.load(section, name) - self.obj = None - - def load(self): - if not self.obj: - self.validate() - handler = self._get_handler() - self.obj = handler(self.section, self.name) - return self.obj - - def _get_handler(self): - if self.section.has_key('handler'): - name = self.section['handler'] - if name in self.handlers: - return self.handlers[name] - return self.handlers[self.DEFAULT_HANDLER] - - -class DatabaseConnectionValidator(object): - - def __init__(self, section, name, validators=[]): - self.section = section - self.name = name - self.validators = validators - - def __call__(self): - return self.validate() - - def validate(self): - """Validates the connection. Returns true if valid. Throws - DodaiDatabaseConnectionConfigurationError on any errors - - """ - raise NotImplementedError() - - def _validate_option(self, name): - try: - value = self.section[name] - except KeyError: - value = None - - if not value: - raise DatabaseEmptyOptionException(self.name, name) - return True - - def _validate_protocol(self): - - self._validate_option('protocol') - - if self.section['protocol'] not in self.PROTOCOLS: - raise DatabaseProtocolException(self.name, - self.section['protocol'], self.DB_TYPE, - self.PROTOCOLS) - return True - - @staticmethod - def load(section, name): - """Static method (factory) that loads the correct database - validation class. - - Attributes: - section: Dictionary of key val connection information - name: String name of the section - - """ - action = None - validators = [DatabaseConnectionValidatorServer, - DatabaseConnectionValidatorFile] - - for validator in validators: - obj = validator.load(section, name) - if obj: - return obj - - return DatabaseConnectionValidatorUnknown(section, name, validators) - -class DatabaseConnectionValidatorServer(DatabaseConnectionValidator): - - DB_TYPE = 'server' - REQUIRED = ['protocol', 'hostname', 'port', 'username', 'password', - 'database'] - PROTOCOLS = ['postgresql', 'mysql', 'mssql', 'oracle'] - - def _validate_port(self): - self._validate_option('port') - - try: - port = int(self.section['port']) - except ValueError: - port = self.section['port'] - else: - if port >=1 and port <=65535: - return True - raise DatabasePortException(self.name, port) - - def validate(self): - """Validates the connection. Returns true if valid. Throws - DodaiDatabaseConnectionConfigurationError on any errors - - """ - self._validate_protocol() - self._validate_port() - self._validate_option('hostname') - self._validate_option('username') - self._validate_option('password') - self._validate_option('database') - return True - - @classmethod - def load(cls, section, name): - """Return this validation class if it is possible that the - given connection information contains enough data to make - a database server connection. - - Attributes: - section: Dictionary of key val connection information - name: String name of the section - - """ - if section.has_key('protocol'): - if section['protocol'] in cls.PROTOCOLS: - return cls(section, name) - keys = section.keys() - for key in keys: - if key in ['hostname', 'port']: - return cls(section, name) - - -class DatabaseConnectionValidatorFile(DatabaseConnectionValidator): - - DB_TYPE = 'file' - REQUIRED = ['protocol', 'filename'] - PROTOCOLS = ['sqlite'] - - def validate(self): - """Validates the connection. Returns true if valid. Throws - DodaiDatabaseConnectionConfigurationError on any errors - - """ - self._validate_protocol() - self._validate_option('filename') - return True - - @classmethod - def load(cls, section, name): - """Return this validation class if it is possible that the - given connection information contains enough data to make - a database file connection. - - Attributes: - section: Dictionary of key val connection information - name: String name of the section - - """ - if section.has_key('protocol'): - if section['protocol'] in cls.PROTOCOLS: - return cls(section, name) - keys = section.keys() - for key in keys: - if key in ['filename']: - return cls(section, name) - - -class DatabaseConnectionValidatorUnknown(DatabaseConnectionValidator): - - DB_TYPE = 'unkonwn' - REQUIRED = [] - PROTOCOLS = [] - - def validate(self): - """Validates the connection. Returns true if valid. Throws - DodaiDatabaseConnectionConfigurationError on any errors - - """ - - raise DatabaseConnectionException(self.name, self.validators) diff --git a/dodai/config/databases/sa.py b/dodai/config/databases/sa.py deleted file mode 100644 index eaf35ea..0000000 --- a/dodai/config/databases/sa.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright (C) 2010 Leonard Thomas -# -# This file is part of Dodai. -# -# Dodai 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, either version 3 of the License, or -# (at your option) any later version. -# -# Dodai 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 Dodai. If not, see . - -class Sa(object): - """Callable object that will wire up sqlalchemy engine and - session objects - - Attributes: - create_engine: The sqlalchemy create_engine object - session_maker: The sqlalchemy session_maker object - result_object: The object that will be populated with - all the connection information along with - the sqlalchemy objects - - """ - def __init__(self, create_engine, session_maker, result_object): - self._create_engine = create_engine - self._session_maker = session_maker - self._result_object = result_object - - def __call__(self, section, name): - db = self._result_object() - db.name = name - db.protocol = section['protocol'] - - if section.has_key('protocol_extra') and section['protocol_extra']: - db.protocol_extra = section['protocol_extra'] - - if section.has_key('filename') and section['filename']: - db.filename = section['filename'] - - if section.has_key('port') and section['port']: - db.port = int(section['port']) - - if section.has_key('database') and section['database']: - db.database = section['database'] - - if section.has_key('schema') and section['schema']: - db.schema = section['schema'] - - if section.has_key('username') and section['username']: - db.username = section['username'] - - if section.has_key('hostname') and section['hostname']: - db.hostname = section['hostname'] - - db.engine = self._build_engine(section) - db.session = self._build_session(db.engine) - return db - - def _build_session(self, engine): - session = self._session_maker(bind=engine) - return session() - - def _build_connection_string(self, section): - out = [] - out.append('{section[protocol]}') - if section.has_key('protocol_extra') and section['protocol_extra']: - out.append('+{section[protocol_extra]}') - out.append('://') - if section.has_key('filename') and section['filename']: - out.append('/{section[filename]}') - else: - out.append('{section[username]}:{section[password]}@') - out.append('{section[hostname]}') - if section.has_key('port') and section['port']: - out.append(':{section[port]}') - out.append('/{section[database]}') - out = ''.join(out) - out = out.format(section=section) - return out - - def _build_engine(self, section): - connection_string = self._build_connection_string(section) - db_obj = self._create_engine(connection_string) - return db_obj diff --git a/dodai/config/files.py b/dodai/config/files.py deleted file mode 100644 index a27e4a2..0000000 --- a/dodai/config/files.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (C) 2010 Leonard Thomas -# -# This file is part of Dodai. -# -# Dodai 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, either version 3 of the License, or -# (at your option) any later version. -# -# Dodai 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 Dodai. If not, see . - -import os -import ConfigParser -from dodai.config.sections import ConfigSections -from dodai.exception import InvalidConfigParser -from dodai.exception import FileDoesNotExist -from dodai.exception import FileIsDirectory - -class ConfigFiles(object): - - REQUIRED_METHODS = ['read', 'sections', 'options', 'get'] - - def __init__(self, ordered_dict_object, section_object, string_object): - - self._ordered_dict_object = ordered_dict_object - self._section_object = section_object - self._string_object = string_object - self.parser_objects = [] - self.files = self._ordered_dict_object() - self.files_read = [] - - def register_parser_object(self, parser_object): - """Registers a config file parser with this object. Raises - InvalidConfigParser error if the parser does not have the 'read', - 'sections', 'options' and 'get' methods. - - """ - if self.check_parser_object(parser_object): - self.parser_objects.append(parser_object) - - def check_parser_object(self, parser_object): - """Checks the given parser object to insure it has all of the required - methods needed to parse files. - - """ - for name in self.REQUIRED_METHODS: - if not hasattr(parser_object, name): - raise InvalidConfigParser(self.REQUIRED_METHODS, - parser_object.__name__) - return True - - def add(self, filename, encoding=None): - """Adds the given filename to this object to be parsed. - - """ - if os.path.exists(filename): - if os.path.isdir(filename): - raise FileIsDirectory(filename) - else: - filename = os.path.realpath(filename) - self.files[filename] = encoding - else: - raise FileDoesNotExist(filename) - - def _read(self): - self.files_read = [] - out = self._ordered_dict_object() - for filename, encoding in self.files.items(): - error = False - for parser_obj in self.parser_objects: - try: - parser = parser_obj() - parser.read(filename) - except Exception, e: - error = True - else: - out[filename] = self._ordered_dict_object() - out[filename]['encoding'] = encoding - out[filename]['parser'] = parser - self.files_read.append(filename) - error = False - break - if error: - raise e - return out - - - def load(self): - """Returns a ConfigSections object, which acts like a dictionary, - that contains all parsed data. - - """ - files = self._read() - sections = ConfigSections(self._ordered_dict_object, - self._section_object, self._string_object) - for filename, data in files.items(): - encoding = data['encoding'] - parser = data['parser'] - sections(parser, encoding) - return sections diff --git a/dodai/config/log.py b/dodai/config/log.py deleted file mode 100644 index fdb5c93..0000000 --- a/dodai/config/log.py +++ /dev/null @@ -1,150 +0,0 @@ -# Copyright (C) 2010 Leonard Thomas -# -# This file is part of Dodai. -# -# Dodai 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, either version 3 of the License, or -# (at your option) any later version. -# -# Dodai 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 Dodai. If not, see . - - -import logging -import logging.handlers -import os - -class ConfigLog(object): - - LEVELS = { - logging.CRITICAL: [ - 'critical', - "%(asctime)s - %(name)s - %(levelname)s - %(message)s", - "%(message)s"], - logging.ERROR: [ - 'error', - "%(asctime)s - %(name)s - %(levelname)s - %(message)s", - "%(message)s"], - logging.WARNING: [ - 'warning', - "%(asctime)s - %(name)s - %(levelname)s - %(message)s", - "%(message)s"], - logging.INFO: [ - 'info', - "%(asctime)s - %(name)s - %(levelname)s - %(message)s", - "%(message)s"], - logging.DEBUG: [ - 'debug', - "%(asctime)s - %(name)s - %(levelname)s - %(message)s", - "%(message)s"] - } - - MAX_BYTES = 10485760 - BACKUP_COUNT = 5 - FILE_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - STDOUT_FORMAT = "%(message)s" - - def __init__(self): - self.log_level = logging.CRITICAL - self.directory = None - self._levels = {} - - def set_log_level(self, level): - try: - level = self._fetch_log_level(level) - except InvalidLevelException: - pass - else: - self.log_level = level - - def set_directory(self, directory): - if os.path.isdir(directory): - self.directory = directory - else: - raise NoDirectoryExistException(directory) - - def get_file_message_format(self, level): - if not self._levels: - self._levels = self.LEVELS - level = self._fetch_log_level(level) - return self._levels[level][1] - - def get_screen_message_format(self, level): - if not self._levels: - self._levels = self.LEVELS - level = self._fetch_log_level(level) - return self._levels[level][2] - - def _fetch_log_level(self, level): - out = None - if isinstance(level, str): - level = level.strip().lower() - if level in self.LEVELS: - out = level - else: - for key, items in self.LEVELS.items(): - if level == items[0]: - out = key - if not out: - raise InvalidLevelException(level) - else: - return out - - def _build_filepath(self, data): - data = os.path.normpath(data) - if data.startswith(os.path.sep): - dir = os.path.dirname(data) - if not os.path.isdir(dir): - raise NoDirectoryExistException(dir) - else: - if not self.directory: - raise DirectoryNotSetException() - else: - data = os.path.join(self.directory, data) - return data - - def load(self, name): - log =logging.getLogger(name) - log.setLevel(self.log_level) - return log - - def attach_file_handler(self, log, filename): - filepath = self._build_filepath(filename) - handler = logging.handlers.RotatingFileHandler( - filepath, maxBytes = self.MAX_BYTES, - backupCount=self.BACKUP_COUNT) - file_format = self.get_file_message_format(self.log_level) - format_obj = logging.Formatter(file_format) - handler.setFormatter(format_obj) - handler.setLevel(self.log_level) - log.addHandler(handler) - - def attach_screen_handler(self, log, level=None): - if level: - level = self._fetch_log_level(level) - else: - level = self.log_level - message_format = self.get_screen_message_format(level) - handler = logging.StreamHandler() - handler.setLevel(level) - format_obj = logging.Formatter(message_format) - handler.setFormatter(format_obj) - log.addHandler(handler) - - -class NoDirectoryExistException(Exception): - pass - - -class DirectoryNotSetException(Exception): - pass - - -class InvalidLevelException(Exception): - pass diff --git a/dodai/config/option.py b/dodai/config/option.py deleted file mode 100644 index 0561881..0000000 --- a/dodai/config/option.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (C) 2010 Leonard Thomas -# -# This file is part of Dodai. -# -# Dodai 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, either version 3 of the License, or -# (at your option) any later version. -# -# Dodai 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 Dodai. If not, see . - - -from optparse import OptionParser - -class ConfigOption(object): - - def __init__(self): - - self.parser = OptionParser() - self._options = None - self._args = [] - - def get_args(self): - self._parse_args() - return self._args - - def get_options(self): - self._parse_args() - return self._options - - def _parse_args(self): - options, args = self.parser.parse_args() - self._options = options - self._args = args - - def add_quiet(self): - - self.parser.add_option("-q", "--quiet", dest="verbose", default=True, - action="store_false", - help="Don't print status messages to the screen") - - def add_verbose(self): - self.parser.add_option("-v", "--verbose", dest="verbose", - action="store_true", - default=False, help="Print status messages to the screen") - - def add_log_level(self, default='critical'): - self.parser.add_option("-l", "--log-level", dest="log_level", - default=default, help="Sets the log level") - - def add_setup(self): - self.parser.add_option('', "--setup", dest="setup", - action="store_true", default=False, - help="run the setup which builds the config "\ - "files.") diff --git a/dodai/config/sections.py b/dodai/config/sections.py deleted file mode 100644 index d789a24..0000000 --- a/dodai/config/sections.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright (C) 2010 Leonard Thomas -# -# This file is part of Dodai. -# -# Dodai 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, either version 3 of the License, or -# (at your option) any later version. -# -# Dodai 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 Dodai. If not, see . - - -class ConfigSections(object): - """ - An iterable object that contains ConfigSection objects - - """ - - def __init__(self, ordered_dict_object, section_object, - string_object = None,): - """ - Iterable object that handles the conversion of a config - parser object to a list of section objects. - - - string_object: This is an object (non instantiated or - callable) that the results of the config's - sections, and options will be stored in. - This enables you to store your values as a - custom object. A good object to use is the - dodai.tools.himo Himo object. If the - string_object is not given the default python - str() object will be used. The callable of - this object must also allow for encoding - to be passed in string_object(data, 'UTF-8') - - - """ - self._string_object = string_object or '' - self._ordered_dict_object = ordered_dict_object - self._section_object = section_object - self._sections = self._ordered_dict_object() - - def __call__(self, parser, encoding=None): - """ - Parses the given parser object into this object's sections. - - parser: The actual parser object that is used to - get the sections. This object must have - the sections(), options() and get() - methods. A good object to use is the native - ConfigParse object. However, you can create - your own - - """ - self._build_sections(parser, encoding) - - def _build_sections(self, parser, encoding): - # Builds ConfigSection objects from the parser object - - for section_name in parser.sections(): - section = self.get_section(section_name, encoding) - self._build_options(parser, section_name, section, encoding) - - def _build_options(self, parser, section_name, section, encoding): - # Adds the options to the section object - - for key in parser.options(section_name): - value = self._build_string_object(parser.get(section_name, key), - encoding) - key = self._build_string_object(key, encoding) - section[key] = value - - def _build_string_object(self, data, encoding=None): - if self._string_object: - return self._string_object(data, encoding) - else: - return data - - def get_section(self, section_name, encoding=None): - """ - Returns a Section (aka dict) object from this object's section - dictionary or creates a new ConfigSection object, which is - stored int this object's section dictionary then is returned - - """ - section_name = self._build_string_object(section_name, encoding) - if section_name in self._sections: - return self._sections[section_name] - else: - section = self._section_object(section_name) - self._sections[section_name] = section - return section - - def __getitem__(self, key): - return self._sections[key] - - def __getattr__(self, key): - try: - out = self._sections[key] - except KeyError: - return getattr(self._sections, key) - else: - return out - - def __iter__(self, *args, **kargs): - return self._sections.__iter__(*args, **kargs) - - - def __len__(self): - return len(self._sections) diff --git a/dodai/db.py b/dodai/db.py deleted file mode 100644 index 5cbe605..0000000 --- a/dodai/db.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (C) 2010 Leonard Thomas -# -# This file is part of Dodai. -# -# Dodai 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, either version 3 of the License, or -# (at your option) any later version. -# -# Dodai 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 Dodai. If not, see . - - -class Db(object): - pass - - def __init__(self): - self.name = None - self.protocol = None - self.hostname = None - self.port = None - self.database = None - self.filename = None - self.engine = None - self.session = None diff --git a/dodai/exception.py b/dodai/exception.py deleted file mode 100644 index 23302c7..0000000 --- a/dodai/exception.py +++ /dev/null @@ -1,289 +0,0 @@ -# Copyright (C) 2010 Leonard Thomas -# -# This file is part of Dodai. -# -# Dodai 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, either version 3 of the License, or -# (at your option) any later version. -# -# Dodai 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 Dodai. If not, see . - -from dodai.tools import list_to_english -from dodai.tools import quote_list - -class DodaiException(Exception): - - def _system_encoding(self): - # Returns the character encoding of the operating system - - encoding = sys.getdefaultencoding() - filesystem_encoding = sys.getfilesystemencoding() - if filesystem_encoding: - encoding = filesystem_encoding - return encoding - - -class HimoAsciiError(DodaiException): - """Exception raised when the Himo object can't convert a character - down to it's root character. - - Attributes: - char: The character that can't be converted down to ascii - """ - MESSAGE="Unable to convert the '{char}' character to ascii" - - def __init__(self, char): - self.char = char - self.msg = self._build_message() - - def _build_message(self): - encoding = self._system_encoding() - try: - char = self.char.encode(encoding) - except UnicodeEncodeError: - char = 'unichr({0})'.format(ord(self.char)) - return self.MESSAGE.format(char=self.char) - - def __str__(self): - return self.msg - -class DodaiDatabaseConnectionConfigurationError(DodaiException): - pass - - -class DatabaseEmptyOptionException(DodaiDatabaseConnectionConfigurationError): - """Exception raised for an empty option in the config - - Attributes: - section_name: The section of the config file that contains - the invalid protocol - option_name: The name of the empty option - - """ - MESSAGE = "In the '{section_name}' section, the '{option_name}' "\ - "was not set or is missing. Please set this option." - - def __init__(self, section_name, option_name): - self.section_name = section_name - self.option_name = option_name - self.msg = self._build_message() - - def _build_message(self): - return self.MESSAGE.format(section_name=self.section_name, - option_name=self.option_name) - - def __str__(self): - return self.msg - - -class DatabasePortException(DodaiDatabaseConnectionConfigurationError): - """Exception raised for invalid database port connection - - Attributes: - section_name: The section of the config file that contains - the invalid protocol - section_port: The port value that was listed in the - config file - - """ - MESSAGE = "In the '{section_name}' section, the port of "\ - "'{section_port}' is invalid. The port must be a "\ - "number between 1 and 65535" - - def __init__(self, section_name, section_port): - self.section_name = section_name - self.section_port = section_port - self.msg = self._build_message() - - def _build_message(self): - return self.MESSAGE.format(section_name=self.section_name, - section_port=self.section_port) - - def __str__(self): - return self.msg - - -class DatabaseHostnameException(DodaiDatabaseConnectionConfigurationError): - """Exception raised for invalid database hostname - - Attributes: - section_name: The section of the config file that contains - the invalid protocol - section_hostname: The hostname value that was listed in the - config file - - """ - MESSAGE = "In the '{section_name}' section, the hostname of "\ - "'{section_hostname}' is invalid. Please use a valid "\ - "hostname." - - MSG_NON = "In the '{section_name}' section, the hostname was "\ - "not set. Please set the hostname." - - def __init__(self, section_name, section_hostname): - self.section_name = section_name - self.section_hostname = section_hostname - self.msg = self._build_message() - - def _build_message(self): - if self.section_hostname: - return self.MESSAGE.format(section_name=self.section_name, - section_hostname=self.section_hostname) - else: - return self.MSG_NON.format(section_name=self.section_name) - - def __str__(self): - return self.msg - - -class DatabaseProtocolException(DodaiDatabaseConnectionConfigurationError): - """Exception raised for invalid database connection protocols - - Attributes: - section_name: The section of the config file that contains - the invalid protocol - section_protocol: The protocol value that was listed in the - config file - database_type: Usually 'server' or 'file' - protocols: List of valid protocols - - """ - MESSAGE = "In the '{section_name}' section, the protocol of "\ - "'{section_protocol}' is invalid. The valid protocols "\ - "for a '{database_type}' connection are: {protocols}" - - def __init__(self, section_name, section_protocol, database_type, - protocols): - self.section_name = section_name - self.section_protocol = section_protocol - self.database_type = database_type - self.protocols = protocols - self.msg = self._build_message() - - def _build_message(self): - protocols = list_to_english(self.protocols) - return self.MESSAGE.format(section_name=self.section_name, - section_protocol=self.section_protocol, - database_type=self.database_type, - protocols=protocols) - - def __str__(self): - return self.msg - - -class DatabaseConnectionException(DodaiDatabaseConnectionConfigurationError): - """Exception raised for missing database connection parameters - - Attributes: - section_name: The section of the config file that contains - the invalid connection information - options_required: A dictionary containing the database_type - as the key and a list of required options - as the value - - """ - MESSAGE = "The '{section_name}' section does not contain all of the "\ - "correct information needed to make a database connection." - MSG_TYPE = "To make a '{database_type}' connection please make sure "\ - "To have all of the following options: {options}." - MSG_END = "Please remember that the option names are case sensitive." - - def __init__(self, section_name, validators): - self.section_name = section_name - self.validators = validators - self.msg = self._build_message() - - def _build_message(self): - out = [] - out.append(self.MESSAGE.format(section_name=self.section_name)) - for validator in self.validators: - options = list_to_english(validator.REQUIRED) - out.append(self.MSG_TYPE.format(database_type=validator.DB_TYPE, - options=options)) - out.append(self.MSG_END) - return ' '.join(out) - - def __str__(self): - return self.msg - - -class UnknownDatabaseConnectionException( - DodaiDatabaseConnectionConfigurationError): - """Exception raised for missing database connection parameters - - Attributes: - section: The requested section of the config file that can not - be found. - - """ - MESSAGE = "Unable to find the '{section}' section to create a "\ - "database connection." - - def __init__(self, section): - self.section = section - self.msg = self._build_message() - - def _build_message(self): - return self.MESSAGE.format(section=self.section) - - def __str__(self): - return self.msg - - -class InvalidConfigParser(DodaiException): - """Exception raised when an invalid parser is registered in the - dodai.config.ConfigFiles object. - - """ - MESSAGE = "The parser object '{name}' that you were trying to register "\ - "is not a valid parser object. Please make sure that this "\ - "object contains all of the following methods: {methods}" - - def __init__(self, required_methods, name): - self.msg = self._build_message(required_methods, name) - - def _build_message(self, required_methods, name): - methods = quote_list(required_methods) - return self.MESSAGE.format(methods=methods, name=name) - - def __str__(self): - return self.msg - - -class FileDoesNotExist(DodaiException): - """Exception raised when a file does not exist. - - """ - MESSAGE = "The file: '{file_}' does not exist." - - def __init__(self, filepath): - self.msg = self._build_message(filepath) - - def _build_message(self, filepath): - return self.MESSAGE.format(file_=filepath) - - def __str__(self): - return self.msg - - -class FileIsDirectory(DodaiException): - """Exception raised when a file is a directory. - - """ - MESSAGE = "The file: '{file_}' is a directory." - - def __init__(self, filepath): - self.msg = self._build_message(filepath) - - def _build_message(self, filepath): - return self.MESSAGE.format(file_=filepath) - - def __str__(self): - return self.msg diff --git a/dodai/tools/__init__.py b/dodai/tools/__init__.py deleted file mode 100644 index bcef547..0000000 --- a/dodai/tools/__init__.py +++ /dev/null @@ -1,143 +0,0 @@ -# Copyright (C) 2010 Leonard Thomas -# -# This file is part of Dodai. -# -# Dodai 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, either version 3 of the License, or -# (at your option) any later version. -# -# Dodai 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 Dodai. If not, see . - -import sys -import os -import platform -from dodai.tools.odict import OrderedDict -import unicodedata - -def home_directory(): - """Returns the full real path to the home directory of the user who - is executing this script. - - """ - out = None - try: - from win32com.shell import shellcon, shell - out = shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, 0, 0) - except ImportError: - out = os.path.expanduser("~") - return os.path.realpath(out) - -def config_directory_system(project_name=None): - """Returns the system config directory with the passed in project - appended to the path. Returns None for windows and java systems. - - """ - path = None - if platform.system and platform.system() not in ['Windows', 'Java']: - if project_name: - path = os.path.join('/etc', project_name.strip()) - else: - path = '/etc' - return path - -def config_directory_user(project_name): - """Returns the config direcotry of the project which is located in - the user's home directory. Returns None for Java and unknown systems - - """ - path = None - project = '.{0}'.format(project_name.strip()) - if platform.system and platform.system() not in ['Java']: - home_dir = home_directory() - path = os.path.join(home_dir, project) - return path - -def config_directory_project(): - """Returns the config directory that is located in the same directory - of the executable that ran this script - - """ - path = os.path.dirname(os.path.abspath(sys.argv[0])) - return os.path.join(path, 'config') - -def config_directories(project_name): - """Returns a list of possible project directories - - """ - dirs = [] - dir = config_directory_system(project_name) - if dir: - dirs.append(dir) - dir = config_directory_user(project_name) - if dir: - dirs.append(dir) - dir = config_directory_project() - if dir: - dirs.append(dir) - return dirs - -def list_to_english(data): - """Takes the input list and creates a string with each option - encased in single quotes and seperated by a comma with exception - of the last option which is prefixed with the word 'and' - - """ - if data: - if len(data) > 1: - out = [] - last = "{0}".format(data.pop()) - for row in data: - out.append("{0}".format(row)) - out = ', '.join(out) - return "{0} and {1}".format(out, last) - else: - return "{0}".format(data.pop()) - -def quote_list(data, double_quotes=False): - """Takes the give list (data) and adds quotes around each item. Returns - a list - - """ - out = [] - if double_quotes: - quote = '"' - else: - quote = "'" - for item in data: - item = "{quote}{item}{quote}".format(quote=quote, item=item) - out.append(item) - return out - -def normalize_unicode(data): - """Normalizes the unicode data so that compare functions will work - correctly. - - """ - data = unicode(data) - data = unicodedata.normalize('NFC', data) - return data - -class Section(OrderedDict): - """An ordered dictionary object that has the added benfit of holding - a name or title of this grouping. - - """ - def __init__(self, title): - self.set_section_title(title) - super(Section,self).__init__() - - def get_section_title(self): - return self.___title___ - - def set_section_title(self, val): - self.___title___ = val - - def __repr__(self): - return "".format(self.get_section_title()) diff --git a/dodai/tools/himo.py b/dodai/tools/himo.py deleted file mode 100644 index f56eaf8..0000000 --- a/dodai/tools/himo.py +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright (C) 2010 Leonard Thomas -# -# This file is part of Dodai. -# -# Dodai 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, either version 3 of the License, or -# (at your option) any later version. -# -# Dodai 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 Dodai. If not, see . - -import chardet -import re -import sys -import unicodedata -from htmlentitydefs import name2codepoint -from htmlentitydefs import codepoint2name -from decimal import Decimal as D -from dodai.exception import HimoAsciiError -from dodai.tools import normalize_unicode - -class String2Himo(object): - """ - This in an object that is used for converting python string objects - into Himo (unicode) objects. - - """ - - def __init__(self, default_encoding=None): - """ - default_encoding is the encoding value to be used as the - classwide default. If default encoding is not set then - the encoding of the system will be used. - - """ - self.default_encoding = default_encoding or self._system_encoding() - self._expression = re.compile(r'&(#?)(x?)(\w+);') - - def __call__(self, data, encoding=None): - """ - Converts the input (data) string object into a Himo object - using the passed in (encoding). If encoding is omitted then - the default_encoding will be used. - - returns a Himo object - - """ - encoding = encoding or self.default_encoding - data = self._as_unicode(data, encoding) - data = self._decode_html(data) - data = normalize_unicode(data) - return Himo(data) - - def _as_unicode(self, data, encoding): - # Returns string as a unicode string. - - if not isinstance(data, unicode): - if not isinstance(data, str): - data = str(data) - try: - data = data.decode(encoding) - except UnicodeDecodeError: - info = chardet.detect(data) - data = data.decode(info['encoding']) - return unicodedata.normalize('NFC', data) - - def _decode_html(self, data): - # Returns a unicode string. If data contains any html encoded - # characters, the characters will be converted to their unicode - # equivalent - - return unicode(self._expression.subn(self._html_decode, data)[0]) - - def _html_decode(self, values): - # Returns the unicode character from the re.subn - - value = values.group(3) - if values.group(1): - if values.group(2): - return unichr(int('0x{0}'.format(value), 16)) - else: - return unichr(int(value)) - else: - try: - char = name2codepoint[value] - except KeyError: - return values.group() - else: - return unichr(char) - - def _system_encoding(self): - # Returns the character encoding of the operating system - - encoding = sys.getdefaultencoding() - filesystem_encoding = sys.getfilesystemencoding() - if filesystem_encoding: - encoding = filesystem_encoding - return encoding - - -class Himo(unicode): - """ - A unicode-string object with some added features to help with - output formatting. Himo means rope or string in Japanese, hence - the string to Himo connection. - - """ - - MAP = {169:u'(C)', 174:u'(R)', 8471:u'(P)'} - - def html(self): - """ - Returns a unicode string containing this object's value - html enetity encoded. - - """ - out = [] - for char in self: - out.append(self._html_char_encode(char)) - return unicode(''.join(out)) - - def _html_char_encode(self, char): - # Returns an html version of the char - - number = ord(char) - try: - char = "&{0};".format(codepoint2name[number]) - except KeyError: - if number > 127: - char = "&#{0};".format(number) - return char - - def decimal(self): - """ - Returns a decimal object with the value of this object - - """ - - return D(self) - - def ascii(self): - """ - Returns an ascii representation of this object value. - Throws HimoAsciiError if this method was unable to - convert a unicode character down to it's root character. - For example if in your string you have a character - like the letter 'e' but it has an accent mark over it, - this method will convert that character to it's root - character. Thus 'e' with an accent mark over it will - replaced with the regular letter 'e'. - - """ - out = [] - for char in self: - if ord(char) < 127: - out.append(char) - elif ord(char) in self.MAP: - out.append(self.MAP[ord(char)]) - else: - num = unicodedata.decomposition(char).split(' ')[0] - if num: - out.append(unichr(int(num, 16))) - else: - raise HimoAsciiError(char) - return str(''.join(out)) - -class HimoAsciiError(Exception): - pass diff --git a/dodai/tools/odict.py b/dodai/tools/odict.py deleted file mode 100644 index 2c8391d..0000000 --- a/dodai/tools/odict.py +++ /dev/null @@ -1,1399 +0,0 @@ -# odict.py -# An Ordered Dictionary object -# Copyright (C) 2005 Nicola Larosa, Michael Foord -# E-mail: nico AT tekNico DOT net, fuzzyman AT voidspace DOT org DOT uk - -# This software is licensed under the terms of the BSD license. -# http://www.voidspace.org.uk/python/license.shtml -# Basically you're free to copy, modify, distribute and relicense it, -# So long as you keep a copy of the license with it. - -# Documentation at http://www.voidspace.org.uk/python/odict.html -# For information about bugfixes, updates and support, please join the -# Pythonutils mailing list: -# http://groups.google.com/group/pythonutils/ -# Comments, suggestions and bug reports welcome. - -"""A dict that keeps keys in insertion order""" -from __future__ import generators - -__author__ = ('Nicola Larosa ,' - 'Michael Foord ') - -__docformat__ = "restructuredtext en" - -__revision__ = '$Id: odict.py 129 2005-09-12 18:15:28Z teknico $' - -__version__ = '0.2.2' - -__all__ = ['OrderedDict', 'SequenceOrderedDict'] - -import sys -INTP_VER = sys.version_info[:2] -if INTP_VER < (2, 2): - raise RuntimeError("Python v.2.2 or later required") - -import types, warnings - -class OrderedDict(dict): - """ - A class of dictionary that keeps the insertion order of keys. - - All appropriate methods return keys, items, or values in an ordered way. - - All normal dictionary methods are available. Update and comparison is - restricted to other OrderedDict objects. - - Various sequence methods are available, including the ability to explicitly - mutate the key ordering. - - __contains__ tests: - - >>> d = OrderedDict(((1, 3),)) - >>> 1 in d - 1 - >>> 4 in d - 0 - - __getitem__ tests: - - >>> OrderedDict(((1, 3), (3, 2), (2, 1)))[2] - 1 - >>> OrderedDict(((1, 3), (3, 2), (2, 1)))[4] - Traceback (most recent call last): - KeyError: 4 - - __len__ tests: - - >>> len(OrderedDict()) - 0 - >>> len(OrderedDict(((1, 3), (3, 2), (2, 1)))) - 3 - - get tests: - - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> d.get(1) - 3 - >>> d.get(4) is None - 1 - >>> d.get(4, 5) - 5 - >>> d - OrderedDict([(1, 3), (3, 2), (2, 1)]) - - has_key tests: - - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> d.has_key(1) - 1 - >>> d.has_key(4) - 0 - """ - - def __init__(self, init_val=(), strict=False): - """ - Create a new ordered dictionary. Cannot init from a normal dict, - nor from kwargs, since items order is undefined in those cases. - - If the ``strict`` keyword argument is ``True`` (``False`` is the - default) then when doing slice assignment - the ``OrderedDict`` you are - assigning from *must not* contain any keys in the remaining dict. - - >>> OrderedDict() - OrderedDict([]) - >>> OrderedDict({1: 1}) - Traceback (most recent call last): - TypeError: undefined order, cannot get items from dict - >>> OrderedDict({1: 1}.items()) - OrderedDict([(1, 1)]) - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> d - OrderedDict([(1, 3), (3, 2), (2, 1)]) - >>> OrderedDict(d) - OrderedDict([(1, 3), (3, 2), (2, 1)]) - """ - self.strict = strict - dict.__init__(self) - if isinstance(init_val, OrderedDict): - self._sequence = init_val.keys() - dict.update(self, init_val) - elif isinstance(init_val, dict): - # we lose compatibility with other ordered dict types this way - raise TypeError('undefined order, cannot get items from dict') - else: - self._sequence = [] - self.update(init_val) - -### Special methods ### - - def __delitem__(self, key): - """ - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> del d[3] - >>> d - OrderedDict([(1, 3), (2, 1)]) - >>> del d[3] - Traceback (most recent call last): - KeyError: 3 - >>> d[3] = 2 - >>> d - OrderedDict([(1, 3), (2, 1), (3, 2)]) - >>> del d[0:1] - >>> d - OrderedDict([(2, 1), (3, 2)]) - """ - if isinstance(key, types.SliceType): - # FIXME: efficiency? - keys = self._sequence[key] - for entry in keys: - dict.__delitem__(self, entry) - del self._sequence[key] - else: - # do the dict.__delitem__ *first* as it raises - # the more appropriate error - dict.__delitem__(self, key) - self._sequence.remove(key) - - def __eq__(self, other): - """ - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> d == OrderedDict(d) - True - >>> d == OrderedDict(((1, 3), (2, 1), (3, 2))) - False - >>> d == OrderedDict(((1, 0), (3, 2), (2, 1))) - False - >>> d == OrderedDict(((0, 3), (3, 2), (2, 1))) - False - >>> d == dict(d) - False - >>> d == False - False - """ - if isinstance(other, OrderedDict): - # FIXME: efficiency? - # Generate both item lists for each compare - return (self.items() == other.items()) - else: - return False - - def __lt__(self, other): - """ - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> c = OrderedDict(((0, 3), (3, 2), (2, 1))) - >>> c < d - True - >>> d < c - False - >>> d < dict(c) - Traceback (most recent call last): - TypeError: Can only compare with other OrderedDicts - """ - if not isinstance(other, OrderedDict): - raise TypeError('Can only compare with other OrderedDicts') - # FIXME: efficiency? - # Generate both item lists for each compare - return (self.items() < other.items()) - - def __le__(self, other): - """ - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> c = OrderedDict(((0, 3), (3, 2), (2, 1))) - >>> e = OrderedDict(d) - >>> c <= d - True - >>> d <= c - False - >>> d <= dict(c) - Traceback (most recent call last): - TypeError: Can only compare with other OrderedDicts - >>> d <= e - True - """ - if not isinstance(other, OrderedDict): - raise TypeError('Can only compare with other OrderedDicts') - # FIXME: efficiency? - # Generate both item lists for each compare - return (self.items() <= other.items()) - - def __ne__(self, other): - """ - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> d != OrderedDict(d) - False - >>> d != OrderedDict(((1, 3), (2, 1), (3, 2))) - True - >>> d != OrderedDict(((1, 0), (3, 2), (2, 1))) - True - >>> d == OrderedDict(((0, 3), (3, 2), (2, 1))) - False - >>> d != dict(d) - True - >>> d != False - True - """ - if isinstance(other, OrderedDict): - # FIXME: efficiency? - # Generate both item lists for each compare - return not (self.items() == other.items()) - else: - return True - - def __gt__(self, other): - """ - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> c = OrderedDict(((0, 3), (3, 2), (2, 1))) - >>> d > c - True - >>> c > d - False - >>> d > dict(c) - Traceback (most recent call last): - TypeError: Can only compare with other OrderedDicts - """ - if not isinstance(other, OrderedDict): - raise TypeError('Can only compare with other OrderedDicts') - # FIXME: efficiency? - # Generate both item lists for each compare - return (self.items() > other.items()) - - def __ge__(self, other): - """ - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> c = OrderedDict(((0, 3), (3, 2), (2, 1))) - >>> e = OrderedDict(d) - >>> c >= d - False - >>> d >= c - True - >>> d >= dict(c) - Traceback (most recent call last): - TypeError: Can only compare with other OrderedDicts - >>> e >= d - True - """ - if not isinstance(other, OrderedDict): - raise TypeError('Can only compare with other OrderedDicts') - # FIXME: efficiency? - # Generate both item lists for each compare - return (self.items() >= other.items()) - - def __repr__(self): - """ - Used for __repr__ and __str__ - - >>> r1 = repr(OrderedDict((('a', 'b'), ('c', 'd'), ('e', 'f')))) - >>> r1 - "OrderedDict([('a', 'b'), ('c', 'd'), ('e', 'f')])" - >>> r2 = repr(OrderedDict((('a', 'b'), ('e', 'f'), ('c', 'd')))) - >>> r2 - "OrderedDict([('a', 'b'), ('e', 'f'), ('c', 'd')])" - >>> r1 == str(OrderedDict((('a', 'b'), ('c', 'd'), ('e', 'f')))) - True - >>> r2 == str(OrderedDict((('a', 'b'), ('e', 'f'), ('c', 'd')))) - True - """ - return '%s([%s])' % (self.__class__.__name__, ', '.join( - ['(%r, %r)' % (key, self[key]) for key in self._sequence])) - - def __setitem__(self, key, val): - """ - Allows slice assignment, so long as the slice is an OrderedDict - >>> d = OrderedDict() - >>> d['a'] = 'b' - >>> d['b'] = 'a' - >>> d[3] = 12 - >>> d - OrderedDict([('a', 'b'), ('b', 'a'), (3, 12)]) - >>> d[:] = OrderedDict(((1, 2), (2, 3), (3, 4))) - >>> d - OrderedDict([(1, 2), (2, 3), (3, 4)]) - >>> d[::2] = OrderedDict(((7, 8), (9, 10))) - >>> d - OrderedDict([(7, 8), (2, 3), (9, 10)]) - >>> d = OrderedDict(((0, 1), (1, 2), (2, 3), (3, 4))) - >>> d[1:3] = OrderedDict(((1, 2), (5, 6), (7, 8))) - >>> d - OrderedDict([(0, 1), (1, 2), (5, 6), (7, 8), (3, 4)]) - >>> d = OrderedDict(((0, 1), (1, 2), (2, 3), (3, 4)), strict=True) - >>> d[1:3] = OrderedDict(((1, 2), (5, 6), (7, 8))) - >>> d - OrderedDict([(0, 1), (1, 2), (5, 6), (7, 8), (3, 4)]) - - >>> a = OrderedDict(((0, 1), (1, 2), (2, 3)), strict=True) - >>> a[3] = 4 - >>> a - OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) - >>> a[::1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) - >>> a - OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) - >>> a[:2] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]) - Traceback (most recent call last): - ValueError: slice assignment must be from unique keys - >>> a = OrderedDict(((0, 1), (1, 2), (2, 3))) - >>> a[3] = 4 - >>> a - OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) - >>> a[::1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) - >>> a - OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) - >>> a[:2] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) - >>> a - OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) - >>> a[::-1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) - >>> a - OrderedDict([(3, 4), (2, 3), (1, 2), (0, 1)]) - - >>> d = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) - >>> d[:1] = 3 - Traceback (most recent call last): - TypeError: slice assignment requires an OrderedDict - - >>> d = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) - >>> d[:1] = OrderedDict([(9, 8)]) - >>> d - OrderedDict([(9, 8), (1, 2), (2, 3), (3, 4)]) - """ - if isinstance(key, types.SliceType): - if not isinstance(val, OrderedDict): - # FIXME: allow a list of tuples? - raise TypeError('slice assignment requires an OrderedDict') - keys = self._sequence[key] - # NOTE: Could use ``range(*key.indices(len(self._sequence)))`` - indexes = range(len(self._sequence))[key] - if key.step is None: - # NOTE: new slice may not be the same size as the one being - # overwritten ! - # NOTE: What is the algorithm for an impossible slice? - # e.g. d[5:3] - pos = key.start or 0 - del self[key] - newkeys = val.keys() - for k in newkeys: - if k in self: - if self.strict: - raise ValueError('slice assignment must be from ' - 'unique keys') - else: - # NOTE: This removes duplicate keys *first* - # so start position might have changed? - del self[k] - self._sequence = (self._sequence[:pos] + newkeys + - self._sequence[pos:]) - dict.update(self, val) - else: - # extended slice - length of new slice must be the same - # as the one being replaced - if len(keys) != len(val): - raise ValueError('attempt to assign sequence of size %s ' - 'to extended slice of size %s' % (len(val), len(keys))) - # FIXME: efficiency? - del self[key] - item_list = zip(indexes, val.items()) - # smallest indexes first - higher indexes not guaranteed to - # exist - item_list.sort() - for pos, (newkey, newval) in item_list: - if self.strict and newkey in self: - raise ValueError('slice assignment must be from unique' - ' keys') - self.insert(pos, newkey, newval) - else: - if key not in self: - self._sequence.append(key) - dict.__setitem__(self, key, val) - - def __getitem__(self, key): - """ - Allows slicing. Returns an OrderedDict if you slice. - >>> b = OrderedDict([(7, 0), (6, 1), (5, 2), (4, 3), (3, 4), (2, 5), (1, 6)]) - >>> b[::-1] - OrderedDict([(1, 6), (2, 5), (3, 4), (4, 3), (5, 2), (6, 1), (7, 0)]) - >>> b[2:5] - OrderedDict([(5, 2), (4, 3), (3, 4)]) - >>> type(b[2:4]) - - """ - if isinstance(key, types.SliceType): - # FIXME: does this raise the error we want? - keys = self._sequence[key] - # FIXME: efficiency? - return OrderedDict([(entry, self[entry]) for entry in keys]) - else: - return dict.__getitem__(self, key) - - __str__ = __repr__ - - def __setattr__(self, name, value): - """ - Implemented so that accesses to ``sequence`` raise a warning and are - diverted to the new ``setkeys`` method. - """ - if name == 'sequence': - warnings.warn('Use of the sequence attribute is deprecated.' - ' Use the keys method instead.', DeprecationWarning) - # NOTE: doesn't return anything - self.setkeys(value) - else: - # FIXME: do we want to allow arbitrary setting of attributes? - # Or do we want to manage it? - object.__setattr__(self, name, value) - - def __getattr__(self, name): - """ - Implemented so that access to ``sequence`` raises a warning. - - >>> d = OrderedDict() - >>> d.sequence - [] - """ - if name == 'sequence': - warnings.warn('Use of the sequence attribute is deprecated.' - ' Use the keys method instead.', DeprecationWarning) - # NOTE: Still (currently) returns a direct reference. Need to - # because code that uses sequence will expect to be able to - # mutate it in place. - return self._sequence - else: - # raise the appropriate error - raise AttributeError("OrderedDict has no '%s' attribute" % name) - - def __deepcopy__(self, memo): - """ - To allow deepcopy to work with OrderedDict. - - >>> from copy import deepcopy - >>> a = OrderedDict([(1, 1), (2, 2), (3, 3)]) - >>> a['test'] = {} - >>> b = deepcopy(a) - >>> b == a - True - >>> b is a - False - >>> a['test'] is b['test'] - False - """ - from copy import deepcopy - return self.__class__(deepcopy(self.items(), memo), self.strict) - - -### Read-only methods ### - - def copy(self): - """ - >>> OrderedDict(((1, 3), (3, 2), (2, 1))).copy() - OrderedDict([(1, 3), (3, 2), (2, 1)]) - """ - return OrderedDict(self) - - def items(self): - """ - ``items`` returns a list of tuples representing all the - ``(key, value)`` pairs in the dictionary. - - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> d.items() - [(1, 3), (3, 2), (2, 1)] - >>> d.clear() - >>> d.items() - [] - """ - return zip(self._sequence, self.values()) - - def keys(self): - """ - Return a list of keys in the ``OrderedDict``. - - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> d.keys() - [1, 3, 2] - """ - return self._sequence[:] - - def values(self, values=None): - """ - Return a list of all the values in the OrderedDict. - - Optionally you can pass in a list of values, which will replace the - current list. The value list must be the same len as the OrderedDict. - - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> d.values() - [3, 2, 1] - """ - return [self[key] for key in self._sequence] - - def iteritems(self): - """ - >>> ii = OrderedDict(((1, 3), (3, 2), (2, 1))).iteritems() - >>> ii.next() - (1, 3) - >>> ii.next() - (3, 2) - >>> ii.next() - (2, 1) - >>> ii.next() - Traceback (most recent call last): - StopIteration - """ - def make_iter(self=self): - keys = self.iterkeys() - while True: - key = keys.next() - yield (key, self[key]) - return make_iter() - - def iterkeys(self): - """ - >>> ii = OrderedDict(((1, 3), (3, 2), (2, 1))).iterkeys() - >>> ii.next() - 1 - >>> ii.next() - 3 - >>> ii.next() - 2 - >>> ii.next() - Traceback (most recent call last): - StopIteration - """ - return iter(self._sequence) - - __iter__ = iterkeys - - def itervalues(self): - """ - >>> iv = OrderedDict(((1, 3), (3, 2), (2, 1))).itervalues() - >>> iv.next() - 3 - >>> iv.next() - 2 - >>> iv.next() - 1 - >>> iv.next() - Traceback (most recent call last): - StopIteration - """ - def make_iter(self=self): - keys = self.iterkeys() - while True: - yield self[keys.next()] - return make_iter() - -### Read-write methods ### - - def clear(self): - """ - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> d.clear() - >>> d - OrderedDict([]) - """ - dict.clear(self) - self._sequence = [] - - def pop(self, key, *args): - """ - No dict.pop in Python 2.2, gotta reimplement it - - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> d.pop(3) - 2 - >>> d - OrderedDict([(1, 3), (2, 1)]) - >>> d.pop(4) - Traceback (most recent call last): - KeyError: 4 - >>> d.pop(4, 0) - 0 - >>> d.pop(4, 0, 1) - Traceback (most recent call last): - TypeError: pop expected at most 2 arguments, got 3 - """ - if len(args) > 1: - raise TypeError, ('pop expected at most 2 arguments, got %s' % - (len(args) + 1)) - if key in self: - val = self[key] - del self[key] - else: - try: - val = args[0] - except IndexError: - raise KeyError(key) - return val - - def popitem(self, i=-1): - """ - Delete and return an item specified by index, not a random one as in - dict. The index is -1 by default (the last item). - - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> d.popitem() - (2, 1) - >>> d - OrderedDict([(1, 3), (3, 2)]) - >>> d.popitem(0) - (1, 3) - >>> OrderedDict().popitem() - Traceback (most recent call last): - KeyError: 'popitem(): dictionary is empty' - >>> d.popitem(2) - Traceback (most recent call last): - IndexError: popitem(): index 2 not valid - """ - if not self._sequence: - raise KeyError('popitem(): dictionary is empty') - try: - key = self._sequence[i] - except IndexError: - raise IndexError('popitem(): index %s not valid' % i) - return (key, self.pop(key)) - - def setdefault(self, key, defval = None): - """ - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> d.setdefault(1) - 3 - >>> d.setdefault(4) is None - True - >>> d - OrderedDict([(1, 3), (3, 2), (2, 1), (4, None)]) - >>> d.setdefault(5, 0) - 0 - >>> d - OrderedDict([(1, 3), (3, 2), (2, 1), (4, None), (5, 0)]) - """ - if key in self: - return self[key] - else: - self[key] = defval - return defval - - def update(self, from_od): - """ - Update from another OrderedDict or sequence of (key, value) pairs - - >>> d = OrderedDict(((1, 0), (0, 1))) - >>> d.update(OrderedDict(((1, 3), (3, 2), (2, 1)))) - >>> d - OrderedDict([(1, 3), (0, 1), (3, 2), (2, 1)]) - >>> d.update({4: 4}) - Traceback (most recent call last): - TypeError: undefined order, cannot get items from dict - >>> d.update((4, 4)) - Traceback (most recent call last): - TypeError: cannot convert dictionary update sequence element "4" to a 2-item sequence - """ - if isinstance(from_od, OrderedDict): - for key, val in from_od.items(): - self[key] = val - elif isinstance(from_od, dict): - # we lose compatibility with other ordered dict types this way - raise TypeError('undefined order, cannot get items from dict') - else: - # FIXME: efficiency? - # sequence of 2-item sequences, or error - for item in from_od: - try: - key, val = item - except TypeError: - raise TypeError('cannot convert dictionary update' - ' sequence element "%s" to a 2-item sequence' % item) - self[key] = val - - def rename(self, old_key, new_key): - """ - Rename the key for a given value, without modifying sequence order. - - For the case where new_key already exists this raise an exception, - since if new_key exists, it is ambiguous as to what happens to the - associated values, and the position of new_key in the sequence. - - >>> od = OrderedDict() - >>> od['a'] = 1 - >>> od['b'] = 2 - >>> od.items() - [('a', 1), ('b', 2)] - >>> od.rename('b', 'c') - >>> od.items() - [('a', 1), ('c', 2)] - >>> od.rename('c', 'a') - Traceback (most recent call last): - ValueError: New key already exists: 'a' - >>> od.rename('d', 'b') - Traceback (most recent call last): - KeyError: 'd' - """ - if new_key == old_key: - # no-op - return - if new_key in self: - raise ValueError("New key already exists: %r" % new_key) - # rename sequence entry - value = self[old_key] - old_idx = self._sequence.index(old_key) - self._sequence[old_idx] = new_key - # rename internal dict entry - dict.__delitem__(self, old_key) - dict.__setitem__(self, new_key, value) - - def setitems(self, items): - """ - This method allows you to set the items in the dict. - - It takes a list of tuples - of the same sort returned by the ``items`` - method. - - >>> d = OrderedDict() - >>> d.setitems(((3, 1), (2, 3), (1, 2))) - >>> d - OrderedDict([(3, 1), (2, 3), (1, 2)]) - """ - self.clear() - # FIXME: this allows you to pass in an OrderedDict as well :-) - self.update(items) - - def setkeys(self, keys): - """ - ``setkeys`` all ows you to pass in a new list of keys which will - replace the current set. This must contain the same set of keys, but - need not be in the same order. - - If you pass in new keys that don't match, a ``KeyError`` will be - raised. - - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> d.keys() - [1, 3, 2] - >>> d.setkeys((1, 2, 3)) - >>> d - OrderedDict([(1, 3), (2, 1), (3, 2)]) - >>> d.setkeys(['a', 'b', 'c']) - Traceback (most recent call last): - KeyError: 'Keylist is not the same as current keylist.' - """ - # FIXME: Efficiency? (use set for Python 2.4 :-) - # NOTE: list(keys) rather than keys[:] because keys[:] returns - # a tuple, if keys is a tuple. - kcopy = list(keys) - kcopy.sort() - self._sequence.sort() - if kcopy != self._sequence: - raise KeyError('Keylist is not the same as current keylist.') - # NOTE: This makes the _sequence attribute a new object, instead - # of changing it in place. - # FIXME: efficiency? - self._sequence = list(keys) - - def setvalues(self, values): - """ - You can pass in a list of values, which will replace the - current list. The value list must be the same len as the OrderedDict. - - (Or a ``ValueError`` is raised.) - - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> d.setvalues((1, 2, 3)) - >>> d - OrderedDict([(1, 1), (3, 2), (2, 3)]) - >>> d.setvalues([6]) - Traceback (most recent call last): - ValueError: Value list is not the same length as the OrderedDict. - """ - if len(values) != len(self): - # FIXME: correct error to raise? - raise ValueError('Value list is not the same length as the ' - 'OrderedDict.') - self.update(zip(self, values)) - -### Sequence Methods ### - - def index(self, key): - """ - Return the position of the specified key in the OrderedDict. - - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> d.index(3) - 1 - >>> d.index(4) - Traceback (most recent call last): - ValueError: list.index(x): x not in list - """ - return self._sequence.index(key) - - def insert(self, index, key, value): - """ - Takes ``index``, ``key``, and ``value`` as arguments. - - Sets ``key`` to ``value``, so that ``key`` is at position ``index`` in - the OrderedDict. - - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> d.insert(0, 4, 0) - >>> d - OrderedDict([(4, 0), (1, 3), (3, 2), (2, 1)]) - >>> d.insert(0, 2, 1) - >>> d - OrderedDict([(2, 1), (4, 0), (1, 3), (3, 2)]) - >>> d.insert(8, 8, 1) - >>> d - OrderedDict([(2, 1), (4, 0), (1, 3), (3, 2), (8, 1)]) - """ - if key in self: - # FIXME: efficiency? - del self[key] - self._sequence.insert(index, key) - dict.__setitem__(self, key, value) - - def reverse(self): - """ - Reverse the order of the OrderedDict. - - >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) - >>> d.reverse() - >>> d - OrderedDict([(2, 1), (3, 2), (1, 3)]) - """ - self._sequence.reverse() - - def sort(self, *args, **kwargs): - """ - Sort the key order in the OrderedDict. - - This method takes the same arguments as the ``list.sort`` method on - your version of Python. - - >>> d = OrderedDict(((4, 1), (2, 2), (3, 3), (1, 4))) - >>> d.sort() - >>> d - OrderedDict([(1, 4), (2, 2), (3, 3), (4, 1)]) - """ - self._sequence.sort(*args, **kwargs) - -class Keys(object): - # FIXME: should this object be a subclass of list? - """ - Custom object for accessing the keys of an OrderedDict. - - Can be called like the normal ``OrderedDict.keys`` method, but also - supports indexing and sequence methods. - """ - - def __init__(self, main): - self._main = main - - def __call__(self): - """Pretend to be the keys method.""" - return self._main._keys() - - def __getitem__(self, index): - """Fetch the key at position i.""" - # NOTE: this automatically supports slicing :-) - return self._main._sequence[index] - - def __setitem__(self, index, name): - """ - You cannot assign to keys, but you can do slice assignment to re-order - them. - - You can only do slice assignment if the new set of keys is a reordering - of the original set. - """ - if isinstance(index, types.SliceType): - # FIXME: efficiency? - # check length is the same - indexes = range(len(self._main._sequence))[index] - if len(indexes) != len(name): - raise ValueError('attempt to assign sequence of size %s ' - 'to slice of size %s' % (len(name), len(indexes))) - # check they are the same keys - # FIXME: Use set - old_keys = self._main._sequence[index] - new_keys = list(name) - old_keys.sort() - new_keys.sort() - if old_keys != new_keys: - raise KeyError('Keylist is not the same as current keylist.') - orig_vals = [self._main[k] for k in name] - del self._main[index] - vals = zip(indexes, name, orig_vals) - vals.sort() - for i, k, v in vals: - if self._main.strict and k in self._main: - raise ValueError('slice assignment must be from ' - 'unique keys') - self._main.insert(i, k, v) - else: - raise ValueError('Cannot assign to keys') - - ### following methods pinched from UserList and adapted ### - def __repr__(self): return repr(self._main._sequence) - - # FIXME: do we need to check if we are comparing with another ``Keys`` - # object? (like the __cast method of UserList) - def __lt__(self, other): return self._main._sequence < other - def __le__(self, other): return self._main._sequence <= other - def __eq__(self, other): return self._main._sequence == other - def __ne__(self, other): return self._main._sequence != other - def __gt__(self, other): return self._main._sequence > other - def __ge__(self, other): return self._main._sequence >= other - # FIXME: do we need __cmp__ as well as rich comparisons? - def __cmp__(self, other): return cmp(self._main._sequence, other) - - def __contains__(self, item): return item in self._main._sequence - def __len__(self): return len(self._main._sequence) - def __iter__(self): return self._main.iterkeys() - def count(self, item): return self._main._sequence.count(item) - def index(self, item, *args): return self._main._sequence.index(item, *args) - def reverse(self): self._main._sequence.reverse() - def sort(self, *args, **kwds): self._main._sequence.sort(*args, **kwds) - def __mul__(self, n): return self._main._sequence*n - __rmul__ = __mul__ - def __add__(self, other): return self._main._sequence + other - def __radd__(self, other): return other + self._main._sequence - - ## following methods not implemented for keys ## - def __delitem__(self, i): raise TypeError('Can\'t delete items from keys') - def __iadd__(self, other): raise TypeError('Can\'t add in place to keys') - def __imul__(self, n): raise TypeError('Can\'t multiply keys in place') - def append(self, item): raise TypeError('Can\'t append items to keys') - def insert(self, i, item): raise TypeError('Can\'t insert items into keys') - def pop(self, i=-1): raise TypeError('Can\'t pop items from keys') - def remove(self, item): raise TypeError('Can\'t remove items from keys') - def extend(self, other): raise TypeError('Can\'t extend keys') - -class Items(object): - """ - Custom object for accessing the items of an OrderedDict. - - Can be called like the normal ``OrderedDict.items`` method, but also - supports indexing and sequence methods. - """ - - def __init__(self, main): - self._main = main - - def __call__(self): - """Pretend to be the items method.""" - return self._main._items() - - def __getitem__(self, index): - """Fetch the item at position i.""" - if isinstance(index, types.SliceType): - # fetching a slice returns an OrderedDict - return self._main[index].items() - key = self._main._sequence[index] - return (key, self._main[key]) - - def __setitem__(self, index, item): - """Set item at position i to item.""" - if isinstance(index, types.SliceType): - # NOTE: item must be an iterable (list of tuples) - self._main[index] = OrderedDict(item) - else: - # FIXME: Does this raise a sensible error? - orig = self._main.keys[index] - key, value = item - if self._main.strict and key in self and (key != orig): - raise ValueError('slice assignment must be from ' - 'unique keys') - # delete the current one - del self._main[self._main._sequence[index]] - self._main.insert(index, key, value) - - def __delitem__(self, i): - """Delete the item at position i.""" - key = self._main._sequence[i] - if isinstance(i, types.SliceType): - for k in key: - # FIXME: efficiency? - del self._main[k] - else: - del self._main[key] - - ### following methods pinched from UserList and adapted ### - def __repr__(self): return repr(self._main.items()) - - # FIXME: do we need to check if we are comparing with another ``Items`` - # object? (like the __cast method of UserList) - def __lt__(self, other): return self._main.items() < other - def __le__(self, other): return self._main.items() <= other - def __eq__(self, other): return self._main.items() == other - def __ne__(self, other): return self._main.items() != other - def __gt__(self, other): return self._main.items() > other - def __ge__(self, other): return self._main.items() >= other - def __cmp__(self, other): return cmp(self._main.items(), other) - - def __contains__(self, item): return item in self._main.items() - def __len__(self): return len(self._main._sequence) # easier :-) - def __iter__(self): return self._main.iteritems() - def count(self, item): return self._main.items().count(item) - def index(self, item, *args): return self._main.items().index(item, *args) - def reverse(self): self._main.reverse() - def sort(self, *args, **kwds): self._main.sort(*args, **kwds) - def __mul__(self, n): return self._main.items()*n - __rmul__ = __mul__ - def __add__(self, other): return self._main.items() + other - def __radd__(self, other): return other + self._main.items() - - def append(self, item): - """Add an item to the end.""" - # FIXME: this is only append if the key isn't already present - key, value = item - self._main[key] = value - - def insert(self, i, item): - key, value = item - self._main.insert(i, key, value) - - def pop(self, i=-1): - key = self._main._sequence[i] - return (key, self._main.pop(key)) - - def remove(self, item): - key, value = item - try: - assert value == self._main[key] - except (KeyError, AssertionError): - raise ValueError('ValueError: list.remove(x): x not in list') - else: - del self._main[key] - - def extend(self, other): - # FIXME: is only a true extend if none of the keys already present - for item in other: - key, value = item - self._main[key] = value - - def __iadd__(self, other): - self.extend(other) - - ## following methods not implemented for items ## - - def __imul__(self, n): raise TypeError('Can\'t multiply items in place') - -class Values(object): - """ - Custom object for accessing the values of an OrderedDict. - - Can be called like the normal ``OrderedDict.values`` method, but also - supports indexing and sequence methods. - """ - - def __init__(self, main): - self._main = main - - def __call__(self): - """Pretend to be the values method.""" - return self._main._values() - - def __getitem__(self, index): - """Fetch the value at position i.""" - if isinstance(index, types.SliceType): - return [self._main[key] for key in self._main._sequence[index]] - else: - return self._main[self._main._sequence[index]] - - def __setitem__(self, index, value): - """ - Set the value at position i to value. - - You can only do slice assignment to values if you supply a sequence of - equal length to the slice you are replacing. - """ - if isinstance(index, types.SliceType): - keys = self._main._sequence[index] - if len(keys) != len(value): - raise ValueError('attempt to assign sequence of size %s ' - 'to slice of size %s' % (len(name), len(keys))) - # FIXME: efficiency? Would be better to calculate the indexes - # directly from the slice object - # NOTE: the new keys can collide with existing keys (or even - # contain duplicates) - these will overwrite - for key, val in zip(keys, value): - self._main[key] = val - else: - self._main[self._main._sequence[index]] = value - - ### following methods pinched from UserList and adapted ### - def __repr__(self): return repr(self._main.values()) - - # FIXME: do we need to check if we are comparing with another ``Values`` - # object? (like the __cast method of UserList) - def __lt__(self, other): return self._main.values() < other - def __le__(self, other): return self._main.values() <= other - def __eq__(self, other): return self._main.values() == other - def __ne__(self, other): return self._main.values() != other - def __gt__(self, other): return self._main.values() > other - def __ge__(self, other): return self._main.values() >= other - def __cmp__(self, other): return cmp(self._main.values(), other) - - def __contains__(self, item): return item in self._main.values() - def __len__(self): return len(self._main._sequence) # easier :-) - def __iter__(self): return self._main.itervalues() - def count(self, item): return self._main.values().count(item) - def index(self, item, *args): return self._main.values().index(item, *args) - - def reverse(self): - """Reverse the values""" - vals = self._main.values() - vals.reverse() - # FIXME: efficiency - self[:] = vals - - def sort(self, *args, **kwds): - """Sort the values.""" - vals = self._main.values() - vals.sort(*args, **kwds) - self[:] = vals - - def __mul__(self, n): return self._main.values()*n - __rmul__ = __mul__ - def __add__(self, other): return self._main.values() + other - def __radd__(self, other): return other + self._main.values() - - ## following methods not implemented for values ## - def __delitem__(self, i): raise TypeError('Can\'t delete items from values') - def __iadd__(self, other): raise TypeError('Can\'t add in place to values') - def __imul__(self, n): raise TypeError('Can\'t multiply values in place') - def append(self, item): raise TypeError('Can\'t append items to values') - def insert(self, i, item): raise TypeError('Can\'t insert items into values') - def pop(self, i=-1): raise TypeError('Can\'t pop items from values') - def remove(self, item): raise TypeError('Can\'t remove items from values') - def extend(self, other): raise TypeError('Can\'t extend values') - -class SequenceOrderedDict(OrderedDict): - """ - Experimental version of OrderedDict that has a custom object for ``keys``, - ``values``, and ``items``. - - These are callable sequence objects that work as methods, or can be - manipulated directly as sequences. - - Test for ``keys``, ``items`` and ``values``. - - >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4))) - >>> d - SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) - >>> d.keys - [1, 2, 3] - >>> d.keys() - [1, 2, 3] - >>> d.setkeys((3, 2, 1)) - >>> d - SequenceOrderedDict([(3, 4), (2, 3), (1, 2)]) - >>> d.setkeys((1, 2, 3)) - >>> d.keys[0] - 1 - >>> d.keys[:] - [1, 2, 3] - >>> d.keys[-1] - 3 - >>> d.keys[-2] - 2 - >>> d.keys[0:2] = [2, 1] - >>> d - SequenceOrderedDict([(2, 3), (1, 2), (3, 4)]) - >>> d.keys.reverse() - >>> d.keys - [3, 1, 2] - >>> d.keys = [1, 2, 3] - >>> d - SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) - >>> d.keys = [3, 1, 2] - >>> d - SequenceOrderedDict([(3, 4), (1, 2), (2, 3)]) - >>> a = SequenceOrderedDict() - >>> b = SequenceOrderedDict() - >>> a.keys == b.keys - 1 - >>> a['a'] = 3 - >>> a.keys == b.keys - 0 - >>> b['a'] = 3 - >>> a.keys == b.keys - 1 - >>> b['b'] = 3 - >>> a.keys == b.keys - 0 - >>> a.keys > b.keys - 0 - >>> a.keys < b.keys - 1 - >>> 'a' in a.keys - 1 - >>> len(b.keys) - 2 - >>> 'c' in d.keys - 0 - >>> 1 in d.keys - 1 - >>> [v for v in d.keys] - [3, 1, 2] - >>> d.keys.sort() - >>> d.keys - [1, 2, 3] - >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4)), strict=True) - >>> d.keys[::-1] = [1, 2, 3] - >>> d - SequenceOrderedDict([(3, 4), (2, 3), (1, 2)]) - >>> d.keys[:2] - [3, 2] - >>> d.keys[:2] = [1, 3] - Traceback (most recent call last): - KeyError: 'Keylist is not the same as current keylist.' - - >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4))) - >>> d - SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) - >>> d.values - [2, 3, 4] - >>> d.values() - [2, 3, 4] - >>> d.setvalues((4, 3, 2)) - >>> d - SequenceOrderedDict([(1, 4), (2, 3), (3, 2)]) - >>> d.values[::-1] - [2, 3, 4] - >>> d.values[0] - 4 - >>> d.values[-2] - 3 - >>> del d.values[0] - Traceback (most recent call last): - TypeError: Can't delete items from values - >>> d.values[::2] = [2, 4] - >>> d - SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) - >>> 7 in d.values - 0 - >>> len(d.values) - 3 - >>> [val for val in d.values] - [2, 3, 4] - >>> d.values[-1] = 2 - >>> d.values.count(2) - 2 - >>> d.values.index(2) - 0 - >>> d.values[-1] = 7 - >>> d.values - [2, 3, 7] - >>> d.values.reverse() - >>> d.values - [7, 3, 2] - >>> d.values.sort() - >>> d.values - [2, 3, 7] - >>> d.values.append('anything') - Traceback (most recent call last): - TypeError: Can't append items to values - >>> d.values = (1, 2, 3) - >>> d - SequenceOrderedDict([(1, 1), (2, 2), (3, 3)]) - - >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4))) - >>> d - SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) - >>> d.items() - [(1, 2), (2, 3), (3, 4)] - >>> d.setitems([(3, 4), (2 ,3), (1, 2)]) - >>> d - SequenceOrderedDict([(3, 4), (2, 3), (1, 2)]) - >>> d.items[0] - (3, 4) - >>> d.items[:-1] - [(3, 4), (2, 3)] - >>> d.items[1] = (6, 3) - >>> d.items - [(3, 4), (6, 3), (1, 2)] - >>> d.items[1:2] = [(9, 9)] - >>> d - SequenceOrderedDict([(3, 4), (9, 9), (1, 2)]) - >>> del d.items[1:2] - >>> d - SequenceOrderedDict([(3, 4), (1, 2)]) - >>> (3, 4) in d.items - 1 - >>> (4, 3) in d.items - 0 - >>> len(d.items) - 2 - >>> [v for v in d.items] - [(3, 4), (1, 2)] - >>> d.items.count((3, 4)) - 1 - >>> d.items.index((1, 2)) - 1 - >>> d.items.index((2, 1)) - Traceback (most recent call last): - ValueError: list.index(x): x not in list - >>> d.items.reverse() - >>> d.items - [(1, 2), (3, 4)] - >>> d.items.reverse() - >>> d.items.sort() - >>> d.items - [(1, 2), (3, 4)] - >>> d.items.append((5, 6)) - >>> d.items - [(1, 2), (3, 4), (5, 6)] - >>> d.items.insert(0, (0, 0)) - >>> d.items - [(0, 0), (1, 2), (3, 4), (5, 6)] - >>> d.items.insert(-1, (7, 8)) - >>> d.items - [(0, 0), (1, 2), (3, 4), (7, 8), (5, 6)] - >>> d.items.pop() - (5, 6) - >>> d.items - [(0, 0), (1, 2), (3, 4), (7, 8)] - >>> d.items.remove((1, 2)) - >>> d.items - [(0, 0), (3, 4), (7, 8)] - >>> d.items.extend([(1, 2), (5, 6)]) - >>> d.items - [(0, 0), (3, 4), (7, 8), (1, 2), (5, 6)] - """ - - def __init__(self, init_val=(), strict=True): - OrderedDict.__init__(self, init_val, strict=strict) - self._keys = self.keys - self._values = self.values - self._items = self.items - self.keys = Keys(self) - self.values = Values(self) - self.items = Items(self) - self._att_dict = { - 'keys': self.setkeys, - 'items': self.setitems, - 'values': self.setvalues, - } - - def __setattr__(self, name, value): - """Protect keys, items, and values.""" - if not '_att_dict' in self.__dict__: - object.__setattr__(self, name, value) - else: - try: - fun = self._att_dict[name] - except KeyError: - OrderedDict.__setattr__(self, name, value) - else: - fun(value) - -if __name__ == '__main__': - if INTP_VER < (2, 3): - raise RuntimeError("Tests require Python v.2.3 or later") - # turn off warnings for tests - warnings.filterwarnings('ignore') - # run the code tests in doctest format - import doctest - m = sys.modules.get('__main__') - globs = m.__dict__.copy() - globs.update({ - 'INTP_VER': INTP_VER, - }) - doctest.testmod(m, globs=globs) - diff --git a/lib/dodai/__init__.py b/lib/dodai/__init__.py new file mode 100644 index 0000000..55bb57d --- /dev/null +++ b/lib/dodai/__init__.py @@ -0,0 +1,72 @@ +# Copyright (C) 2010 Leonard Thomas +# +# This file is part of Dodai. +# +# Dodai 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, either version 3 of the License, or +# (at your option) any later version. +# +# Dodai 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 Dodai. If not, see . + +import os +import sys +from dodai.config import Config +from dodai.tools import config_directories + +class Configure(Config): + + CONFIG_FILES = ['config', 'configs', 'configure', 'connect', + 'connections', 'connection', 'setup'] + CONFIG_EXTENSIONS = ['cfg', 'txt', 'ini'] + + + def __init__(self, project_name, config_files=None): + self.project_name = project_name + self._load_config_files() + self._add_files(config_files) + self._add_config_files_to_database_handler() + + def _config_files(self): + # Returns a list of possible config file names + out = [] + for name in self.CONFIG_FILES: + out.append(name) + for ext in self.CONFIG_EXTENSIONS: + name = "{0}.{1}".format(name, ext) + out.append(name) + return out + + def _load_config_files(self): + # Adds any default config file if it exists + out = [] + directories = config_directories(self.project_name) + for dir in directories: + if os.path.isdir(dir): + for name in self._config_files(): + path = os.path.join(dir, name) + self._add_file(path) + + def _add_files(self, filenames): + # Adds a list or tuple of filenames + if filenames: + if isinstance(filenames, list) or isinstance(filenames, tuple): + for filename in filenames: + self._add_file(filename) + else: + self._add_file(filenames) + + def _add_file(self, filename): + # Adds the given filename to the dodai.config.files object + if os.path.isfile(filename): + self.files.add(filename) + + def _add_config_files_to_database_handler(self): + sections = self.files.load() + self.databases.add(sections) diff --git a/lib/dodai/config/__init__.py b/lib/dodai/config/__init__.py new file mode 100644 index 0000000..e849f8e --- /dev/null +++ b/lib/dodai/config/__init__.py @@ -0,0 +1,76 @@ +# Copyright (C) 2010 Leonard Thomas +# +# This file is part of Dodai. +# +# Dodai 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, either version 3 of the License, or +# (at your option) any later version. +# +# Dodai 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 Dodai. If not, see . + + +class Config(object): + + def __call__(self): + obj = ConfigResults() + if hasattr(self, 'vars'): + for key, val in self.vars.items(): + setattr(obj, key, val) + return obj + + @property + def files(self): + if not hasattr(self, '_files'): + from dodai.config.files import ConfigFiles + from dodai.tools.odict import OrderedDict + from dodai.tools import Section + from dodai.tools.himo import String2Himo + from ConfigParser import ConfigParser + s2h = String2Himo() + self._files = ConfigFiles(OrderedDict, Section, s2h) + self._files.register_parser_object(ConfigParser) + return self._files + + @property + def options(self): + if not hasattr(self, '_options'): + from dodai.config.option import ConfigOption + self._options = ConfigOption() + return self._options + + @property + def logs(self): + if not hasattr(self, '_logs'): + from dodai.config.log import ConfigLog + self._logs = ConfigLog() + return self._logs + + @property + def databases(self): + if not hasattr(self, '_databases'): + # Wire up the sqlalchemy objects to use in the sa object + # which will be used as the default database handler + from dodai.config.databases import ConfigDatabases + from dodai.config.databases.sa import Sa + from sqlalchemy.orm import sessionmaker + from sqlalchemy import create_engine + from dodai.db import Db + sa = Sa(create_engine, sessionmaker, Db) + self._databases = ConfigDatabases(sa, 'sa') + return self._databases + + def set(self, key, val): + if not hasattr(self, 'vars'): + self.vars = {} + self.vars[key] = val + + +class ConfigResults(object): + pass diff --git a/lib/dodai/config/databases/__init__.py b/lib/dodai/config/databases/__init__.py new file mode 100644 index 0000000..c91e77f --- /dev/null +++ b/lib/dodai/config/databases/__init__.py @@ -0,0 +1,285 @@ +# Copyright (C) 2010 Leonard Thomas +# +# This file is part of Dodai. +# +# Dodai 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, either version 3 of the License, or +# (at your option) any later version. +# +# Dodai 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 Dodai. If not, see . + + +from dodai.exception import DatabaseEmptyOptionException +from dodai.exception import DatabasePortException +from dodai.exception import DatabaseHostnameException +from dodai.exception import DatabaseProtocolException +from dodai.exception import DatabaseConnectionException +from dodai.exception import UnknownDatabaseConnectionException +from dodai.tools import list_to_english + +class ConfigDatabases(object): + """An object that is used for creating database connection objects + + """ + def __init__(self, default_handler, default_name): + self.connections = {} + self._handlers = {} + DatabaseConnection.DEFAULT_HANDLER = default_name + self.add_handler(default_name, default_handler) + + def add(self, sections): + """Adds a dictionary of sections to this object that could + be used for creating database connection objects. The dictionary + should be formatted as follows: + + sections[name] = {option: value, option: value, ....} + + The 'name' is the section name. It is an identifier that is used + to load the database: + + The options and values can have the following: + + protocol: The database engine (postgresql, mysql, sqlite, + oracle, mssql etc..) + + hostname: The hostname or ip address of the database server + + port: The port that should be used to connect to the + database server should be a number between 1 + and 65535 + + username: The username used to connect to the database server + + password: The password used to connect to the database server + + database: The database to connect to once connected to the + server + + schema: The schema that will be used. This option does + not do anything but will exist in the db object + for use + + filename: The file path to the database file (sqlite) + + protocol_extra: Used to tell the handler which python db engine + to use. For example one might want to tell + the sqlalchemy handler to use the psycopg2 + python object. + + handler: The handler used to build the orm object. Dodai + only works with sqlalchemy so the default 'sa' + is used. + + """ + for name, section in sections.items(): + self.connections[name] = DatabaseConnection(section, name, + self._handlers) + + def load(self, section_name): + """Returns a database connection object of the given section_name. + Throws Exceptions for any type of configuration errors or missing + configuration data + + """ + if section_name in self.connections: + return self.connections[section_name].load() + else: + raise UnknownDatabaseConnectionException(section_name) + + def add_handler(self, name, handler): + """Adds the given handler and name to this objects handlers + + """ + self._handlers[name] = handler + +class DatabaseConnection(object): + + DEFAULT_HANDLER = None + + def __init__(self, section, name, handlers): + self.section = section + self.name = name + self.handlers = handlers + self.validate = DatabaseConnectionValidator.load(section, name) + self.obj = None + + def load(self): + if not self.obj: + self.validate() + handler = self._get_handler() + self.obj = handler(self.section, self.name) + return self.obj + + def _get_handler(self): + if self.section.has_key('handler'): + name = self.section['handler'] + if name in self.handlers: + return self.handlers[name] + return self.handlers[self.DEFAULT_HANDLER] + + +class DatabaseConnectionValidator(object): + + def __init__(self, section, name, validators=[]): + self.section = section + self.name = name + self.validators = validators + + def __call__(self): + return self.validate() + + def validate(self): + """Validates the connection. Returns true if valid. Throws + DodaiDatabaseConnectionConfigurationError on any errors + + """ + raise NotImplementedError() + + def _validate_option(self, name): + try: + value = self.section[name] + except KeyError: + value = None + + if not value: + raise DatabaseEmptyOptionException(self.name, name) + return True + + def _validate_protocol(self): + + self._validate_option('protocol') + + if self.section['protocol'] not in self.PROTOCOLS: + raise DatabaseProtocolException(self.name, + self.section['protocol'], self.DB_TYPE, + self.PROTOCOLS) + return True + + @staticmethod + def load(section, name): + """Static method (factory) that loads the correct database + validation class. + + Attributes: + section: Dictionary of key val connection information + name: String name of the section + + """ + action = None + validators = [DatabaseConnectionValidatorServer, + DatabaseConnectionValidatorFile] + + for validator in validators: + obj = validator.load(section, name) + if obj: + return obj + + return DatabaseConnectionValidatorUnknown(section, name, validators) + +class DatabaseConnectionValidatorServer(DatabaseConnectionValidator): + + DB_TYPE = 'server' + REQUIRED = ['protocol', 'hostname', 'port', 'username', 'password', + 'database'] + PROTOCOLS = ['postgresql', 'mysql', 'mssql', 'oracle'] + + def _validate_port(self): + self._validate_option('port') + + try: + port = int(self.section['port']) + except ValueError: + port = self.section['port'] + else: + if port >=1 and port <=65535: + return True + raise DatabasePortException(self.name, port) + + def validate(self): + """Validates the connection. Returns true if valid. Throws + DodaiDatabaseConnectionConfigurationError on any errors + + """ + self._validate_protocol() + self._validate_port() + self._validate_option('hostname') + self._validate_option('username') + self._validate_option('password') + self._validate_option('database') + return True + + @classmethod + def load(cls, section, name): + """Return this validation class if it is possible that the + given connection information contains enough data to make + a database server connection. + + Attributes: + section: Dictionary of key val connection information + name: String name of the section + + """ + if section.has_key('protocol'): + if section['protocol'] in cls.PROTOCOLS: + return cls(section, name) + keys = section.keys() + for key in keys: + if key in ['hostname', 'port']: + return cls(section, name) + + +class DatabaseConnectionValidatorFile(DatabaseConnectionValidator): + + DB_TYPE = 'file' + REQUIRED = ['protocol', 'filename'] + PROTOCOLS = ['sqlite'] + + def validate(self): + """Validates the connection. Returns true if valid. Throws + DodaiDatabaseConnectionConfigurationError on any errors + + """ + self._validate_protocol() + self._validate_option('filename') + return True + + @classmethod + def load(cls, section, name): + """Return this validation class if it is possible that the + given connection information contains enough data to make + a database file connection. + + Attributes: + section: Dictionary of key val connection information + name: String name of the section + + """ + if section.has_key('protocol'): + if section['protocol'] in cls.PROTOCOLS: + return cls(section, name) + keys = section.keys() + for key in keys: + if key in ['filename']: + return cls(section, name) + + +class DatabaseConnectionValidatorUnknown(DatabaseConnectionValidator): + + DB_TYPE = 'unkonwn' + REQUIRED = [] + PROTOCOLS = [] + + def validate(self): + """Validates the connection. Returns true if valid. Throws + DodaiDatabaseConnectionConfigurationError on any errors + + """ + + raise DatabaseConnectionException(self.name, self.validators) diff --git a/lib/dodai/config/databases/sa.py b/lib/dodai/config/databases/sa.py new file mode 100644 index 0000000..eaf35ea --- /dev/null +++ b/lib/dodai/config/databases/sa.py @@ -0,0 +1,90 @@ +# Copyright (C) 2010 Leonard Thomas +# +# This file is part of Dodai. +# +# Dodai 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, either version 3 of the License, or +# (at your option) any later version. +# +# Dodai 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 Dodai. If not, see . + +class Sa(object): + """Callable object that will wire up sqlalchemy engine and + session objects + + Attributes: + create_engine: The sqlalchemy create_engine object + session_maker: The sqlalchemy session_maker object + result_object: The object that will be populated with + all the connection information along with + the sqlalchemy objects + + """ + def __init__(self, create_engine, session_maker, result_object): + self._create_engine = create_engine + self._session_maker = session_maker + self._result_object = result_object + + def __call__(self, section, name): + db = self._result_object() + db.name = name + db.protocol = section['protocol'] + + if section.has_key('protocol_extra') and section['protocol_extra']: + db.protocol_extra = section['protocol_extra'] + + if section.has_key('filename') and section['filename']: + db.filename = section['filename'] + + if section.has_key('port') and section['port']: + db.port = int(section['port']) + + if section.has_key('database') and section['database']: + db.database = section['database'] + + if section.has_key('schema') and section['schema']: + db.schema = section['schema'] + + if section.has_key('username') and section['username']: + db.username = section['username'] + + if section.has_key('hostname') and section['hostname']: + db.hostname = section['hostname'] + + db.engine = self._build_engine(section) + db.session = self._build_session(db.engine) + return db + + def _build_session(self, engine): + session = self._session_maker(bind=engine) + return session() + + def _build_connection_string(self, section): + out = [] + out.append('{section[protocol]}') + if section.has_key('protocol_extra') and section['protocol_extra']: + out.append('+{section[protocol_extra]}') + out.append('://') + if section.has_key('filename') and section['filename']: + out.append('/{section[filename]}') + else: + out.append('{section[username]}:{section[password]}@') + out.append('{section[hostname]}') + if section.has_key('port') and section['port']: + out.append(':{section[port]}') + out.append('/{section[database]}') + out = ''.join(out) + out = out.format(section=section) + return out + + def _build_engine(self, section): + connection_string = self._build_connection_string(section) + db_obj = self._create_engine(connection_string) + return db_obj diff --git a/lib/dodai/config/files.py b/lib/dodai/config/files.py new file mode 100644 index 0000000..a27e4a2 --- /dev/null +++ b/lib/dodai/config/files.py @@ -0,0 +1,106 @@ +# Copyright (C) 2010 Leonard Thomas +# +# This file is part of Dodai. +# +# Dodai 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, either version 3 of the License, or +# (at your option) any later version. +# +# Dodai 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 Dodai. If not, see . + +import os +import ConfigParser +from dodai.config.sections import ConfigSections +from dodai.exception import InvalidConfigParser +from dodai.exception import FileDoesNotExist +from dodai.exception import FileIsDirectory + +class ConfigFiles(object): + + REQUIRED_METHODS = ['read', 'sections', 'options', 'get'] + + def __init__(self, ordered_dict_object, section_object, string_object): + + self._ordered_dict_object = ordered_dict_object + self._section_object = section_object + self._string_object = string_object + self.parser_objects = [] + self.files = self._ordered_dict_object() + self.files_read = [] + + def register_parser_object(self, parser_object): + """Registers a config file parser with this object. Raises + InvalidConfigParser error if the parser does not have the 'read', + 'sections', 'options' and 'get' methods. + + """ + if self.check_parser_object(parser_object): + self.parser_objects.append(parser_object) + + def check_parser_object(self, parser_object): + """Checks the given parser object to insure it has all of the required + methods needed to parse files. + + """ + for name in self.REQUIRED_METHODS: + if not hasattr(parser_object, name): + raise InvalidConfigParser(self.REQUIRED_METHODS, + parser_object.__name__) + return True + + def add(self, filename, encoding=None): + """Adds the given filename to this object to be parsed. + + """ + if os.path.exists(filename): + if os.path.isdir(filename): + raise FileIsDirectory(filename) + else: + filename = os.path.realpath(filename) + self.files[filename] = encoding + else: + raise FileDoesNotExist(filename) + + def _read(self): + self.files_read = [] + out = self._ordered_dict_object() + for filename, encoding in self.files.items(): + error = False + for parser_obj in self.parser_objects: + try: + parser = parser_obj() + parser.read(filename) + except Exception, e: + error = True + else: + out[filename] = self._ordered_dict_object() + out[filename]['encoding'] = encoding + out[filename]['parser'] = parser + self.files_read.append(filename) + error = False + break + if error: + raise e + return out + + + def load(self): + """Returns a ConfigSections object, which acts like a dictionary, + that contains all parsed data. + + """ + files = self._read() + sections = ConfigSections(self._ordered_dict_object, + self._section_object, self._string_object) + for filename, data in files.items(): + encoding = data['encoding'] + parser = data['parser'] + sections(parser, encoding) + return sections diff --git a/lib/dodai/config/log.py b/lib/dodai/config/log.py new file mode 100644 index 0000000..fdb5c93 --- /dev/null +++ b/lib/dodai/config/log.py @@ -0,0 +1,150 @@ +# Copyright (C) 2010 Leonard Thomas +# +# This file is part of Dodai. +# +# Dodai 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, either version 3 of the License, or +# (at your option) any later version. +# +# Dodai 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 Dodai. If not, see . + + +import logging +import logging.handlers +import os + +class ConfigLog(object): + + LEVELS = { + logging.CRITICAL: [ + 'critical', + "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + "%(message)s"], + logging.ERROR: [ + 'error', + "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + "%(message)s"], + logging.WARNING: [ + 'warning', + "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + "%(message)s"], + logging.INFO: [ + 'info', + "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + "%(message)s"], + logging.DEBUG: [ + 'debug', + "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + "%(message)s"] + } + + MAX_BYTES = 10485760 + BACKUP_COUNT = 5 + FILE_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + STDOUT_FORMAT = "%(message)s" + + def __init__(self): + self.log_level = logging.CRITICAL + self.directory = None + self._levels = {} + + def set_log_level(self, level): + try: + level = self._fetch_log_level(level) + except InvalidLevelException: + pass + else: + self.log_level = level + + def set_directory(self, directory): + if os.path.isdir(directory): + self.directory = directory + else: + raise NoDirectoryExistException(directory) + + def get_file_message_format(self, level): + if not self._levels: + self._levels = self.LEVELS + level = self._fetch_log_level(level) + return self._levels[level][1] + + def get_screen_message_format(self, level): + if not self._levels: + self._levels = self.LEVELS + level = self._fetch_log_level(level) + return self._levels[level][2] + + def _fetch_log_level(self, level): + out = None + if isinstance(level, str): + level = level.strip().lower() + if level in self.LEVELS: + out = level + else: + for key, items in self.LEVELS.items(): + if level == items[0]: + out = key + if not out: + raise InvalidLevelException(level) + else: + return out + + def _build_filepath(self, data): + data = os.path.normpath(data) + if data.startswith(os.path.sep): + dir = os.path.dirname(data) + if not os.path.isdir(dir): + raise NoDirectoryExistException(dir) + else: + if not self.directory: + raise DirectoryNotSetException() + else: + data = os.path.join(self.directory, data) + return data + + def load(self, name): + log =logging.getLogger(name) + log.setLevel(self.log_level) + return log + + def attach_file_handler(self, log, filename): + filepath = self._build_filepath(filename) + handler = logging.handlers.RotatingFileHandler( + filepath, maxBytes = self.MAX_BYTES, + backupCount=self.BACKUP_COUNT) + file_format = self.get_file_message_format(self.log_level) + format_obj = logging.Formatter(file_format) + handler.setFormatter(format_obj) + handler.setLevel(self.log_level) + log.addHandler(handler) + + def attach_screen_handler(self, log, level=None): + if level: + level = self._fetch_log_level(level) + else: + level = self.log_level + message_format = self.get_screen_message_format(level) + handler = logging.StreamHandler() + handler.setLevel(level) + format_obj = logging.Formatter(message_format) + handler.setFormatter(format_obj) + log.addHandler(handler) + + +class NoDirectoryExistException(Exception): + pass + + +class DirectoryNotSetException(Exception): + pass + + +class InvalidLevelException(Exception): + pass diff --git a/lib/dodai/config/option.py b/lib/dodai/config/option.py new file mode 100644 index 0000000..0561881 --- /dev/null +++ b/lib/dodai/config/option.py @@ -0,0 +1,61 @@ +# Copyright (C) 2010 Leonard Thomas +# +# This file is part of Dodai. +# +# Dodai 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, either version 3 of the License, or +# (at your option) any later version. +# +# Dodai 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 Dodai. If not, see . + + +from optparse import OptionParser + +class ConfigOption(object): + + def __init__(self): + + self.parser = OptionParser() + self._options = None + self._args = [] + + def get_args(self): + self._parse_args() + return self._args + + def get_options(self): + self._parse_args() + return self._options + + def _parse_args(self): + options, args = self.parser.parse_args() + self._options = options + self._args = args + + def add_quiet(self): + + self.parser.add_option("-q", "--quiet", dest="verbose", default=True, + action="store_false", + help="Don't print status messages to the screen") + + def add_verbose(self): + self.parser.add_option("-v", "--verbose", dest="verbose", + action="store_true", + default=False, help="Print status messages to the screen") + + def add_log_level(self, default='critical'): + self.parser.add_option("-l", "--log-level", dest="log_level", + default=default, help="Sets the log level") + + def add_setup(self): + self.parser.add_option('', "--setup", dest="setup", + action="store_true", default=False, + help="run the setup which builds the config "\ + "files.") diff --git a/lib/dodai/config/sections.py b/lib/dodai/config/sections.py new file mode 100644 index 0000000..d789a24 --- /dev/null +++ b/lib/dodai/config/sections.py @@ -0,0 +1,117 @@ +# Copyright (C) 2010 Leonard Thomas +# +# This file is part of Dodai. +# +# Dodai 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, either version 3 of the License, or +# (at your option) any later version. +# +# Dodai 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 Dodai. If not, see . + + +class ConfigSections(object): + """ + An iterable object that contains ConfigSection objects + + """ + + def __init__(self, ordered_dict_object, section_object, + string_object = None,): + """ + Iterable object that handles the conversion of a config + parser object to a list of section objects. + + + string_object: This is an object (non instantiated or + callable) that the results of the config's + sections, and options will be stored in. + This enables you to store your values as a + custom object. A good object to use is the + dodai.tools.himo Himo object. If the + string_object is not given the default python + str() object will be used. The callable of + this object must also allow for encoding + to be passed in string_object(data, 'UTF-8') + + + """ + self._string_object = string_object or '' + self._ordered_dict_object = ordered_dict_object + self._section_object = section_object + self._sections = self._ordered_dict_object() + + def __call__(self, parser, encoding=None): + """ + Parses the given parser object into this object's sections. + + parser: The actual parser object that is used to + get the sections. This object must have + the sections(), options() and get() + methods. A good object to use is the native + ConfigParse object. However, you can create + your own + + """ + self._build_sections(parser, encoding) + + def _build_sections(self, parser, encoding): + # Builds ConfigSection objects from the parser object + + for section_name in parser.sections(): + section = self.get_section(section_name, encoding) + self._build_options(parser, section_name, section, encoding) + + def _build_options(self, parser, section_name, section, encoding): + # Adds the options to the section object + + for key in parser.options(section_name): + value = self._build_string_object(parser.get(section_name, key), + encoding) + key = self._build_string_object(key, encoding) + section[key] = value + + def _build_string_object(self, data, encoding=None): + if self._string_object: + return self._string_object(data, encoding) + else: + return data + + def get_section(self, section_name, encoding=None): + """ + Returns a Section (aka dict) object from this object's section + dictionary or creates a new ConfigSection object, which is + stored int this object's section dictionary then is returned + + """ + section_name = self._build_string_object(section_name, encoding) + if section_name in self._sections: + return self._sections[section_name] + else: + section = self._section_object(section_name) + self._sections[section_name] = section + return section + + def __getitem__(self, key): + return self._sections[key] + + def __getattr__(self, key): + try: + out = self._sections[key] + except KeyError: + return getattr(self._sections, key) + else: + return out + + def __iter__(self, *args, **kargs): + return self._sections.__iter__(*args, **kargs) + + + def __len__(self): + return len(self._sections) diff --git a/lib/dodai/db.py b/lib/dodai/db.py new file mode 100644 index 0000000..5cbe605 --- /dev/null +++ b/lib/dodai/db.py @@ -0,0 +1,30 @@ +# Copyright (C) 2010 Leonard Thomas +# +# This file is part of Dodai. +# +# Dodai 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, either version 3 of the License, or +# (at your option) any later version. +# +# Dodai 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 Dodai. If not, see . + + +class Db(object): + pass + + def __init__(self): + self.name = None + self.protocol = None + self.hostname = None + self.port = None + self.database = None + self.filename = None + self.engine = None + self.session = None diff --git a/lib/dodai/exception.py b/lib/dodai/exception.py new file mode 100644 index 0000000..23302c7 --- /dev/null +++ b/lib/dodai/exception.py @@ -0,0 +1,289 @@ +# Copyright (C) 2010 Leonard Thomas +# +# This file is part of Dodai. +# +# Dodai 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, either version 3 of the License, or +# (at your option) any later version. +# +# Dodai 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 Dodai. If not, see . + +from dodai.tools import list_to_english +from dodai.tools import quote_list + +class DodaiException(Exception): + + def _system_encoding(self): + # Returns the character encoding of the operating system + + encoding = sys.getdefaultencoding() + filesystem_encoding = sys.getfilesystemencoding() + if filesystem_encoding: + encoding = filesystem_encoding + return encoding + + +class HimoAsciiError(DodaiException): + """Exception raised when the Himo object can't convert a character + down to it's root character. + + Attributes: + char: The character that can't be converted down to ascii + """ + MESSAGE="Unable to convert the '{char}' character to ascii" + + def __init__(self, char): + self.char = char + self.msg = self._build_message() + + def _build_message(self): + encoding = self._system_encoding() + try: + char = self.char.encode(encoding) + except UnicodeEncodeError: + char = 'unichr({0})'.format(ord(self.char)) + return self.MESSAGE.format(char=self.char) + + def __str__(self): + return self.msg + +class DodaiDatabaseConnectionConfigurationError(DodaiException): + pass + + +class DatabaseEmptyOptionException(DodaiDatabaseConnectionConfigurationError): + """Exception raised for an empty option in the config + + Attributes: + section_name: The section of the config file that contains + the invalid protocol + option_name: The name of the empty option + + """ + MESSAGE = "In the '{section_name}' section, the '{option_name}' "\ + "was not set or is missing. Please set this option." + + def __init__(self, section_name, option_name): + self.section_name = section_name + self.option_name = option_name + self.msg = self._build_message() + + def _build_message(self): + return self.MESSAGE.format(section_name=self.section_name, + option_name=self.option_name) + + def __str__(self): + return self.msg + + +class DatabasePortException(DodaiDatabaseConnectionConfigurationError): + """Exception raised for invalid database port connection + + Attributes: + section_name: The section of the config file that contains + the invalid protocol + section_port: The port value that was listed in the + config file + + """ + MESSAGE = "In the '{section_name}' section, the port of "\ + "'{section_port}' is invalid. The port must be a "\ + "number between 1 and 65535" + + def __init__(self, section_name, section_port): + self.section_name = section_name + self.section_port = section_port + self.msg = self._build_message() + + def _build_message(self): + return self.MESSAGE.format(section_name=self.section_name, + section_port=self.section_port) + + def __str__(self): + return self.msg + + +class DatabaseHostnameException(DodaiDatabaseConnectionConfigurationError): + """Exception raised for invalid database hostname + + Attributes: + section_name: The section of the config file that contains + the invalid protocol + section_hostname: The hostname value that was listed in the + config file + + """ + MESSAGE = "In the '{section_name}' section, the hostname of "\ + "'{section_hostname}' is invalid. Please use a valid "\ + "hostname." + + MSG_NON = "In the '{section_name}' section, the hostname was "\ + "not set. Please set the hostname." + + def __init__(self, section_name, section_hostname): + self.section_name = section_name + self.section_hostname = section_hostname + self.msg = self._build_message() + + def _build_message(self): + if self.section_hostname: + return self.MESSAGE.format(section_name=self.section_name, + section_hostname=self.section_hostname) + else: + return self.MSG_NON.format(section_name=self.section_name) + + def __str__(self): + return self.msg + + +class DatabaseProtocolException(DodaiDatabaseConnectionConfigurationError): + """Exception raised for invalid database connection protocols + + Attributes: + section_name: The section of the config file that contains + the invalid protocol + section_protocol: The protocol value that was listed in the + config file + database_type: Usually 'server' or 'file' + protocols: List of valid protocols + + """ + MESSAGE = "In the '{section_name}' section, the protocol of "\ + "'{section_protocol}' is invalid. The valid protocols "\ + "for a '{database_type}' connection are: {protocols}" + + def __init__(self, section_name, section_protocol, database_type, + protocols): + self.section_name = section_name + self.section_protocol = section_protocol + self.database_type = database_type + self.protocols = protocols + self.msg = self._build_message() + + def _build_message(self): + protocols = list_to_english(self.protocols) + return self.MESSAGE.format(section_name=self.section_name, + section_protocol=self.section_protocol, + database_type=self.database_type, + protocols=protocols) + + def __str__(self): + return self.msg + + +class DatabaseConnectionException(DodaiDatabaseConnectionConfigurationError): + """Exception raised for missing database connection parameters + + Attributes: + section_name: The section of the config file that contains + the invalid connection information + options_required: A dictionary containing the database_type + as the key and a list of required options + as the value + + """ + MESSAGE = "The '{section_name}' section does not contain all of the "\ + "correct information needed to make a database connection." + MSG_TYPE = "To make a '{database_type}' connection please make sure "\ + "To have all of the following options: {options}." + MSG_END = "Please remember that the option names are case sensitive." + + def __init__(self, section_name, validators): + self.section_name = section_name + self.validators = validators + self.msg = self._build_message() + + def _build_message(self): + out = [] + out.append(self.MESSAGE.format(section_name=self.section_name)) + for validator in self.validators: + options = list_to_english(validator.REQUIRED) + out.append(self.MSG_TYPE.format(database_type=validator.DB_TYPE, + options=options)) + out.append(self.MSG_END) + return ' '.join(out) + + def __str__(self): + return self.msg + + +class UnknownDatabaseConnectionException( + DodaiDatabaseConnectionConfigurationError): + """Exception raised for missing database connection parameters + + Attributes: + section: The requested section of the config file that can not + be found. + + """ + MESSAGE = "Unable to find the '{section}' section to create a "\ + "database connection." + + def __init__(self, section): + self.section = section + self.msg = self._build_message() + + def _build_message(self): + return self.MESSAGE.format(section=self.section) + + def __str__(self): + return self.msg + + +class InvalidConfigParser(DodaiException): + """Exception raised when an invalid parser is registered in the + dodai.config.ConfigFiles object. + + """ + MESSAGE = "The parser object '{name}' that you were trying to register "\ + "is not a valid parser object. Please make sure that this "\ + "object contains all of the following methods: {methods}" + + def __init__(self, required_methods, name): + self.msg = self._build_message(required_methods, name) + + def _build_message(self, required_methods, name): + methods = quote_list(required_methods) + return self.MESSAGE.format(methods=methods, name=name) + + def __str__(self): + return self.msg + + +class FileDoesNotExist(DodaiException): + """Exception raised when a file does not exist. + + """ + MESSAGE = "The file: '{file_}' does not exist." + + def __init__(self, filepath): + self.msg = self._build_message(filepath) + + def _build_message(self, filepath): + return self.MESSAGE.format(file_=filepath) + + def __str__(self): + return self.msg + + +class FileIsDirectory(DodaiException): + """Exception raised when a file is a directory. + + """ + MESSAGE = "The file: '{file_}' is a directory." + + def __init__(self, filepath): + self.msg = self._build_message(filepath) + + def _build_message(self, filepath): + return self.MESSAGE.format(file_=filepath) + + def __str__(self): + return self.msg diff --git a/lib/dodai/tools/__init__.py b/lib/dodai/tools/__init__.py new file mode 100644 index 0000000..bcef547 --- /dev/null +++ b/lib/dodai/tools/__init__.py @@ -0,0 +1,143 @@ +# Copyright (C) 2010 Leonard Thomas +# +# This file is part of Dodai. +# +# Dodai 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, either version 3 of the License, or +# (at your option) any later version. +# +# Dodai 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 Dodai. If not, see . + +import sys +import os +import platform +from dodai.tools.odict import OrderedDict +import unicodedata + +def home_directory(): + """Returns the full real path to the home directory of the user who + is executing this script. + + """ + out = None + try: + from win32com.shell import shellcon, shell + out = shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, 0, 0) + except ImportError: + out = os.path.expanduser("~") + return os.path.realpath(out) + +def config_directory_system(project_name=None): + """Returns the system config directory with the passed in project + appended to the path. Returns None for windows and java systems. + + """ + path = None + if platform.system and platform.system() not in ['Windows', 'Java']: + if project_name: + path = os.path.join('/etc', project_name.strip()) + else: + path = '/etc' + return path + +def config_directory_user(project_name): + """Returns the config direcotry of the project which is located in + the user's home directory. Returns None for Java and unknown systems + + """ + path = None + project = '.{0}'.format(project_name.strip()) + if platform.system and platform.system() not in ['Java']: + home_dir = home_directory() + path = os.path.join(home_dir, project) + return path + +def config_directory_project(): + """Returns the config directory that is located in the same directory + of the executable that ran this script + + """ + path = os.path.dirname(os.path.abspath(sys.argv[0])) + return os.path.join(path, 'config') + +def config_directories(project_name): + """Returns a list of possible project directories + + """ + dirs = [] + dir = config_directory_system(project_name) + if dir: + dirs.append(dir) + dir = config_directory_user(project_name) + if dir: + dirs.append(dir) + dir = config_directory_project() + if dir: + dirs.append(dir) + return dirs + +def list_to_english(data): + """Takes the input list and creates a string with each option + encased in single quotes and seperated by a comma with exception + of the last option which is prefixed with the word 'and' + + """ + if data: + if len(data) > 1: + out = [] + last = "{0}".format(data.pop()) + for row in data: + out.append("{0}".format(row)) + out = ', '.join(out) + return "{0} and {1}".format(out, last) + else: + return "{0}".format(data.pop()) + +def quote_list(data, double_quotes=False): + """Takes the give list (data) and adds quotes around each item. Returns + a list + + """ + out = [] + if double_quotes: + quote = '"' + else: + quote = "'" + for item in data: + item = "{quote}{item}{quote}".format(quote=quote, item=item) + out.append(item) + return out + +def normalize_unicode(data): + """Normalizes the unicode data so that compare functions will work + correctly. + + """ + data = unicode(data) + data = unicodedata.normalize('NFC', data) + return data + +class Section(OrderedDict): + """An ordered dictionary object that has the added benfit of holding + a name or title of this grouping. + + """ + def __init__(self, title): + self.set_section_title(title) + super(Section,self).__init__() + + def get_section_title(self): + return self.___title___ + + def set_section_title(self, val): + self.___title___ = val + + def __repr__(self): + return "".format(self.get_section_title()) diff --git a/lib/dodai/tools/himo.py b/lib/dodai/tools/himo.py new file mode 100644 index 0000000..f56eaf8 --- /dev/null +++ b/lib/dodai/tools/himo.py @@ -0,0 +1,174 @@ +# Copyright (C) 2010 Leonard Thomas +# +# This file is part of Dodai. +# +# Dodai 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, either version 3 of the License, or +# (at your option) any later version. +# +# Dodai 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 Dodai. If not, see . + +import chardet +import re +import sys +import unicodedata +from htmlentitydefs import name2codepoint +from htmlentitydefs import codepoint2name +from decimal import Decimal as D +from dodai.exception import HimoAsciiError +from dodai.tools import normalize_unicode + +class String2Himo(object): + """ + This in an object that is used for converting python string objects + into Himo (unicode) objects. + + """ + + def __init__(self, default_encoding=None): + """ + default_encoding is the encoding value to be used as the + classwide default. If default encoding is not set then + the encoding of the system will be used. + + """ + self.default_encoding = default_encoding or self._system_encoding() + self._expression = re.compile(r'&(#?)(x?)(\w+);') + + def __call__(self, data, encoding=None): + """ + Converts the input (data) string object into a Himo object + using the passed in (encoding). If encoding is omitted then + the default_encoding will be used. + + returns a Himo object + + """ + encoding = encoding or self.default_encoding + data = self._as_unicode(data, encoding) + data = self._decode_html(data) + data = normalize_unicode(data) + return Himo(data) + + def _as_unicode(self, data, encoding): + # Returns string as a unicode string. + + if not isinstance(data, unicode): + if not isinstance(data, str): + data = str(data) + try: + data = data.decode(encoding) + except UnicodeDecodeError: + info = chardet.detect(data) + data = data.decode(info['encoding']) + return unicodedata.normalize('NFC', data) + + def _decode_html(self, data): + # Returns a unicode string. If data contains any html encoded + # characters, the characters will be converted to their unicode + # equivalent + + return unicode(self._expression.subn(self._html_decode, data)[0]) + + def _html_decode(self, values): + # Returns the unicode character from the re.subn + + value = values.group(3) + if values.group(1): + if values.group(2): + return unichr(int('0x{0}'.format(value), 16)) + else: + return unichr(int(value)) + else: + try: + char = name2codepoint[value] + except KeyError: + return values.group() + else: + return unichr(char) + + def _system_encoding(self): + # Returns the character encoding of the operating system + + encoding = sys.getdefaultencoding() + filesystem_encoding = sys.getfilesystemencoding() + if filesystem_encoding: + encoding = filesystem_encoding + return encoding + + +class Himo(unicode): + """ + A unicode-string object with some added features to help with + output formatting. Himo means rope or string in Japanese, hence + the string to Himo connection. + + """ + + MAP = {169:u'(C)', 174:u'(R)', 8471:u'(P)'} + + def html(self): + """ + Returns a unicode string containing this object's value + html enetity encoded. + + """ + out = [] + for char in self: + out.append(self._html_char_encode(char)) + return unicode(''.join(out)) + + def _html_char_encode(self, char): + # Returns an html version of the char + + number = ord(char) + try: + char = "&{0};".format(codepoint2name[number]) + except KeyError: + if number > 127: + char = "&#{0};".format(number) + return char + + def decimal(self): + """ + Returns a decimal object with the value of this object + + """ + + return D(self) + + def ascii(self): + """ + Returns an ascii representation of this object value. + Throws HimoAsciiError if this method was unable to + convert a unicode character down to it's root character. + For example if in your string you have a character + like the letter 'e' but it has an accent mark over it, + this method will convert that character to it's root + character. Thus 'e' with an accent mark over it will + replaced with the regular letter 'e'. + + """ + out = [] + for char in self: + if ord(char) < 127: + out.append(char) + elif ord(char) in self.MAP: + out.append(self.MAP[ord(char)]) + else: + num = unicodedata.decomposition(char).split(' ')[0] + if num: + out.append(unichr(int(num, 16))) + else: + raise HimoAsciiError(char) + return str(''.join(out)) + +class HimoAsciiError(Exception): + pass diff --git a/lib/dodai/tools/odict.py b/lib/dodai/tools/odict.py new file mode 100644 index 0000000..2c8391d --- /dev/null +++ b/lib/dodai/tools/odict.py @@ -0,0 +1,1399 @@ +# odict.py +# An Ordered Dictionary object +# Copyright (C) 2005 Nicola Larosa, Michael Foord +# E-mail: nico AT tekNico DOT net, fuzzyman AT voidspace DOT org DOT uk + +# This software is licensed under the terms of the BSD license. +# http://www.voidspace.org.uk/python/license.shtml +# Basically you're free to copy, modify, distribute and relicense it, +# So long as you keep a copy of the license with it. + +# Documentation at http://www.voidspace.org.uk/python/odict.html +# For information about bugfixes, updates and support, please join the +# Pythonutils mailing list: +# http://groups.google.com/group/pythonutils/ +# Comments, suggestions and bug reports welcome. + +"""A dict that keeps keys in insertion order""" +from __future__ import generators + +__author__ = ('Nicola Larosa ,' + 'Michael Foord ') + +__docformat__ = "restructuredtext en" + +__revision__ = '$Id: odict.py 129 2005-09-12 18:15:28Z teknico $' + +__version__ = '0.2.2' + +__all__ = ['OrderedDict', 'SequenceOrderedDict'] + +import sys +INTP_VER = sys.version_info[:2] +if INTP_VER < (2, 2): + raise RuntimeError("Python v.2.2 or later required") + +import types, warnings + +class OrderedDict(dict): + """ + A class of dictionary that keeps the insertion order of keys. + + All appropriate methods return keys, items, or values in an ordered way. + + All normal dictionary methods are available. Update and comparison is + restricted to other OrderedDict objects. + + Various sequence methods are available, including the ability to explicitly + mutate the key ordering. + + __contains__ tests: + + >>> d = OrderedDict(((1, 3),)) + >>> 1 in d + 1 + >>> 4 in d + 0 + + __getitem__ tests: + + >>> OrderedDict(((1, 3), (3, 2), (2, 1)))[2] + 1 + >>> OrderedDict(((1, 3), (3, 2), (2, 1)))[4] + Traceback (most recent call last): + KeyError: 4 + + __len__ tests: + + >>> len(OrderedDict()) + 0 + >>> len(OrderedDict(((1, 3), (3, 2), (2, 1)))) + 3 + + get tests: + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.get(1) + 3 + >>> d.get(4) is None + 1 + >>> d.get(4, 5) + 5 + >>> d + OrderedDict([(1, 3), (3, 2), (2, 1)]) + + has_key tests: + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.has_key(1) + 1 + >>> d.has_key(4) + 0 + """ + + def __init__(self, init_val=(), strict=False): + """ + Create a new ordered dictionary. Cannot init from a normal dict, + nor from kwargs, since items order is undefined in those cases. + + If the ``strict`` keyword argument is ``True`` (``False`` is the + default) then when doing slice assignment - the ``OrderedDict`` you are + assigning from *must not* contain any keys in the remaining dict. + + >>> OrderedDict() + OrderedDict([]) + >>> OrderedDict({1: 1}) + Traceback (most recent call last): + TypeError: undefined order, cannot get items from dict + >>> OrderedDict({1: 1}.items()) + OrderedDict([(1, 1)]) + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d + OrderedDict([(1, 3), (3, 2), (2, 1)]) + >>> OrderedDict(d) + OrderedDict([(1, 3), (3, 2), (2, 1)]) + """ + self.strict = strict + dict.__init__(self) + if isinstance(init_val, OrderedDict): + self._sequence = init_val.keys() + dict.update(self, init_val) + elif isinstance(init_val, dict): + # we lose compatibility with other ordered dict types this way + raise TypeError('undefined order, cannot get items from dict') + else: + self._sequence = [] + self.update(init_val) + +### Special methods ### + + def __delitem__(self, key): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> del d[3] + >>> d + OrderedDict([(1, 3), (2, 1)]) + >>> del d[3] + Traceback (most recent call last): + KeyError: 3 + >>> d[3] = 2 + >>> d + OrderedDict([(1, 3), (2, 1), (3, 2)]) + >>> del d[0:1] + >>> d + OrderedDict([(2, 1), (3, 2)]) + """ + if isinstance(key, types.SliceType): + # FIXME: efficiency? + keys = self._sequence[key] + for entry in keys: + dict.__delitem__(self, entry) + del self._sequence[key] + else: + # do the dict.__delitem__ *first* as it raises + # the more appropriate error + dict.__delitem__(self, key) + self._sequence.remove(key) + + def __eq__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d == OrderedDict(d) + True + >>> d == OrderedDict(((1, 3), (2, 1), (3, 2))) + False + >>> d == OrderedDict(((1, 0), (3, 2), (2, 1))) + False + >>> d == OrderedDict(((0, 3), (3, 2), (2, 1))) + False + >>> d == dict(d) + False + >>> d == False + False + """ + if isinstance(other, OrderedDict): + # FIXME: efficiency? + # Generate both item lists for each compare + return (self.items() == other.items()) + else: + return False + + def __lt__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> c = OrderedDict(((0, 3), (3, 2), (2, 1))) + >>> c < d + True + >>> d < c + False + >>> d < dict(c) + Traceback (most recent call last): + TypeError: Can only compare with other OrderedDicts + """ + if not isinstance(other, OrderedDict): + raise TypeError('Can only compare with other OrderedDicts') + # FIXME: efficiency? + # Generate both item lists for each compare + return (self.items() < other.items()) + + def __le__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> c = OrderedDict(((0, 3), (3, 2), (2, 1))) + >>> e = OrderedDict(d) + >>> c <= d + True + >>> d <= c + False + >>> d <= dict(c) + Traceback (most recent call last): + TypeError: Can only compare with other OrderedDicts + >>> d <= e + True + """ + if not isinstance(other, OrderedDict): + raise TypeError('Can only compare with other OrderedDicts') + # FIXME: efficiency? + # Generate both item lists for each compare + return (self.items() <= other.items()) + + def __ne__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d != OrderedDict(d) + False + >>> d != OrderedDict(((1, 3), (2, 1), (3, 2))) + True + >>> d != OrderedDict(((1, 0), (3, 2), (2, 1))) + True + >>> d == OrderedDict(((0, 3), (3, 2), (2, 1))) + False + >>> d != dict(d) + True + >>> d != False + True + """ + if isinstance(other, OrderedDict): + # FIXME: efficiency? + # Generate both item lists for each compare + return not (self.items() == other.items()) + else: + return True + + def __gt__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> c = OrderedDict(((0, 3), (3, 2), (2, 1))) + >>> d > c + True + >>> c > d + False + >>> d > dict(c) + Traceback (most recent call last): + TypeError: Can only compare with other OrderedDicts + """ + if not isinstance(other, OrderedDict): + raise TypeError('Can only compare with other OrderedDicts') + # FIXME: efficiency? + # Generate both item lists for each compare + return (self.items() > other.items()) + + def __ge__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> c = OrderedDict(((0, 3), (3, 2), (2, 1))) + >>> e = OrderedDict(d) + >>> c >= d + False + >>> d >= c + True + >>> d >= dict(c) + Traceback (most recent call last): + TypeError: Can only compare with other OrderedDicts + >>> e >= d + True + """ + if not isinstance(other, OrderedDict): + raise TypeError('Can only compare with other OrderedDicts') + # FIXME: efficiency? + # Generate both item lists for each compare + return (self.items() >= other.items()) + + def __repr__(self): + """ + Used for __repr__ and __str__ + + >>> r1 = repr(OrderedDict((('a', 'b'), ('c', 'd'), ('e', 'f')))) + >>> r1 + "OrderedDict([('a', 'b'), ('c', 'd'), ('e', 'f')])" + >>> r2 = repr(OrderedDict((('a', 'b'), ('e', 'f'), ('c', 'd')))) + >>> r2 + "OrderedDict([('a', 'b'), ('e', 'f'), ('c', 'd')])" + >>> r1 == str(OrderedDict((('a', 'b'), ('c', 'd'), ('e', 'f')))) + True + >>> r2 == str(OrderedDict((('a', 'b'), ('e', 'f'), ('c', 'd')))) + True + """ + return '%s([%s])' % (self.__class__.__name__, ', '.join( + ['(%r, %r)' % (key, self[key]) for key in self._sequence])) + + def __setitem__(self, key, val): + """ + Allows slice assignment, so long as the slice is an OrderedDict + >>> d = OrderedDict() + >>> d['a'] = 'b' + >>> d['b'] = 'a' + >>> d[3] = 12 + >>> d + OrderedDict([('a', 'b'), ('b', 'a'), (3, 12)]) + >>> d[:] = OrderedDict(((1, 2), (2, 3), (3, 4))) + >>> d + OrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> d[::2] = OrderedDict(((7, 8), (9, 10))) + >>> d + OrderedDict([(7, 8), (2, 3), (9, 10)]) + >>> d = OrderedDict(((0, 1), (1, 2), (2, 3), (3, 4))) + >>> d[1:3] = OrderedDict(((1, 2), (5, 6), (7, 8))) + >>> d + OrderedDict([(0, 1), (1, 2), (5, 6), (7, 8), (3, 4)]) + >>> d = OrderedDict(((0, 1), (1, 2), (2, 3), (3, 4)), strict=True) + >>> d[1:3] = OrderedDict(((1, 2), (5, 6), (7, 8))) + >>> d + OrderedDict([(0, 1), (1, 2), (5, 6), (7, 8), (3, 4)]) + + >>> a = OrderedDict(((0, 1), (1, 2), (2, 3)), strict=True) + >>> a[3] = 4 + >>> a + OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a[::1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a + OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a[:2] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]) + Traceback (most recent call last): + ValueError: slice assignment must be from unique keys + >>> a = OrderedDict(((0, 1), (1, 2), (2, 3))) + >>> a[3] = 4 + >>> a + OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a[::1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a + OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a[:2] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a + OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a[::-1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a + OrderedDict([(3, 4), (2, 3), (1, 2), (0, 1)]) + + >>> d = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> d[:1] = 3 + Traceback (most recent call last): + TypeError: slice assignment requires an OrderedDict + + >>> d = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> d[:1] = OrderedDict([(9, 8)]) + >>> d + OrderedDict([(9, 8), (1, 2), (2, 3), (3, 4)]) + """ + if isinstance(key, types.SliceType): + if not isinstance(val, OrderedDict): + # FIXME: allow a list of tuples? + raise TypeError('slice assignment requires an OrderedDict') + keys = self._sequence[key] + # NOTE: Could use ``range(*key.indices(len(self._sequence)))`` + indexes = range(len(self._sequence))[key] + if key.step is None: + # NOTE: new slice may not be the same size as the one being + # overwritten ! + # NOTE: What is the algorithm for an impossible slice? + # e.g. d[5:3] + pos = key.start or 0 + del self[key] + newkeys = val.keys() + for k in newkeys: + if k in self: + if self.strict: + raise ValueError('slice assignment must be from ' + 'unique keys') + else: + # NOTE: This removes duplicate keys *first* + # so start position might have changed? + del self[k] + self._sequence = (self._sequence[:pos] + newkeys + + self._sequence[pos:]) + dict.update(self, val) + else: + # extended slice - length of new slice must be the same + # as the one being replaced + if len(keys) != len(val): + raise ValueError('attempt to assign sequence of size %s ' + 'to extended slice of size %s' % (len(val), len(keys))) + # FIXME: efficiency? + del self[key] + item_list = zip(indexes, val.items()) + # smallest indexes first - higher indexes not guaranteed to + # exist + item_list.sort() + for pos, (newkey, newval) in item_list: + if self.strict and newkey in self: + raise ValueError('slice assignment must be from unique' + ' keys') + self.insert(pos, newkey, newval) + else: + if key not in self: + self._sequence.append(key) + dict.__setitem__(self, key, val) + + def __getitem__(self, key): + """ + Allows slicing. Returns an OrderedDict if you slice. + >>> b = OrderedDict([(7, 0), (6, 1), (5, 2), (4, 3), (3, 4), (2, 5), (1, 6)]) + >>> b[::-1] + OrderedDict([(1, 6), (2, 5), (3, 4), (4, 3), (5, 2), (6, 1), (7, 0)]) + >>> b[2:5] + OrderedDict([(5, 2), (4, 3), (3, 4)]) + >>> type(b[2:4]) + + """ + if isinstance(key, types.SliceType): + # FIXME: does this raise the error we want? + keys = self._sequence[key] + # FIXME: efficiency? + return OrderedDict([(entry, self[entry]) for entry in keys]) + else: + return dict.__getitem__(self, key) + + __str__ = __repr__ + + def __setattr__(self, name, value): + """ + Implemented so that accesses to ``sequence`` raise a warning and are + diverted to the new ``setkeys`` method. + """ + if name == 'sequence': + warnings.warn('Use of the sequence attribute is deprecated.' + ' Use the keys method instead.', DeprecationWarning) + # NOTE: doesn't return anything + self.setkeys(value) + else: + # FIXME: do we want to allow arbitrary setting of attributes? + # Or do we want to manage it? + object.__setattr__(self, name, value) + + def __getattr__(self, name): + """ + Implemented so that access to ``sequence`` raises a warning. + + >>> d = OrderedDict() + >>> d.sequence + [] + """ + if name == 'sequence': + warnings.warn('Use of the sequence attribute is deprecated.' + ' Use the keys method instead.', DeprecationWarning) + # NOTE: Still (currently) returns a direct reference. Need to + # because code that uses sequence will expect to be able to + # mutate it in place. + return self._sequence + else: + # raise the appropriate error + raise AttributeError("OrderedDict has no '%s' attribute" % name) + + def __deepcopy__(self, memo): + """ + To allow deepcopy to work with OrderedDict. + + >>> from copy import deepcopy + >>> a = OrderedDict([(1, 1), (2, 2), (3, 3)]) + >>> a['test'] = {} + >>> b = deepcopy(a) + >>> b == a + True + >>> b is a + False + >>> a['test'] is b['test'] + False + """ + from copy import deepcopy + return self.__class__(deepcopy(self.items(), memo), self.strict) + + +### Read-only methods ### + + def copy(self): + """ + >>> OrderedDict(((1, 3), (3, 2), (2, 1))).copy() + OrderedDict([(1, 3), (3, 2), (2, 1)]) + """ + return OrderedDict(self) + + def items(self): + """ + ``items`` returns a list of tuples representing all the + ``(key, value)`` pairs in the dictionary. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.items() + [(1, 3), (3, 2), (2, 1)] + >>> d.clear() + >>> d.items() + [] + """ + return zip(self._sequence, self.values()) + + def keys(self): + """ + Return a list of keys in the ``OrderedDict``. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.keys() + [1, 3, 2] + """ + return self._sequence[:] + + def values(self, values=None): + """ + Return a list of all the values in the OrderedDict. + + Optionally you can pass in a list of values, which will replace the + current list. The value list must be the same len as the OrderedDict. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.values() + [3, 2, 1] + """ + return [self[key] for key in self._sequence] + + def iteritems(self): + """ + >>> ii = OrderedDict(((1, 3), (3, 2), (2, 1))).iteritems() + >>> ii.next() + (1, 3) + >>> ii.next() + (3, 2) + >>> ii.next() + (2, 1) + >>> ii.next() + Traceback (most recent call last): + StopIteration + """ + def make_iter(self=self): + keys = self.iterkeys() + while True: + key = keys.next() + yield (key, self[key]) + return make_iter() + + def iterkeys(self): + """ + >>> ii = OrderedDict(((1, 3), (3, 2), (2, 1))).iterkeys() + >>> ii.next() + 1 + >>> ii.next() + 3 + >>> ii.next() + 2 + >>> ii.next() + Traceback (most recent call last): + StopIteration + """ + return iter(self._sequence) + + __iter__ = iterkeys + + def itervalues(self): + """ + >>> iv = OrderedDict(((1, 3), (3, 2), (2, 1))).itervalues() + >>> iv.next() + 3 + >>> iv.next() + 2 + >>> iv.next() + 1 + >>> iv.next() + Traceback (most recent call last): + StopIteration + """ + def make_iter(self=self): + keys = self.iterkeys() + while True: + yield self[keys.next()] + return make_iter() + +### Read-write methods ### + + def clear(self): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.clear() + >>> d + OrderedDict([]) + """ + dict.clear(self) + self._sequence = [] + + def pop(self, key, *args): + """ + No dict.pop in Python 2.2, gotta reimplement it + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.pop(3) + 2 + >>> d + OrderedDict([(1, 3), (2, 1)]) + >>> d.pop(4) + Traceback (most recent call last): + KeyError: 4 + >>> d.pop(4, 0) + 0 + >>> d.pop(4, 0, 1) + Traceback (most recent call last): + TypeError: pop expected at most 2 arguments, got 3 + """ + if len(args) > 1: + raise TypeError, ('pop expected at most 2 arguments, got %s' % + (len(args) + 1)) + if key in self: + val = self[key] + del self[key] + else: + try: + val = args[0] + except IndexError: + raise KeyError(key) + return val + + def popitem(self, i=-1): + """ + Delete and return an item specified by index, not a random one as in + dict. The index is -1 by default (the last item). + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.popitem() + (2, 1) + >>> d + OrderedDict([(1, 3), (3, 2)]) + >>> d.popitem(0) + (1, 3) + >>> OrderedDict().popitem() + Traceback (most recent call last): + KeyError: 'popitem(): dictionary is empty' + >>> d.popitem(2) + Traceback (most recent call last): + IndexError: popitem(): index 2 not valid + """ + if not self._sequence: + raise KeyError('popitem(): dictionary is empty') + try: + key = self._sequence[i] + except IndexError: + raise IndexError('popitem(): index %s not valid' % i) + return (key, self.pop(key)) + + def setdefault(self, key, defval = None): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.setdefault(1) + 3 + >>> d.setdefault(4) is None + True + >>> d + OrderedDict([(1, 3), (3, 2), (2, 1), (4, None)]) + >>> d.setdefault(5, 0) + 0 + >>> d + OrderedDict([(1, 3), (3, 2), (2, 1), (4, None), (5, 0)]) + """ + if key in self: + return self[key] + else: + self[key] = defval + return defval + + def update(self, from_od): + """ + Update from another OrderedDict or sequence of (key, value) pairs + + >>> d = OrderedDict(((1, 0), (0, 1))) + >>> d.update(OrderedDict(((1, 3), (3, 2), (2, 1)))) + >>> d + OrderedDict([(1, 3), (0, 1), (3, 2), (2, 1)]) + >>> d.update({4: 4}) + Traceback (most recent call last): + TypeError: undefined order, cannot get items from dict + >>> d.update((4, 4)) + Traceback (most recent call last): + TypeError: cannot convert dictionary update sequence element "4" to a 2-item sequence + """ + if isinstance(from_od, OrderedDict): + for key, val in from_od.items(): + self[key] = val + elif isinstance(from_od, dict): + # we lose compatibility with other ordered dict types this way + raise TypeError('undefined order, cannot get items from dict') + else: + # FIXME: efficiency? + # sequence of 2-item sequences, or error + for item in from_od: + try: + key, val = item + except TypeError: + raise TypeError('cannot convert dictionary update' + ' sequence element "%s" to a 2-item sequence' % item) + self[key] = val + + def rename(self, old_key, new_key): + """ + Rename the key for a given value, without modifying sequence order. + + For the case where new_key already exists this raise an exception, + since if new_key exists, it is ambiguous as to what happens to the + associated values, and the position of new_key in the sequence. + + >>> od = OrderedDict() + >>> od['a'] = 1 + >>> od['b'] = 2 + >>> od.items() + [('a', 1), ('b', 2)] + >>> od.rename('b', 'c') + >>> od.items() + [('a', 1), ('c', 2)] + >>> od.rename('c', 'a') + Traceback (most recent call last): + ValueError: New key already exists: 'a' + >>> od.rename('d', 'b') + Traceback (most recent call last): + KeyError: 'd' + """ + if new_key == old_key: + # no-op + return + if new_key in self: + raise ValueError("New key already exists: %r" % new_key) + # rename sequence entry + value = self[old_key] + old_idx = self._sequence.index(old_key) + self._sequence[old_idx] = new_key + # rename internal dict entry + dict.__delitem__(self, old_key) + dict.__setitem__(self, new_key, value) + + def setitems(self, items): + """ + This method allows you to set the items in the dict. + + It takes a list of tuples - of the same sort returned by the ``items`` + method. + + >>> d = OrderedDict() + >>> d.setitems(((3, 1), (2, 3), (1, 2))) + >>> d + OrderedDict([(3, 1), (2, 3), (1, 2)]) + """ + self.clear() + # FIXME: this allows you to pass in an OrderedDict as well :-) + self.update(items) + + def setkeys(self, keys): + """ + ``setkeys`` all ows you to pass in a new list of keys which will + replace the current set. This must contain the same set of keys, but + need not be in the same order. + + If you pass in new keys that don't match, a ``KeyError`` will be + raised. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.keys() + [1, 3, 2] + >>> d.setkeys((1, 2, 3)) + >>> d + OrderedDict([(1, 3), (2, 1), (3, 2)]) + >>> d.setkeys(['a', 'b', 'c']) + Traceback (most recent call last): + KeyError: 'Keylist is not the same as current keylist.' + """ + # FIXME: Efficiency? (use set for Python 2.4 :-) + # NOTE: list(keys) rather than keys[:] because keys[:] returns + # a tuple, if keys is a tuple. + kcopy = list(keys) + kcopy.sort() + self._sequence.sort() + if kcopy != self._sequence: + raise KeyError('Keylist is not the same as current keylist.') + # NOTE: This makes the _sequence attribute a new object, instead + # of changing it in place. + # FIXME: efficiency? + self._sequence = list(keys) + + def setvalues(self, values): + """ + You can pass in a list of values, which will replace the + current list. The value list must be the same len as the OrderedDict. + + (Or a ``ValueError`` is raised.) + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.setvalues((1, 2, 3)) + >>> d + OrderedDict([(1, 1), (3, 2), (2, 3)]) + >>> d.setvalues([6]) + Traceback (most recent call last): + ValueError: Value list is not the same length as the OrderedDict. + """ + if len(values) != len(self): + # FIXME: correct error to raise? + raise ValueError('Value list is not the same length as the ' + 'OrderedDict.') + self.update(zip(self, values)) + +### Sequence Methods ### + + def index(self, key): + """ + Return the position of the specified key in the OrderedDict. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.index(3) + 1 + >>> d.index(4) + Traceback (most recent call last): + ValueError: list.index(x): x not in list + """ + return self._sequence.index(key) + + def insert(self, index, key, value): + """ + Takes ``index``, ``key``, and ``value`` as arguments. + + Sets ``key`` to ``value``, so that ``key`` is at position ``index`` in + the OrderedDict. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.insert(0, 4, 0) + >>> d + OrderedDict([(4, 0), (1, 3), (3, 2), (2, 1)]) + >>> d.insert(0, 2, 1) + >>> d + OrderedDict([(2, 1), (4, 0), (1, 3), (3, 2)]) + >>> d.insert(8, 8, 1) + >>> d + OrderedDict([(2, 1), (4, 0), (1, 3), (3, 2), (8, 1)]) + """ + if key in self: + # FIXME: efficiency? + del self[key] + self._sequence.insert(index, key) + dict.__setitem__(self, key, value) + + def reverse(self): + """ + Reverse the order of the OrderedDict. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.reverse() + >>> d + OrderedDict([(2, 1), (3, 2), (1, 3)]) + """ + self._sequence.reverse() + + def sort(self, *args, **kwargs): + """ + Sort the key order in the OrderedDict. + + This method takes the same arguments as the ``list.sort`` method on + your version of Python. + + >>> d = OrderedDict(((4, 1), (2, 2), (3, 3), (1, 4))) + >>> d.sort() + >>> d + OrderedDict([(1, 4), (2, 2), (3, 3), (4, 1)]) + """ + self._sequence.sort(*args, **kwargs) + +class Keys(object): + # FIXME: should this object be a subclass of list? + """ + Custom object for accessing the keys of an OrderedDict. + + Can be called like the normal ``OrderedDict.keys`` method, but also + supports indexing and sequence methods. + """ + + def __init__(self, main): + self._main = main + + def __call__(self): + """Pretend to be the keys method.""" + return self._main._keys() + + def __getitem__(self, index): + """Fetch the key at position i.""" + # NOTE: this automatically supports slicing :-) + return self._main._sequence[index] + + def __setitem__(self, index, name): + """ + You cannot assign to keys, but you can do slice assignment to re-order + them. + + You can only do slice assignment if the new set of keys is a reordering + of the original set. + """ + if isinstance(index, types.SliceType): + # FIXME: efficiency? + # check length is the same + indexes = range(len(self._main._sequence))[index] + if len(indexes) != len(name): + raise ValueError('attempt to assign sequence of size %s ' + 'to slice of size %s' % (len(name), len(indexes))) + # check they are the same keys + # FIXME: Use set + old_keys = self._main._sequence[index] + new_keys = list(name) + old_keys.sort() + new_keys.sort() + if old_keys != new_keys: + raise KeyError('Keylist is not the same as current keylist.') + orig_vals = [self._main[k] for k in name] + del self._main[index] + vals = zip(indexes, name, orig_vals) + vals.sort() + for i, k, v in vals: + if self._main.strict and k in self._main: + raise ValueError('slice assignment must be from ' + 'unique keys') + self._main.insert(i, k, v) + else: + raise ValueError('Cannot assign to keys') + + ### following methods pinched from UserList and adapted ### + def __repr__(self): return repr(self._main._sequence) + + # FIXME: do we need to check if we are comparing with another ``Keys`` + # object? (like the __cast method of UserList) + def __lt__(self, other): return self._main._sequence < other + def __le__(self, other): return self._main._sequence <= other + def __eq__(self, other): return self._main._sequence == other + def __ne__(self, other): return self._main._sequence != other + def __gt__(self, other): return self._main._sequence > other + def __ge__(self, other): return self._main._sequence >= other + # FIXME: do we need __cmp__ as well as rich comparisons? + def __cmp__(self, other): return cmp(self._main._sequence, other) + + def __contains__(self, item): return item in self._main._sequence + def __len__(self): return len(self._main._sequence) + def __iter__(self): return self._main.iterkeys() + def count(self, item): return self._main._sequence.count(item) + def index(self, item, *args): return self._main._sequence.index(item, *args) + def reverse(self): self._main._sequence.reverse() + def sort(self, *args, **kwds): self._main._sequence.sort(*args, **kwds) + def __mul__(self, n): return self._main._sequence*n + __rmul__ = __mul__ + def __add__(self, other): return self._main._sequence + other + def __radd__(self, other): return other + self._main._sequence + + ## following methods not implemented for keys ## + def __delitem__(self, i): raise TypeError('Can\'t delete items from keys') + def __iadd__(self, other): raise TypeError('Can\'t add in place to keys') + def __imul__(self, n): raise TypeError('Can\'t multiply keys in place') + def append(self, item): raise TypeError('Can\'t append items to keys') + def insert(self, i, item): raise TypeError('Can\'t insert items into keys') + def pop(self, i=-1): raise TypeError('Can\'t pop items from keys') + def remove(self, item): raise TypeError('Can\'t remove items from keys') + def extend(self, other): raise TypeError('Can\'t extend keys') + +class Items(object): + """ + Custom object for accessing the items of an OrderedDict. + + Can be called like the normal ``OrderedDict.items`` method, but also + supports indexing and sequence methods. + """ + + def __init__(self, main): + self._main = main + + def __call__(self): + """Pretend to be the items method.""" + return self._main._items() + + def __getitem__(self, index): + """Fetch the item at position i.""" + if isinstance(index, types.SliceType): + # fetching a slice returns an OrderedDict + return self._main[index].items() + key = self._main._sequence[index] + return (key, self._main[key]) + + def __setitem__(self, index, item): + """Set item at position i to item.""" + if isinstance(index, types.SliceType): + # NOTE: item must be an iterable (list of tuples) + self._main[index] = OrderedDict(item) + else: + # FIXME: Does this raise a sensible error? + orig = self._main.keys[index] + key, value = item + if self._main.strict and key in self and (key != orig): + raise ValueError('slice assignment must be from ' + 'unique keys') + # delete the current one + del self._main[self._main._sequence[index]] + self._main.insert(index, key, value) + + def __delitem__(self, i): + """Delete the item at position i.""" + key = self._main._sequence[i] + if isinstance(i, types.SliceType): + for k in key: + # FIXME: efficiency? + del self._main[k] + else: + del self._main[key] + + ### following methods pinched from UserList and adapted ### + def __repr__(self): return repr(self._main.items()) + + # FIXME: do we need to check if we are comparing with another ``Items`` + # object? (like the __cast method of UserList) + def __lt__(self, other): return self._main.items() < other + def __le__(self, other): return self._main.items() <= other + def __eq__(self, other): return self._main.items() == other + def __ne__(self, other): return self._main.items() != other + def __gt__(self, other): return self._main.items() > other + def __ge__(self, other): return self._main.items() >= other + def __cmp__(self, other): return cmp(self._main.items(), other) + + def __contains__(self, item): return item in self._main.items() + def __len__(self): return len(self._main._sequence) # easier :-) + def __iter__(self): return self._main.iteritems() + def count(self, item): return self._main.items().count(item) + def index(self, item, *args): return self._main.items().index(item, *args) + def reverse(self): self._main.reverse() + def sort(self, *args, **kwds): self._main.sort(*args, **kwds) + def __mul__(self, n): return self._main.items()*n + __rmul__ = __mul__ + def __add__(self, other): return self._main.items() + other + def __radd__(self, other): return other + self._main.items() + + def append(self, item): + """Add an item to the end.""" + # FIXME: this is only append if the key isn't already present + key, value = item + self._main[key] = value + + def insert(self, i, item): + key, value = item + self._main.insert(i, key, value) + + def pop(self, i=-1): + key = self._main._sequence[i] + return (key, self._main.pop(key)) + + def remove(self, item): + key, value = item + try: + assert value == self._main[key] + except (KeyError, AssertionError): + raise ValueError('ValueError: list.remove(x): x not in list') + else: + del self._main[key] + + def extend(self, other): + # FIXME: is only a true extend if none of the keys already present + for item in other: + key, value = item + self._main[key] = value + + def __iadd__(self, other): + self.extend(other) + + ## following methods not implemented for items ## + + def __imul__(self, n): raise TypeError('Can\'t multiply items in place') + +class Values(object): + """ + Custom object for accessing the values of an OrderedDict. + + Can be called like the normal ``OrderedDict.values`` method, but also + supports indexing and sequence methods. + """ + + def __init__(self, main): + self._main = main + + def __call__(self): + """Pretend to be the values method.""" + return self._main._values() + + def __getitem__(self, index): + """Fetch the value at position i.""" + if isinstance(index, types.SliceType): + return [self._main[key] for key in self._main._sequence[index]] + else: + return self._main[self._main._sequence[index]] + + def __setitem__(self, index, value): + """ + Set the value at position i to value. + + You can only do slice assignment to values if you supply a sequence of + equal length to the slice you are replacing. + """ + if isinstance(index, types.SliceType): + keys = self._main._sequence[index] + if len(keys) != len(value): + raise ValueError('attempt to assign sequence of size %s ' + 'to slice of size %s' % (len(name), len(keys))) + # FIXME: efficiency? Would be better to calculate the indexes + # directly from the slice object + # NOTE: the new keys can collide with existing keys (or even + # contain duplicates) - these will overwrite + for key, val in zip(keys, value): + self._main[key] = val + else: + self._main[self._main._sequence[index]] = value + + ### following methods pinched from UserList and adapted ### + def __repr__(self): return repr(self._main.values()) + + # FIXME: do we need to check if we are comparing with another ``Values`` + # object? (like the __cast method of UserList) + def __lt__(self, other): return self._main.values() < other + def __le__(self, other): return self._main.values() <= other + def __eq__(self, other): return self._main.values() == other + def __ne__(self, other): return self._main.values() != other + def __gt__(self, other): return self._main.values() > other + def __ge__(self, other): return self._main.values() >= other + def __cmp__(self, other): return cmp(self._main.values(), other) + + def __contains__(self, item): return item in self._main.values() + def __len__(self): return len(self._main._sequence) # easier :-) + def __iter__(self): return self._main.itervalues() + def count(self, item): return self._main.values().count(item) + def index(self, item, *args): return self._main.values().index(item, *args) + + def reverse(self): + """Reverse the values""" + vals = self._main.values() + vals.reverse() + # FIXME: efficiency + self[:] = vals + + def sort(self, *args, **kwds): + """Sort the values.""" + vals = self._main.values() + vals.sort(*args, **kwds) + self[:] = vals + + def __mul__(self, n): return self._main.values()*n + __rmul__ = __mul__ + def __add__(self, other): return self._main.values() + other + def __radd__(self, other): return other + self._main.values() + + ## following methods not implemented for values ## + def __delitem__(self, i): raise TypeError('Can\'t delete items from values') + def __iadd__(self, other): raise TypeError('Can\'t add in place to values') + def __imul__(self, n): raise TypeError('Can\'t multiply values in place') + def append(self, item): raise TypeError('Can\'t append items to values') + def insert(self, i, item): raise TypeError('Can\'t insert items into values') + def pop(self, i=-1): raise TypeError('Can\'t pop items from values') + def remove(self, item): raise TypeError('Can\'t remove items from values') + def extend(self, other): raise TypeError('Can\'t extend values') + +class SequenceOrderedDict(OrderedDict): + """ + Experimental version of OrderedDict that has a custom object for ``keys``, + ``values``, and ``items``. + + These are callable sequence objects that work as methods, or can be + manipulated directly as sequences. + + Test for ``keys``, ``items`` and ``values``. + + >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4))) + >>> d + SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> d.keys + [1, 2, 3] + >>> d.keys() + [1, 2, 3] + >>> d.setkeys((3, 2, 1)) + >>> d + SequenceOrderedDict([(3, 4), (2, 3), (1, 2)]) + >>> d.setkeys((1, 2, 3)) + >>> d.keys[0] + 1 + >>> d.keys[:] + [1, 2, 3] + >>> d.keys[-1] + 3 + >>> d.keys[-2] + 2 + >>> d.keys[0:2] = [2, 1] + >>> d + SequenceOrderedDict([(2, 3), (1, 2), (3, 4)]) + >>> d.keys.reverse() + >>> d.keys + [3, 1, 2] + >>> d.keys = [1, 2, 3] + >>> d + SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> d.keys = [3, 1, 2] + >>> d + SequenceOrderedDict([(3, 4), (1, 2), (2, 3)]) + >>> a = SequenceOrderedDict() + >>> b = SequenceOrderedDict() + >>> a.keys == b.keys + 1 + >>> a['a'] = 3 + >>> a.keys == b.keys + 0 + >>> b['a'] = 3 + >>> a.keys == b.keys + 1 + >>> b['b'] = 3 + >>> a.keys == b.keys + 0 + >>> a.keys > b.keys + 0 + >>> a.keys < b.keys + 1 + >>> 'a' in a.keys + 1 + >>> len(b.keys) + 2 + >>> 'c' in d.keys + 0 + >>> 1 in d.keys + 1 + >>> [v for v in d.keys] + [3, 1, 2] + >>> d.keys.sort() + >>> d.keys + [1, 2, 3] + >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4)), strict=True) + >>> d.keys[::-1] = [1, 2, 3] + >>> d + SequenceOrderedDict([(3, 4), (2, 3), (1, 2)]) + >>> d.keys[:2] + [3, 2] + >>> d.keys[:2] = [1, 3] + Traceback (most recent call last): + KeyError: 'Keylist is not the same as current keylist.' + + >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4))) + >>> d + SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> d.values + [2, 3, 4] + >>> d.values() + [2, 3, 4] + >>> d.setvalues((4, 3, 2)) + >>> d + SequenceOrderedDict([(1, 4), (2, 3), (3, 2)]) + >>> d.values[::-1] + [2, 3, 4] + >>> d.values[0] + 4 + >>> d.values[-2] + 3 + >>> del d.values[0] + Traceback (most recent call last): + TypeError: Can't delete items from values + >>> d.values[::2] = [2, 4] + >>> d + SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> 7 in d.values + 0 + >>> len(d.values) + 3 + >>> [val for val in d.values] + [2, 3, 4] + >>> d.values[-1] = 2 + >>> d.values.count(2) + 2 + >>> d.values.index(2) + 0 + >>> d.values[-1] = 7 + >>> d.values + [2, 3, 7] + >>> d.values.reverse() + >>> d.values + [7, 3, 2] + >>> d.values.sort() + >>> d.values + [2, 3, 7] + >>> d.values.append('anything') + Traceback (most recent call last): + TypeError: Can't append items to values + >>> d.values = (1, 2, 3) + >>> d + SequenceOrderedDict([(1, 1), (2, 2), (3, 3)]) + + >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4))) + >>> d + SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> d.items() + [(1, 2), (2, 3), (3, 4)] + >>> d.setitems([(3, 4), (2 ,3), (1, 2)]) + >>> d + SequenceOrderedDict([(3, 4), (2, 3), (1, 2)]) + >>> d.items[0] + (3, 4) + >>> d.items[:-1] + [(3, 4), (2, 3)] + >>> d.items[1] = (6, 3) + >>> d.items + [(3, 4), (6, 3), (1, 2)] + >>> d.items[1:2] = [(9, 9)] + >>> d + SequenceOrderedDict([(3, 4), (9, 9), (1, 2)]) + >>> del d.items[1:2] + >>> d + SequenceOrderedDict([(3, 4), (1, 2)]) + >>> (3, 4) in d.items + 1 + >>> (4, 3) in d.items + 0 + >>> len(d.items) + 2 + >>> [v for v in d.items] + [(3, 4), (1, 2)] + >>> d.items.count((3, 4)) + 1 + >>> d.items.index((1, 2)) + 1 + >>> d.items.index((2, 1)) + Traceback (most recent call last): + ValueError: list.index(x): x not in list + >>> d.items.reverse() + >>> d.items + [(1, 2), (3, 4)] + >>> d.items.reverse() + >>> d.items.sort() + >>> d.items + [(1, 2), (3, 4)] + >>> d.items.append((5, 6)) + >>> d.items + [(1, 2), (3, 4), (5, 6)] + >>> d.items.insert(0, (0, 0)) + >>> d.items + [(0, 0), (1, 2), (3, 4), (5, 6)] + >>> d.items.insert(-1, (7, 8)) + >>> d.items + [(0, 0), (1, 2), (3, 4), (7, 8), (5, 6)] + >>> d.items.pop() + (5, 6) + >>> d.items + [(0, 0), (1, 2), (3, 4), (7, 8)] + >>> d.items.remove((1, 2)) + >>> d.items + [(0, 0), (3, 4), (7, 8)] + >>> d.items.extend([(1, 2), (5, 6)]) + >>> d.items + [(0, 0), (3, 4), (7, 8), (1, 2), (5, 6)] + """ + + def __init__(self, init_val=(), strict=True): + OrderedDict.__init__(self, init_val, strict=strict) + self._keys = self.keys + self._values = self.values + self._items = self.items + self.keys = Keys(self) + self.values = Values(self) + self.items = Items(self) + self._att_dict = { + 'keys': self.setkeys, + 'items': self.setitems, + 'values': self.setvalues, + } + + def __setattr__(self, name, value): + """Protect keys, items, and values.""" + if not '_att_dict' in self.__dict__: + object.__setattr__(self, name, value) + else: + try: + fun = self._att_dict[name] + except KeyError: + OrderedDict.__setattr__(self, name, value) + else: + fun(value) + +if __name__ == '__main__': + if INTP_VER < (2, 3): + raise RuntimeError("Tests require Python v.2.3 or later") + # turn off warnings for tests + warnings.filterwarnings('ignore') + # run the code tests in doctest format + import doctest + m = sys.modules.get('__main__') + globs = m.__dict__.copy() + globs.update({ + 'INTP_VER': INTP_VER, + }) + doctest.testmod(m, globs=globs) + diff --git a/pavement.py b/pavement.py deleted file mode 100644 index b880183..0000000 --- a/pavement.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright (C) 2010 Leonard Thomas -# -# This file is part of Dodai. -# -# Dodai 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, either version 3 of the License, or -# (at your option) any later version. -# -# Dodai 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 Dodai. If not, see . - -PACKAGE = 'dodai' -LICENSE='GPLv3' -VERSION = '0.3' -AUTHOR='Leonard Thomas' -AUTHOR_EMAIL='six@choushi.net' -URL='http://code.google.com/p/dodai' -DOWNLOAD_URL = 'http://code.google.com/p/dodai/downloads/list' -PY_VERSION_LOW = '2.6.0' -PY_VERSION_HIGH = '3.0.0' -PLATFORMS = ['Linux'] -SETUP_REQUIRES=['sqlalchemy', 'chardet'] -CLASSIFIERS = [ - 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU Affero General Public License v3', - 'Natural Language :: English', - 'Operating System :: POSIX', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.6', - 'Topic :: Database', - 'Topic :: Software Development', - 'Topic :: Software Development :: Libraries', - 'Topic :: Software Development :: Libraries :: Application Frameworks', - 'Topic :: Software Development :: Libraries :: Python Modules',] -DESCRIPTION = 'Tools for writing python scripts' -LONG_DESCRIPTION = "This module is to be a foundation to your command line "\ - "python scripts. This module provides for quick access to logger, "\ - "configparser, optionparse and databases via sqlalchemy."\ - - - -import sys -import platform -import paver -import paver.setuputils -from paver.easy import options -from paver.easy import Bunch -from paver.easy import task -from paver.easy import needs -from paver.misctasks import generate_setup -from paver.misctasks import minilib -from setuptools import setup -from setuptools import find_packages - -#from paver.easy import * -#from paver.misctasks import generate_setup, minilib -#from setuptools import setup, find_packages -#import paver.setuputils -paver.setuputils.install_distutils_tasks() - -options( - setup=Bunch( - name=PACKAGE, - version=VERSION, - zip_safe=False, - description=DESCRIPTION, - author=AUTHOR, - author_email=AUTHOR_EMAIL, - url=URL, - download_url=DOWNLOAD_URL, - packages=find_packages(), - classifiers=CLASSIFIERS, - long_description=LONG_DESCRIPTION, - license=LICENSE, - setup_requires=SETUP_REQUIRES, - install_requires=['sqlalchemy', 'chardet'], - platforms = PLATFORMS - -)) - - -def is_valid_version(): - if sys.version >= PY_VERSION_LOW and sys.version < PY_VERSION_HIGH: - return True - else: - return False - -def is_valid_platform(): - if platform.system() in PLATFORMS: - return True - else: - return False - -@task -def build(): - if not is_valid_version(): - raise Exception('Invalid Python version') - if not is_valid_platform(): - error='{0} not install on: {1}'.format(PACKAGE, platform.system()) - raise Exception(error) - - -@task -@needs('build', 'generate_setup', 'minilib', 'setuptools.command.sdist') -def sdist(): - """Overrides sdist to make sure that our setup.py is generated.""" - pass diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7cae721 --- /dev/null +++ b/setup.py @@ -0,0 +1,216 @@ +# +# Copyright 2010 Leonard Thomas +# +# This file is part of Dodai. +# +# Dodai 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, either version 3 of the License, or +# (at your option) any later version. +# +# Dodai 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 Dodai. If not, see . + +import os +import sys +import platform +from setuptools import setup +from setuptools import find_packages +from ctypes.util import find_library +from ctypes.util import _findLib_ldconfig as find_library_ldconfig +from subprocess import Popen, PIPE +from copy import deepcopy + +ARGS = { + 'name': 'dodai', + 'version': '0.4.1', + 'install_requires': [ + 'sqlalchemy', + 'chardet', + 'couchdb', + 'ordereddict', + 'psycopg2', + 'mysql-python', + 'cx_oracle' + ], + 'platforms': [ + 'Linux', + 'Darwin', + ], + 'author': 'Leonard Thomas', + 'author_email': 'six@choushi.net', + 'url': 'http://code.google.com/p/dodai', + 'download_url': 'http://code.google.com/p/dodai/downloads/list', + 'license': 'GPLv3', + 'classifiers': [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU Affero General Public License v3', + 'Natural Language :: English', + 'Operating System :: POSIX', + 'Operating System :: POSIX :: Linux', + 'Operating System :: MacOS', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Topic :: Database', + 'Topic :: Software Development', + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + 'description': "Module code for quick easy access to parsed text based "\ + "config file data and configured database engines.", + 'long_description': "This module provides code for quick easy access to "\ + "parsed text based config file data and configured database "\ + "engines. All config file data is returned ordered and transformed "\ + "to unicode objects and database connections are returned as "\ + "sqlalchemy objects.", + 'entry_points': { + 'distutils.commands': [ + 'devstrap = dodai.build:devstrap', + ] + }, + 'zip_safe': False, + 'packages': find_packages('lib'), + 'package_dir': {'':'lib'}, +} +PYTHON_VERSION_LOW = '2.6' +PYTHON_VERSION_HIGH = '3.0' + +class BaseLibCheck(object): + + def _has_library(self, lib): + if find_library(lib): + return True + return False + + def _which(self, name): + cmd = ['which', name] + return Popen(cmd, stdout=PIPE).communicate()[0] + + +class PostgressLibCheck(BaseLibCheck): + + PACKAGE = 'psycopg2' + LIB = 'pq' + + def __call__(self, package): + if package.lower().startswith(self.PACKAGE): + if self._has_library(self.LIB): + return True + return False + + +class MysqlLibCheck(BaseLibCheck): + + PACKAGE = 'mysql-python' + LIB = 'mysqlpp' + + def __call__(self, package): + if package.lower().startswith(self.PACKAGE): + if self._has_library(self.LIB): + if self._which('mysql_config'): + return True + return False + + +class OracleLibCheck(BaseLibCheck): + + PACKAGE = 'cx_oracle' + LIB = 'clntsh' + + def __call__(self, package): + if package.lower().startswith(self.PACKAGE): + if 'ORACLE_HOME' in os.environ: + if os.environ['ORACLE_HOME']: + return True + else: + if self._has_library(self.LIB): + self._set_oracle_home() + return True + return False + + def _set_oracle_home(self): + path = find_library_ldconfig(self.LIB) + os.environ['ORACLE_HOME'] = os.path.dirname(path) + + +class OrderedDictLibCheck(BaseLibCheck): + + PACKAGE = 'ordereddict' + + def __call__(self, package): + if package.lower().startswith(self.PACKAGE): + if sys.version < '2.7': + return True + return False + + +class ArgparseLibCheck(BaseLibCheck): + + PACKAGE = 'argparse' + + def __call__(self, package): + if package.lower().startswith(self.PACKAGE): + if sys.version < '2.7': + return True + return False + + +class PassLibCheck(BaseLibCheck): + + def __init__(self, chain): + self._packages = [] + for obj in chain: + self._packages.append(obj.PACKAGE.lower()) + + def __call__(self, package): + if package.lower() not in self._packages: + return True + +def get_install_requires(): + out = [] + chain = [ + PostgressLibCheck(), + MysqlLibCheck(), + OracleLibCheck(), + OrderedDictLibCheck(), + ArgparseLibCheck(), + ] + chain.append(PassLibCheck(chain)) + for package in ARGS['install_requires']: + for can_install in chain: + if can_install(package): + out.append(package) + return out + +def valid_python_version(): + if sys.version < PYTHON_VERSION_LOW: + return False + if sys.version >= PYTHON_VERSION_HIGH: + return False + return True + +def build_args(): + args = deepcopy(ARGS) + args['install_requires'] = get_install_requires() + return args + +if not valid_python_version(): + message = """ + {package} is not able to install: Because the version of + Python that is being used, '{version}', is not compatable + with {package}. {package} will only install with Python + versions {low} through {high}\n + """.format(package=ARGS['name'].lower().capitalize(), + low=PYTHON_VERSION_LOW, high=PYTHON_VERSION_HIGH) + sys.stderr.write(message) + sys.exit(1) +else: + setup(**build_args()) + -- cgit v1.2.3