Changeset - 0ea99a6aab68
[Not reviewed]
0 5 0
Lance Edgar - 10 years ago 2015-03-18 20:03:15
ledgar@sacfoodcoop.com
Various changes to allow custom commands to sit in front of non-Rattail database.

In particular some of the Continuum versioning code required protection
from import failures, and various assumptions about the "default" database
session were refactored.
5 files changed with 67 insertions and 24 deletions:
0 comments (0 inline, 0 general)
rattail/commands.py
Show inline comments
 
@@ -105,12 +105,33 @@ See the file COPYING.txt for more information.
 
    def __init__(self):
 
        self.subcommands = load_entry_points('{0}.commands'.format(self.name))
 

	
 
    def __unicode__(self):
 
        return unicode(self.name)
 

	
 
    @property
 
    def db_config_section(self):
 
        """
 
        Name of section in config file which should have database connection
 
        info.  This defaults to ``'rattail.db'`` but may be overridden so the
 
        command framework can sit in front of a non-Rattail database if needed.
 

	
 
        This is used to auto-configure a "default" database engine for the app,
 
        when any command is invoked.
 
        """
 
        return 'rattail.db'
 

	
 
    @property
 
    def db_session_factory(self):
 
        """
 
        Reference to the "primary" ``Session`` class, which will be configured
 
        automatically during app startup.  Defaults to :class:`rattail.db.Session`.
 
        """
 
        from rattail.db import Session
 
        return Session
 

	
 
    def iter_subcommands(self):
 
        """
 
        Iterate over the subcommands.
 

	
 
        This is a generator which yields each associated :class:`Subcommand`
 
        class sorted by :attr:`Subcommand.name`.
 
@@ -227,16 +248,18 @@ Commands:\n""".format(self.description, self.name))
 
            # Configure the default database engine.
 
            try:
 
                from rattail.db.util import configure_session_factory
 
            except ImportError:
 
                pass            # assume no sqlalchemy
 
            else:
 
                configure_session_factory(config)
 
                configure_session_factory(config, section=self.db_config_section,
 
                                          session_factory=self.db_session_factory)
 

	
 
                # Configure Continuum versioning.
 
                if config.getboolean('rattail.db', 'versioning.enabled'):
 
                # Maybe configure Continuum versioning.
 
                if (self.db_config_section == 'rattail.db'
 
                    and config.getboolean('rattail.db', 'versioning.enabled')):
 
                    from rattail.db.continuum  import configure_versioning
 
                    configure_versioning(config)
 

	
 
        # And finally, do something of real value...
 
        cmd = self.subcommands[cmd](self)
 
        cmd.config = config
 
@@ -541,37 +564,41 @@ class FileMonitorCommand(Subcommand):
 

	
 

	
 
class ImportSubcommand(Subcommand):
 
    """
 
    Base class for subcommands which use the data importing system.
 
    """
 
    supports_versioning = True
 

	
 
    def add_parser_args(self, parser):
 
        parser.add_argument('--no-versioning', action='store_true',
 
                            help="Disables versioning during the import.  This is "
 
                            "intended to be useful e.g. during initial import, where "
 
                            "the process can be quite slow even without the overhead "
 
                            "of versioning.")
 
        if self.supports_versioning:
 
            parser.add_argument('--no-versioning', action='store_true',
 
                                help="Disables versioning during the import.  This is "
 
                                "intended to be useful e.g. during initial import, where "
 
                                "the process can be quite slow even without the overhead "
 
                                "of versioning.")
 
        parser.add_argument('--dry-run', action='store_true',
 
                            help="Go through the motions and allow logging to occur, "
 
                            "but do not actually commit the transaction at the end.")
 
        parser.add_argument('models', nargs='*', metavar='MODEL',
 
                            help="One or more models to import.  If not specified, "
 
                            "then all supported models will be imported.")
 

	
 
    def run(self, args):
 
        from rattail.db import Session
 
        from rattail.db.continuum  import disable_versioning
 

	
 
        log.info("begin {0} for data model(s): {1}".format(
 
                self.name, ', '.join(args.models or ["ALL"])))
 

	
 
        if args.no_versioning:
 
            disable_versioning()
 
        Session = self.parent.db_session_factory
 
        if self.supports_versioning:
 
            if args.no_versioning:
 
                from rattail.db.continuum  import disable_versioning
 
                disable_versioning()
 
            session = Session(continuum_user=self.continuum_user)
 
        else:
 
            session = Session()
 

	
 
        session = Session(continuum_user=self.continuum_user)
 
        self.import_data(args, session)
 

	
 
        if args.dry_run:
 
            session.rollback()
 
            log.info("dry run, so transaction was rolled back")
 
        else:
rattail/db/changes.py
Show inline comments
 
@@ -30,13 +30,17 @@ import logging
 

	
 
from sqlalchemy.orm import object_mapper, RelationshipProperty
 
from sqlalchemy.orm.interfaces import SessionExtension
 
from sqlalchemy.orm.session import Session
 

	
 
from rattail.core import get_uuid
 
from rattail.db.continuum import versioning_manager
 

	
 
try:
 
    from rattail.db.continuum import versioning_manager
 
except ImportError:             # assume no continuum
 
    versioning_manager = None
 

	
 

	
 
__all__ = ['record_changes']
 

	
 
log = logging.getLogger(__name__)
 

	
 
@@ -82,13 +86,14 @@ class ChangeRecorder(object):
 
        """
 
        Method invoked when session ``before_flush`` event occurs.
 
        """
 
        # TODO: Not sure if our event replaces the one registered by Continuum,
 
        # or what.  But this appears to be necessary to keep that system
 
        # working when we enable ours...
 
        versioning_manager.before_flush(session, flush_context, instances)
 
        if versioning_manager:
 
            versioning_manager.before_flush(session, flush_context, instances)
 

	
 
        for instance in session.deleted:
 
            log.debug("ChangeRecorder: found deleted instance: {0}".format(repr(instance)))
 
            self.record_change(session, instance, deleted=True)
 
        for instance in session.new:
 
            log.debug("ChangeRecorder: found new instance: {0}".format(repr(instance)))
rattail/db/importing/core.py
Show inline comments
 
@@ -72,12 +72,20 @@ class Importer(Object):
 

	
 
    def __init__(self, config, session, **kwargs):
 
        self.config = config
 
        self.session = session
 
        super(Importer, self).__init__(**kwargs)
 

	
 
    @property
 
    def model_module(self):
 
        """
 
        Reference to a module which contains all available / necessary data
 
        models.  By default this is ``rattail.db.model``.
 
        """
 
        return model
 

	
 
    @property
 
    def model_class(self):
 
        return getattr(model, self.__class__.__name__[:-8])
 

	
 
    @property
 
    def model_name(self):
rattail/db/util.py
Show inline comments
 
@@ -145,25 +145,28 @@ def configure_session(config, session):
 
    if config.getboolean('rattail.db', 'changes.record'):
 
        ignore_role_changes = config.getboolean(
 
            'rattail.db', 'changes.ignore_roles', default=True)
 
        record_changes(session, ignore_role_changes)
 

	
 

	
 
def configure_session_factory(config, session_factory=None):
 
def configure_session_factory(config, section='rattail.db', session_factory=None):
 
    """
 
    Configure a session factory using the provided settings.
 

	
 
    :param config: Object containing database configuration.
 

	
 
    :param section: Optional section name within which the configuration
 
       options are defined.  If not specified, ``'rattail.db'`` is assumed.
 

	
 
    :param session_factory: Optional session factory; if none is specified then
 
        :attr:`Session` will be assumed.
 
    """
 
    if session_factory is None:
 
        session_factory = Session
 

	
 
    engine = get_default_engine(config)
 
    engine = get_default_engine(config, section=section)
 
    if engine:
 
        session_factory.configure(bind=engine)
 

	
 
    configure_session(config, session_factory)
 

	
 

	
tests/db/test_init.py
Show inline comments
 
@@ -20,23 +20,23 @@ class TestConfigureSessionFactory(TestCase):
 
    def setUp(self):
 
        self.config = AppConfigParser(u'rattail')
 
        self.config.add_section(u'rattail.db')
 
        self.Session = sessionmaker()
 

	
 
    def test_session_is_not_bound_if_no_engine_is_defined_by_config(self):
 
        configure_session_factory(self.config, self.Session)
 
        configure_session_factory(self.config, session_factory=self.Session)
 
        session = self.Session()
 
        self.assertTrue(session.bind is None)
 
        session.close()
 

	
 
    def test_session_is_correctly_bound_if_engine_is_defined_by_config(self):
 
        self.config.set(u'rattail.db', u'sqlalchemy.url', u'sqlite:////a/very/custom/db')
 
        session = self.Session()
 
        self.assertTrue(session.bind is None)
 
        session.close()
 
        configure_session_factory(self.config, self.Session)
 
        configure_session_factory(self.config, session_factory=self.Session)
 
        session = self.Session()
 
        self.assertTrue(isinstance(session.bind, Engine))
 
        self.assertEqual(unicode(session.bind.url), u'sqlite:////a/very/custom/db')
 
        session.close()
 

	
 
    def test_global_session_is_configured_by_default(self):
 
@@ -52,33 +52,33 @@ class TestConfigureSessionFactory(TestCase):
 
        # Must undo that configuration, this thing is global.
 
        db.Session.configure(bind=None)
 

	
 
    @patch(u'rattail.db.changes.record_changes')
 
    def test_changes_will_not_be_recorded_by_default(self, record_changes):
 
        self.config.set(u'rattail.db', u'sqlalchemy.url', u'sqlite://')
 
        configure_session_factory(self.config, self.Session)
 
        configure_session_factory(self.config, session_factory=self.Session)
 
        self.assertFalse(record_changes.called)
 

	
 
    @patch('rattail.db.util.record_changes')
 
    def test_changes_will_be_recorded_by_so_configured(self, record_changes):
 
        self.config.set(u'rattail.db', u'sqlalchemy.url', u'sqlite://')
 
        self.config.set(u'rattail.db', u'changes.record', u'true')
 
        configure_session_factory(self.config, self.Session)
 
        configure_session_factory(self.config, session_factory=self.Session)
 
        # Role changes are ignored by default.
 
        record_changes.assert_called_once_with(self.Session, True)
 

	
 
    @patch('rattail.db.util.record_changes')
 
    def test_changes_will_still_be_recorded_with_deprecated_config(self, record_changes):
 
        self.config.set(u'rattail.db', u'sqlalchemy.url', u'sqlite://')
 
        self.config.set(u'rattail.db', u'changes.record', u'true')
 
        configure_session_factory(self.config, self.Session)
 
        configure_session_factory(self.config, session_factory=self.Session)
 
        # Role changes are ignored by default.
 
        record_changes.assert_called_once_with(self.Session, True)
 

	
 
    @patch('rattail.db.util.record_changes')
 
    def test_config_determines_if_role_changes_are_ignored(self, record_changes):
 
        self.config.set(u'rattail.db', u'sqlalchemy.url', u'sqlite://')
 
        self.config.set(u'rattail.db', u'changes.record', u'true')
 
        self.config.set(u'rattail.db', u'changes.ignore_roles', u'false')
 
        configure_session_factory(self.config, self.Session)
 
        configure_session_factory(self.config, session_factory=self.Session)
 
        # Role changes are ignored by default; False means config works.
 
        record_changes.assert_called_once_with(self.Session, False)
0 comments (0 inline, 0 general)