diff --git a/rattail/db/batches/__init__.py b/rattail/db/batches/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ad7863ddb32c0975e3f2ee79659c6f5aec23abc9 --- /dev/null +++ b/rattail/db/batches/__init__.py @@ -0,0 +1,32 @@ +#!/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 . +# +################################################################################ + +""" +``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 * diff --git a/rattail/db/batches/data.py b/rattail/db/batches/data.py new file mode 100644 index 0000000000000000000000000000000000000000..6d8c2b8edb4a4041e5b796bb6aa944d653846cf7 --- /dev/null +++ b/rattail/db/batches/data.py @@ -0,0 +1,111 @@ +#!/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 . +# +################################################################################ + +""" +``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() diff --git a/rattail/db/batches/executors.py b/rattail/db/batches/executors.py new file mode 100644 index 0000000000000000000000000000000000000000..fc2415cb1066c90a23919754198b4411d715d2a2 --- /dev/null +++ b/rattail/db/batches/executors.py @@ -0,0 +1,109 @@ +#!/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 . +# +################################################################################ + +""" +``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 diff --git a/rattail/db/batches/makers.py b/rattail/db/batches/makers.py new file mode 100644 index 0000000000000000000000000000000000000000..4c21c16d216c75783855decb116691e81cc1b3df --- /dev/null +++ b/rattail/db/batches/makers.py @@ -0,0 +1,128 @@ +#!/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 . +# +################################################################################ + +""" +``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) diff --git a/rattail/db/batches/types.py b/rattail/db/batches/types.py new file mode 100644 index 0000000000000000000000000000000000000000..329ab4c674fda8aaa2696fb7473b57ebd7a23ece --- /dev/null +++ b/rattail/db/batches/types.py @@ -0,0 +1,94 @@ +#!/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 . +# +################################################################################ + +""" +``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") diff --git a/rattail/db/extension/model.py b/rattail/db/extension/model.py index 336361b5da10dc96c8b83d1cbe0833d331c2b877..db3f34476a8df167f138cd4d3be8a2cddbe65305 100644 --- a/rattail/db/extension/model.py +++ b/rattail/db/extension/model.py @@ -218,10 +218,16 @@ class Batch(Base): self.rowclass.__table__.drop(bind=session.bind, checkfirst=True) def execute(self, progress=None): - provider = self.get_provider() - assert provider - if not provider.execute(self, progress): - return False + try: + provider = self.get_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) diff --git a/rattail/exceptions.py b/rattail/exceptions.py index 16a1f34792f5509f2e85c427185e1029b86452ca..8c093e293576cca8f4b416eda0789f335c1c90a6 100644 --- a/rattail/exceptions.py +++ b/rattail/exceptions.py @@ -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