fix: add --list-models
option for import/export commands
also rename the command decorators for consistency
This commit is contained in:
parent
7ee551d446
commit
15b2cb07ba
31
docs/glossary.rst
Normal file
31
docs/glossary.rst
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
.. _glossary:
|
||||||
|
|
||||||
|
Glossary
|
||||||
|
========
|
||||||
|
|
||||||
|
.. glossary::
|
||||||
|
:sorted:
|
||||||
|
|
||||||
|
import handler
|
||||||
|
This a type of :term:`handler` which is responsible for a
|
||||||
|
particular set of data import/export task(s).
|
||||||
|
|
||||||
|
The import handler manages data connections and transactions, and
|
||||||
|
invokes one or more :term:`importers <importer>` to process the
|
||||||
|
data.
|
||||||
|
|
||||||
|
Note that "import/export handler" is the more proper term to use
|
||||||
|
here but it is often shortened to just "import handler" for
|
||||||
|
convenience.
|
||||||
|
|
||||||
|
importer
|
||||||
|
In the context of WuttaSync, this refers to a type of object
|
||||||
|
which can process data for an import/export job, i.e. create,
|
||||||
|
update or delete records on the "target" based on the "source"
|
||||||
|
data it reads.
|
||||||
|
|
||||||
|
See also :term:`import handler` which can "contain" one or more
|
||||||
|
importers.
|
||||||
|
|
||||||
|
Note that "importer/exporter" is the more proper term to use here
|
||||||
|
but it is often shortened to just "importer" for convenience.
|
|
@ -22,6 +22,7 @@ database`, it may be used for any "source → target" data flow.
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
:caption: Documentation
|
:caption: Documentation
|
||||||
|
|
||||||
|
glossary
|
||||||
narr/install
|
narr/install
|
||||||
narr/cli
|
narr/cli
|
||||||
|
|
||||||
|
|
|
@ -25,11 +25,12 @@ WuttaSync - ``wutta`` subcommands
|
||||||
|
|
||||||
This namespace exposes the following:
|
This namespace exposes the following:
|
||||||
|
|
||||||
* :func:`~wuttasync.cli.base.importer_command()`
|
* :func:`~wuttasync.cli.base.import_command()`
|
||||||
* :func:`~wuttasync.cli.base.file_importer_command()`
|
* :func:`~wuttasync.cli.base.file_import_command()`
|
||||||
|
* :class:`~wuttasync.cli.base.ImportCommandHandler`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .base import importer_command, file_importer_command
|
from .base import import_command, file_import_command, ImportCommandHandler
|
||||||
|
|
||||||
# nb. must bring in all modules for discovery to work
|
# nb. must bring in all modules for discovery to work
|
||||||
from . import import_csv
|
from . import import_csv
|
||||||
|
|
|
@ -25,6 +25,8 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
@ -32,14 +34,118 @@ from typing_extensions import Annotated
|
||||||
import makefun
|
import makefun
|
||||||
import typer
|
import typer
|
||||||
|
|
||||||
|
from wuttjamaican.app import GenericHandler
|
||||||
|
from wuttasync.importing import ImportHandler
|
||||||
|
|
||||||
def importer_command_template(
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ImportCommandHandler(GenericHandler):
|
||||||
|
"""
|
||||||
|
This is the :term:`handler` responsible for import/export command
|
||||||
|
line runs.
|
||||||
|
|
||||||
|
Normally, the command (actually :term:`subcommand`) logic will
|
||||||
|
create this handler and call its :meth:`run()` method.
|
||||||
|
|
||||||
|
This handler does not know how to import/export data, but it knows
|
||||||
|
how to make its :attr:`import_handler` do it.
|
||||||
|
|
||||||
|
:param import_handler: During construction, caller can specify the
|
||||||
|
:attr:`import_handler` as any of:
|
||||||
|
|
||||||
|
* import handler instance
|
||||||
|
* import handler factory (e.g. class)
|
||||||
|
* import handler spec (cf. :func:`~wuttjamaican:wuttjamaican.util.load_object()`)
|
||||||
|
|
||||||
|
For example::
|
||||||
|
|
||||||
|
handler = ImportCommandHandler(
|
||||||
|
config, import_handler='wuttasync.importing.csv:FromCsvToWutta')
|
||||||
|
"""
|
||||||
|
|
||||||
|
import_handler = None
|
||||||
|
"""
|
||||||
|
Reference to the :term:`import handler` instance, which is to be
|
||||||
|
invoked when command runs. See also :meth:`run()`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config, import_handler=None):
|
||||||
|
super().__init__(config)
|
||||||
|
|
||||||
|
if import_handler:
|
||||||
|
if isinstance(import_handler, ImportHandler):
|
||||||
|
self.import_handler = import_handler
|
||||||
|
elif callable(import_handler):
|
||||||
|
self.import_handler = import_handler(self.config)
|
||||||
|
else: # spec
|
||||||
|
factory = self.app.load_object(import_handler)
|
||||||
|
self.import_handler = factory(self.config)
|
||||||
|
|
||||||
|
def run(self, params, progress=None):
|
||||||
|
"""
|
||||||
|
Run the import/export job(s) based on command line params.
|
||||||
|
|
||||||
|
This mostly just calls
|
||||||
|
:meth:`~wuttasync.importing.handlers.ImportHandler.process_data()`
|
||||||
|
for the :attr:`import_handler`.
|
||||||
|
|
||||||
|
Unless ``--list-models`` was specified on the command line in
|
||||||
|
which case we do :meth:`list_models()` instead.
|
||||||
|
|
||||||
|
:param params: Dict of params from command line. This must
|
||||||
|
include a ``'models'`` key, the rest are optional.
|
||||||
|
|
||||||
|
:param progress: Optional progress indicator factory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# maybe just list models and bail
|
||||||
|
if params.get('list_models'):
|
||||||
|
self.list_models(params)
|
||||||
|
return
|
||||||
|
|
||||||
|
# otherwise process some data
|
||||||
|
kw = dict(params)
|
||||||
|
models = kw.pop('models')
|
||||||
|
log.debug("using handler: %s", self.import_handler.get_spec())
|
||||||
|
# TODO: need to use all/default models if none specified
|
||||||
|
# (and should know models by now for logging purposes)
|
||||||
|
log.debug("running %s %s for: %s",
|
||||||
|
self.import_handler,
|
||||||
|
self.import_handler.orientation.value,
|
||||||
|
', '.join(models))
|
||||||
|
log.debug("params are: %s", kw)
|
||||||
|
self.import_handler.process_data(*models, **kw)
|
||||||
|
|
||||||
|
def list_models(self, params):
|
||||||
|
"""
|
||||||
|
Query the :attr:`import_handler`'s supported target models and
|
||||||
|
print the info to stdout.
|
||||||
|
|
||||||
|
This is what happens when command line has ``--list-models``.
|
||||||
|
"""
|
||||||
|
sys.stdout.write("ALL MODELS:\n")
|
||||||
|
sys.stdout.write("==============================\n")
|
||||||
|
for key in self.import_handler.importers:
|
||||||
|
sys.stdout.write(key)
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
sys.stdout.write("==============================\n")
|
||||||
|
|
||||||
|
|
||||||
|
def import_command_template(
|
||||||
|
|
||||||
# model keys
|
# model keys
|
||||||
models: Annotated[
|
models: Annotated[
|
||||||
Optional[List[str]],
|
Optional[List[str]],
|
||||||
typer.Argument(help="Model(s) to process. Can specify one or more, "
|
typer.Argument(help="Model(s) to process. Can specify one or more, "
|
||||||
"or omit to process all default models.")] = None,
|
"or omit to process default models.")] = None,
|
||||||
|
|
||||||
|
# list models
|
||||||
|
list_models: Annotated[
|
||||||
|
bool,
|
||||||
|
typer.Option('--list-models', '-l',
|
||||||
|
help="List available target models and exit.")] = False,
|
||||||
|
|
||||||
# allow create?
|
# allow create?
|
||||||
create: Annotated[
|
create: Annotated[
|
||||||
|
@ -75,22 +181,22 @@ def importer_command_template(
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Stub function which provides a common param signature; used with
|
Stub function which provides a common param signature; used with
|
||||||
:func:`importer_command()`.
|
:func:`import_command()`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def importer_command(fn):
|
def import_command(fn):
|
||||||
"""
|
"""
|
||||||
Decorator for import/export commands. Adds common params based on
|
Decorator for import/export commands. Adds common params based on
|
||||||
:func:`importer_command_template()`.
|
:func:`import_command_template()`.
|
||||||
|
|
||||||
To use this, e.g. for ``poser import-foo`` command::
|
To use this, e.g. for ``poser import-foo`` command::
|
||||||
|
|
||||||
from poser.cli import poser_typer
|
from poser.cli import poser_typer
|
||||||
from wuttasync.cli import importer_command
|
from wuttasync.cli import import_command, ImportCommandHandler
|
||||||
|
|
||||||
@poser_typer.command()
|
@poser_typer.command()
|
||||||
@importer_command
|
@import_command
|
||||||
def import_foo(
|
def import_foo(
|
||||||
ctx: typer.Context,
|
ctx: typer.Context,
|
||||||
**kwargs
|
**kwargs
|
||||||
|
@ -98,16 +204,15 @@ def importer_command(fn):
|
||||||
\"""
|
\"""
|
||||||
Import data from Foo API to Poser DB
|
Import data from Foo API to Poser DB
|
||||||
\"""
|
\"""
|
||||||
from poser.importing.foo import FromFooToPoser
|
|
||||||
|
|
||||||
config = ctx.parent.wutta_config
|
config = ctx.parent.wutta_config
|
||||||
kw = dict(ctx.params)
|
handler = ImportCommandHandler(
|
||||||
models = kw.pop('models')
|
config, import_handler='poser.importing.foo:FromFooToPoser')
|
||||||
handler = FromFooToPoser(config)
|
handler.run(ctx.params)
|
||||||
handler.process_data(*models, **kw)
|
|
||||||
|
See also :class:`ImportCommandHandler`.
|
||||||
"""
|
"""
|
||||||
original_sig = inspect.signature(fn)
|
original_sig = inspect.signature(fn)
|
||||||
reference_sig = inspect.signature(importer_command_template)
|
reference_sig = inspect.signature(import_command_template)
|
||||||
|
|
||||||
params = list(original_sig.parameters.values())
|
params = list(original_sig.parameters.values())
|
||||||
for i, param in enumerate(reference_sig.parameters.values()):
|
for i, param in enumerate(reference_sig.parameters.values()):
|
||||||
|
@ -120,7 +225,7 @@ def importer_command(fn):
|
||||||
return makefun.create_function(final_sig, fn)
|
return makefun.create_function(final_sig, fn)
|
||||||
|
|
||||||
|
|
||||||
def file_importer_command_template(
|
def file_import_command_template(
|
||||||
input_file_path: Annotated[
|
input_file_path: Annotated[
|
||||||
Path,
|
Path,
|
||||||
typer.Option('--input-path',
|
typer.Option('--input-path',
|
||||||
|
@ -132,23 +237,23 @@ def file_importer_command_template(
|
||||||
"""
|
"""
|
||||||
Stub function to provide signature for import/export commands
|
Stub function to provide signature for import/export commands
|
||||||
which require input file. Used with
|
which require input file. Used with
|
||||||
:func:`file_importer_command()`.
|
:func:`file_import_command()`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def file_importer_command(fn):
|
def file_import_command(fn):
|
||||||
"""
|
"""
|
||||||
Decorator for import/export commands which require input file.
|
Decorator for import/export commands which require input file.
|
||||||
Adds common params based on
|
Adds common params based on
|
||||||
:func:`file_importer_command_template()`.
|
:func:`file_import_command_template()`.
|
||||||
|
|
||||||
To use this, it's the same method as shown for
|
To use this, it's the same method as shown for
|
||||||
:func:`importer_command()` except in this case you would use the
|
:func:`import_command()` except in this case you would use the
|
||||||
``file_importer_command`` decorator.
|
``file_import_command`` decorator.
|
||||||
"""
|
"""
|
||||||
original_sig = inspect.signature(fn)
|
original_sig = inspect.signature(fn)
|
||||||
plain_import_sig = inspect.signature(importer_command_template)
|
plain_import_sig = inspect.signature(import_command_template)
|
||||||
file_import_sig = inspect.signature(file_importer_command_template)
|
file_import_sig = inspect.signature(file_import_command_template)
|
||||||
desired_params = (
|
desired_params = (
|
||||||
list(plain_import_sig.parameters.values())
|
list(plain_import_sig.parameters.values())
|
||||||
+ list(file_import_sig.parameters.values()))
|
+ list(file_import_sig.parameters.values()))
|
||||||
|
|
|
@ -30,11 +30,11 @@ import typer
|
||||||
|
|
||||||
from wuttjamaican.cli import wutta_typer
|
from wuttjamaican.cli import wutta_typer
|
||||||
|
|
||||||
from .base import file_importer_command
|
from .base import file_import_command, ImportCommandHandler
|
||||||
|
|
||||||
|
|
||||||
@wutta_typer.command()
|
@wutta_typer.command()
|
||||||
@file_importer_command
|
@file_import_command
|
||||||
def import_csv(
|
def import_csv(
|
||||||
ctx: typer.Context,
|
ctx: typer.Context,
|
||||||
**kwargs
|
**kwargs
|
||||||
|
@ -42,10 +42,7 @@ def import_csv(
|
||||||
"""
|
"""
|
||||||
Import data from CSV file(s) to Wutta DB
|
Import data from CSV file(s) to Wutta DB
|
||||||
"""
|
"""
|
||||||
from wuttasync.importing.csv import FromCsvToWutta
|
|
||||||
|
|
||||||
config = ctx.parent.wutta_config
|
config = ctx.parent.wutta_config
|
||||||
kw = dict(ctx.params)
|
handler = ImportCommandHandler(
|
||||||
models = kw.pop('models')
|
config, import_handler='wuttasync.importing.csv:FromCsvToWutta')
|
||||||
handler = FromCsvToWutta(config)
|
handler.run(ctx.params)
|
||||||
handler.process_data(*models, **kw)
|
|
||||||
|
|
|
@ -373,6 +373,8 @@ class Importer:
|
||||||
updated = []
|
updated = []
|
||||||
deleted = []
|
deleted = []
|
||||||
|
|
||||||
|
log.debug("using key fields: %s", ', '.join(self.get_keys()))
|
||||||
|
|
||||||
# get complete set of normalized source data
|
# get complete set of normalized source data
|
||||||
if source_data is None:
|
if source_data is None:
|
||||||
source_data = self.normalize_source_data(progress=progress)
|
source_data = self.normalize_source_data(progress=progress)
|
||||||
|
|
|
@ -2,8 +2,59 @@
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from wuttasync.cli import base as mod
|
from wuttasync.cli import base as mod
|
||||||
|
from wuttjamaican.testing import DataTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestImportCommandHandler(DataTestCase):
|
||||||
|
|
||||||
|
def make_handler(self, **kwargs):
|
||||||
|
return mod.ImportCommandHandler(self.config, **kwargs)
|
||||||
|
|
||||||
|
def test_import_handler(self):
|
||||||
|
|
||||||
|
# none
|
||||||
|
handler = self.make_handler()
|
||||||
|
self.assertIsNone(handler.import_handler)
|
||||||
|
|
||||||
|
FromCsvToWutta = self.app.load_object('wuttasync.importing.csv:FromCsvToWutta')
|
||||||
|
|
||||||
|
# as spec
|
||||||
|
handler = self.make_handler(import_handler=FromCsvToWutta.get_spec())
|
||||||
|
self.assertIsInstance(handler.import_handler, FromCsvToWutta)
|
||||||
|
|
||||||
|
# as factory
|
||||||
|
handler = self.make_handler(import_handler=FromCsvToWutta)
|
||||||
|
self.assertIsInstance(handler.import_handler, FromCsvToWutta)
|
||||||
|
|
||||||
|
# as instance
|
||||||
|
myhandler = FromCsvToWutta(self.config)
|
||||||
|
handler = self.make_handler(import_handler=myhandler)
|
||||||
|
self.assertIs(handler.import_handler, myhandler)
|
||||||
|
|
||||||
|
def test_run(self):
|
||||||
|
handler = self.make_handler(import_handler='wuttasync.importing.csv:FromCsvToWutta')
|
||||||
|
|
||||||
|
with patch.object(handler, 'list_models') as list_models:
|
||||||
|
handler.run({'list_models': True})
|
||||||
|
list_models.assert_called_once_with({'list_models': True})
|
||||||
|
|
||||||
|
with patch.object(handler, 'import_handler') as import_handler:
|
||||||
|
handler.run({'models': []})
|
||||||
|
import_handler.process_data.assert_called_once_with()
|
||||||
|
|
||||||
|
def test_list_models(self):
|
||||||
|
handler = self.make_handler(import_handler='wuttasync.importing.csv:FromCsvToWutta')
|
||||||
|
|
||||||
|
with patch.object(mod, 'sys') as sys:
|
||||||
|
handler.list_models({})
|
||||||
|
# just test a few random things we expect to see
|
||||||
|
self.assertTrue(sys.stdout.write.has_call('ALL MODELS:\n'))
|
||||||
|
self.assertTrue(sys.stdout.write.has_call('Person'))
|
||||||
|
self.assertTrue(sys.stdout.write.has_call('User'))
|
||||||
|
self.assertTrue(sys.stdout.write.has_call('Upgrade'))
|
||||||
|
|
||||||
|
|
||||||
class TestImporterCommand(TestCase):
|
class TestImporterCommand(TestCase):
|
||||||
|
@ -15,7 +66,7 @@ class TestImporterCommand(TestCase):
|
||||||
sig1 = inspect.signature(myfunc)
|
sig1 = inspect.signature(myfunc)
|
||||||
self.assertIn('kwargs', sig1.parameters)
|
self.assertIn('kwargs', sig1.parameters)
|
||||||
self.assertNotIn('dry_run', sig1.parameters)
|
self.assertNotIn('dry_run', sig1.parameters)
|
||||||
wrapt = mod.importer_command(myfunc)
|
wrapt = mod.import_command(myfunc)
|
||||||
sig2 = inspect.signature(wrapt)
|
sig2 = inspect.signature(wrapt)
|
||||||
self.assertNotIn('kwargs', sig2.parameters)
|
self.assertNotIn('kwargs', sig2.parameters)
|
||||||
self.assertIn('dry_run', sig2.parameters)
|
self.assertIn('dry_run', sig2.parameters)
|
||||||
|
@ -31,7 +82,7 @@ class TestFileImporterCommand(TestCase):
|
||||||
self.assertIn('kwargs', sig1.parameters)
|
self.assertIn('kwargs', sig1.parameters)
|
||||||
self.assertNotIn('dry_run', sig1.parameters)
|
self.assertNotIn('dry_run', sig1.parameters)
|
||||||
self.assertNotIn('input_file_path', sig1.parameters)
|
self.assertNotIn('input_file_path', sig1.parameters)
|
||||||
wrapt = mod.file_importer_command(myfunc)
|
wrapt = mod.file_import_command(myfunc)
|
||||||
sig2 = inspect.signature(wrapt)
|
sig2 = inspect.signature(wrapt)
|
||||||
self.assertNotIn('kwargs', sig2.parameters)
|
self.assertNotIn('kwargs', sig2.parameters)
|
||||||
self.assertIn('dry_run', sig2.parameters)
|
self.assertIn('dry_run', sig2.parameters)
|
||||||
|
|
|
@ -1,24 +1,19 @@
|
||||||
#-*- coding: utf-8; -*-
|
#-*- coding: utf-8; -*-
|
||||||
|
|
||||||
import os
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from wuttasync.cli import import_csv as mod
|
from wuttasync.cli import import_csv as mod, ImportCommandHandler
|
||||||
from wuttasync.importing.csv import FromCsvToWutta
|
|
||||||
|
|
||||||
|
|
||||||
here = os.path.dirname(__file__)
|
|
||||||
example_conf = os.path.join(here, 'example.conf')
|
|
||||||
|
|
||||||
|
|
||||||
class TestImportCsv(TestCase):
|
class TestImportCsv(TestCase):
|
||||||
|
|
||||||
def test_basic(self):
|
def test_basic(self):
|
||||||
ctx = MagicMock(params={'models': [],
|
params = {'models': [],
|
||||||
'create': True, 'update': True, 'delete': False,
|
'create': True, 'update': True, 'delete': False,
|
||||||
'dry_run': True})
|
'dry_run': True}
|
||||||
with patch.object(FromCsvToWutta, 'process_data') as process_data:
|
ctx = MagicMock(params=params)
|
||||||
|
with patch.object(ImportCommandHandler, 'run') as run:
|
||||||
mod.import_csv(ctx)
|
mod.import_csv(ctx)
|
||||||
process_data.assert_called_once_with(create=True, update=True, delete=False,
|
run.assert_called_once_with(params)
|
||||||
dry_run=True)
|
|
||||||
|
|
Loading…
Reference in a new issue