Changeset - c9b0c7cf50ed
[Not reviewed]
0 6 0
Lance Edgar (lance) - 11 years ago 2013-12-18 23:50:37
lance@edbob.org
Removed reliance on `edbob.db.engines`.
6 files changed with 32 insertions and 30 deletions:
0 comments (0 inline, 0 general)
rattail/commands.py
Show inline comments
 
@@ -14,48 +14,49 @@
 
#
 
#  Rattail 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 Rattail.  If not, see <http://www.gnu.org/licenses/>.
 
#
 
################################################################################
 

	
 
"""
 
``rattail.commands`` -- Commands
 
"""
 

	
 
import sys
 
import platform
 

	
 
import edbob
 
from edbob import commands
 
from edbob.commands import Subcommand
 

	
 
from ._version import __version__
 
from .db import model
 
from .console import Progress
 

	
 

	
 
class Command(commands.Command):
 
    """
 
    The primary command for Rattail.
 
    """
 
    
 
    name = 'rattail'
 
    version = __version__
 
    description = "Retail Software Framework"
 
    long_description = """
 
Rattail is a retail software framework.
 

	
 
Copyright (c) 2010-2012 Lance Edgar <lance@edbob.org>
 

	
 
This program comes with ABSOLUTELY NO WARRANTY.  This is free software,
 
and you are welcome to redistribute it under certain conditions.
 
See the file COPYING.txt for more information.
 
"""
 

	
 

	
 
class AddUser(Subcommand):
 
    """
 
    Adds a user to the database.
 
@@ -138,49 +139,48 @@ class DatabaseSyncCommand(Subcommand):
 

	
 
        elif args.subcommand == 'stop':
 
            dbsync.stop_daemon(args.pidfile)
 

	
 

	
 
class Dump(Subcommand):
 
    """
 
    Do a simple data dump.
 
    """
 

	
 
    name = 'dump'
 
    description = "Dump data to file."
 

	
 
    def add_parser_args(self, parser):
 
        parser.add_argument(
 
            '--output', '-o', metavar='FILE',
 
            help="Optional path to output file.  If none is specified, "
 
            "data will be written to standard output.")
 
        parser.add_argument(
 
            'model', help="Model whose data will be dumped.")
 

	
 
    def run(self, args):
 
        from .db import get_session_class
 
        from .db.dump import dump_data
 
        from .console import Progress
 

	
 
        if hasattr(model, args.model):
 
            cls = getattr(model, args.model)
 
        else:
 
            sys.stderr.write("Unknown model: {0}\n".format(args.model))
 
            sys.exit(1)
 

	
 
        progress = None
 
        if self.show_progress:
 
            progress = Progress
 

	
 
        if args.output:
 
            output = open(args.output, 'wb')
 
        else:
 
            output = sys.stdout
 

	
 
        Session = get_session_class(edbob.config)
 
        session = Session()
 
        dump_data(session, cls, output, progress=progress)
 
        session.close()
 

	
 
        if output is not sys.stdout:
 
            output.close()
 

	
 
@@ -281,59 +281,58 @@ class InitializeDatabase(Subcommand):
 
    def add_parser_args(self, parser):
 
        parser.add_argument('url', metavar='URL',
 
                            help="Database engine URL")
 

	
 
    def run(self, args):
 
        from sqlalchemy import create_engine
 
        from .db.model import Base
 
        from alembic.util import obfuscate_url_pw
 
        
 
        engine = create_engine(args.url)
 
        Base.metadata.create_all(engine)
 
        print("Created initial tables for database:")
 
        print("  {0}".format(obfuscate_url_pw(engine.url)))
 

	
 

	
 
class LoadHostDataCommand(Subcommand):
 
    """
 
    Loads data from the Rattail host database, if one is configured.
 
    """
 

	
 
    name = 'load-host-data'
 
    description = "Load data from host database"
 

	
 
    def run(self, args):
 
        from .console import Progress
 
        from rattail.db import load
 
        from .db import get_engines
 
        from .db import load
 

	
 
        edbob.init_modules(['edbob.db'])
 

	
 
        if 'host' not in edbob.engines:
 
            print "Host engine URL not configured."
 
            return
 
        engines = get_engines(edbob.config)
 
        if 'host' not in engines:
 
            sys.stderr.write("Host engine URL not configured.\n")
 
            sys.exit(1)
 

	
 
        proc = load.LoadProcessor()
 
        proc.load_all_data(edbob.engines['host'], Progress)
 
        proc.load_all_data(engines['host'], Progress)
 

	
 

	
 
class MakeConfigCommand(Subcommand):
 
    """
 
    Creates a sample configuration file.
 
    """
 

	
 
    name = 'make-config'
 
    description = "Create a configuration file"
 

	
 
    def add_parser_args(self, parser):
 
        parser.add_argument('path', default='rattail.conf', metavar='PATH',
 
                            help="Path to the new file")
 
        parser.add_argument('-f', '--force', action='store_true',
 
                            help="Overwrite an existing file")
 

	
 

	
 
    def run(self, args):
 
        import os
 
        import os.path
 
        import shutil
 
        from rattail.files import resource_path
 

	
 
        dest = os.path.abspath(args.path)
rattail/db/sync/__init__.py
Show inline comments
 
@@ -18,67 +18,67 @@
 
#  more details.
 
#
 
#  You should have received a copy of the GNU Affero General Public License
 
#  along with Rattail.  If not, see <http://www.gnu.org/licenses/>.
 
#
 
################################################################################
 

	
 
"""
 
``rattail.db.sync`` -- Database Synchronization
 
"""
 

	
 
import sys
 
import time
 
import logging
 

	
 
if sys.platform == 'win32': # pragma no cover
 
    import win32api
 

	
 
import sqlalchemy.exc
 
from sqlalchemy.orm import sessionmaker, class_mapper
 
from sqlalchemy.exc import OperationalError
 

	
 
import edbob
 

	
 
from rattail.db import model
 
from .. import model
 
from .. import get_engines
 

	
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
def get_sync_engines():
 
    edbob.init_modules(['edbob.db'])
 

	
 
    keys = edbob.config.get('rattail.db', 'syncs')
 
    if not keys:
 
        return None
 

	
 
    engines = {}
 
    engines = get_engines(edbob.config)
 
    sync_engines = {}
 
    for key in keys.split(','):
 
        key = key.strip()
 
        engines[key] = edbob.engines[key]
 
    log.debug("get_sync_engines: Found engine keys: %s" % ','.join(engines.keys()))
 
    return engines
 
        sync_engines[key] = engines[key]
 
    log.debug("get_sync_engines: Found engine keys: %s" % ','.join(sync_engines.keys()))
 
    return sync_engines
 

	
 

	
 
class Synchronizer(object):
 
    """
 
    Default implementation of database synchronization logic.  Subclass this if
 
    you have special processing needs.
 
    """
 

	
 
    # This defines the `model` module which will be used to obtain references
 
    # to model classes (`Product` etc.).  If you need to synchronize custom
 
    # model classes of which Rattail is not aware, you must override this.
 
    # Note that the module you specify must be a superset of Rattail.
 
    model = model
 

	
 
    def __init__(self, local_engine, remote_engines):
 
        self.Session = sessionmaker()
 
        self.local_engine = local_engine
 
        self.remote_engines = remote_engines
 

	
 
    def loop(self):
 
        log.info("Synchronizer.loop: using remote engines: {0}".format(
 
                ', '.join(self.remote_engines.iterkeys())))
 
        while True:
 
            try:
rattail/db/sync/linux.py
Show inline comments
 
@@ -6,60 +6,61 @@
 
#  Copyright © 2010-2012 Lance Edgar
 
#
 
#  This file is part of Rattail.
 
#
 
#  Rattail 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.
 
#
 
#  Rattail 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 Rattail.  If not, see <http://www.gnu.org/licenses/>.
 
#
 
################################################################################
 

	
 
"""
 
``rattail.db.sync.linux`` -- Database Synchronization for Linux
 
"""
 

	
 
import edbob
 
from edbob import db
 

	
 
from ...daemon import Daemon
 
from .. import get_default_engine
 
from . import get_sync_engines, synchronize_changes
 

	
 

	
 
class SyncDaemon(Daemon):
 

	
 
    def run(self):
 
        remote_engines = get_sync_engines()
 
        if remote_engines:
 
            synchronize_changes(db.engine, remote_engines)
 
            local_engine = get_default_engine(edbob.config)
 
            synchronize_changes(local_engine, remote_engines)
 

	
 

	
 
def get_daemon(pidfile=None):
 
    """
 
    Get a :class:`SyncDaemon` instance.
 
    """
 

	
 
    if pidfile is None:
 
        pidfile = edbob.config.get('rattail.db', 'sync.pid_path',
 
                                   default='/var/run/rattail/dbsync.pid')
 
    return SyncDaemon(pidfile)
 

	
 

	
 
def start_daemon(pidfile=None, daemonize=True):
 
    """
 
    Start the database synchronization daemon.
 
    """
 

	
 
    get_daemon(pidfile).start(daemonize)
 

	
 

	
 
def stop_daemon(pidfile=None):
 
    """
 
    Stop the database synchronization daemon.
rattail/db/sync/win32.py
Show inline comments
 
@@ -10,71 +10,70 @@
 
#  Rattail 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.
 
#
 
#  Rattail 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 Rattail.  If not, see <http://www.gnu.org/licenses/>.
 
#
 
################################################################################
 

	
 
"""
 
``rattail.db.sync.win32`` -- Database Synchronization for Windows
 
"""
 

	
 
import sys
 
import logging
 
import threading
 

	
 
import edbob
 
from edbob import db
 

	
 
from rattail.win32.service import Service
 
from rattail.db.sync import get_sync_engines, synchronize_changes
 
from ...win32.service import Service
 
from .. import get_default_engine
 
from . import get_sync_engines, synchronize_changes
 

	
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class DatabaseSynchronizerService(Service):
 
    """
 
    Implements database synchronization as a Windows service.
 
    """
 

	
 
    _svc_name_ = 'RattailDatabaseSynchronizer'
 
    _svc_display_name_ = "Rattail : Database Synchronization Service"
 
    _svc_description_ = ("Monitors the local Rattail database for changes, "
 
                         "and synchronizes them to the configured remote "
 
                         "database(s).")
 

	
 
    appname = 'rattail'
 

	
 
    def Initialize(self):
 
        """
 
        Service initialization.
 
        """
 

	
 
        if not Service.Initialize(self):
 
            return False
 

	
 
        edbob.init_modules(['rattail.db'])
 

	
 
        local_engine = get_default_engine(edbob.config)
 
        remote_engines = get_sync_engines()
 
        if not remote_engines:
 
            return False
 

	
 
        thread = threading.Thread(target=synchronize_changes,
 
                                  args=(db.engine, remote_engines))
 
                                  args=(local_engine, remote_engines))
 
        thread.daemon = True
 
        thread.start()
 
        return True
 

	
 

	
 
if __name__ == '__main__':
 
    if sys.platform == 'win32':
 
        import win32serviceutil
 
        win32serviceutil.HandleCommandLine(DatabaseSynchronizerService)
tests/db/sync/test_init.py
Show inline comments
 
@@ -333,57 +333,57 @@ class SynchronizerTests(SyncTestCase):
 
        self.assertEqual(session.query(model.ProductCost).count(), 0)
 
        session.rollback()
 
        session.close()
 

	
 
    def test_delete_CustomerGroup(self):
 
        synchronizer = sync.Synchronizer(self.local_engine, self.remote_engines)
 

	
 
        session = self.Session(bind=self.local_engine)
 
        group = model.CustomerGroup()
 
        customer = model.Customer()
 
        customer.groups.append(group)
 
        session.add(customer)
 
        session.flush()
 
        assignment = session.query(model.CustomerGroupAssignment).one()
 
        self.assertEqual(assignment.customer_uuid, customer.uuid)
 
        self.assertEqual(assignment.group_uuid, group.uuid)
 
        synchronizer.delete_CustomerGroup(session, group)
 
        self.assertEqual(session.query(model.CustomerGroupAssignment).count(), 0)
 
        session.rollback()
 
        session.close()
 

	
 

	
 
class ModuleTests(TestCase):
 

	
 
    @patch('rattail.db.sync.edbob')
 
    def test_get_sync_engines(self, edbob):
 
    @patch.multiple('rattail.db.sync', edbob=DEFAULT, get_engines=DEFAULT)
 
    def test_get_sync_engines(self, edbob, get_engines):
 

	
 
        # nothing configured
 
        edbob.config.get.return_value = None
 
        self.assertIsNone(sync.get_sync_engines())
 

	
 
        # fake config with 2 out of 3 engines synced
 
        edbob.engines = {
 
        get_engines.return_value = {
 
            'one': 'first',
 
            'two': 'second',
 
            'three': 'third',
 
            }
 
        edbob.config.get.return_value = 'one, two'
 
        engines = sync.get_sync_engines()
 
        self.assertEqual(engines, {'one': 'first', 'two': 'second'})
 

	
 
    @patch.multiple('rattail.db.sync', edbob=DEFAULT, Synchronizer=DEFAULT)
 
    def test_synchronize_changes(self, edbob, Synchronizer):
 

	
 
        local_engine = Mock()
 
        remote_engines = Mock()
 

	
 
        # default synchronizer class
 
        edbob.config.get.return_value = None
 
        sync.synchronize_changes(local_engine, remote_engines)
 
        Synchronizer.assert_called_once_with(local_engine, remote_engines)
 
        Synchronizer.return_value.loop.assert_called_once_with()
 

	
 
        # custom synchronizer class
 
        edbob.config.get.return_value = 'some_class'
 
        sync.synchronize_changes(local_engine, remote_engines)
 
        edbob.load_spec.return_value.assert_called_once_with(local_engine, remote_engines)
tests/db/sync/test_linux.py
Show inline comments
 

	
 
from unittest import TestCase
 
from mock import patch, DEFAULT
 

	
 
from rattail.db.sync import linux
 

	
 

	
 
class SyncDaemonTests(TestCase):
 

	
 
    @patch.multiple('rattail.db.sync.linux',
 
                    db=DEFAULT,
 
                    edbob=DEFAULT,
 
                    get_default_engine=DEFAULT,
 
                    get_sync_engines=DEFAULT,
 
                    synchronize_changes=DEFAULT)
 
    def test_run(self, db, get_sync_engines, synchronize_changes):
 
    def test_run(self, edbob, get_default_engine, get_sync_engines, synchronize_changes):
 

	
 
        daemon = linux.SyncDaemon('/tmp/rattail_dbsync.pid')
 

	
 
        # no remote engines configured
 
        get_sync_engines.return_value = None
 
        daemon.run()
 
        get_sync_engines.assert_called_once_with()
 
        self.assertFalse(get_default_engine.called)
 
        self.assertFalse(synchronize_changes.called)
 

	
 
        # with remote engines configured
 
        get_sync_engines.return_value = 'fake_remotes'
 
        get_default_engine.return_value = 'fake_local'
 
        daemon.run()
 
        synchronize_changes.assert_called_once_with(db.engine, 'fake_remotes')
 
        synchronize_changes.assert_called_once_with('fake_local', 'fake_remotes')
 

	
 

	
 
class ModuleTests(TestCase):
 

	
 
    @patch.multiple('rattail.db.sync.linux', edbob=DEFAULT, SyncDaemon=DEFAULT)
 
    def test_get_daemon(self, edbob, SyncDaemon):
 

	
 
        # pid file provided
 
        linux.get_daemon('some_pidfile')
 
        self.assertFalse(edbob.config.get.called)
 
        SyncDaemon.assert_called_once_with('some_pidfile')
 

	
 
        # no pid file; fall back to config
 
        SyncDaemon.reset_mock()
 
        edbob.config.get.return_value = 'configured_pidfile'
 
        linux.get_daemon()
 
        edbob.config.get.assert_called_once_with('rattail.db', 'sync.pid_path',
 
                                                 default='/var/run/rattail/dbsync.pid')
 
        SyncDaemon.assert_called_once_with('configured_pidfile')
 

	
 
    @patch('rattail.db.sync.linux.get_daemon')
 
    def test_start_daemon(self, get_daemon):
 
        linux.start_daemon(pidfile='some_pidfile', daemonize='maybe')
 
        get_daemon.assert_called_once_with('some_pidfile')
0 comments (0 inline, 0 general)