diff --git a/docs/api/index.rst b/docs/api/index.rst
index 5ea81b98d4c66148c600ff345e0e4c96bccc684c..977dd71547d9cc6ad17bd60bf85b567552297b91 100644
--- a/docs/api/index.rst
+++ b/docs/api/index.rst
@@ -65,4 +65,5 @@ attributes and method signatures etc.
rattail/upgrades
rattail/util
rattail/vendors.catalogs
+ rattail/vendors.handler
rattail/win32
diff --git a/docs/api/rattail/vendors.handler.rst b/docs/api/rattail/vendors.handler.rst
new file mode 100644
index 0000000000000000000000000000000000000000..7cf6e94cf25dc765a565bcfe8ca40883c6f3512c
--- /dev/null
+++ b/docs/api/rattail/vendors.handler.rst
@@ -0,0 +1,6 @@
+
+``rattail.vendors.handler``
+===========================
+
+.. automodule:: rattail.vendors.handler
+ :members:
diff --git a/rattail/app.py b/rattail/app.py
index a546cdf84ee4030fb0a8a107ee5341490f8756c6..57dfaef6c7aaed4d28ad68477cd977320b639774 100644
--- a/rattail/app.py
+++ b/rattail/app.py
@@ -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
diff --git a/rattail/batch/vendorcatalog.py b/rattail/batch/vendorcatalog.py
index 22643a5229020adf8b5c702940772a5e2025573f..eac086a6d23e5bf644f2db5493d51641d6738c3a 100644
--- a/rattail/batch/vendorcatalog.py
+++ b/rattail/batch/vendorcatalog.py
@@ -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))
diff --git a/rattail/vendors/__init__.py b/rattail/vendors/__init__.py
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e21ad1f6188372e4f762c5d3b5f6d3bb5ed94b75 100644
--- a/rattail/vendors/__init__.py
+++ b/rattail/vendors/__init__.py
@@ -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 .
+#
+################################################################################
+"""
+Vendor stuff
+"""
+
+from __future__ import unicode_literals, absolute_import
+
+from .handler import VendorHandler
diff --git a/rattail/vendors/catalogs.py b/rattail/vendors/catalogs.py
index 02485c24afff2bfb14f242b3e09e7dcc4a0abb69..bb3d4a08edd21b8138fa6f469fc0a14369fd146a 100644
--- a/rattail/vendors/catalogs.py
+++ b/rattail/vendors/catalogs.py
@@ -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()
diff --git a/rattail/vendors/handler.py b/rattail/vendors/handler.py
new file mode 100644
index 0000000000000000000000000000000000000000..2f4652ff6e20bfe858d39bc61b6615700c84b254
--- /dev/null
+++ b/rattail/vendors/handler.py
@@ -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 .
+#
+################################################################################
+"""
+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)
diff --git a/tests/vendors/test_handler.py b/tests/vendors/test_handler.py
new file mode 100644
index 0000000000000000000000000000000000000000..c08f35ddb416ffbb4e1bd3f519dc9f3cc266e006
--- /dev/null
+++ b/tests/vendors/test_handler.py
@@ -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)