From 0ea99a6aab68c39857b607cae116a907623e1c2b 2015-03-18 20:03:15 From: Lance Edgar Date: 2015-03-18 20:03:15 Subject: [PATCH] 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. --- diff --git a/rattail/commands.py b/rattail/commands.py index e520caf3d49635568d5ea48d2c5381c4c3cecdbe..18fd611c90dd9141c748c407dbcb9a826b1804c2 100644 --- a/rattail/commands.py +++ b/rattail/commands.py @@ -108,6 +108,27 @@ See the file COPYING.txt for more information. 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. @@ -230,10 +251,12 @@ Commands:\n""".format(self.description, self.name)) 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) @@ -544,13 +567,15 @@ 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.") @@ -559,16 +584,18 @@ class ImportSubcommand(Subcommand): "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: diff --git a/rattail/db/changes.py b/rattail/db/changes.py index 066bc84c5f0788367338a32283cd4e58f1c75030..47c864dde32fc253faeac81617836a25da83ced4 100644 --- a/rattail/db/changes.py +++ b/rattail/db/changes.py @@ -33,7 +33,11 @@ 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'] @@ -85,7 +89,8 @@ class ChangeRecorder(object): # 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))) diff --git a/rattail/db/importing/core.py b/rattail/db/importing/core.py index 4a137df88403494332046067ac7128d94b1c08d7..79e8bae2d610c846639bb1d9399e1db8d8c7e16f 100644 --- a/rattail/db/importing/core.py +++ b/rattail/db/importing/core.py @@ -75,6 +75,14 @@ class Importer(Object): 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]) diff --git a/rattail/db/util.py b/rattail/db/util.py index ebad30d7fc6a48e82e0108233a3df1a38b105e32..0a8f3852b5bf91e57a4ac996edb7f304eab51141 100644 --- a/rattail/db/util.py +++ b/rattail/db/util.py @@ -148,19 +148,22 @@ def configure_session(config, session): 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) diff --git a/tests/db/test_init.py b/tests/db/test_init.py index f0dc5925ec370c5b3f6ea8d8b09995aacec303ca..8b2e840fa46a75ae46f2972d7700d2b2457720d0 100644 --- a/tests/db/test_init.py +++ b/tests/db/test_init.py @@ -23,7 +23,7 @@ class TestConfigureSessionFactory(TestCase): 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() @@ -33,7 +33,7 @@ class TestConfigureSessionFactory(TestCase): 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') @@ -55,14 +55,14 @@ class TestConfigureSessionFactory(TestCase): @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) @@ -70,7 +70,7 @@ class TestConfigureSessionFactory(TestCase): 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) @@ -79,6 +79,6 @@ class TestConfigureSessionFactory(TestCase): 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)