Source code for rattail.db.batch.handler

# -*- coding: utf-8 -*-
################################################################################
#
#  Rattail -- Retail Software Framework
#  Copyright © 2010-2015 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/>.
#
################################################################################
"""
Basic Batch Handler
"""

from __future__ import unicode_literals

import os
import shutil
import datetime

from rattail.db import Session
from rattail.db import model
from rattail.filemon import Action
from rattail.util import load_object


[docs]class BatchHandler(object): """ Base class for all batch handlers. This isn't really useful by itself but it is expected that other batches will derive from it. """ show_progress = False def __init__(self, config): self.config = config @property def batch_model_class(self): """ Reference to the data model class of the batch type for which this handler is responsible. """ raise NotImplementedError("Must set the 'batch_model_class' attribute " "for class '{0}'".format(self.__class__.__name__))
[docs] def make_batch(self, session, **kwargs): """ Create a new batch instance and return it. All keyword arguments are passed to the batch model constructor. Note that some keyword arguments may be required, depending on the type of batch. """ return self.batch_model_class(**kwargs)
[docs] def get_execute_title(self, batch): """ Get a human-friendly string describing the execution step for a batch. Most handlers should probably override this to provide something more useful than the default, which is just "Execute this batch". """ return "Execute this batch"
[docs] def refresh_data(self, session, batch, progress=None): """ Refresh all data for the batch. """ del batch.data_rows[:]
[docs] def make_rows(self, session, batch, data, progress=None): """ Create batch rows from the given data set. """ prog = None if progress: prog = progress("Refreshing data for batch", len(data)) cancel = False for i, row in enumerate(data, 1): row.sequence = i batch.data_rows.append(row) if self.cognize_row(session, row) is not False: if i % 250 == 0: # seems to help progres UI session.flush() else: batch.data_rows.remove(row) if prog and not prog.update(i): cancel = True break if prog: prog.destroy() return not cancel
[docs] def cognize_row(self, session, row): """ This method should further populate the row's fields, using database lookups and business rules etc. as needed. Each handler class must define this method. :param session: SQLAlchemy database session object. :param row: A batch row instance, whose fields reflect the initial source data only, e.g. that which was parsed from a file. :returns: Typically this method needn't return anything. However if it returns ``False`` then the row will *not* be added to the batch. """ raise NotImplementedError
[docs] def executable(self, batch): """ This method should return a boolean indicating whether or not execution should be allowed for the batch, given its current condition. The default simply returns ``True`` but you may override as needed. Note that this (currently) only affects the enabled/disabled state of the Execute button within the Tailbone batch view. """ return True
def execute(self, batch, progress=None): raise NotImplementedError
[docs]class FileBatchHandler(BatchHandler): """ Base class for all file-based batch handlers. Adds some conveniences for managing data file storage. .. note:: Current implementation only supports one data file per batch. """ @property def root_datadir(self): """ The absolute path of the root folder in which data for this particular type of batch is stored. The structure of this path is as follows: .. code-block:: none /{root_batch_data_dir}/{batch_type_key} * ``{root_batch_data_dir}`` - Value of the 'batch.files' option in the [rattail] section of config file. * ``{batch_type_key}`` - Unique key for the type of batch it is. .. note:: While it is likely that the data folder returned by this method already exists, this method does not guarantee it. """ return os.path.join(self.config.require('rattail', 'batch.files'), self.batch_model_class.batch_key)
[docs] def datadir(self, batch): """ Returns the absolute path of the folder in which the batch's source data file(s) resides. Note that the batch must already have been persisted to the database. The structure of the path returned is as follows: .. code-block:: none /{root_datadir}/{uuid[:2]}/{uuid[2:]} * ``{root_datadir}`` - Value returned by :meth:`root_datadir()`. * ``{uuid[:2]}`` - First two characters of batch UUID. * ``{uuid[2:]}`` - All batch UUID characters *after* the first two. .. note:: While it is likely that the data folder returned by this method already exists, this method does not guarantee any such thing. It is typically assumed that the path will have been created by a previous call to :meth:`make_batch()` however. """ return os.path.join(self.root_datadir, batch.uuid[:2], batch.uuid[2:])
[docs] def data_path(self, batch): """ Returns the full path to the batch's one and only data file. As with :meth:`datadir()`, this method does not guarantee the existence of the file. """ return os.path.join(self.datadir(batch), batch.filename)
[docs] def make_batch(self, session, path, **kwargs): """ Create a new batch as per usual, plus save a copy of the data file (at ``path``) to the configured batch storage folder. """ kwargs.setdefault('filename', 'tmp') batch = self.batch_model_class(**kwargs) session.add(batch) session.flush() self.set_data_file(batch, path) return batch
[docs] def set_data_file(self, batch, path): """ Assign the data file found at ``path`` to the batch. This overwrites the batch's :attr:`filename` attribute and places a copy of the data file in the batch's data folder. """ batch.filename = os.path.basename(path) datadir = self.datadir(batch) os.makedirs(datadir) shutil.copyfile(path, os.path.join(datadir, batch.filename))
[docs]class MakeFileBatch(Action): """ Filemon action for making new file-based batches. """
[docs] def __call__(self, path, handler='', user=''): """ Make a batch from the given file path. :param path: Path to the source data file for the batch. :param handler: Spec string for the batch handler class, e.g. ``'rattail.db.batch.vendorcatalog.handler:VendorCatalogHandler'``. :param user: Username of the user which is to be credited with creating and cognizing the batch. """ handler = load_object(handler)(self.config) session = Session() user = session.query(model.User).filter_by(username=user).one() batch = handler.make_batch(session, path, created_by=user) handler.refresh_data(session, batch) batch.cognized = datetime.datetime.utcnow() batch.cognized_by = user session.commit() session.close()