Changeset - 4d50f1b769a4
[Not reviewed]
0 2 5
Lance Edgar (lance) - 12 years ago 2013-02-25 15:28:37
lance@edbob.org
Added new batch system.

This commit adds a new batch system which will eventually replace the
old one. Hopefully they can play nicely in parallel, in the meantime.
7 files changed with 531 insertions and 1 deletions:
0 comments (0 inline, 0 general)
rattail/db/batches/__init__.py
Show inline comments
 
new file 100644
 
#!/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.batches`` -- Batch API
 
"""
 

	
 
from rattail.db.batches.types import *
 
from rattail.db.batches.data import *
 
from rattail.db.batches.makers import *
 
from rattail.db.batches.executors import *
rattail/db/batches/data.py
Show inline comments
 
new file 100644
 
#!/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.batches.data`` -- Batch Data Providers
 
"""
 

	
 
import csv
 

	
 
from edbob.files import count_lines
 

	
 

	
 
__all__ = ['ProductQueryDataProvider', 'CSVDataProxy', 'CSVDataProvider']
 

	
 

	
 
class BatchDataProvider(object):
 

	
 
    def __len__(self):
 
        raise NotImplementedError
 

	
 
    def __iter__(self):
 
        raise NotImplementedError
 

	
 

	
 
class QueryDataProvider(BatchDataProvider):
 

	
 
    def __init__(self, query):
 
        self.query = query
 

	
 
    def __len__(self):
 
        return self.query.count()
 

	
 
    def __iter__(self):
 
        for data in self.query:
 
            yield data
 

	
 

	
 
class ProductDataProxy(object):
 

	
 
    def __init__(self, product):
 
        self.product = product
 

	
 
    def __getattr__(self, name):
 
        if name == 'F01':
 
            return self.product.upc
 
        if name == 'F02':
 
            return self.product.description
 
        if name == 'F22':
 
            return self.product.size
 
        if name == 'F155':
 
            if self.product.brand:
 
                return self.product.brand.name
 
            return ''
 
        raise AttributeError("Product has no attribute '%s'" % name)
 

	
 

	
 
class ProductQueryDataProvider(QueryDataProvider):
 

	
 
    def __iter__(self):
 
        for product in self.query:
 
            yield ProductDataProxy(product)
 

	
 

	
 
class CSVDataProxy(object):
 

	
 
    def __init__(self, row):
 
        self.row = row
 

	
 
    def __getattr__(self, name):
 
        if name in self.row:
 
            return self.row[name]
 
        raise AttributeError("CSV data row has no attribute '%s'" % name)
 

	
 

	
 
class CSVDataProvider(BatchDataProvider):
 

	
 
    proxy_class = CSVDataProxy
 

	
 
    def __init__(self, csv_path):
 
        self.csv_path = csv_path
 

	
 
    def __len__(self):
 
        return count_lines(self.csv_path)
 

	
 
    def __iter__(self):
 
        csv_file = open(self.csv_path, 'rb')
 
        reader = csv.DictReader(csv_file)
 
        for row in reader:
 
            yield self.proxy_class(row)
 
        csv_file.close()
rattail/db/batches/executors.py
Show inline comments
 
new file 100644
 
#!/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.batches.executors`` -- Batch Executors
 
"""
 

	
 
import pkg_resources
 

	
 
from sqlalchemy.orm import object_session
 

	
 
# from rattail.db.model import LabelProfile, Product
 
from rattail.exceptions import BatchExecutorNotFound, BatchTypeNotSupported
 

	
 

	
 
__all__ = ['get_batch_executor', 'BatchExecutor']
 

	
 

	
 
batch_executors = None
 

	
 

	
 
def get_batch_executor(name):
 
    global batch_executors
 
    if batch_executors is None:
 
        batch_executors = {}
 
        for entrypoint in pkg_resources.iter_entry_points('rattail.batches.executors'):
 
            batch_executors[entrypoint.name] = entrypoint.load()
 
    if name in batch_executors:
 
        return batch_executors[name]()
 
    raise BatchExecutorNotFound(name)
 

	
 

	
 
class BatchExecutor(object):
 

	
 
    batch_type = None
 

	
 
    def execute(self, batch, progress=None):
 
        # if batch.type != self.batch_type:
 
        #     raise BatchTypeNotSupported(self, batch.type)
 
        session = object_session(batch)
 
        return self.execute_batch(session, batch, progress)
 

	
 
    def execute_batch(self, session, batch, progress=None):
 
        raise NotImplementedError
 

	
 

	
 
# class LabelsBatchExecutor(BatchExecutor):
 

	
 
#     batch_type = 'labels'
 

	
 
#     def execute_batch(self, session, batch, progress=None):
 
#         prog = None
 
#         if progress:
 
#             prog = progress("Loading product data", batch.rowcount)
 

	
 
#         profiles = {}
 

	
 
#         cancel = False
 
#         for i, row in enumerate(batch.rows, 1):
 

	
 
#             profile = profiles.get(row.F95)
 
#             if not profile:
 
#                 q = session.query(LabelProfile)
 
#                 q = q.filter(LabelProfile.code == row.F95)
 
#                 profile = q.one()
 
#                 profile.labels = []
 
#                 profiles[row.F95] = profile
 

	
 
#             q = session.query(Product)
 
#             q = q.filter(Product.upc == row.F01)
 
#             product = q.one()
 

	
 
#             profile.labels.append((product, row.F94))
 
#             if prog and not prog.update(i):
 
#                 cancel = True
 
#                 break
 

	
 
#         if not cancel:
 
#             for profile in profiles.itervalues():
 
#                 printer = profile.get_printer()
 
#                 assert printer
 
#                 if not printer.print_labels(profile.labels):
 
#                     cancel = True
 
#                     break
 

	
 
#         if prog:
 
#             prog.destroy()
 
#         return not cancel
rattail/db/batches/makers.py
Show inline comments
 
new file 100644
 
#!/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.batches.makers`` -- Batch Makers
 
"""
 

	
 
from rattail.db.batches.types import get_batch_type
 
from rattail.db.batches.data import BatchDataProvider
 
# from rattail.db.model import Batch, LabelProfile
 
from rattail.db.extension.model import Batch
 
from rattail.sil import consume_batch_id
 

	
 

	
 
class BatchMaker(object):
 

	
 
    progress_message = "Making batch(es)"
 

	
 
    def __init__(self, session, source=None):
 
        self.session = session
 
        self.source = source
 
        self.batches = {}
 

	
 
    def get_batch(self, name):
 
        if name not in self.batches:
 
            self.batches[name] = self.make_batch(name)
 
        return self.batches[name]
 

	
 
    def make_batch(self, name):
 
        if hasattr(self, 'make_batch_%s' % name):
 
            batch = getattr(self, 'make_batch_%s' % name)()
 

	
 
        else:
 
            batch_type = get_batch_type(name)
 
            batch = Batch()
 
            batch_type.initialize(batch)
 
            if self.source:
 
                batch.source = self.source
 
            batch.id = consume_batch_id()
 

	
 
        self.session.add(batch)
 
        self.session.flush()
 
        batch.create_table()
 
        return batch
 

	
 
    def make_batches_begin(self, data):
 
        pass
 

	
 
    def make_batches(self, data, progress=None):
 
        if not isinstance(data, BatchDataProvider):
 
            raise TypeError("Sorry, you must pass a BatchDataProvider instance")
 

	
 
        result = self.make_batches_begin(data)
 
        if result is not None and not result:
 
            return False
 

	
 
        prog = None
 
        if progress and len(data):
 
            prog = progress(self.progress_message, len(data))
 

	
 
        cancel = False
 
        for i, data_row in enumerate(data, 1):
 
            self.process_data_row(data_row)
 
            if prog and not prog.update(i):
 
                cancel = True
 
                break
 
            self.session.flush()
 

	
 
        if prog:
 
            prog.destroy()
 

	
 
        if not cancel:
 
            result = self.make_batches_end()
 
            if result is not None:
 
                cancel = not result
 

	
 
        return not cancel
 

	
 
    def make_batches_end(self):
 
        pass
 

	
 
    def process_data_row(self, data_row):
 
        raise NotImplementedError
 

	
 

	
 
# class LabelsBatchMaker(BatchMaker):
 

	
 
#     default_profile = None
 
#     default_quantity = 1
 

	
 
#     def make_batches_begin(self, data):
 
#         if not self.default_profile:
 
#             q = self.session.query(LabelProfile)
 
#             q = q.order_by(LabelProfile.ordinal)
 
#             self.default_profile = q.first()
 
#             assert self.default_profile
 

	
 
#     def process_data_row(self, data_row):
 
#         batch = self.get_batch('labels')
 
#         row = batch.rowclass()
 
#         row.F01 = data_row.F01
 
#         row.F155 = data_row.F155
 
#         row.F02 = data_row.F02
 
#         row.F22 = data_row.F22
 
#         row.F95 = self.default_profile.code
 
#         row.F94 = self.default_quantity
 
#         batch.add_row(row)        
rattail/db/batches/types.py
Show inline comments
 
new file 100644
 
#!/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.batches.types`` -- Batch Types
 
"""
 

	
 
import datetime
 
import pkg_resources
 

	
 
from edbob.time import local_time
 

	
 
from rattail.exceptions import BatchTypeNotFound
 

	
 

	
 
__all__ = ['get_batch_type', 'BatchType']
 

	
 

	
 
batch_types = None
 

	
 

	
 
def get_batch_type(name):
 
    global batch_types
 
    if batch_types is None:
 
        batch_types = {}
 
        for entrypoint in pkg_resources.iter_entry_points('rattail.batches.types'):
 
            batch_types[entrypoint.name] = entrypoint.load()
 
    if name in batch_types:
 
        return batch_types[name]()
 
    raise BatchTypeNotFound(name)
 

	
 

	
 
class BatchType(object):
 

	
 
    name = None
 
    description = None
 
    source = None
 
    destination = None
 
    action_type = None
 

	
 
    purge_date_offset = None
 

	
 
    def initialize(self, batch):
 
        batch.provider = self.name
 
        batch.description = self.description
 
        batch.source = self.source
 
        batch.destination = self.destination
 
        batch.action_type = self.action_type
 
        self.set_purge_date(batch)
 
        self.add_columns(batch)
 

	
 
    def set_purge_date(self, batch):
 
        if self.purge_date_offset is not None:
 
            today = local_time().date()
 
            purge_offset = datetime.timedelta(days=self.purge_date_offset)
 
            batch.purge = today + purge_offset
 

	
 
    def add_columns(self, batch):
 
        raise NotImplementedError
 

	
 

	
 
# class LabelsBatchType(BatchType):
 

	
 
#     name = 'labels'
 
#     description = "Print Labels"
 

	
 
#     def add_columns(self, batch):
 
#         batch.add_column('F01')
 
#         batch.add_column('F155')
 
#         batch.add_column('F02')
 
#         batch.add_column('F22', display_name="Size")
 
#         batch.add_column('F95', display_name="Label")
 
#         batch.add_column('F94', display_name="Quantity")
rattail/db/extension/model.py
Show inline comments
 
@@ -218,10 +218,16 @@ class Batch(Base):
 
        self.rowclass.__table__.drop(bind=session.bind, checkfirst=True)
 

	
 
    def execute(self, progress=None):
 
        try:
 
            provider = self.get_provider()
 
        assert provider
 
            if not provider.execute(self, progress):
 
                return False
 

	
 
        except batches.BatchProviderNotFound:
 
            executor = self.get_executor()
 
            if not executor.execute(self, progress):
 
                return False
 

	
 
        self.executed = edbob.utc_time(naive=True)
 
        object_session(self).flush()
 
        return True
 
@@ -230,6 +236,11 @@ class Batch(Base):
 
        assert self.provider
 
        return batches.get_provider(self.provider)
 

	
 
    def get_executor(self):
 
        from rattail.db.batches import get_batch_executor
 
        assert self.provider
 
        return get_batch_executor(self.provider)
 

	
 
    def iter_rows(self):
 
        session = object_session(self)
 
        q = session.query(self.rowclass)
rattail/exceptions.py
Show inline comments
 
@@ -27,6 +27,51 @@
 
"""
 

	
 

	
 
class RattailError(Exception):
 
    """
 
    Base class for all Rattail exceptions.
 
    """
 

	
 

	
 
class BatchError(RattailError):
 
    """
 
    Base class for all batch-related errors.
 
    """
 

	
 

	
 
class BatchTypeNotFound(BatchError):
 

	
 
    def __init__(self, name):
 
        self.name = name
 

	
 
    def __str__(self):
 
        return "Batch type not found: %s" % self.name
 

	
 
    
 
class BatchTypeNotSupported(BatchError):
 
    """
 
    Raised when a :class:`rattail.db.batches.BatchExecutor` instance is asked
 
    to execute a batch, but the batch is of an unsupported type.
 
    """
 

	
 
    def __init__(self, executor, batch_type):
 
        self.executor = executor
 
        self.batch_type = batch_type
 

	
 
    def __str__(self):
 
        return "Batch type '%s' is not supported by executor: %s" % (
 
            self.batch_type, repr(self.executor))
 

	
 

	
 
class BatchExecutorNotFound(BatchError):
 

	
 
    def __init__(self, name):
 
        self.name = name
 

	
 
    def __str__(self):
 
        return "Batch executor not found: %s" % self.name
 

	
 
    
 
class LabelPrintingError(Exception):
 

	
 
    pass
0 comments (0 inline, 0 general)