extended commit (see note)
- Added ``Store`` and related models. - Added ``Customer.email_preference`` field. - Added ``load-host-data`` command. - Added ``Change`` model. - Added ``rattail.db.record_changes()`` function. - Added database synchronization service (Windows only).
This commit is contained in:
parent
efcfaea44f
commit
84229d9577
|
@ -28,6 +28,7 @@
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
import edbob
|
||||||
from edbob import commands
|
from edbob import commands
|
||||||
|
|
||||||
import rattail
|
import rattail
|
||||||
|
@ -124,6 +125,28 @@ class InitCommand(commands.Subcommand):
|
||||||
print ' %s' % engine.url
|
print ' %s' % engine.url
|
||||||
|
|
||||||
|
|
||||||
|
class LoadHostDataCommand(commands.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 edbob.console import Progress
|
||||||
|
from rattail.db import load
|
||||||
|
|
||||||
|
edbob.init_modules(['edbob.db'])
|
||||||
|
|
||||||
|
if 'host' not in edbob.engines:
|
||||||
|
print "Host engine URL not configured."
|
||||||
|
return
|
||||||
|
|
||||||
|
proc = load.LoadProcessor()
|
||||||
|
proc.load_all_data(edbob.engines['host'], Progress)
|
||||||
|
|
||||||
|
|
||||||
def main(*args):
|
def main(*args):
|
||||||
"""
|
"""
|
||||||
The primary entry point for the Rattail command system.
|
The primary entry point for the Rattail command system.
|
||||||
|
|
|
@ -26,11 +26,60 @@
|
||||||
``rattail.db`` -- Database Stuff
|
``rattail.db`` -- Database Stuff
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from sqlalchemy.event import listen
|
||||||
|
|
||||||
import edbob
|
import edbob
|
||||||
|
|
||||||
import rattail
|
import rattail
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def before_flush(session, flush_context, instances):
|
||||||
|
"""
|
||||||
|
Listens for session flush events. This function is responsible for adding
|
||||||
|
stub records to the 'changes' table, which will in turn be used by the
|
||||||
|
database synchronizer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def record_change(instance, deleted=False):
|
||||||
|
if instance.__class__ is rattail.Change:
|
||||||
|
return
|
||||||
|
if not hasattr(instance, 'uuid'):
|
||||||
|
return
|
||||||
|
if not instance.uuid:
|
||||||
|
instance.uuid = edbob.get_uuid()
|
||||||
|
change = session.query(rattail.Change).get(
|
||||||
|
(instance.__class__.__name__, instance.uuid))
|
||||||
|
if not change:
|
||||||
|
change = rattail.Change(
|
||||||
|
class_name=instance.__class__.__name__,
|
||||||
|
uuid=instance.uuid)
|
||||||
|
session.add(change)
|
||||||
|
change.deleted = deleted
|
||||||
|
log.debug("before_flush: recorded change: %s" % repr(change))
|
||||||
|
|
||||||
|
for instance in session.deleted:
|
||||||
|
log.debug("before_flush: deleted instance: %s" % repr(instance))
|
||||||
|
record_change(instance, deleted=True)
|
||||||
|
|
||||||
|
for instance in session.new:
|
||||||
|
log.debug("before_flush: new instance: %s" % repr(instance))
|
||||||
|
record_change(instance)
|
||||||
|
|
||||||
|
for instance in session.dirty:
|
||||||
|
if session.is_modified(instance, passive=True):
|
||||||
|
log.debug("before_flush: dirty instance: %s" % repr(instance))
|
||||||
|
record_change(instance)
|
||||||
|
|
||||||
|
|
||||||
|
def record_changes(session):
|
||||||
|
listen(session, 'before_flush', before_flush)
|
||||||
|
|
||||||
|
|
||||||
def init(config):
|
def init(config):
|
||||||
"""
|
"""
|
||||||
Initialize the Rattail database framework.
|
Initialize the Rattail database framework.
|
||||||
|
@ -41,3 +90,6 @@ def init(config):
|
||||||
|
|
||||||
from rattail.db.extension import enum
|
from rattail.db.extension import enum
|
||||||
edbob.graft(rattail, enum)
|
edbob.graft(rattail, enum)
|
||||||
|
|
||||||
|
if config.get('rattail.db', 'record_changes') == 'True':
|
||||||
|
record_changes(edbob.Session)
|
||||||
|
|
|
@ -45,7 +45,8 @@ from rattail import batches
|
||||||
from rattail.gpc import GPCType
|
from rattail.gpc import GPCType
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Department', 'Subdepartment', 'Brand', 'Category', 'Vendor',
|
__all__ = ['Change', 'Store', 'StoreEmailAddress', 'StorePhoneNumber',
|
||||||
|
'Department', 'Subdepartment', 'Brand', 'Category', 'Vendor',
|
||||||
'VendorContact', 'VendorPhoneNumber', 'Product', 'ProductCost',
|
'VendorContact', 'VendorPhoneNumber', 'Product', 'ProductCost',
|
||||||
'ProductPrice', 'Customer', 'CustomerEmailAddress',
|
'ProductPrice', 'Customer', 'CustomerEmailAddress',
|
||||||
'CustomerPhoneNumber', 'CustomerGroup', 'CustomerGroupAssignment',
|
'CustomerPhoneNumber', 'CustomerGroup', 'CustomerGroupAssignment',
|
||||||
|
@ -53,6 +54,22 @@ __all__ = ['Department', 'Subdepartment', 'Brand', 'Category', 'Vendor',
|
||||||
'EmployeePhoneNumber', 'BatchColumn', 'Batch', 'LabelProfile']
|
'EmployeePhoneNumber', 'BatchColumn', 'Batch', 'LabelProfile']
|
||||||
|
|
||||||
|
|
||||||
|
class Change(Base):
|
||||||
|
"""
|
||||||
|
Represents a changed (or deleted) record, which is pending synchronization
|
||||||
|
to another database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = 'changes'
|
||||||
|
|
||||||
|
class_name = Column(String(25), primary_key=True)
|
||||||
|
uuid = Column(String(32), primary_key=True)
|
||||||
|
deleted = Column(Boolean)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Change: %s, %s>" % (self.class_name, self.uuid)
|
||||||
|
|
||||||
|
|
||||||
class BatchColumn(Base):
|
class BatchColumn(Base):
|
||||||
"""
|
"""
|
||||||
Represents a :class:`SilColumn` associated with a :class:`Batch`.
|
Represents a :class:`SilColumn` associated with a :class:`Batch`.
|
||||||
|
@ -207,6 +224,84 @@ class Batch(Base):
|
||||||
return q
|
return q
|
||||||
|
|
||||||
|
|
||||||
|
class StoreEmailAddress(EmailAddress):
|
||||||
|
"""
|
||||||
|
Represents an email address associated with a :class:`Store`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__mapper_args__ = {'polymorphic_identity': 'Store'}
|
||||||
|
|
||||||
|
|
||||||
|
class StorePhoneNumber(PhoneNumber):
|
||||||
|
"""
|
||||||
|
Represents a phone (or fax) number associated with a :class:`Store`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__mapper_args__ = {'polymorphic_identity': 'Store'}
|
||||||
|
|
||||||
|
|
||||||
|
class Store(Base):
|
||||||
|
"""
|
||||||
|
Represents a store (physical or otherwise) within the organization.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = 'stores'
|
||||||
|
|
||||||
|
uuid = uuid_column()
|
||||||
|
id = Column(String(10))
|
||||||
|
name = Column(String(100))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Store: %s, %s>" % (self.id, self.name)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return unicode(self.name or '')
|
||||||
|
|
||||||
|
def add_email_address(self, address, type='Info'):
|
||||||
|
email = StoreEmailAddress(address=address, type=type)
|
||||||
|
self.emails.append(email)
|
||||||
|
|
||||||
|
def add_phone_number(self, number, type='Voice'):
|
||||||
|
phone = StorePhoneNumber(number=number, type=type)
|
||||||
|
self.phones.append(phone)
|
||||||
|
|
||||||
|
Store.emails = relationship(
|
||||||
|
StoreEmailAddress,
|
||||||
|
backref='store',
|
||||||
|
primaryjoin=StoreEmailAddress.parent_uuid == Store.uuid,
|
||||||
|
foreign_keys=[StoreEmailAddress.parent_uuid],
|
||||||
|
collection_class=ordering_list('preference', count_from=1),
|
||||||
|
order_by=StoreEmailAddress.preference,
|
||||||
|
cascade='save-update, merge, delete, delete-orphan')
|
||||||
|
|
||||||
|
Store.email = relationship(
|
||||||
|
StoreEmailAddress,
|
||||||
|
primaryjoin=and_(
|
||||||
|
StoreEmailAddress.parent_uuid == Store.uuid,
|
||||||
|
StoreEmailAddress.preference == 1),
|
||||||
|
foreign_keys=[StoreEmailAddress.parent_uuid],
|
||||||
|
uselist=False,
|
||||||
|
viewonly=True)
|
||||||
|
|
||||||
|
Store.phones = relationship(
|
||||||
|
StorePhoneNumber,
|
||||||
|
backref='store',
|
||||||
|
primaryjoin=StorePhoneNumber.parent_uuid == Store.uuid,
|
||||||
|
foreign_keys=[StorePhoneNumber.parent_uuid],
|
||||||
|
collection_class=ordering_list('preference', count_from=1),
|
||||||
|
order_by=StorePhoneNumber.preference,
|
||||||
|
cascade='save-update, merge, delete, delete-orphan')
|
||||||
|
|
||||||
|
Store.phone = relationship(
|
||||||
|
StorePhoneNumber,
|
||||||
|
primaryjoin=and_(
|
||||||
|
StorePhoneNumber.parent_uuid == Store.uuid,
|
||||||
|
StorePhoneNumber.preference == 1),
|
||||||
|
foreign_keys=[StorePhoneNumber.parent_uuid],
|
||||||
|
uselist=False,
|
||||||
|
viewonly=True)
|
||||||
|
|
||||||
|
|
||||||
class Brand(Base):
|
class Brand(Base):
|
||||||
"""
|
"""
|
||||||
Represents a brand or similar product line.
|
Represents a brand or similar product line.
|
||||||
|
@ -220,8 +315,8 @@ class Brand(Base):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Brand: %s>" % self.name
|
return "<Brand: %s>" % self.name
|
||||||
|
|
||||||
def __str__(self):
|
def __unicode__(self):
|
||||||
return str(self.name or '')
|
return unicode(self.name or '')
|
||||||
|
|
||||||
|
|
||||||
class Department(Base):
|
class Department(Base):
|
||||||
|
@ -661,6 +756,7 @@ class Customer(Base):
|
||||||
uuid = uuid_column()
|
uuid = uuid_column()
|
||||||
id = Column(String(20))
|
id = Column(String(20))
|
||||||
name = Column(String(255))
|
name = Column(String(255))
|
||||||
|
email_preference = Column(Integer)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Customer: %s, %s>" % (self.id, self.name or self.person)
|
return "<Customer: %s, %s>" % (self.id, self.name or self.person)
|
||||||
|
|
151
rattail/db/load.py
Normal file
151
rattail/db/load.py
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# 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.load`` -- Load Data from Host
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
|
import edbob
|
||||||
|
import edbob.db
|
||||||
|
|
||||||
|
import rattail
|
||||||
|
|
||||||
|
|
||||||
|
class LoadProcessor(edbob.Object):
|
||||||
|
|
||||||
|
def load_all_data(self, host_engine, progress=None):
|
||||||
|
|
||||||
|
edbob.init_modules(['edbob.db', 'rattail.db'])
|
||||||
|
|
||||||
|
self.host_session = edbob.Session(bind=host_engine)
|
||||||
|
self.session = edbob.Session()
|
||||||
|
|
||||||
|
cancel = False
|
||||||
|
for cls in self.relevant_classes():
|
||||||
|
if not self.load_class_data(cls, progress):
|
||||||
|
cancel = True
|
||||||
|
break
|
||||||
|
|
||||||
|
self.host_session.close()
|
||||||
|
if cancel:
|
||||||
|
self.session.rollback()
|
||||||
|
else:
|
||||||
|
self.session.commit()
|
||||||
|
self.session.close()
|
||||||
|
return not cancel
|
||||||
|
|
||||||
|
def load_class_data(self, cls, progress=None):
|
||||||
|
query = self.host_session.query(cls)
|
||||||
|
if hasattr(self, 'query_%s' % cls.__name__):
|
||||||
|
query = getattr(self, 'query_%s' % cls.__name__)(query)
|
||||||
|
|
||||||
|
count = query.count()
|
||||||
|
if not count:
|
||||||
|
return True
|
||||||
|
|
||||||
|
prog = None
|
||||||
|
if progress:
|
||||||
|
prog = progress("Loading %s data" % cls.__name__, count)
|
||||||
|
|
||||||
|
cancel = False
|
||||||
|
for i, instance in enumerate(query, 1):
|
||||||
|
if hasattr(self, 'merge_%s' % cls.__name__):
|
||||||
|
getattr(self, 'merge_%s' % cls.__name__)(instance)
|
||||||
|
else:
|
||||||
|
self.session.merge(instance)
|
||||||
|
self.session.flush()
|
||||||
|
if prog and not prog.update(i):
|
||||||
|
cancel = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if prog:
|
||||||
|
prog.destroy()
|
||||||
|
return not cancel
|
||||||
|
|
||||||
|
def relevant_classes(self):
|
||||||
|
yield edbob.Person
|
||||||
|
yield edbob.User
|
||||||
|
yield rattail.Store
|
||||||
|
yield rattail.Department
|
||||||
|
yield rattail.Subdepartment
|
||||||
|
yield rattail.Category
|
||||||
|
yield rattail.Brand
|
||||||
|
yield rattail.Vendor
|
||||||
|
yield rattail.Product
|
||||||
|
yield rattail.CustomerGroup
|
||||||
|
yield rattail.Customer
|
||||||
|
yield rattail.Employee
|
||||||
|
|
||||||
|
def query_Customer(self, q):
|
||||||
|
q = q.options(joinedload(rattail.Customer.phones))
|
||||||
|
q = q.options(joinedload(rattail.Customer.emails))
|
||||||
|
q = q.options(joinedload(rattail.Customer._people))
|
||||||
|
q = q.options(joinedload(rattail.Customer._groups))
|
||||||
|
return q
|
||||||
|
|
||||||
|
def query_CustomerPerson(self, q):
|
||||||
|
q = q.options(joinedload(rattail.CustomerPerson.person))
|
||||||
|
return q
|
||||||
|
|
||||||
|
def query_Employee(self, q):
|
||||||
|
q = q.options(joinedload(rattail.Employee.phones))
|
||||||
|
q = q.options(joinedload(rattail.Employee.emails))
|
||||||
|
return q
|
||||||
|
|
||||||
|
def query_Person(self, q):
|
||||||
|
q = q.options(joinedload(edbob.Person.phones))
|
||||||
|
q = q.options(joinedload(edbob.Person.emails))
|
||||||
|
return q
|
||||||
|
|
||||||
|
def query_Product(self, q):
|
||||||
|
q = q.options(joinedload(rattail.Product.costs))
|
||||||
|
q = q.options(joinedload(rattail.Product.prices))
|
||||||
|
return q
|
||||||
|
|
||||||
|
def merge_Product(self, host_product):
|
||||||
|
# This logic is necessary due to the inter-dependency between Product
|
||||||
|
# and ProductPrice tables. merge() will cause a flush(); however it
|
||||||
|
# apparently will not honor the 'post_update=True' flag on the relevant
|
||||||
|
# relationships.. I'm unclear whether this is a "bug" with SQLAlchemy,
|
||||||
|
# but the workaround is simple enough that I'm leaving it for now.
|
||||||
|
product = self.session.merge(host_product)
|
||||||
|
product.regular_price_uuid = None
|
||||||
|
product.current_price_uuid = None
|
||||||
|
if host_product.regular_price_uuid:
|
||||||
|
product.regular_price = self.session.merge(host_product.regular_price)
|
||||||
|
if host_product.current_price_uuid:
|
||||||
|
product.current_price = self.session.merge(host_product.current_price)
|
||||||
|
|
||||||
|
def query_Store(self, q):
|
||||||
|
q = q.options(joinedload(rattail.Store.phones))
|
||||||
|
q = q.options(joinedload(rattail.Store.emails))
|
||||||
|
return q
|
||||||
|
|
||||||
|
def query_Vendor(self, q):
|
||||||
|
q = q.options(joinedload(rattail.Vendor.contacts))
|
||||||
|
q = q.options(joinedload(rattail.Vendor.phones))
|
||||||
|
q = q.options(joinedload(rattail.Vendor.emails))
|
||||||
|
return q
|
28
rattail/db/sync/__init__.py
Normal file
28
rattail/db/sync/__init__.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# 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`` -- Database Synchronization
|
||||||
|
"""
|
||||||
|
|
165
rattail/db/sync/win32.py
Normal file
165
rattail/db/sync/win32.py
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# 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.win32`` -- Database Synchronization for Windows
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
|
if sys.platform == 'win32': # docs should build for everyone
|
||||||
|
import win32api
|
||||||
|
import win32serviceutil
|
||||||
|
|
||||||
|
from sqlalchemy import engine_from_config
|
||||||
|
from sqlalchemy.orm import class_mapper
|
||||||
|
|
||||||
|
import edbob
|
||||||
|
from edbob.win32 import Service
|
||||||
|
|
||||||
|
import rattail
|
||||||
|
|
||||||
|
|
||||||
|
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(['edbob.db', 'rattail.db'])
|
||||||
|
|
||||||
|
keys = edbob.config.get('rattail.db', 'syncs')
|
||||||
|
if not keys:
|
||||||
|
return False
|
||||||
|
|
||||||
|
engines = {}
|
||||||
|
for key in keys.split(','):
|
||||||
|
key = key.strip()
|
||||||
|
engines[key] = edbob.engines[key]
|
||||||
|
|
||||||
|
thread = threading.Thread(target=synchronize_changes,
|
||||||
|
args=(engines,))
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def dependency_sort(x, y):
|
||||||
|
map_x = class_mapper(getattr(edbob, x))
|
||||||
|
map_y = class_mapper(getattr(edbob, y))
|
||||||
|
|
||||||
|
dep_x = []
|
||||||
|
table_y = map_y.tables[0].name
|
||||||
|
for column in map_x.columns:
|
||||||
|
for key in column.foreign_keys:
|
||||||
|
if key.column.table.name == table_y:
|
||||||
|
return 1
|
||||||
|
dep_x.append(key)
|
||||||
|
|
||||||
|
dep_y = []
|
||||||
|
table_x = map_x.tables[0].name
|
||||||
|
for column in map_y.columns:
|
||||||
|
for key in column.foreign_keys:
|
||||||
|
if key.column.table.name == table_x:
|
||||||
|
return -1
|
||||||
|
dep_y.append(key)
|
||||||
|
|
||||||
|
if dep_x and not dep_y:
|
||||||
|
return 1
|
||||||
|
if dep_y and not dep_x:
|
||||||
|
return -1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def synchronize_changes(engines):
|
||||||
|
|
||||||
|
while True:
|
||||||
|
local_session = edbob.Session()
|
||||||
|
local_changes = local_session.query(rattail.Change)
|
||||||
|
|
||||||
|
if local_changes.count():
|
||||||
|
|
||||||
|
class_names = []
|
||||||
|
for class_name in local_session.query(rattail.Change.class_name.distinct()):
|
||||||
|
class_names.append(class_name[0])
|
||||||
|
class_names.sort(cmp=dependency_sort)
|
||||||
|
|
||||||
|
remote_sessions = []
|
||||||
|
for remote_engine in engines.itervalues():
|
||||||
|
remote_sessions.append(
|
||||||
|
edbob.Session(bind=remote_engine))
|
||||||
|
|
||||||
|
for class_name in class_names:
|
||||||
|
|
||||||
|
for change in local_changes.filter_by(class_name=class_name):
|
||||||
|
cls = getattr(edbob, change.class_name)
|
||||||
|
|
||||||
|
if change.deleted:
|
||||||
|
for remote_session in remote_sessions:
|
||||||
|
remote_instance = remote_session.query(cls).get(change.uuid)
|
||||||
|
if remote_instance:
|
||||||
|
remote_session.delete(remote_instance)
|
||||||
|
remote_session.flush()
|
||||||
|
|
||||||
|
else: # new/dirty
|
||||||
|
local_instance = local_session.query(cls).get(change.uuid)
|
||||||
|
for remote_session in remote_sessions:
|
||||||
|
remote_session.merge(local_instance)
|
||||||
|
remote_session.flush()
|
||||||
|
|
||||||
|
local_session.delete(change)
|
||||||
|
local_session.flush()
|
||||||
|
|
||||||
|
for remote_session in remote_sessions:
|
||||||
|
remote_session.commit()
|
||||||
|
remote_session.close()
|
||||||
|
local_session.commit()
|
||||||
|
|
||||||
|
local_session.close()
|
||||||
|
win32api.Sleep(3000)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
win32serviceutil.HandleCommandLine(DatabaseSynchronizerService)
|
1
setup.py
1
setup.py
|
@ -113,6 +113,7 @@ rattail = rattail.db.extension:RattailExtension
|
||||||
|
|
||||||
[rattail.commands]
|
[rattail.commands]
|
||||||
filemon = rattail.commands:FileMonitorCommand
|
filemon = rattail.commands:FileMonitorCommand
|
||||||
|
load-host-data = rattail.commands:LoadHostDataCommand
|
||||||
|
|
||||||
[rattail.batches.providers]
|
[rattail.batches.providers]
|
||||||
print_labels = rattail.batches.providers.labels:PrintLabels
|
print_labels = rattail.batches.providers.labels:PrintLabels
|
||||||
|
|
Loading…
Reference in a new issue