
Added initial database schema and ability to install it, added init-db command to pyramid scaffold.
426 lines
14 KiB
Python
426 lines
14 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
################################################################################
|
|
#
|
|
# edbob -- Pythonic Software Framework
|
|
# Copyright © 2010-2012 Lance Edgar
|
|
#
|
|
# This file is part of edbob.
|
|
#
|
|
# edbob is free software: you can redistribute it and/or modify it under the
|
|
# terms of the GNU Affero General Public License as published by the Free
|
|
# Software Foundation, either version 3 of the License, or (at your option)
|
|
# any later version.
|
|
#
|
|
# edbob 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 Affero General Public License for
|
|
# more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
################################################################################
|
|
|
|
"""
|
|
``edbob.db.extensions`` -- Database Extensions
|
|
"""
|
|
|
|
import logging
|
|
# from pkg_resources import iter_entry_points
|
|
|
|
import sqlalchemy.exc
|
|
# from sqlalchemy.orm import clear_mappers
|
|
|
|
# import migrate.versioning.api
|
|
# from migrate.versioning.schema import ControlledSchema
|
|
|
|
# import rattail
|
|
# from rattail.db import exc as exceptions
|
|
# from rattail.db import Session
|
|
# from rattail.db.classes import ActiveExtension
|
|
# from rattail.db.mappers import make_mappers
|
|
# from rattail.db.model import get_metadata
|
|
# from rattail.db.util import get_repository_path, get_repository_version
|
|
|
|
import edbob
|
|
import edbob.db
|
|
from edbob.db import exceptions
|
|
from edbob.db import Session
|
|
from edbob.db.classes import ActiveExtension
|
|
from edbob.db.util import get_database_version
|
|
from edbob.util import requires_impl
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
available_extensions = edbob.entry_point_map('edbob.db.extensions')
|
|
active_extensions = {}
|
|
|
|
|
|
class Extension(edbob.Object):
|
|
"""
|
|
Base class for schema/ORM extensions.
|
|
"""
|
|
|
|
# Set this to a list of strings (extension names) as needed within your
|
|
# derived class.
|
|
required_extensions = []
|
|
|
|
@property
|
|
@requires_impl(is_property=True)
|
|
def name(self):
|
|
"""
|
|
The name of the extension.
|
|
"""
|
|
pass
|
|
|
|
@property
|
|
@requires_impl(is_property=True)
|
|
def schema(self):
|
|
"""
|
|
Should return a reference to the extension's ``schema`` module, which
|
|
is assumed to be a SQLAlchemy-Migrate repository.
|
|
"""
|
|
pass
|
|
|
|
def add_class(self, cls):
|
|
"""
|
|
Convenience method for use in :meth:`extend_classes()`.
|
|
"""
|
|
|
|
from edbob.db import classes
|
|
|
|
name = cls.__name__
|
|
edbob.graft(classes, {name:cls}, name)
|
|
|
|
def extend_classes(self):
|
|
"""
|
|
Any extra classes provided by the extension should be added to the ORM
|
|
whenever this method is called.
|
|
|
|
Note that the :meth:`add_class()` convenience method is designed to be
|
|
used when adding classes.
|
|
"""
|
|
pass
|
|
|
|
def extend_mappers(self, metadata):
|
|
"""
|
|
All SQLAlchemy mapping to be done by the extension should be done
|
|
within this method.
|
|
|
|
Any extra classes the extension provides will typically be mapped here.
|
|
Any manipulation the extension needs to perform on the ``edbob`` core
|
|
ORM should be done here as well.
|
|
"""
|
|
pass
|
|
|
|
def get_metadata(self):
|
|
"""
|
|
Should return a :class:`sqlalchemy.MetaData` instance containing the
|
|
schema definition for the extension, or ``None``.
|
|
"""
|
|
|
|
return None
|
|
|
|
def remove_class(self, name):
|
|
"""
|
|
Convenience method for use in :meth:`restore_classes()`.
|
|
"""
|
|
|
|
from edbob.db import classes
|
|
|
|
if name in classes.__all__:
|
|
classes.__all__.remove(name)
|
|
if hasattr(classes, name):
|
|
del classes.__dict__[name]
|
|
|
|
def restore_classes(self):
|
|
"""
|
|
This method should remove any extra classes which were added within
|
|
:meth:`extend_classes()`. Note that there is a :meth:`remove_class()`
|
|
method for convenience in doing so.
|
|
"""
|
|
pass
|
|
|
|
|
|
# def activate_extension(extension, engine=None):
|
|
# """
|
|
# Activates the :class:`RattailExtension` instance represented by
|
|
# ``extension`` (which can be the actual instance, or the extension's name)
|
|
# by installing its schema and registering it within the database, and
|
|
# immediately applies it to the current ORM API.
|
|
|
|
# If ``engine`` is not provided, then ``rattail.engine`` is assumed.
|
|
# """
|
|
|
|
# if engine is None:
|
|
# engine = rattail.engine
|
|
|
|
# if not isinstance(extension, RattailExtension):
|
|
# extension = get_extension(extension)
|
|
|
|
# log.info("Activating extension: %s" % extension.name)
|
|
# install_extension_schema(extension, engine)
|
|
|
|
# session = Session(bind=engine)
|
|
# if not session.query(ActiveExtension).get(extension.name):
|
|
# session.add(ActiveExtension(name=extension.name))
|
|
# session.commit()
|
|
# session.close()
|
|
|
|
# merge_extension_metadata(extension)
|
|
# extension.extend_classes()
|
|
# extension.extend_mappers(rattail.metadata)
|
|
# _active_extensions[extension.name] = extension
|
|
|
|
|
|
# def deactivate_extension(extension, engine=None):
|
|
# """
|
|
# Uninstalls an extension's schema from the primary database, and immediately
|
|
# requests it to restore the ORM API.
|
|
|
|
# If ``engine`` is not provided, then ``rattail.engine`` is assumed.
|
|
# """
|
|
|
|
# if engine is None:
|
|
# engine = rattail.engine
|
|
|
|
# if not isinstance(extension, RattailExtension):
|
|
# extension = get_extension(extension)
|
|
|
|
# log.info("Deactivating extension: %s" % extension.name)
|
|
# if extension.name in _active_extensions:
|
|
# del _active_extensions[extension.name]
|
|
|
|
# session = Session()
|
|
# ext = session.query(ActiveExtension).get(extension.name)
|
|
# if ext:
|
|
# session.delete(ext)
|
|
# session.commit()
|
|
# session.close()
|
|
|
|
# uninstall_extension_schema(extension, engine)
|
|
# unmerge_extension_metadata(extension)
|
|
# extension.restore_classes()
|
|
|
|
# clear_mappers()
|
|
# make_mappers(rattail.metadata)
|
|
# for name in sorted(_active_extensions, extension_sorter(_active_extensions)):
|
|
# _active_extensions[name].extend_mappers(rattail.metadata)
|
|
|
|
|
|
def extend_framework():
|
|
"""
|
|
Attempts to connect to the primary database and, if successful, inspects it
|
|
to determine which extensions are active within it. Any such extensions
|
|
found will be used to extend the ORM/API in-place.
|
|
"""
|
|
|
|
engine = edbob.db.engine
|
|
|
|
# Check primary database connection.
|
|
try:
|
|
engine.connect()
|
|
except sqlalchemy.exc.OperationalError:
|
|
return
|
|
|
|
# Check database version to see if core schema is installed.
|
|
try:
|
|
db_version = get_database_version(engine)
|
|
except exceptions.CoreSchemaNotInstalled:
|
|
return
|
|
|
|
# Since extensions may depend on one another, we must first retrieve the
|
|
# list of active extensions' names from the database and *then* sort them
|
|
# according to their stated dependencies. (This information is only known
|
|
# after instantiating the extensions.)
|
|
|
|
session = Session()
|
|
try:
|
|
active_extensions = session.query(ActiveExtension).all()
|
|
except sqlalchemy.exc.ProgrammingError:
|
|
session.close()
|
|
return
|
|
|
|
extensions = {}
|
|
for ext in active_extensions:
|
|
extensions[ext.name] = get_extension(ext.name)
|
|
session.close()
|
|
|
|
for name in sorted(extensions, extension_sorter(extensions)):
|
|
ext = extensions[name]
|
|
log.info("Applying active extension: %s" % name)
|
|
merge_extension_metadata(ext)
|
|
ext.extend_classes()
|
|
ext.extend_mappers(rattail.metadata)
|
|
active_extensions[name] = ext
|
|
|
|
|
|
# def extension_active(extension):
|
|
# """
|
|
# Returns boolean indicating whether or not the given ``extension`` is active
|
|
# within the current database.
|
|
# """
|
|
|
|
# if not isinstance(extension, RattailExtension):
|
|
# extension = get_extension(extension)
|
|
# return extension.name in _active_extensions
|
|
|
|
|
|
def extension_sorter(extensions):
|
|
"""
|
|
Returns a function to be used for sorting extensions according to their
|
|
inter-dependencies. ``extensions`` should be a dictionary containing the
|
|
extensions which are to be sorted.
|
|
"""
|
|
|
|
def sorter(name_x, name_y):
|
|
ext_x = extensions[name_x]
|
|
ext_y = extensions[name_y]
|
|
|
|
if name_y in ext_x.required_extensions:
|
|
return 1
|
|
if name_x in ext_y.required_extensions:
|
|
return -1
|
|
|
|
if ext_x.required_extensions and not ext_y.required_extensions:
|
|
return 1
|
|
if ext_y.required_extensions and not ext_x.required_extensions:
|
|
return -1
|
|
return 0
|
|
|
|
return sorter
|
|
|
|
|
|
def get_extension(name):
|
|
"""
|
|
Returns a :class:`Extension` instance, according to ``name``. An error is
|
|
raised if the extension cannot be found.
|
|
"""
|
|
|
|
if name in available_extensions:
|
|
return available_extensions[name]()
|
|
raise exceptions.ExtensionNotFound(name)
|
|
|
|
|
|
# def install_extension_schema(extension, engine=None):
|
|
# """
|
|
# Installs an extension's schema to the database and adds version control for
|
|
# it.
|
|
# """
|
|
|
|
# if engine is None:
|
|
# engine = rattail.engine
|
|
|
|
# # Extensionls aren't required to provide metadata...
|
|
# ext_meta = extension.get_metadata()
|
|
# if not ext_meta:
|
|
# return
|
|
|
|
# # ...but if they do they must also provide a SQLAlchemy-Migrate repository!
|
|
# assert extension.schema, "Extension does not implement 'schema': %s" % extension.name
|
|
|
|
# meta = rattail.metadata
|
|
# for table in meta.sorted_tables:
|
|
# table.tometadata(ext_meta)
|
|
# for table in ext_meta.sorted_tables:
|
|
# if table.name not in meta.tables:
|
|
# table.create(bind=engine, checkfirst=True)
|
|
|
|
# migrate.versioning.api.version_control(
|
|
# str(engine.url), get_repository_path(extension), get_repository_version(extension))
|
|
|
|
|
|
def merge_extension_metadata(ext):
|
|
"""
|
|
Merges an extension's metadata with the global ``edbob.db.metadata``
|
|
instance.
|
|
|
|
.. note::
|
|
``edbob`` uses this internally; you should not need to.
|
|
"""
|
|
|
|
ext_meta = ext.get_metadata()
|
|
if not ext_meta:
|
|
return
|
|
meta = edbob.db.metadata
|
|
for table in meta.sorted_tables:
|
|
table.tometadata(ext_meta)
|
|
for table in ext_meta.sorted_tables:
|
|
if table.name not in meta.tables:
|
|
table.tometadata(meta)
|
|
|
|
|
|
# def uninstall_extension_schema(extension, engine=None):
|
|
# """
|
|
# Uninstalls an extension's tables from the database represented by
|
|
# ``engine`` (or ``rattail.engine`` if none is provided), and removes
|
|
# SQLAlchemy-Migrate version control for the extension.
|
|
# """
|
|
|
|
# if engine is None:
|
|
# engine = rattail.engine
|
|
|
|
# ext_meta = extension.get_metadata()
|
|
# if not ext_meta:
|
|
# return
|
|
|
|
# schema = ControlledSchema(engine, get_repository_path(extension))
|
|
# engine.execute(schema.table.delete().where(
|
|
# schema.table.c.repository_id == schema.repository.id))
|
|
|
|
# meta = get_metadata()
|
|
# for table in meta.sorted_tables:
|
|
# table.tometadata(ext_meta)
|
|
# for table in reversed(ext_meta.sorted_tables):
|
|
# if table.name not in meta.tables:
|
|
# table.drop(bind=engine)
|
|
|
|
|
|
# def unmerge_extension_metadata(extension):
|
|
# """
|
|
# Removes an extension's metadata from the global ``rattail.metadata``
|
|
# instance.
|
|
# """
|
|
|
|
# ext_meta = extension.get_metadata()
|
|
# if not ext_meta:
|
|
# return
|
|
|
|
# meta = rattail.metadata
|
|
# ext_tables = ext_meta.tables.keys()
|
|
# for table in reversed(meta.sorted_tables):
|
|
# if table.name in ext_tables:
|
|
# meta.remove(table)
|
|
|
|
|
|
# # def merge_extension_permissions(extension):
|
|
# # '''
|
|
# # Helper function to merge an extension's permission definitions with those of
|
|
# # the framework. (This should only be called by the framework itself.)
|
|
# # '''
|
|
# # from rattail.v1.perms import permissions
|
|
# # log.debug('Merging permissions from extension: %s' % extension.name)
|
|
# # for group_name in extension.permissions:
|
|
# # if group_name not in permissions:
|
|
# # permissions[group_name] = extension.permissions[group_name]
|
|
# # elif extension.permissions[group_name][0] != permissions[group_name][0]:
|
|
# # log.warning("Extension '%s' tries to override UUID of permission group '%s' (but is denied)" % (
|
|
# # extension.name, group_name))
|
|
# # else:
|
|
# # # Extensions may override permission group display names.
|
|
# # if extension.permissions[group_name][1]:
|
|
# # permissions[group_name][1] = extension.permissions[group_name][1]
|
|
# # perms = permissions[group_name][2]
|
|
# # ext_perms = extension.permissions[group_name][2]
|
|
# # for perm_name in ext_perms:
|
|
# # if perm_name not in perms:
|
|
# # perms[perm_name] = ext_perms[perm_name]
|
|
# # elif ext_perms[perm_name][0] != perms[perm_name][0]:
|
|
# # log.warning("Extension '%s' tries to override UUID of permission '%s' (but is denied)" % (
|
|
# # extension.name, '.'.join((group_name, perm_name))))
|
|
# # else:
|
|
# # # Extensions may override permission display names.
|
|
# # if ext_perms[perm_name][1]:
|
|
# # perms[perm_name][1] = ext_perms[perm_name][1]
|