Add vendor handler, to better organize catalog parser logic
This commit is contained in:
parent
1ac0139fd3
commit
4de258d09b
8 changed files with 320 additions and 14 deletions
|
|
@ -65,4 +65,5 @@ attributes and method signatures etc.
|
|||
rattail/upgrades
|
||||
rattail/util
|
||||
rattail/vendors.catalogs
|
||||
rattail/vendors.handler
|
||||
rattail/win32
|
||||
|
|
|
|||
6
docs/api/rattail/vendors.handler.rst
Normal file
6
docs/api/rattail/vendors.handler.rst
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
``rattail.vendors.handler``
|
||||
===========================
|
||||
|
||||
.. automodule:: rattail.vendors.handler
|
||||
:members:
|
||||
|
|
@ -670,6 +670,20 @@ class AppHandler(object):
|
|||
self.trainwreck_handler = Handler(self.config)
|
||||
return self.trainwreck_handler
|
||||
|
||||
def get_vendor_handler(self, **kwargs):
|
||||
"""
|
||||
Get the configured "vendor" handler.
|
||||
|
||||
:returns: The :class:`~rattail.vendors.handler.VendorHandler`
|
||||
instance for the app.
|
||||
"""
|
||||
if not hasattr(self, 'vendor_handler'):
|
||||
spec = self.config.get('rattail', 'vendors.handler',
|
||||
default='rattail.vendors:VendorHandler')
|
||||
factory = self.load_object(spec)
|
||||
self.vendor_handler = factory(self.config, **kwargs)
|
||||
return self.vendor_handler
|
||||
|
||||
def progress_loop(self, *args, **kwargs):
|
||||
"""
|
||||
Run a given function for a given sequence, and optionally show
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ from sqlalchemy import orm
|
|||
|
||||
from rattail.db import model
|
||||
from rattail.batch import BatchHandler
|
||||
from rattail.vendors.catalogs import require_catalog_parser
|
||||
|
||||
|
||||
class VendorCatalogHandler(BatchHandler):
|
||||
|
|
@ -63,7 +62,7 @@ class VendorCatalogHandler(BatchHandler):
|
|||
def setup(self, batch, progress=None):
|
||||
self.vendor = batch.vendor
|
||||
self.products = {'upc': {}, 'vendor_code': {}}
|
||||
session = orm.object_session(batch)
|
||||
session = self.app.get_session(batch)
|
||||
products = session.query(model.Product)\
|
||||
.options(orm.joinedload(model.Product.brand))\
|
||||
.options(orm.joinedload(model.Product.costs))
|
||||
|
|
@ -112,21 +111,18 @@ class VendorCatalogHandler(BatchHandler):
|
|||
if not batch.parser_key:
|
||||
raise ValueError("batch does not have a parser_key: {}".format(batch))
|
||||
|
||||
session = orm.object_session(batch)
|
||||
session = self.app.get_session(batch)
|
||||
path = batch.filepath(self.config)
|
||||
parser = require_catalog_parser(batch.parser_key)
|
||||
# TODO: should add `config` kwarg to CatalogParser constructor
|
||||
parser.config = self.config
|
||||
parser.app = self.app
|
||||
parser.model = self.model
|
||||
parser.enum = self.enum
|
||||
vendor_handler = self.app.get_vendor_handler()
|
||||
parser = vendor_handler.get_catalog_parser(batch.parser_key,
|
||||
require=True)
|
||||
parser.session = session
|
||||
parser.vendor = batch.vendor
|
||||
batch.effective = parser.parse_effective_date(path)
|
||||
|
||||
def append(row, i):
|
||||
self.add_row(batch, row)
|
||||
if i % 1000 == 0: # pragma: no cover
|
||||
if i % 500 == 0: # pragma: no cover
|
||||
session.flush()
|
||||
|
||||
data = list(parser.parse_rows(path, progress=progress))
|
||||
|
|
|
|||
29
rattail/vendors/__init__.py
vendored
29
rattail/vendors/__init__.py
vendored
|
|
@ -0,0 +1,29 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2022 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 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 General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Vendor stuff
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
from .handler import VendorHandler
|
||||
21
rattail/vendors/catalogs.py
vendored
21
rattail/vendors/catalogs.py
vendored
|
|
@ -26,6 +26,7 @@ Vendor Catalogs
|
|||
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
import warnings
|
||||
from decimal import Decimal
|
||||
|
||||
import six
|
||||
|
|
@ -136,38 +137,50 @@ class CatalogParserNotFound(RattailError):
|
|||
return "Vendor catalog parser with key {} cannot be located.".format(self.key)
|
||||
|
||||
|
||||
def get_catalog_parsers():
|
||||
def get_catalog_parsers(): # pragma: no cover
|
||||
"""
|
||||
Returns a dictionary of installed vendor catalog parser classes.
|
||||
"""
|
||||
warnings.warn("function is deprecated, please use "
|
||||
"VendorHandler.get_all_catalog_parsers() instead",
|
||||
DeprecationWarning)
|
||||
return load_entry_points('rattail.vendors.catalogs.parsers')
|
||||
|
||||
|
||||
def get_catalog_parser(key):
|
||||
def get_catalog_parser(key): # pragma: no cover
|
||||
"""
|
||||
Fetch a vendor catalog parser by key. If the parser class can be located,
|
||||
this will return an instance thereof; otherwise returns ``None``.
|
||||
"""
|
||||
warnings.warn("function is deprecated, please use "
|
||||
"VendorHandler.get_catalog_parser() instead",
|
||||
DeprecationWarning)
|
||||
parser = get_catalog_parsers().get(key)
|
||||
if parser:
|
||||
return parser()
|
||||
return None
|
||||
|
||||
|
||||
def require_catalog_parser(key):
|
||||
def require_catalog_parser(key): # pragma: no cover
|
||||
"""
|
||||
Fetch a vendor catalog parser by key. If the parser class can be located,
|
||||
this will return an instance thereof; otherwise raises an exception.
|
||||
"""
|
||||
warnings.warn("function is deprecated, please use "
|
||||
"VendorHandler.get_catalog_parser() instead",
|
||||
DeprecationWarning)
|
||||
parser = get_catalog_parser(key)
|
||||
if not parser:
|
||||
raise CatalogParserNotFound(key)
|
||||
return parser
|
||||
|
||||
|
||||
def iter_catalog_parsers():
|
||||
def iter_catalog_parsers(): # pragma: no cover
|
||||
"""
|
||||
Returns an iterator over the installed vendor catalog parsers.
|
||||
"""
|
||||
warnings.warn("function is deprecated, please use "
|
||||
"VendorHandler.get_all_catalog_parsers() instead",
|
||||
DeprecationWarning)
|
||||
parsers = get_catalog_parsers()
|
||||
return parsers.values()
|
||||
|
|
|
|||
137
rattail/vendors/handler.py
vendored
Normal file
137
rattail/vendors/handler.py
vendored
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2022 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 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 General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Vendors Handler
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
from rattail.app import GenericHandler
|
||||
from rattail.util import load_entry_points
|
||||
|
||||
|
||||
class VendorHandler(GenericHandler):
|
||||
"""
|
||||
Base class and default implementation for vendor handlers.
|
||||
"""
|
||||
|
||||
def choice_uses_dropdown(self):
|
||||
"""
|
||||
Returns boolean indicating whether a vendor choice should be
|
||||
presented to the user via a dropdown (select) element, vs. an
|
||||
autocomplete field. The latter is the default because
|
||||
potentially the vendor list can be quite large, so we avoid
|
||||
loading them all in the dropdown unless so configured.
|
||||
|
||||
:returns: Boolean; if true then a dropdown should be used;
|
||||
otherwise (false) autocomplete is used.
|
||||
"""
|
||||
return self.config.getbool('rattail', 'vendors.choice_uses_dropdown',
|
||||
default=False)
|
||||
|
||||
def get_vendor(self, session, key, **kwargs):
|
||||
"""
|
||||
Locate and return the vendor corresponding to the given key.
|
||||
|
||||
The key can be a UUID value, but most often it will instead be
|
||||
a "generic" key specific to this purpose. Any generic key can
|
||||
be defined within the settings, pointing to a valid vendor.
|
||||
|
||||
For instance, we can define a key of ``'poser.acme'`` to
|
||||
denote the hypothetical "Acme Distribution" vendor, and we add
|
||||
a namespace unique to our app just to be safe.
|
||||
|
||||
We then create a setting in the DB pointing to our *actual*
|
||||
vendor by way of its UUID:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
INSERT INTO SETTING (name, value)
|
||||
VALUES ('rattail.vendor.poser.acme',
|
||||
'7e6d69a2700911ec93533ca9f40bc550');
|
||||
|
||||
From then on we could easily fetch the vendor by this key.
|
||||
This is mainly useful to allow catalog and invoice parsers to
|
||||
"loosely" associate with a particular vendor by way of this
|
||||
key, which could be shared across organizations etc.
|
||||
"""
|
||||
from rattail.db.api.vendors import get_vendor
|
||||
return get_vendor(session, key)
|
||||
|
||||
def get_all_catalog_parsers(self):
|
||||
"""
|
||||
Should return *all* catalog parsers known to exist.
|
||||
|
||||
Note that this returns classes and not instances.
|
||||
|
||||
:returns: List of
|
||||
:class:`~rattail.vendors.catalogs.CatalogParser` classes.
|
||||
"""
|
||||
Parsers = list(
|
||||
load_entry_points('rattail.vendors.catalogs.parsers').values())
|
||||
Parsers.sort(key=lambda Parser: Parser.display)
|
||||
return Parsers
|
||||
|
||||
def get_supported_catalog_parsers(self):
|
||||
"""
|
||||
Should return only those catalog parsers which are "supported"
|
||||
by the current app. Usually "supported" just means what we
|
||||
want to expose to the user.
|
||||
|
||||
Note that this returns classes and not instances.
|
||||
|
||||
:returns: List of
|
||||
:class:`~rattail.vendors.catalogs.CatalogParser` classes.
|
||||
"""
|
||||
Parsers = self.get_all_catalog_parsers()
|
||||
|
||||
supported_keys = self.config.getlist(
|
||||
'rattail', 'vendors.supported_catalog_parsers')
|
||||
if supported_keys is None:
|
||||
supported_keys = self.config.getlist(
|
||||
'tailbone', 'batch.vendorcatalog.supported_parsers')
|
||||
if supported_keys:
|
||||
Parsers = [Parser for Parser in Parsers
|
||||
if Parser.key in supported_keys]
|
||||
|
||||
return Parsers
|
||||
|
||||
def get_catalog_parser(self, key, require=False):
|
||||
"""
|
||||
Retrieve the catalog parser for the given parser key.
|
||||
|
||||
Note that this returns an instance, not the class.
|
||||
|
||||
:param key: Unique key indicating which parser to get.
|
||||
|
||||
:returns: A :class:`~rattail.vendors.catalogs.CatalogParser`
|
||||
instance.
|
||||
"""
|
||||
from rattail.vendors.catalogs import CatalogParserNotFound
|
||||
|
||||
for Parser in self.get_all_catalog_parsers():
|
||||
if Parser.key == key:
|
||||
return Parser(self.config)
|
||||
|
||||
if require:
|
||||
raise CatalogParserNotFound(key)
|
||||
110
tests/vendors/test_handler.py
vendored
Normal file
110
tests/vendors/test_handler.py
vendored
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from rattail.vendors import handler as mod
|
||||
from rattail.vendors.catalogs import CatalogParserNotFound
|
||||
from rattail.config import make_config
|
||||
from rattail.db import Session
|
||||
|
||||
|
||||
class TestVendorHandler(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.config = self.make_config()
|
||||
self.handler = self.make_handler()
|
||||
|
||||
def make_config(self):
|
||||
return make_config([], extend=False)
|
||||
|
||||
def make_handler(self):
|
||||
return mod.VendorHandler(self.config)
|
||||
|
||||
def test_choice_uses_dropdown(self):
|
||||
|
||||
# do not use dropdown by default
|
||||
result = self.handler.choice_uses_dropdown()
|
||||
self.assertFalse(result)
|
||||
|
||||
# but do use dropdown if so configured
|
||||
self.config.setdefault('rattail', 'vendors.choice_uses_dropdown',
|
||||
'true')
|
||||
result = self.handler.choice_uses_dropdown()
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_get_vendor(self):
|
||||
engine = sa.create_engine('sqlite://')
|
||||
model = self.config.get_model()
|
||||
model.Base.metadata.create_all(bind=engine)
|
||||
session = Session(bind=engine)
|
||||
app = self.config.get_app()
|
||||
|
||||
# no vendor if none exist yet!
|
||||
result = self.handler.get_vendor(session, 'acme')
|
||||
self.assertIsNone(result)
|
||||
|
||||
# let's make the vendor and make sure uuid fetch works
|
||||
uuid = app.make_uuid()
|
||||
acme = model.Vendor(uuid=uuid, name="Acme")
|
||||
session.add(acme)
|
||||
result = self.handler.get_vendor(session, uuid)
|
||||
self.assertIs(result, acme)
|
||||
|
||||
# if we search by key it still does not work
|
||||
result = self.handler.get_vendor(session, 'acme')
|
||||
self.assertIsNone(result)
|
||||
|
||||
# but we can configure the key reference, then it will
|
||||
setting = model.Setting(name='rattail.vendor.acme', value=uuid)
|
||||
session.add(setting)
|
||||
result = self.handler.get_vendor(session, 'acme')
|
||||
self.assertIs(result, acme)
|
||||
|
||||
def test_get_all_catalog_parsers(self):
|
||||
|
||||
# some are always installed; make sure they come back
|
||||
Parsers = self.handler.get_all_catalog_parsers()
|
||||
self.assertTrue(len(Parsers))
|
||||
|
||||
def test_get_supported_catalog_parsers(self):
|
||||
|
||||
# by default all parsers are considered supported, so these
|
||||
# calls should effectively yield the same result
|
||||
all_parsers = self.handler.get_all_catalog_parsers()
|
||||
supported = self.handler.get_supported_catalog_parsers()
|
||||
self.assertEqual(len(all_parsers), len(supported))
|
||||
|
||||
# now pretend only one is supported, using legacy setting
|
||||
self.config.setdefault('tailbone', 'batch.vendorcatalog.supported_parsers',
|
||||
'rattail.contrib.generic')
|
||||
supported = self.handler.get_supported_catalog_parsers()
|
||||
self.assertEqual(len(supported), 1)
|
||||
Parser = supported[0]
|
||||
self.assertEqual(Parser.key, 'rattail.contrib.generic')
|
||||
|
||||
# now pretend two are supported, using preferred setting
|
||||
self.config.setdefault('rattail', 'vendors.supported_catalog_parsers',
|
||||
'rattail.contrib.generic, rattail.contrib.kehe')
|
||||
supported = self.handler.get_supported_catalog_parsers()
|
||||
self.assertEqual(len(supported), 2)
|
||||
keys = [Parser.key for Parser in supported]
|
||||
self.assertEqual(keys, ['rattail.contrib.generic', 'rattail.contrib.kehe'])
|
||||
|
||||
def test_get_catalog_parser(self):
|
||||
|
||||
# generic parser comes back fine
|
||||
parser = self.handler.get_catalog_parser('rattail.contrib.generic')
|
||||
self.assertIsNotNone(parser)
|
||||
self.assertEqual(parser.key, 'rattail.contrib.generic')
|
||||
|
||||
# unknown key returns nothing
|
||||
parser = self.handler.get_catalog_parser('this_should_not_exist')
|
||||
self.assertIsNone(parser)
|
||||
|
||||
# and can raise an error if we require
|
||||
self.assertRaises(CatalogParserNotFound, self.handler.get_catalog_parser,
|
||||
'this_should_not_exist', require=True)
|
||||
Loading…
Add table
Add a link
Reference in a new issue